Command Palette

Search for a command to run...

PHASE 6Intermediate ~31 min· topic 7 of 8

Topic 6.7

Regular Expressions

In one line

A regular expression (regex) is a small pattern language for describing text, such as "three digits, a dash, four digits". Java's java.util.regex package compiles a pattern once (Pattern) and uses it to test, search, extract and replace text (Matcher), and String.matches, split and replaceAll use it too.

Think of it like this

A lost-and-found description. You don't say "find the red bag with serial number 4471"; you say "find any bag that is red, has a zip, and a tag with four numbers on it". The helper walks along the shelf and checks every bag against that description. A regex is that description for text, and the regex engine is the helper walking along the String looking for anything that fits.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Regular expression (regex)
A pattern written in a small special language that describes what some text should look like.
Metacharacter
A character with a special meaning in a regex, such as ., *, +, ?, [, ( or |.
Character class
A set of characters written in square brackets, like [aeiou], that matches any one of them.
Quantifier
A symbol that says how many times the thing before it may repeat, like + (one or more) or {3} (exactly three).
Capturing group
A part of the pattern in round brackets whose matched text you can read back later.
Backtracking
When the engine undoes some earlier choices and tries another way to make the pattern fit.
Anchor
A symbol that matches a position, not a character: ^ start, $ end, \b a word boundary.
ReDoS
An attack where specially chosen input makes a badly written regex take a huge amount of time, freezing the program.

Step by step

01From pattern to Matcher

Pattern.compile("\\d+") turns the regex text into a tree of matching nodes. That parse is the expensive part, so do it once. pattern.matcher(text) is cheap: it just creates a cursor over the input.

find() scans forward from where the last match ended. Each time it returns true, group(), start() and end() describe that match. When it returns false, there are no more matches.

Main.javawhole filejava
private static final Pattern NUMBER = Pattern.compile("\\d+");   // compile once

Matcher m = NUMBER.matcher("order 66 shipped in 3 days");
while (m.find()) {
    System.out.println(m.group() + " at " + m.start());          // 66 at 6, then 3 at 20
}
From pattern to Matcherdiagram
Rendering diagram…

02matches vs find vs lookingAt

matches() succeeds only if the entire input fits. "abc123".matches("\\d+") is false, because abc isn't digits. That's what you want for validation ("is this a 6-digit PIN?").

find() looks for the pattern anywhere, and can be called again for the next one. lookingAt() requires a fit at the start but not the end.

A common mistake is adding ^...$ to matches() (harmless, they're implied) or forgetting them in find() when validating (dangerous: find() on \d{6} accepts abc123456xyz).

03The building blocks

Character classes: [aeiou] one vowel; [^0-9] anything but a digit; [a-zA-Z_] a letter or underscore. Inside brackets most metacharacters lose their meaning: [.] is a literal dot.

Quantifiers apply to the item just before them: ab+ is a followed by one or more b; (ab)+ repeats the pair. \d{4} is exactly 4 digits; \d{2,4} between 2 and 4.

Anchors and boundaries: ^ and $ match the start and end of input (or of each line with MULTILINE). \b matches between a word character and a non-word character, so \bcat\b finds cat but not category.

04Greedy, reluctant and possessive

Matching <.+> against <b>bold</b>: .+ first eats everything to the end, then the engine needs a >, so it backtracks one character at a time until the last > fits. Result: the whole string.

<.+?> is reluctant: .+? takes one character, checks whether > follows, and takes more only if needed. Result: <b>. Often clearer still is a negated class, <[^>]+>, which can't run past a > at all and never needs to backtrack.

Possessive .++ takes as much as possible and never gives any back, so <.++> fails here (nothing is left for the >). Possessive quantifiers and atomic groups (?>...) are tools for stopping runaway backtracking.

Greedy, reluctant and possessivediagram
Rendering diagram…

05Groups, names and replacements

(\d{4})-(\d{2})-(\d{2}) has three capturing groups, numbered by their opening bracket from the left. After find(), group(1) is the year.

Named groups make patterns self-documenting: (?<year>\d{4}). In replaceAll, refer to groups as $1 or ${year}; that's why a plain $ in replacement text must be escaped (Topic 6.4's break-it).

Java 9's matcher.replaceAll(match -> ...) computes each replacement with a function, for example upper-casing every match or looking a value up in a map.

06Flags and inline flags

Pass flags to compile, combined with |: Pattern.compile("^error: (.*)$", Pattern.MULTILINE | Pattern.CASE_INSENSITIVE). Or write them inside the pattern: (?im)^error: (.*)$, which also works with String.matches and split.

CASE_INSENSITIVE only folds ASCII letters unless you also add UNICODE_CASE. COMMENTS lets you lay out a long pattern over several lines with # comments, which makes complex validation readable, especially inside a text block (Topic 6.5).

07ReDoS: when a regex becomes a weapon

The threat: Java's engine is a backtracking engine. A pattern with nested or overlapping quantifiers, such as (a+)+$, (\w+\s?)+$ or (a|aa)+$, can be forced to try exponentially many paths when the input almost matches but fails at the very end. A request with a 40-character string can pin a CPU core for minutes.

How to detect it: threads stuck in java.util.regex.Pattern$...match frames in a thread dump (jstack, Topic 14.5), CPU spikes tied to one endpoint, and static analysers (Sonar, Semgrep and CodeQL have ReDoS rules).

How to defend: avoid nested quantifiers over the same characters; use negated classes and possessive quantifiers or atomic groups; cap input length before matching; never compile patterns from user input (or Pattern.quote it); and for untrusted patterns use a linear-time engine such as RE2/J. Verify with a unit test that runs the pattern against a long near-miss input under a time limit (JUnit's assertTimeout).

Try it yourself

  1. 1

    Validate an Indian PIN code properly

    Indian PIN codes are 6 digits and don't start with 0. Change the PIN pattern to "[1-9]\\d{5}" and add "011001" to the test list. Predict all five results before running.

  2. 2

    find vs matches for validation

    In the first example, check "abc123456xyz" with Pattern.compile("\\d{6}").matcher(...).find() and with .matches(). Predict both, then explain why find is wrong for validation unless you add ^ and $.

  3. 3

    Swap names

    Use replaceAll with two groups to turn "Kumar, Amit" into "Amit Kumar". Write the pattern first on paper and predict the result, then run it. Then rewrite it with named groups.

Code & diagrams

matches, find and the basic building blocks New tab
Sign in to run this example in your browser.

Expected output

12345 matches \d+: true
abc123 matches \d+: false
found 66 at 6-8
found 3 at 20-21
lookingAt: true, matches: false
vowels starred: s*m*s* ch**
words: [chai, is, hot]
four-letter words: chai very
greedy:    <b>bold</b><i>it</i>
reluctant: <b>
negated:   <b>
possessive found: false
Groups, named groups, backreferences and replacements Java 9+ New tab

replaceAll(Function) and results() arrived in Java 9; named groups in Java 7.

Sign in to run this example in your browser.

Expected output

year 2026, month 10, day 04
year 2026, month 10, day 09
reordered: 04/10/2026
user ravi.k, domain hectal.in
masked: ***@hectal.in
repeated word: is
repeated word: test
literal split: 1 | 1=2
safe replacement: cost: $5
function replace: CHAI and more CHAI
match count: 4
Flags, validation and streams of matches Java 15+ New tab

asMatchPredicate() (Java 11) requires a whole-string match, like matches(). The commented pattern is a text block, so this example needs Java 15.

Sign in to run this example in your browser.

Expected output

errors: [disk full, timeout]
dot without DOTALL: false
dot with (?s): true
case-insensitive (?i): true
411001 is a PIN: true
41100 is a PIN: false
4110011 is a PIN: false
41100a is a PIN: false
commented pattern: 555 / 1234
Spotting and defusing a ReDoS-prone pattern (fragment)java
// RISKY: nested quantifiers over the same characters.
// On "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!" the engine tries ~2^30 ways before failing.
Pattern risky = Pattern.compile("^(a+)+$");

// SAFER: the same language without nesting; fails in linear time.
Pattern safe = Pattern.compile("^a+$");

// SAFER: possessive quantifier / atomic group never backtracks into the group.
Pattern possessive = Pattern.compile("^(?>a+)+$");

// DEFENCE IN DEPTH for user input:
if (input.length() > 200) {
    throw new IllegalArgumentException("input too long");    // cap before matching
}
// Verify in tests: assertTimeout(Duration.ofMillis(100), () -> safe.matcher(longNearMiss).matches());

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Forget to double the backslash

Write "123".matches("\d+") with a single backslash.

terminal
$ javac Main.java
── what you'll see ──
Main.java:3: error: illegal escape character
System.out.println("123".matches("\d+"));
^
1 error

Break #2

An unclosed bracket

Call Pattern.compile("[a-z").

terminal
$ javac Main.java && java Main
── what you'll see ──
Exception in thread "main" java.util.regex.PatternSyntaxException: Unclosed character class near index 3
[a-z
^
at java.base/java.util.regex.Pattern.error(Pattern.java:2204)
...

Break #3

Read a group before matching

Create a matcher and call m.group() without calling find() or matches() first.

terminal
$ javac Main.java && java Main
── what you'll see ──
Exception in thread "main" java.lang.IllegalStateException: No match found
at java.base/java.util.regex.Matcher.checkMatch(Matcher.java:1852)
...
at Main.main(Main.java:3)

Myth vs fact

Myth

String.matches searches inside the text.

Fact

It requires the whole String to match. To search, use Pattern.compile(...).matcher(s).find().

Myth

Regex is always the fastest way to process text.

Fact

Simple jobs like contains, startsWith or splitting on one character are faster with plain String methods. A regex is worth it when the pattern is genuinely a pattern.

Myth

A regex can validate anything, like full email addresses or HTML.

Fact

Regex can't parse nested structures such as HTML, and the full email grammar is far too complex. Use a simple sanity-check pattern plus a real parser or a confirmation email.

Myth

Bad regexes are only slow, not a security problem.

Fact

Catastrophic backtracking on user input is a denial-of-service vulnerability (ReDoS). It has taken down major websites. Review patterns that run on untrusted input.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    java.util.regex is a backtracking engine (like Perl's and PCRE's), which is why it supports backreferences and lookarounds ((?=...), (?<=...)) but can be exponential. RE2/J (Google) guarantees linear time by giving up backreferences; choose it for patterns supplied by users.

  • ▸

    String.split with a single non-meta character takes a fast path without the regex engine, and String.replace uses no regex at all. String.matches, replaceAll and multi-character split compile a new Pattern each call, which shows up in profiles; hoist them into static final Pattern fields.

  • ▸

    A Matcher can be reused with reset(newInput) to avoid allocating per input in tight loops, and region(start, end) restricts matching to part of the input without copying it.

  • ▸

    Pattern supports Unicode properties like \p{L} (any letter) and \p{IsAlphabetic}, and since Java 9 grapheme clusters with \X and \b{g} (a user-perceived character, such as a letter plus its accent). UNICODE_CHARACTER_CLASS makes \w, \d and \b Unicode-aware, so \w matches letters such as accented ones (Topic 6.8).

Remember this

  1. 1

    In a pattern, most characters match themselves (chai matches the text chai). Metacharacters have special meanings: . any character (except a line break), [abc] one of a set, [^abc] anything not in the set, [a-z] a range, | or, ( ) a group, ^ and $ start and end, and quantifiers * (0 or more), + (1 or more), ? (0 or 1), {n}, {n,} and {n,m}. Shorthand classes: \d a digit, \w a word character ([a-zA-Z0-9_]), \s whitespace, and upper-case \D, \W, \S for their opposites. \b is a word boundary.

  2. 2

    In Java source, the backslash is also the String escape character, so every regex backslash must be doubled: the regex \d+ is written "\\d+", and a literal dot \. is "\\.". A literal backslash in the text needs four: "\\\\". Forgetting this gives the compile error illegal escape character. Pattern.quote(s) turns any text into a pattern that matches it literally.

  3. 3

    **Pattern.compile(regex) parses the pattern into an internal program, once. pattern.matcher(text)** creates a Matcher that runs it against one input. Three ways to match: matches() (the whole input must fit), lookingAt() (the start must fit) and find() (search for the next fitting piece, call it in a loop). String.matches(regex) is Pattern.matches, which compiles the pattern every time and requires a whole-string match.

  4. 4

    Groups ( ) capture the text they matched: after a successful find, m.group(1) is the first group, m.group() (or group(0)) the whole match, and start()/end() the positions. Named groups (?<year>\d{4}) (Java 7) are read with m.group("year"). In replacements, $1 or ${year} insert a group, and in the pattern, \1 is a backreference meaning "the same text group 1 matched". (?:...) groups without capturing.

  5. 5

    Quantifiers are greedy by default: <.+> grabs as much as possible, then gives back characters (backtracking) until the rest of the pattern fits. Add ? to make them reluctant (<.+?>, as little as possible) or + to make them possessive (.++, never give back). Backtracking is what makes some patterns explode: (a+)+b on a long run of as with no b tries an exponential number of ways. That's ReDoS (regular-expression denial of service), a real security issue for patterns run on user input.

  6. 6

    Flags change the rules: Pattern.CASE_INSENSITIVE ((?i)), MULTILINE ((?m): ^ and $ match at each line), DOTALL ((?s): . also matches line breaks) and COMMENTS ((?x): whitespace and # comments allowed in the pattern). A Pattern is immutable and thread-safe, so keep it in a static final field; a Matcher holds state and is not thread-safe. Java 9 added Matcher.results() (a stream of matches) and replaceAll(Function), and Java 11 added Pattern.asMatchPredicate().

Explain it without notes

01

What's the difference between matches(), find() and lookingAt()?

02

Why must you write "\\d" in Java source to mean the regex \d?

03

Explain greedy, reluctant and possessive quantifiers with an example.

04

What is ReDoS, how would you detect it, and how do you defend against it?

05

Why should a Pattern be stored in a static final field, and can it be shared between threads?

Practice

01

Write static boolean isValidUsername(String s): 3 to 16 characters, starting with a letter, then letters, digits or underscores. Test "asha_92", "9lives", "ab" and "ravi.k".

02

Extract every hashtag (a # followed by word characters) from "Loving #chai and #samosa at #Pune2026!" and print them as a list.

03

Collapse runs of whitespace in "too many \t spaces" into single spaces and print the result in square brackets.

Trade-offs

  • ↔

    Regexes are compact and powerful but hard to read and review. For anything beyond a line, use COMMENTS mode, named groups and tests with tricky inputs, or replace the regex with a few lines of plain parsing code.

  • ↔

    Java's backtracking engine supports backreferences and lookarounds but can be exponential. Linear-time engines are safer for untrusted patterns and inputs but support fewer features.

  • ↔

    Convenience methods (String.matches, replaceAll) are fine for one-off use; in loops and hot paths, precompiled Patterns avoid repeated compilation.

Done when you can

  • Done when you can write patterns with classes, quantifiers, anchors and groups.

  • Done when you escape backslashes correctly in Java source.

  • Done when you choose between matches, find and lookingAt correctly.

  • Done when you can extract groups and named groups and use $1/${name} in replacements.

  • Done when you know greedy vs reluctant vs possessive and can spot ReDoS-prone patterns.

  • Done when you precompile patterns as static final fields.