AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

03 Strings / Lesson 7 of 7

Regular expressions in Java

16 min · +10 XP · 2 exercises · 5 questions

Read the text16 min
Exercises0/2
Quiz5 q

Quick answer

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.

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

PatternMatches
.any character except newline
\d \w \sdigit, word char [A-Za-z0-9_], whitespace
\D \W \Sthe complements
[abc] [a-z] [^0-9]character class, range, negated class
^ $start / end of input (or line, with MULTILINE)
\bword 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|balternation
(…)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.compile parses the regex; String.matches/replaceAll/split compile on every call. In a loop, hoist static final Pattern P = Pattern.compile(...). Pattern is immutable and thread-safe; Matcher is 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.quoteReplacement for 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.
  • "matches vs find?" Whole input vs anywhere.
  • "Why compile the pattern outside the loop?" Compilation is the expensive part; Pattern is 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.
  • matches needs the whole string; find searches; groups are 1-based, $1 in replacements.
  • Greedy by default; lazy with ?; negated classes beat .*? for delimiters.
  • Hoist Pattern.compile out of loops; Matcher is per-use.
  • split, replaceAll, matches take regexes; replace does 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.

Mark as readSign in to keep your place.

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

PatternMatches
.any character except newline
\d \w \sdigit, word char [A-Za-z0-9_], whitespace
\D \W \Sthe complements
[abc] [a-z] [^0-9]character class, range, negated class
^ $start / end of input (or line, with MULTILINE)
\bword 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|balternation
(…)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.compile parses the regex; String.matches/replaceAll/split compile on every call. In a loop, hoist static final Pattern P = Pattern.compile(...). Pattern is immutable and thread-safe; Matcher is 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.quoteReplacement for 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.
  • "matches vs find?" Whole input vs anywhere.
  • "Why compile the pattern outside the loop?" Compilation is the expensive part; Pattern is 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.
  • matches needs the whole string; find searches; groups are 1-based, $1 in replacements.
  • Greedy by default; lazy with ?; negated classes beat .*? for delimiters.
  • Hoist Pattern.compile out of loops; Matcher is per-use.
  • split, replaceAll, matches take regexes; replace does 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=2087

Dates 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 →