Regular Expressions in Java
A regular expression is essentially a search pattern defined by a sequence of characters.
Regular Expressions in Java
A regular expression is essentially a search pattern defined by a sequence of characters.
While String methods like contains(), startsWith(), or indexOf() are great for simple tasks, they fail when things get fuzzy. How do you find "a three-digit number followed by a hyphen," or "an email address that might end in .com or .org"?
Regex allows you to define the structure of the data you are looking for, rather than just specific literal characters.
In Java, regex is primarily used for three things:
- Validation: Checking if an input string matches a required format (e.g., passwords, zip codes).
- Searching/Extraction: Finding specific substrings within a larger body of text.
- Replacement: Identifying patterns and replacing them with something else (e.g., reformatting dates).
The Java Regex Engine Room: java.util.regex
Java’s regex capabilities reside almost entirely within the java.util.regex package. Unlike some languages where regex is built directly into the string syntax, Java uses a two-class system. Think of it as a two-step dance:
1. The Pattern Class (The Blueprint)
A Pattern object is a compiled representation of your regular expression. You don't create it with a constructor; instead, you use the static Pattern.compile() method. Compiling a pattern is expensive, so if you are using the same regex repeatedly, compile it once and reuse it.
2. The Matcher Class (The Engine)
Once you have a Pattern, you use it to create a Matcher object based on a specific input string. The Matcher is the engine that actually performs the search against the text, keeping track of where matches are found.
The Basic Workflow:
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class RegexIntro {
public static void main(String[] args) {
String text = "Java is fun, but Java regex is powerful.";
String regex = "Java";
// 1. Compile the Pattern
Pattern pattern = Pattern.compile(regex);
// 2. Create the Matcher for a specific input
Matcher matcher = pattern.matcher(text);
int matches = 0;
// 3. Perform operations
while (matcher.find()) {
System.out.println("Found match at index: " + matcher.start() + " and value: '" + matcher.group() + "' match end at: " + matcher.end() + " ");
matches++;
}
System.out.println("Total matches: " + matches); // should be 2
}
}
Output:
Found match at index: 0 and value: 'Java' match end at: 4
Found match at index: 17 and value: 'Java' match end at: 21
Total matches: 2
The “Java Backslash” Gotcha
Before diving into syntax, we must address the elephant in the Java room: backslashes.
In standard regex syntax, a backslash \ is used to escape special characters (e.g., turning a literal "d" into \d, meaning "any digit").
However, in Java strings, the backslash itself is an escape character.
Therefore, to create a single literal backslash in a regex pattern in Java code, you must use two: \\.
- Regex to find a digit:
\d - Java string requirement:
"\\d" - Regex to find a literal dot character:
\. - Java string requirement:
"\\."
The Essential Syntax Cheat Sheet
Here are the building blocks you will use 90% of the time.



