Regular Expressions
Java's java.util.regex package (Pattern and Matcher) brings full regular-expression matching to string processing — powerful for validation and extraction, but capable of catastrophic performance if written carelessly.
A regular expression (regex) is a compact, symbolic language for describing patterns in text — matching literal substrings, character classes, repeated sequences, and structured groups. Java exposes this through java.util.regex.Pattern (a compiled, reusable representation of a regex) and java.util.regex.Matcher (a stateful engine that applies a compiled Pattern against a specific input), plus convenience methods directly on String like matches(), replaceAll(), and split().
Pattern and Matcher: The Core API
Pattern.compile(regex) parses and compiles a regex string into a Pattern object, which is immutable and thread-safe, and can be reused across many matches. Calling pattern.matcher(input) produces a Matcher bound to that specific input, which is stateful (it tracks position as you call find() repeatedly) and not thread-safe.
Compile a Pattern once and reuse it (e.g. as a static final field) when matching against many inputs repeatedly — compiling a regex is relatively expensive compared to using it.
import java.util.regex.Pattern;
import java.util.regex.Matcher;
Pattern pattern = Pattern.compile("\\d+"); // one or more digits
Matcher matcher = pattern.matcher("Order #4521, item #17");
while (matcher.find()) {
System.out.println(matcher.group()); // 4521, then 17
}Literals, Character Classes, and Escaping
A literal character in a regex matches itself. A character class in square brackets, like [aeiou], matches any one character from the set; [^aeiou] negates it, matching any character NOT in the set. Predefined shorthand classes exist: \d (digit), \w (word character: letter, digit, underscore), \s (whitespace), and their uppercase negations \D, \W, \S. Because backslash is also Java's string-escape character, a regex backslash must itself be escaped in a Java string literal — \\d in Java source produces the regex \d.
In regex syntax (not Java strings), a bare '.' means 'any character'; to match a literal dot it must be escaped as \\. — and because Java strings also use backslash for escaping, that becomes \\\\. in Java source, or more simply written with a text block or Pattern.quote() for literal text.
Pattern vowels = Pattern.compile("[aeiouAEIOU]");
Pattern notDigits = Pattern.compile("\\D+");
Pattern dot = Pattern.compile("\\."); // matches a literal dot, not 'any character'Quantifiers: Controlling Repetition
Quantifiers specify how many times the preceding element may repeat: * (zero or more), + (one or more), ? (zero or one), {n} (exactly n), {n,} (n or more), {n,m} (between n and m, inclusive). By default, quantifiers are greedy — they match as much as possible before backtracking to satisfy the rest of the pattern. Appending ? to a quantifier (e.g. *?, +?) makes it reluctant/lazy, matching as little as possible first.
String html = "<b>bold</b> and <i>italic</i>";
Matcher greedy = Pattern.compile("<.+>").matcher(html);
greedy.find();
System.out.println(greedy.group()); // <b>bold</b> and <i>italic</i> (greedy: matches too much)
Matcher lazy = Pattern.compile("<.+?>").matcher(html);
lazy.find();
System.out.println(lazy.group()); // <b> (lazy: stops at the first '>')Groups and Capturing
Parentheses (...) create a capturing group, letting part of the match be extracted separately via matcher.group(n) (group 0 is always the whole match; group 1 is the first parenthesized subgroup, etc.). Since Java 7, groups can be named with (?<name>...) and retrieved via matcher.group("name"), which is far more readable than numeric indices for complex patterns. A non-capturing group (?:...) groups elements for quantification/alternation purposes without creating a numbered/named capture.
Pattern datePattern = Pattern.compile("(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})");
Matcher m = datePattern.matcher("Event on 2024-06-15.");
if (m.find()) {
System.out.println(m.group("year")); // 2024
System.out.println(m.group("month")); // 06
System.out.println(m.group(0)); // 2024-06-15 (entire match)
}Anchors and Boundaries
Anchors match a position rather than a character: ^ matches the start of input (or start of a line in MULTILINE mode), $ matches the end of input (or end of a line in MULTILINE mode), and \b matches a word boundary (the transition between a word character and a non-word character). Anchors are essential to prevent a pattern from matching an unwanted substring in the middle of a larger string.
String.matches() (and Matcher.matches()) always requires the entire input to match, so ^/$ are implicit there; they matter more with find(), which looks for a match anywhere in the string by default.
System.out.println("cat".matches("^cat$")); // true — matches() implicitly anchors the whole string anyway
System.out.println("concatenate".matches("cat")); // false
Pattern wordCat = Pattern.compile("\\bcat\\b");
System.out.println(wordCat.matcher("concatenate").find()); // false — 'cat' inside 'concatenate' has no word boundary around it
System.out.println(wordCat.matcher("the cat sat").find()); // trueAlternation and Common Building Blocks
The pipe | means 'or' between alternatives, typically grouped with parentheses to scope it correctly. Combining classes, quantifiers, groups, and alternation builds up practical patterns for common validation tasks — though for anything security- or correctness-critical (like full email address validation), a battle-tested library is usually a better choice than a hand-rolled regex, since the full email spec is notoriously complex.
Pattern simpleEmail = Pattern.compile("^[\\w.+-]+@[\\w-]+\\.[a-zA-Z]{2,}$");
System.out.println(simpleEmail.matcher("[email protected]").matches()); // true
System.out.println(simpleEmail.matcher("not-an-email").matches()); // false
Pattern color = Pattern.compile("red|green|blue");
System.out.println(color.matcher("my favorite is blue").find()); // trueReplacement: replaceAll, replaceFirst, and Matcher-Based Replacement
String.replaceAll(regex, replacement) and String.replaceFirst(regex, replacement) replace matches directly using regex syntax for the pattern; the replacement string can reference captured groups with $1, $2, etc. (or ${name} for named groups). For more control — e.g. computing each replacement dynamically — Matcher provides appendReplacement()/appendTail(), or, since Java 9, Matcher.results() combined with replaceAll(Function).
(?=\\d{4}) is a positive lookahead: it asserts four digits follow, without consuming them, so the mask leaves the last four digits untouched.
String masked = "Card: 4111-1111-1111-1111".replaceAll("\\d(?=\\d{4})", "*");
System.out.println(masked); // Card: ****-****-****-1111
String swapped = "John Smith".replaceAll("(\\w+) (\\w+)", "$2 $1");
System.out.println(swapped); // Smith JohnString Convenience Methods Backed by Regex
String.matches(regex) tests whether the entire string matches; String.split(regex) splits on every match of the pattern as a delimiter. These are convenient for one-off use but recompile the pattern every call — for regexes reused across many calls (e.g. inside a loop or hot path), compiling a Pattern once and reusing it is measurably faster.
String csv = "a, b, c ,d";
String[] parts = csv.split("\\s*,\\s*");
System.out.println(java.util.Arrays.toString(parts)); // [a, b, c, d]Catastrophic Backtracking: A Real Performance Hazard
Java's regex engine is backtracking-based: when a match attempt fails, it retries with different combinations of how earlier quantifiers consumed input. Certain pattern shapes — most notoriously nested or adjacent quantifiers over overlapping character sets, like (a+)+ or (a|a)*b applied to a non-matching input — cause the number of backtracking attempts to grow exponentially with input length. This is 'catastrophic backtracking': a regex that runs instantly on short strings can hang for seconds, minutes, or effectively forever on a moderately longer adversarial input, and it is a real, documented denial-of-service vector (ReDoS) when regexes process untrusted input.
The pattern (a+)+ has many equivalent ways to partition a run of a's among the inner and outer +, and when the overall match ultimately fails (no trailing 'b'), the engine tries an exponential number of partitions before giving up.
Mitigate ReDoS risk by avoiding nested/overlapping quantifiers on attacker-controlled input, testing regexes against long adversarial strings, preferring simpler equivalent patterns, and where available bounding match time or input length before regex processing.
// DANGEROUS shape: nested quantifiers over overlapping alternatives
Pattern dangerous = Pattern.compile("(a+)+b");
String evil = "a".repeat(30) + "c"; // no trailing 'b' -> forces exhaustive backtracking
// dangerous.matcher(evil).matches(); // can hang for a very long time on realistic input sizesPractical Usage Patterns
Store frequently-used Patterns as static final fields to compile once and reuse. Prefer named groups for anything with more than one or two captures, for readability. For simple literal-substring searches with no actual pattern syntax needed, String.contains() or indexOf() is both simpler and faster than a regex — reach for regex specifically when you need character classes, quantifiers, or alternation, not for plain substring checks.
public class InputValidator {
private static final Pattern USERNAME = Pattern.compile("^[a-zA-Z][a-zA-Z0-9_]{2,15}$");
static boolean isValidUsername(String candidate) {
return USERNAME.matcher(candidate).matches();
}
}Mental Model
Think of Pattern as a compiled, reusable 'rulebook' and Matcher as a single reading of that rulebook against one specific piece of text, tracking where it currently is. find() advances through the text looking for the next match; matches() insists the whole text obeys the rulebook from start to end.
Key Takeaways
Pattern (compiled, immutable, thread-safe, reusable) and Matcher (stateful, bound to one input) are the core java.util.regex API; String.matches/replaceAll/split are convenience shortcuts that recompile each call.
Quantifiers are greedy by default (*, +, ?, {n,m}); append ? to make them reluctant/lazy.
Capturing groups (...) — or named groups (?<name>...) — extract subparts of a match; (?:...) groups without capturing.
Anchors (^, $, \b) match positions, not characters, and are essential to avoid unintended partial matches with find().
Nested/overlapping quantifiers on ambiguous input can trigger catastrophic (exponential-time) backtracking — a genuine ReDoS risk when matching untrusted input.
Compile a regex once (as a static Pattern) and reuse it when matching many inputs, rather than recompiling on every call.
if (input.matches(".*" + userSuppliedPattern + ".*")) { ... } // untrusted regex fragment
// Avoid embedding untrusted input as regex syntax; escape it with Pattern.quote() if it must be matched literally if (input.contains(Pattern.quote(userSuppliedPattern))) { ... } // or use literal string search
Embedding untrusted input directly as regex syntax risks both incorrect matches and, for complex enough attacker-crafted patterns, catastrophic backtracking; treat untrusted input as literal text unless deliberately building a safe pattern.
for (String s: millionStrings) { if (s.matches("\\d{3}-\\d{4}")) {... } // recompiles the regex every call private static final Pattern PHONE = Pattern.compile("\\d{3}-\\d{4}"); for (String s: millionStrings) { if (PHONE.matcher(s).matches()) {... } }
String.matches() compiles a new Pattern on every invocation; compiling once and reusing the Pattern is significantly faster in a loop or hot path.
Pattern.compile("(a+)+b"); // nested quantifiers over overlapping alternatives
Pattern.compile("a+b"); // equivalent for well-formed input, without the backtracking blowup
Nested quantifiers like (a+)+ create exponentially many ways to partition a run of matching characters, which can hang the engine on certain non-matching inputs; simplify the pattern to avoid the ambiguity.
String dotEscaped = "a.b".replaceAll(".", "_"); // '.' means 'any character' in regex, not a literal dot
String dotEscaped = "a.b".replaceAll("\\.", "_"); // escaped dot matches only literal '.'
Regex metacharacters like '.' have special meaning; forgetting to escape a literal character that happens to be a metacharacter changes what actually gets matched.
What is the difference between Pattern and Matcher?
Pattern is a compiled, immutable, thread-safe representation of a regex, meant to be reused; Matcher is a stateful object bound to one specific input string, produced by pattern.matcher(input), used to find/extract matches.
What is the difference between a greedy and a reluctant (lazy) quantifier?
A greedy quantifier (e.g. +, *) matches as much as possible first, backtracking only if needed; a reluctant quantifier (+?, *?) matches as little as possible first, expanding only if needed to satisfy the rest of the pattern.
What is a named capturing group and why use one?
(?<name>...) captures a subgroup retrievable via matcher.group("name") instead of a numeric index; it makes patterns with multiple captures far more readable and less error-prone to maintain.
Why do ^ and $ behave differently in String.matches() versus a Matcher's find()?
matches() (and Matcher.matches()) already requires the whole string to match, so anchors add little there; find() searches anywhere in the string by default, so ^/$ (or \b) are what constrain the match to a specific position.
What is catastrophic backtracking and why is it a security concern?
It's exponential-time behavior in a backtracking regex engine caused by ambiguous nested/overlapping quantifiers, where a failing match on a longer input triggers an explosion of retry combinations; matching untrusted input against such a pattern is a real denial-of-service (ReDoS) risk.
Why should a frequently used regex be compiled once via Pattern.compile() rather than repeatedly calling String.matches()?
String.matches() compiles a new Pattern object on every call, which is wasteful overhead when the same regex is applied many times; compiling once and reusing the Pattern avoids that repeated cost.
What does the character class shorthand \w match, and how does \W differ from it?
\w matches a word character (letters, digits, underscore); \W matches any character that is NOT a word character — the uppercase form negates the lowercase shorthand.
Write a program that extracts all email-like substrings from a block of text using a compiled Pattern and Matcher.
import java.util.regex.Pattern; import java.util.regex.Matcher; public class EmailExtractor { private static final Pattern EMAIL = Pattern.compile("[\\w.+-]+@[\\w-]+\\.[a-zA-Z]{2,}"); public static void main(String[] args) { String text = "Contact [email protected] or [email protected] for details."; Matcher m = EMAIL.matcher(text); while (m.find()) { System.out.println(m.group()); } } }
The Pattern is compiled once as a static field; matcher(text).find() is called in a loop to advance through the string and print each successive match.
What does this print?
user/host — group(1) is the first parenthesized capture (\\w+ before @), group(2) is the second (\\w+ after @); group(0) would be the full match "user@host".
Write a program that uses replaceAll with a captured group reference to convert dates from "MM/DD/YYYY" to "YYYY-MM-DD" format.
public class DateReformatter { public static void main(String[] args) { String input = "Meeting on 06/15/2024 and 12/01/2023."; String converted = input.replaceAll("(\\d{2})/(\\d{2})/(\\d{4})", "$3-$1-$2"); System.out.println(converted); } }
The pattern captures month, day, and year as groups 1, 2, and 3; the replacement string reorders them with $3-$1-$2, producing "Meeting on 2024-06-15 and 2023-12-01."
Matcher m = Pattern.compile("(\\w+)@(\\w+)").matcher("user@host");
m.find();
System.out.println(m.group(1) + "/" + m.group(2));