Java Regular Expressions
A regular expression (regex) is a compact pattern language for describing text: a single pattern like \d{3}-\d{4} can validate a phone number, pull a date out of a log line, or find every email address in a document. Java exposes this power through the java.util.regex package, built around two classes: Pattern and Matcher. Regex shows up constantly in real Java code — input validation, parsing, text cleanup, search-and-replace — so understanding it well pays off in almost every project you write.
Overview: What Regular Expressions Are and How They Work
A regex is a sequence of characters that describes a set of strings. Some characters match themselves literally (like a or 7); others are metacharacters with special meaning (like ., *, or \d). When you call Pattern.compile(regex), Java does not just store the text of the regex — it parses it into an internal tree of matching instructions and compiles that into a fast, reusable matching engine. This compiled Pattern object is immutable and thread-safe, so it is safe to share across threads and it is meant to be compiled once and reused many times.
A Pattern by itself does not know about any particular text. To actually search a specific piece of input, you create a Matcher by calling pattern.matcher(input). The Matcher is stateful: it remembers where the last match ended, what the capturing groups matched, and where to resume searching. Because of this internal state, a single Matcher instance is not thread-safe and should not be shared across threads.
Java’s regex engine (like most mainstream engines, including Perl’s and .NET’s) is a backtracking engine built on the idea of a nondeterministic finite automaton (NFA). Rather than exploring every possibility at once, it tries one path through the pattern, and if that path fails to produce a match, it backtracks to the last decision point and tries an alternative. This is powerful and flexible, but it also means certain patterns can become extremely slow on certain inputs — more on that in the mistakes section below.
You will typically reach for regex through one of three APIs: the convenience methods on String (matches, replaceAll, replaceFirst, split), or the more powerful Pattern/Matcher pair when you need capturing groups, repeated searches, or performance. The String methods are easiest for one-off use, but each call internally compiles a brand-new Pattern, so using them inside a loop is wasteful — precompile with Pattern.compile instead.
Syntax
The general shape of using the API looks like this:
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(input);
boolean found = matcher.find();
String match = matcher.group();
regex— the pattern text, as a Java string (remember that backslashes must be doubled, since\\din source code produces the regex\d).Pattern.compile(regex)— parses and compiles the pattern once into a reusable, thread-safePatternobject.pattern.matcher(input)— binds the compiled pattern to a specific piece of text, returning a statefulMatcher.matcher.matches()— true only if the entire input matches the pattern.matcher.find()— searches for the next subsequence that matches; call repeatedly to walk through all matches.matcher.group()/group(n)— the text of the whole match, or of capturing groupn(group 0 is the whole match).
Common regex building blocks:
| Construct | Meaning |
|---|---|
. |
Any character except a line terminator |
\d / \D |
A digit / a non-digit |
\w / \W |
A word character (letter, digit, underscore) / not a word character |
\s / \S |
A whitespace character / not whitespace |
^ and $ |
Start and end of input (or of a line, with Pattern.MULTILINE) |
*, +, ? |
Zero-or-more, one-or-more, zero-or-one repetitions |
{n,m} |
Between n and m repetitions |
[...] |
A character class, e.g. [aeiou] matches any one vowel |
(...) |
A capturing group — its match is remembered and retrievable |
(?:...) |
A non-capturing group — groups for structure without saving the match |
| |
Alternation, meaning “or” |
Examples
Example 1: Finding a number with find() and validating with matches().
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class Main {
public static void main(String[] args) {
String input = "Hello, World! 123";
String pattern = "\\d+";
Pattern p = Pattern.compile(pattern);
Matcher m = p.matcher(input);
if (m.find()) {
System.out.println("Found number: " + m.group());
} else {
System.out.println("No number found");
}
System.out.println("Contains digits: " + input.matches(".*\\d+.*"));
}
}
Output:
Found number: 123
Contains digits: true
Here \\d+ in the Java source becomes the regex \d+ (one or more digits) once the string literal is parsed. find() scans the input looking for the first place that matches anywhere, and returns 123. Notice the difference with matches(): it requires the whole string to match, which is why the pattern used there is wrapped in .* on both sides.
Example 2: Capturing groups to pull structured data out of text.
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class Main {
public static void main(String[] args) {
String date = "2026-07-19";
Pattern p = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})");
Matcher m = p.matcher(date);
if (m.matches()) {
System.out.println("Year: " + m.group(1));
System.out.println("Month: " + m.group(2));
System.out.println("Day: " + m.group(3));
}
}
}
Output:
Year: 2026
Month: 07
Day: 19
Each pair of parentheses is a capturing group, numbered left-to-right starting at 1 (group 0 is always the entire match). This is the standard way to pull structured pieces — a year, a month, a day — out of text that follows a known shape, without manual substring slicing.
Example 3: A more realistic pass — extracting emails, redacting them, and splitting messy CSV text.
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class Main {
public static void main(String[] args) {
String text = "Contact us at support@example.com or sales@example.org for help.";
Pattern emailPattern = Pattern.compile("[\\w.+-]+@[\\w-]+\\.[\\w.-]+");
Matcher matcher = emailPattern.matcher(text);
while (matcher.find()) {
System.out.println("Found email: " + matcher.group());
}
String redacted = emailPattern.matcher(text).replaceAll("[EMAIL]");
System.out.println(redacted);
String csv = "apple, banana, cherry , date";
String[] items = csv.split("\\s*,\\s*");
for (String item : items) {
System.out.println("Item: " + item.trim());
}
}
}
Output:
Found email: support@example.com
Found email: sales@example.org
Contact us at [EMAIL] or [EMAIL] for help.
Item: apple
Item: banana
Item: cherry
Item: date
The email pattern uses character classes ([\w.+-]) to describe what a username and domain look like, without listing every character by hand. Calling find() in a while loop is the standard idiom for visiting every match in a string, since each call resumes searching right after the previous match ended. replaceAll reuses the same pattern to substitute every match at once. Finally, split("\\s*,\\s*") splits on a comma together with any surrounding whitespace, so the results come out already trimmed — a much more robust approach than splitting on a bare comma and calling trim() afterward (though we call trim() here too, as cheap insurance).
How It Works Step by Step (Under the Hood)
When Pattern.compile runs, Java’s regex parser walks the pattern text and builds a chain of small matching nodes — one for a literal character, one for a character class, one for a quantifier, and so on — linked together into a structure the engine can walk quickly. This compiled structure is cached inside the Pattern object, which is why reusing one Pattern is much cheaper than calling String.matches() repeatedly (each call to String.matches() silently compiles a fresh Pattern and throws it away).
When you call matcher.find(), the engine starts at the matcher’s current position and tries to walk the compiled node chain against the input. Quantifiers like * and + are greedy by default: they first try to consume as much input as possible, then backtrack — giving back one character at a time — whenever a later part of the pattern fails to match. For example, matching a.*b against "aXbXb" first lets .* swallow the whole rest of the string, then backs off character by character until it finds a trailing b, ultimately matching the whole string rather than stopping at the first b. If you want the opposite behavior, add a ? to make the quantifier reluctant (.*?), which tries to match as little as possible and only expands when forced to.
If find() fails at the starting position, the matcher advances one character and tries again, repeating until it succeeds or runs out of input. Each successful match updates the matcher’s internal group boundaries, which is what group(), start(), and end() read from afterward. This backtracking-and-retry process is exactly why certain patterns (nested repetition over ambiguous input) can explode in running time — a phenomenon called catastrophic backtracking, covered next.
Common Mistakes
Mistake 1: Forgetting to double backslashes in a Java string literal. Because backslash is also Java’s own string-escape character, a regex like \d must be written as \\d in source code. Writing a single backslash produces an escape sequence Java does not recognize, which is a compile error, not a runtime surprise:
String pattern = "\d+";
This fails with an “illegal escape character” compiler error, because \d is not one of Java’s recognized string escapes (\n, \t, \\, and a few others). The fix is simple — escape the backslash itself:
String pattern = "\\d+";
System.out.println(pattern);
Output:
\d+
Mistake 2: Expecting matches() to find a match anywhere in the string. Unlike most other languages’ default regex behavior, Java’s String.matches() and Matcher.matches() require the pattern to match the entire input, start to end. A beginner checking whether a string “contains” digits often writes this and is surprised by the result:
String input = "Hello123World";
boolean result = input.matches("\\d+");
System.out.println(result);
Output:
false
Even though the string clearly contains digits, matches() returns false because \d+ alone cannot account for the surrounding letters. To check for a match anywhere in the string, either wrap the pattern in .* on both sides, or better, use find():
String input = "Hello123World";
boolean result = input.matches(".*\\d+.*");
System.out.println(result);
Output:
true
Best Practices
- Precompile with
Pattern.compile()and reuse thePatternobject whenever a regex is used more than once, especially inside loops — do not callString.matches()/replaceAll()/split()repeatedly with the same pattern. - Share
Patternobjects freely across threads (they are immutable and thread-safe), but create a freshMatcherper thread or per use, sinceMatchercarries mutable state. - Prefer non-capturing groups
(?:...)over capturing groups(...)when you only need grouping for alternation or quantifiers and do not need the matched text back — it keeps group numbering simpler and is marginally faster. - Watch out for catastrophic backtracking: patterns with nested or overlapping quantifiers (like
(a+)+b) can take exponential time on certain non-matching input. Avoid ambiguous nested repetition, and test regexes against adversarial input, not just happy-path input. - Use raw character classes (
[abc]) instead of long alternations (a|b|c) where possible — they are easier to read and usually faster to match. - Always doubly consider whether you need
matches()(whole-string) orfind()(anywhere in string) — picking the wrong one is one of the most common regex bugs in Java. - For anything beyond a simple pattern, add a comment explaining what the regex is meant to match — regex syntax is dense and future readers (including you) will thank you.
Practice Exercises
- Exercise 1: Write a program that checks whether a given string is a valid simple username: 3 to 16 characters, containing only letters, digits, and underscores. Test it against
"user_01"(should be valid) and"a!"(should be invalid). - Exercise 2: Given the sentence
"The rain in Spain falls mainly on the plain", useMatcher.find()in a loop to print every word that ends inain, along with its starting index (usematcher.start()). - Exercise 3: Write a program that takes a string like
"price: 19.99, tax: 1.50, total: 21.49"and uses a capturing group to extract and print each decimal number in the string.
Summary
- Java’s regex support lives in
java.util.regex, centered on the immutable, thread-safePatternand the stateful, per-useMatcher. matches()requires the whole input to match;find()searches for a match anywhere and can be called repeatedly to visit every match.- Capturing groups
(...)let you pull structured pieces out of matched text viagroup(n); use(?:...)when you do not need the captured value. - Backslashes must be doubled in Java string literals (
\\din source becomes the regex\d), and forgetting this is a compile error, not a silent bug. - The engine is backtracking-based: greedy quantifiers grab as much as possible and give back on failure, which is powerful but can cause severe slowdowns on ambiguous nested patterns.
- Precompile and reuse
Patternobjects instead of repeatedly calling the convenience methods onStringwhen a regex is used more than once.
