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,\ba 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.
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
}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.
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
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
find vs matches for validation
In the first example, check
"abc123456xyz"withPattern.compile("\\d{6}").matcher(...).find()and with.matches(). Predict both, then explain whyfindis wrong for validation unless you add^and$. - 3
Swap names
Use
replaceAllwith 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
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: falsereplaceAll(Function) and results() arrived in Java 9; named groups in Java 7.
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: 4asMatchPredicate() (Java 11) requires a whole-string match, like matches(). The commented pattern is a text block, so this example needs Java 15.
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// 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.
Break #2
An unclosed bracket
Call Pattern.compile("[a-z").
Break #3
Read a group before matching
Create a matcher and call m.group() without calling find() or matches() first.
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.regexis 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.splitwith a single non-meta character takes a fast path without the regex engine, andString.replaceuses no regex at all.String.matches,replaceAlland multi-charactersplitcompile a newPatterneach call, which shows up in profiles; hoist them intostatic final Patternfields. - ▸
A
Matchercan be reused withreset(newInput)to avoid allocating per input in tight loops, andregion(start, end)restricts matching to part of the input without copying it. - ▸
Patternsupports Unicode properties like\p{L}(any letter) and\p{IsAlphabetic}, and since Java 9 grapheme clusters with\Xand\b{g}(a user-perceived character, such as a letter plus its accent).UNICODE_CHARACTER_CLASSmakes\w,\dand\bUnicode-aware, so\wmatches letters such as accented ones (Topic 6.8).
Remember this
- 1
In a pattern, most characters match themselves (
chaimatches the textchai). 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:\da digit,\wa word character ([a-zA-Z0-9_]),\swhitespace, and upper-case\D,\W,\Sfor their opposites.\bis a word boundary. - 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 errorillegal escape character.Pattern.quote(s)turns any text into a pattern that matches it literally. - 3
**
Pattern.compile(regex)parses the pattern into an internal program, once.pattern.matcher(text)** creates aMatcherthat runs it against one input. Three ways to match:matches()(the whole input must fit),lookingAt()(the start must fit) andfind()(search for the next fitting piece, call it in a loop).String.matches(regex)isPattern.matches, which compiles the pattern every time and requires a whole-string match. - 4
Groups
( )capture the text they matched: after a successfulfind,m.group(1)is the first group,m.group()(orgroup(0)) the whole match, andstart()/end()the positions. Named groups(?<year>\d{4})(Java 7) are read withm.group("year"). In replacements,$1or${year}insert a group, and in the pattern,\1is a backreference meaning "the same text group 1 matched".(?:...)groups without capturing. - 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+)+bon a long run ofas with nobtries an exponential number of ways. That's ReDoS (regular-expression denial of service), a real security issue for patterns run on user input. - 6
Flags change the rules:
Pattern.CASE_INSENSITIVE((?i)),MULTILINE((?m):^and$match at each line),DOTALL((?s):.also matches line breaks) andCOMMENTS((?x): whitespace and#comments allowed in the pattern). APatternis immutable and thread-safe, so keep it in astatic finalfield; aMatcherholds state and is not thread-safe. Java 9 addedMatcher.results()(a stream of matches) andreplaceAll(Function), and Java 11 addedPattern.asMatchPredicate().
Explain it without notes
What's the difference between matches(), find() and lookingAt()?
Why must you write "\\d" in Java source to mean the regex \d?
Explain greedy, reluctant and possessive quantifiers with an example.
What is ReDoS, how would you detect it, and how do you defend against it?
Why should a Pattern be stored in a static final field, and can it be shared between threads?
Practice
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".
Extract every hashtag (a # followed by word characters) from "Loving #chai and #samosa at #Pune2026!" and print them as a list.
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
COMMENTSmode, 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, precompiledPatterns 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,findandlookingAtcorrectly.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 finalfields.