Java Regex Explained: Pattern, Matcher and matches vs find
Regular expressions in Java with Pattern and Matcher: syntax, groups, matches versus find, greedy versus lazy quantifiers, and why to compile once.
- Course: Java study plan
- Module: Strings
- Kind: Lesson
- Reading time: 16 min
- Runtime: OpenJDK 18
How do you use regex in Java?
To use a regular expression in Java, compile it once with Pattern.compile and call matcher(text) on the result; the Matcher then answers matches() for the whole input, find() for each occurrence and group(n) for captured parts. For one-off checks, String.matches, replaceAll and split take a regex directly. Every backslash in the regex is doubled inside the Java string literal.
Lesson
Regular expressions describe patterns in text: "one or more digits", "an email-shaped thing", "a word at the start of a line". Java's java.util.regex is a full-featured engine hiding behind a few String methods and two classes, Pattern and Matcher. This lesson covers the syntax you need, the API around it, and the three mistakes — unescaped metacharacters, matches versus find, and recompiling in loops — that account for most regex bugs.
Where regexes appear
s.matches("\\d+") // whole string is one or more digits?
s.replaceAll("\\s+", " ") // collapse whitespace
s.replaceFirst("^0+", "") // strip leading zeros
s.split("[,;]\\s*") // split on comma or semicolon plus optional spaces
Pattern.compile("…").matcher(s) // the full API
The pattern is written inside a Java string, so every regex backslash is doubled: the regex \d+ is the Java literal "\\d+".
Syntax essentials
| Pattern | Matches |
|---|---|
. | any character except newline |
\d \w \s | digit, word char [A-Za-z0-9_], whitespace |
\D \W \S | the complements |
[abc] [a-z] [^0-9] | character class, range, negated class |
^ $ | start / end of input (or line, with MULTILINE) |
\b | word boundary |
x* x+ x? | zero or more, one or more, zero or one |
x{3} x{2,5} x{2,} | exact, range, at-least |
x*? x+? | lazy versions — as few as possible |
a|b | alternation |
(…) | capturing group |
(?:…) | non-capturing group |
(?<name>…) | named group |
(?i) | case-insensitive flag inline |
\. \( \\ | escaped literals |
Metacharacters that must be escaped when meant literally: . ^ $ * + ? ( ) [ ] { } | \. Inside a character class only \, ], ^ (first) and - (not at an edge) are special.
Pattern and Matcher
import java.util.regex.*;
Pattern p = Pattern.compile("(\\w+)@(\\w+)\\.com");
Matcher m = p.matcher("mail ada@example.com or bob@test.com");
while (m.find()) { // next match anywhere in the input
m.group() // "ada@example.com" — the whole match
m.group(1) // "ada"
m.group(2) // "example"
m.start(), m.end() // indices of the match
}
m.reset();
m.matches() // does the ENTIRE input match? false here
m.lookingAt() // does the input START with a match? false here
The three verbs:
matches()— the whole input must match."abc123".matches("\\d+")is false.find()— search for the next occurrence anywhere. Call repeatedly.lookingAt()— anchored at the start, not the end.
String.matches is the matches() verb; there is no String.find — use a Matcher.
Groups are numbered by their opening parenthesis, left to right, starting at 1; group(0) is the whole match. Named groups: m.group("user"). A group that did not participate returns null.
Replacing with groups
"2026-09-16".replaceAll("(\\d{4})-(\\d{2})-(\\d{2})", "$3/$2/$1") // "16/09/2026"
"price: 10".replaceAll("\\d+", "<$0>") // "price: <10>"
In the replacement string $1, $0 and ${name} refer to groups, and \ and $ must be escaped (Matcher.quoteReplacement(s) does it) — a replacement containing a literal $ is a common source of IllegalArgumentException: Illegal group reference.
For computed replacements:
Matcher m = Pattern.compile("\\d+").matcher(text);
String doubled = m.replaceAll(r -> String.valueOf(Integer.parseInt(r.group()) * 2)); // Java 9+
Greedy versus lazy
"<b>bold</b> and <i>it</i>".replaceAll("<.*>", "") // "" — greedy .* ate everything between the first < and the last >
"<b>bold</b> and <i>it</i>".replaceAll("<.*?>", "") // "bold and it" — lazy stops at the first >
"<b>bold</b>".replaceAll("<[^>]*>", "") // same, and faster: a negated class cannot overrun
Quantifiers are greedy by default; ? after one makes it lazy. A negated character class ([^>]*) is often clearer and never backtracks past the delimiter.
Performance and safety
- Compile once.
Pattern.compileparses the regex;String.matches/replaceAll/splitcompile on every call. In a loop, hoiststatic final Pattern P = Pattern.compile(...).Patternis immutable and thread-safe;Matcheris not — create one per use. - Catastrophic backtracking. Nested quantifiers like
(a+)+$on a non-matching input take exponential time. Prefer possessive quantifiers (a++), atomic groups, or a simpler pattern. Never run a user-supplied regex on user-supplied input without a timeout. Pattern.quote(literal)escapes a string for use inside a pattern;Matcher.quoteReplacementfor the replacement side.
Flags
Pattern.compile("^error", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE)
CASE_INSENSITIVE (inline (?i)), MULTILINE — ^/$ match at line boundaries ((?m)), DOTALL — . matches newlines too ((?s)), UNICODE_CASE, COMMENTS — whitespace and # comments allowed in the pattern ((?x)).
Patterns worth memorising
"\\d+" // integer
"-?\\d+(\\.\\d+)?" // signed decimal
"[A-Za-z_][A-Za-z0-9_]*" // Java-ish identifier
"\\s+" // whitespace run (for split)
"[^\\p{L}\\p{N}]+" // anything not a letter or number (Unicode)
"(?i)^(yes|y|true)$" // affirmative answer
"^[\\w.+-]+@[\\w-]+\\.[\\w.-]+$" // pragmatic email shape (not RFC-complete — nothing short is)
"\\b\\w+\\b" // words
\p{L} (letter), \p{N} (number), \p{Lu} (uppercase letter) are Unicode categories and are preferable to [A-Za-z] when input may be non-English.
When not to use a regex
A regex is a tool for patterns. For "does it contain this literal", contains/indexOf is faster and clearer. For parsing nested structures (JSON, HTML, expressions) a regex is the wrong tool — the language is not regular. For simple splitting on one character, split(",") or indexOf loops. If a regex is longer than the line it lives on, break it into named pieces or write a small parser.
Interview angle
- "
"a.b".split(".")returns?" An empty array —.matches every character, all pieces are empty and trailing empties are dropped. - "
matchesvsfind?" Whole input vs anywhere. - "Why compile the pattern outside the loop?" Compilation is the expensive part;
Patternis thread-safe and reusable. - "What is greedy matching?" Quantifiers take as much as possible and backtrack;
?after a quantifier makes it lazy.
Key takeaways
- Double every backslash in the Java string; escape
. ( ) [ ] { } | + * ? ^ $when literal. matchesneeds the whole string;findsearches; groups are 1-based,$1in replacements.- Greedy by default; lazy with
?; negated classes beat.*?for delimiters. - Hoist
Pattern.compileout of loops;Matcheris per-use. split,replaceAll,matchestake regexes;replacedoes not.
Common questions
What is the difference between matches() and find() in Java?
matches() succeeds only if the entire input matches the pattern, so "abc123".matches("[0-9]+") is false. find() searches for the next match anywhere in the input and can be called repeatedly in a loop; lookingAt() requires a match at the start only.
Why do I need double backslashes in a Java regex?
The regex is written inside a Java string literal, where a backslash begins an escape such as a newline or a tab. To pass one backslash through to the regex engine, the source must write two, so every regex escape, such as the digit class, appears with a doubled backslash.
What is the difference between greedy and lazy quantifiers in Java regex?
Greedy quantifiers such as * and + match as much as possible and then back off, so <.*> spans from the first < to the last >. A ? after one, as in *?, makes it lazy, matching as little as possible; a negated class like [^>]* is often clearer and faster.
Why should I compile a regex Pattern once?
Compiling parses the regex and is the expensive step, and String.matches, replaceAll and split compile their pattern on every call. Hoist it into a static final Pattern and reuse it: a Pattern is immutable and thread-safe, while a Matcher is not, so create one per use.
How do I use capture groups in Java regex?
Wrap part of the pattern in parentheses; after a successful find() or matches(), m.group(1) returns the first group's text and m.group(0) the whole match. Groups are numbered by their opening parenthesis from 1, and a replacement string refers to them as $1, $2.
Exercises
Numbers in the noise
Read one line and find every integer in it (an optional minus sign followed by one or more digits) using a compiled Pattern and Matcher.find(). Print the numbers space-separated on the first line (or none if there are none), and sum=<total> on the second, using a long for the total.
Example: Order 66 shipped -3 items, ref 2024 →
66 -3 2024
sum=2087Dates and masks
Read lines until end of input. For every date of the form yyyy-mm-dd found in a line (pattern (\d{4})-(\d{2})-(\d{2}) with three groups) print date: y=<yyyy> m=<mm> d=<dd>. Then print the line with every run of digits replaced by a single # (replaceAll("\\d+", "#")), prefixed masked: .
Example input
Paid 2024-03-01 and 2024-04-15, invoice 88
→
date: y=2024 m=03 d=01
date: y=2024 m=04 d=15
masked: Paid #-#-# and #-#-#, invoice #In this module: Strings
← Characters, Unicode and encodings · Checkpoint — Strings →