Let’s look at a few real-world scenarios.
Example 1: Validating a Username
Requirement: A username must start with a letter, contain only letters, numbers, or underscores, and be between 5 and 15 characters long
The Regex: ^[a-zA-Z]\w{4,14}$
^: Start of string anchor.[a-zA-Z]: The first character must be a letter.\w{4,14}: Followed by 4 to 14 word characters.$: End of string anchor.
Java code:
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class UsernameValidator {
// Compile once for performance!
private static final Pattern USERNAME_PATTERN =
Pattern.compile("^[a-zA-Z]\\w{4,14}$");
public static boolean isValidUsername(String username) {
Matcher matcher = USERNAME_PATTERN.matcher(username);
// matches() checks if the WHOLE string matches the pattern
return matcher.matches();
}
public static void main(String[] args) {
System.out.println("john_wick123: " + isValidUsername("john_wick123")); // true
System.out.println("yo: " + isValidUsername("yo")); // false (too short)
System.out.println("123john: " + isValidUsername("123john")); // false (starts with number)
System.out.println("john_wick123athotelcontinental: " + isValidUsername("john_wick123athotelcontinental")); // false (too long)
}
}
Note the use of matcher.matches() here, which requires the entire string to match the pattern. If we used matcher.find(), it would return true if the pattern existed anywhere inside the string.
Example 2: Validating an email
Validation rules: An email address must begin with a letter, can have letters, numbers, _ and “.”.It must have an @ symbol separating the username from the domain, the domain name must have a . in between. There must be at least two characters after the “.”
The Regex: ^[A-Za-z][A-Za-z0–9._]@[A-Za-z0–9.-]+.[A-Za-z]{2,}$*
^[A-Za-z]→ must start with a letter[A-Za-z0-9._]*→ username can contain letters, digits,_,.@→ mandatory separator[A-Za-z0-9.-]+→ domain name (letters, digits, dot, dash)\.[A-Za-z]{2,}$→ must contain a dot followed by at least two letters
Java code:
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class CustomEmailValidator {
// Regex based on your rules:
// 1. Must begin with a letter
// 2. Username can have letters, numbers, underscore, and dot
// 3. Must contain '@' separating username and domain
// 4. Domain must have a '.' in between (e.g., example.com)
private static final String EMAIL_REGEX =
"^[A-Za-z][A-Za-z0-9._]*@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$";
private static final Pattern EMAIL_PATTERN =
Pattern.compile(EMAIL_REGEX);
public static boolean isValidEmail(String email) {
if (email == null) {
return false;
}
Matcher matcher = EMAIL_PATTERN.matcher(email);
return matcher.matches();
}
public static void main(String[] args) {
String[] testEmails = {
"kaustubh.saha@example.com", // valid
"1user@domain.com", // invalid (starts with digit)
"user_name@sub.domain.org", // valid
"user.name@domain.com", // valid (dot allowed in username)
"user@domain", // invalid (no dot in domain)
"user@domain.c" // invalid (TLD too short)
};
for (String email : testEmails) {
System.out.printf("%s -> %s%n",
email, isValidEmail(email) ? "Valid" : "Invalid");
}
}
}
Output:
kaustubh.saha@example.com -> Valid
1user@domain.com -> Invalid
user_name@sub.domain.org -> Valid
user.name@domain.com -> Valid
user@domain -> Invalid
user@domain.c -> Invalid
Example 3: Validating Canadian postal codes
In Canada, postal codes follow a very specific format:
- Pattern:
A1A 1A1 - First character: a letter (A–Z, excluding D, F, I, O, Q, U, W, Z in practice, but regex usually allows all letters).
- Second character: a digit (0–9).
- Third character: a letter.
- Optional space.
- Fourth character: a digit.
- Fifth character: a letter.
- Sixth character: a digit.
The Regex: ^[A-Za-z]\d[A-Za-z][ ]?\d[A-Za-z]\d$
- First character: a letter [A-Za-z]
- Second character: a number \d
- Third character: a letter [A-Za-z]
- Optional space: [ ]?
- Fourth character: a number \d
- Fifth character: [A-Za-z]
- Sixth character: a number \d
Java code:
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class CanadianPostalCodeValidator {
// Regex for Canadian postal code: Letter-Digit-Letter [space optional] Digit-Letter-Digit
private static final String POSTAL_CODE_REGEX =
"^[A-Za-z]\\d[A-Za-z][ ]?\\d[A-Za-z]\\d$";
private static final Pattern POSTAL_CODE_PATTERN =
Pattern.compile(POSTAL_CODE_REGEX);
public static boolean isValidPostalCode(String postalCode) {
if (postalCode == null) {
return false;
}
Matcher matcher = POSTAL_CODE_PATTERN.matcher(postalCode);
return matcher.matches();
}
public static void main(String[] args) {
String[] testCodes = {
"M5V3L9", // valid (Toronto)
"K1A0B1", // valid (Ottawa)
"H0H0H0", // valid (Santa Claus!)
"123456", // invalid
"ABCD123", // invalid
"M5V 3L9" // valid (space allowed)
};
for (String code : testCodes) {
System.out.printf("%s -> %s%n",
code, isValidPostalCode(code) ? "Valid" : "Invalid");
}
}
}
Output:
K1A0B1 -> Valid
H0H0H0 -> Valid
123456 -> Invalid
ABCD123 -> Invalid
M5V 3L9 -> Valid
Capturing groups:
Capturing groups are a feature in regular expressions that allow you to treat multiple characters as a single unit. They serve two primary purposes: extracting specific parts of a matching string and applying operators (like repetition) to a sequence of characters rather than a single character.
In Java, capturing groups are defined by enclosing a part of your regex pattern in parentheses ( ).
Key Functions of Capturing Groups
Extraction (The most common use): When a regex engine finds a match for a pattern inside parentheses, it “captures” that specific substring and stores it in memory. You can retrieve this substring later using the Java Matcher API. This is essential for parsing data (e.g., extracting the area code from a phone number).
Grouping for Quantifiers: They allow you to apply quantifiers (like *, +, or ?) to a whole block of text.
abc+matches "ab" followed by one or more "c"s (e.g., "abcc").(abc)+matches one or more repetitions of the entire sequence "abc" (e.g., "abcabc")
Java numbers capturing groups based on the order of their opening parentheses, counting from left to right. Group 0: Always represents the entire matched string (the full regex match). Group 1: The first opening parenthesis Group 2: The second opening parenthesis, and so on
Let's consider the text “Order ID: 12345, Item: Widget” and the regex “Order ID: (\d+), Item: (\w+)”.
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class CapturingGroupsExample {
public static void main(String[] args) {
String text = "Order ID: 12345, Item: Widget";
// Define regex with two capturing groups:
// Group 1: (\d+) captures the digits
// Group 2: (\w+) captures the word characters after "Item: "
String regex = "Order ID: (\\d+), Item: (\\w+)";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(text);
if (matcher.find()) {
System.out.println("Full match: " + matcher.group(0)); // "Order ID: 12345, Item: Widget"
System.out.println("ID: " + matcher.group(1)); // "12345"
System.out.println("Item: " + matcher.group(2)); // "Widget"
}
}
}
Output:
Full match: Order ID: 12345, Item: Widget
ID: 12345
Item: Widget
Similarly, let’s look at another example:
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class DateExtractor {
public static void main(String[] args) {
String logEntry = "Error at 2023-10-25; User login failed. Previous attempt 2023-10-24.";
// Note double backslashes
String dateRegex = "(\\d{4})-(\\d{2})-(\\d{2})";
Pattern pattern = Pattern.compile(dateRegex);
Matcher matcher = pattern.matcher(logEntry);
System.out.println("Searching log entry...");
while (matcher.find()) {
// group(0) is the entire match (e.g., "2023-10-25")
System.out.println("Found Full Date: " + matcher.group(0));
// group(1), (2), etc., correspond to the parentheses sets
System.out.println(" - Year: " + matcher.group(1));
System.out.println(" - Month: " + matcher.group(2));
System.out.println(" - Day: " + matcher.group(3));
}
}
}
output:
Searching log entry...
Found Full Date: 2023-10-25
- Year: 2023
- Month: 10
- Day: 25
Found Full Date: 2023-10-24
- Year: 2023
- Month: 10
- Day: 24
Using regex to replace values:
Simple Replacement (Using String.replaceAll)
The easiest way to use regex replacement is the replaceAll() method directly on the String class.
- Goal: Replace all occurrences of “cat” (case-insensitive) with “dog”.
- Regex:
(?i)cat(The(?i)flag turns on case-insensitivity.)
public class SimpleReplace {
public static void main(String[] args) {
String input = "I have a Cat, a cat, and a CAT.";
// Replace all case variations of "cat" with "dog"
String result = input.replaceAll("(?i)cat", "dog");
System.out.println(result);
// Output: I have a dog, a dog, and a dog.
}
}
Output:
I have a dog, a dog, and a dog.
Reformatting with Backreferences
This is where regex shines. You can use Capturing Groups in your search pattern and refer to them in your replacement string using $1, $2, etc.
- Goal: Convert dates from “YYYY-MM-DD” format to “DD/MM/YYYY”.
- Regex:
(\d{4})-(\d{2})-(\d{2}) - Replacement String:
$3/$2/$1(Day/Month/Year)
public class ReformatDate {
public static void main(String[] args) {
String data = "Event date: 2023-10-25.";
// $1 is Year, $2 is Month, $3 is Day.
// We rearrange them in the replacement string.
String formatted = data.replaceAll("(\\d{4})-(\\d{2})-(\\d{2})", "$3/$2/$1");
System.out.println(formatted);
// Output: Event date: 25/10/2023.
}
}
output:
Event date: 25/10/2023.
Advanced: Dynamic Logic Replacement
Sometimes you need to perform calculations or logic on the match before replacing it (e.g., “multiply every number found by 2”). You cannot do this with String.replaceAll.
For this, you use the Matcher class methods: appendReplacement() and appendTail().`
- Goal: Find all prices in a string and apply a 50% discount.
- Regex:
\$(\d+)(Matches a literal$followed by digits).
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class DynamicReplace {
public static void main(String[] args) {
String text = "Items cost $10, $20, and $50.";
Pattern pattern = Pattern.compile("\\$(\\d+)");
Matcher matcher = pattern.matcher(text);
StringBuilder sb = new StringBuilder();
while (matcher.find()) {
// 1. Get the number captured in group 1
int originalPrice = Integer.parseInt(matcher.group(1));
// 2. Calculate the new price (logic)
int discountedPrice = originalPrice / 2;
// 3. Replace the match with the new calculated value
// Note: We must escape the $ symbol in the replacement string too!
matcher.appendReplacement(sb, "\\$" + discountedPrice);
}
// 4. Append whatever text is left after the last match
matcher.appendTail(sb);
System.out.println(sb.toString());
// Output: Items cost $5, $10, and $25.
}
}
output:
Items cost $5, $10, and $25.
How appendReplacement works:
- It reads characters from the input string and appends them to the
StringBuilderuntil it hits the match. - It appends your replacement string instead of the matched text.
- It keeps track of the position so the loop can continue.
appendTailis crucial to ensure the text after the final match is added to the end.
Modern Alternative (Java 9+)
If you are on Java 9 or newer, you can simplify the “Dynamic Logic” example using Matcher.replaceAll(Function):
// Java 9+ syntax
String result = matcher.replaceAll(matchResult -> {
int price = Integer.parseInt(matchResult.group(1));
return "\\$" + (price / 2);
});
Matching multiline text
Matching multiline text in Java is a common stumbling block because of a specific default behavior: The dot . meta-character does not match newline characters (\n or \r).
To match text that spans across multiple lines, you generally need to enable “Dotall” mode (also known as single-line mode).
Here are the three ways to handle this.
a) The Inline Flag (?s) (Easiest)
You can add (?s) to the very beginning of your regex string. This tells the engine to turn on "Dotall" mode, meaning the dot . will now match everything, including newlines.
String text = "Start\nMiddle\nEnd";
// Without (?s), this fails because '.' stops at the first \n
// With (?s), '.' matches the newlines
String regex = "(?s)Start.*End";
boolean matches = text.matches(regex); // Returns true
b) The Pattern.DOTALL Flag (Best for Clean Code)
If you are compiling a Pattern object, passing the Pattern.DOTALL flag is cleaner than embedding weird codes in your string.
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class MultilineMatch {
public static void main(String[] args) {
String content = "<div>\n <p>Hello World</p>\n</div>";
// We want to capture everything inside the div tags
String regex = "<div>(.*)</div>";
// Pass the flag as the second argument
Pattern pattern = Pattern.compile(regex, Pattern.DOTALL);
Matcher matcher = pattern.matcher(content);
if (matcher.find()) {
System.out.println(matcher.group(1));
// Output:
// <p>Hello World</p>
}
}
}
Note: Do not confuse Pattern.DOTALL with Pattern.MULTILINE. Pattern.DOTALL makes . match newlines. Pattern.MULTILINE changes ^ and $ to match the start/end of each line, not just the whole string.
c) The “Any Character” Hack [\s\S]
Before flags were widely understood or supported in all regex engines, developers used a character class trick.
\smatches whitespace (including newlines).\Smatches non-whitespace.[\s\S]therefore matches everything, regardless of settings.
// Works without any flags
String regex = "Start[\\s\\S]*End";
Preventing “greedy” matching in replacements
By default, standard quantifiers in Java (*, +, {n,}) are greedy. This means the engine will consume as much text as possible while still satisfying the pattern.
Scenario: You want to replace HTML tags with empty strings to strip them.
- Input:
<b>Bold</b> and <i>Italic</i> - Greedy Regex:
<.*> - What happens: The engine sees the first
<. It then uses.*to eat everything until the very last>in the string. - Match:
<b>Bold</b> and <i>Italic</i>(One giant match) - Result after replacement: (Everything is gone!)
To prevent “greedy” matching, you must use Reluctant Quantifiers (often called “lazy” quantifiers). To make a quantifier “lazy” (stop as soon as it finds the first valid ending), you simply add a ? after it.

While .*? works, it can sometimes be slow because the engine has to backtrack constantly (checking every character to see "Is this the end? No? Okay keep going.").
Imagine you want to redact everything inside quotes. Essentially, the greedy regex would be".*" while the lazy equivalent would be ".*?"
A more performant and precise way to handle this example is to say: “Match a quote, then match anything that IS NOT a quote, then match a quote.”
- Regex:
"[^"]*" [^"]means "Any character except a double quote".
This is often safer than lazy matching because it physically cannot cross the boundary of another quote.
메타데이터
- post_id
- b806152aaa93
- slug
- regular-expressions-in-java-b806152aaa93
- url
- https://medium.com/@kaustubh.saha/regular-expressions-in-java-b806152aaa93
- canonical_url
- https://medium.com/@kaustubh.saha/regular-expressions-in-java-b806152aaa93
- author_url
- https://medium.com/@kaustubh.saha
- status
- ok
- fetched_at
- 2026-06-26 03:39:16