A regular expression describes a pattern of text. Java's support lives in
java.util.regex, and the hardest part is not the syntax — it is knowing when a regex is
the right tool and keeping the ones you write readable.
The three ways in
class Demo {
void run() {
String text = "order-1099";
// 1. String methods — convenient, recompile the pattern every call
System.out.println(text.matches("order-\\d+")); // true
System.out.println(text.replaceAll("\\d+", "#")); // order-#
System.out.println("a1b2c3".split("\\d").length); // 3
// 2. Pattern + Matcher — compile once, reuse
Pattern pattern = Pattern.compile("order-(\\d+)");
Matcher matcher = pattern.matcher(text);
if (matcher.matches()) {
System.out.println(matcher.group(1)); // 1099
}
}
}
Compile the pattern once if it is used more than occasionally. A
Pattern is immutable and thread-safe, so it belongs in a static final field;
a Matcher is neither and must be created per use.
Note matches() requires the whole string to match, while
find() looks for a match anywhere. Confusing the two is the most common first mistake.
The syntax you will actually use
| Pattern | Matches |
|---|---|
. | any character except a newline |
\d \w \s | digit, word character, whitespace |
\D \W \S | the negation of each |
[abc] [^abc] [a-z] | one of, none of, a range |
* + ? | zero or more, one or more, zero or one |
{2} {2,} {2,5} | exactly, at least, between |
^ $ | start and end of input |
\b | a word boundary |
(...) (?:...) | a capturing group, a non-capturing group |
| | either side |
In Java every backslash is doubled, because the string literal consumes one before the regex engine
sees it. \d in a pattern is "\\d" in source — and it is the single most
common source of confusion when copying a pattern from elsewhere.
Text blocks do not change this; they are not raw strings.
Groups
class Demo {
void run() {
Pattern pattern = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})");
Matcher matcher = pattern.matcher("due 2026-08-20 exactly");
if (matcher.find()) {
System.out.println(matcher.group()); // 2026-08-20 — group 0 is the whole match
System.out.println(matcher.group(1)); // 2026
System.out.println(matcher.group(3)); // 20
System.out.println(matcher.start()); // 4 — where it matched
}
}
}
Numbered groups are fragile: insert a group at the front and every number shifts. Name them instead, which is both safer and self-documenting:
class Demo {
void run() {
Pattern pattern = Pattern.compile("(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})");
Matcher matcher = pattern.matcher("2026-08-20");
if (matcher.matches()) {
System.out.println(matcher.group("year") + "/" + matcher.group("month")); // 2026/08
}
}
}
Finding every match
class Demo {
void run() {
Pattern pattern = Pattern.compile("\\b\\w+@\\w+\\.\\w+\\b");
String text = "mail ana@example.com or bo@test.org today";
// Iterating
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
System.out.println(matcher.group());
}
// Or as a stream, since Java 9
System.out.println(pattern.matcher(text).results()
.map(MatchResult::group)
.toList()); // [ana@example.com, bo@test.org]
}
}
Greedy, and the bug it causes
class Demo {
void run() {
String html = "<b>bold</b> and <i>italic</i>";
Matcher greedy = Pattern.compile("<(.+)>").matcher(html);
if (greedy.find()) {
System.out.println(greedy.group()); // the ENTIRE string — .+ took everything
}
Matcher lazy = Pattern.compile("<(.+?)>").matcher(html);
if (lazy.find()) {
System.out.println(lazy.group()); // <b> — stops at the first >
}
}
}
Quantifiers are greedy by default: they take as much as possible and give back only if the match
fails. Adding ? makes them lazy. When a pattern matches far more than you intended, this
is nearly always why.
Catastrophic backtracking
Worth knowing by name, because it turns a validator into a denial-of-service vector. A pattern with nested quantifiers can take exponential time on input that almost matches:
class Demo {
void run() {
// (a+)+ against a long run of a's followed by something else:
// the engine tries every way of splitting the a's before giving up.
Pattern dangerous = Pattern.compile("(a+)+b");
// Safe: no nested quantifier, one way to match
Pattern safe = Pattern.compile("a+b");
System.out.println(safe.matcher("aaab").matches()); // true
}
}
The rule: avoid a quantifier applied to a group that already contains one. If a regex is validating user input, either keep it simple or bound the input length first.
Replacing with a computed value
Two replacement methods are worth knowing beyond the literal form. Backreferences let the
replacement reuse captured groups, and Matcher.replaceAll can take a function:
class Demo {
void run() {
// $1, $2 refer to captured groups; ${name} to named ones
System.out.println("2026-08-20".replaceAll(
"(\\d{4})-(\\d{2})-(\\d{2})", "$3/$2/$1")); // 20/08/2026
// A function, when the replacement has to be computed — Java 9+
Pattern pattern = Pattern.compile("\\d+");
System.out.println(pattern.matcher("a1 b22 c333")
.replaceAll(m -> String.valueOf(m.group().length()))); // a1 b2 c3
}
}
One trap in the literal form: $ and \ are special in the
replacement string too. If the replacement comes from user input or a variable, wrap it in
Matcher.quoteReplacement, or a stray $1 in the data becomes a group
reference. The same applies to patterns — Pattern.quote escapes a string you want
matched literally.
Useful flags
class Demo {
void run() {
System.out.println(Pattern.compile("hello", Pattern.CASE_INSENSITIVE)
.matcher("HELLO").matches()); // true
// MULTILINE: ^ and $ match at each line, not just the whole input
System.out.println(Pattern.compile("^b", Pattern.MULTILINE)
.matcher("a\nb").find()); // true
// DOTALL: . also matches newlines
System.out.println(Pattern.compile("a.b", Pattern.DOTALL)
.matcher("a\nb").matches()); // true
}
}
When not to use a regex
Regex is the wrong tool more often than it is the right one:
- Do not parse HTML, XML or JSON with it. They are not regular languages. Use a parser.
- Do not validate email addresses with it. The correct pattern is thousands of
characters. Check for an
@with something on both sides and send a confirmation email. - Prefer plain
Stringmethods when they fit —contains,startsWithandspliton a literal are clearer and faster than a pattern.
When you do write one, name it and comment it. A static final Pattern called
ORDER_ID with a line explaining the format is maintainable; the same expression inline
is a puzzle for whoever reads it next.
Next
Database access is next — JDBC, and the things that matter regardless of which framework sits on top of it.