Java Regular Expressions
›Short versionOpen if you only have a minute.
- Pattern.compile builds the expression once. Matcher applies it to one input. Pattern is shareable. Matcher is not.
- find() locates the next region. matches() requires the entire input. lookingAt() requires only the start.
- In Java source, a regex backslash is written twice: "\\d+" is the pattern \d+.
- Greedy quantifiers backtrack. Nested ones, such as (a+)+$, can take exponential time on a near miss. That is catastrophic backtracking.
- Possessive quantifiers (++ , *+) do not give characters back. A linear pattern such as a+$ fails the same input immediately.
- Pattern.quote protects literal user text. A regex is a poor parser for a real grammar such as email.
A regular expression is a small program that accepts or rejects text. Java runs it from java.util.regex: you compile a , then ask a to walk one input. The power is real, and so is the failure mode. The engine finds a match by trying alternatives. A pattern that nests can try so many alternatives that a few dozen characters stall the thread. That stall is .
Compile once, then search
import java.util.regex.Matcher;
import java.util.regex.Pattern;
Pattern pattern = Pattern.compile("java", Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher("I love Java programming!");
if (matcher.find()) {
System.out.println("Match found");
}find succeeds because the word appears as a region inside a longer sentence. matches on that same matcher would fail after a reset, because the sentence is not the word java alone. An invalid expression throws PatternSyntaxException from compile, so a pattern built from configuration fails at startup if you compile it then, and fails on the request path if you compile it per call.
A few pieces cover most everyday patterns. . is any character except a line terminator. \d is a digit, \s is whitespace, \w is a word character. mark positions. +, *, ?, and {n} count repetitions. Pattern.MULTILINE makes ^ and $ care about lines inside the input.
The string sees the backslash first
Pattern digits = Pattern.compile("\\d+"); // regex \d+
Pattern word = Pattern.compile("\\bcat\\b"); // regex \bcat\b
Matcher matcher = digits.matcher("abc123");
System.out.println(matcher.find()); // true: a digit region exists
System.out.println(matcher.matches()); // false: the whole input is not digits
String literal = Pattern.quote("file.txt");
// dots in user text stay dots is two parsers in a row. The Java string turns \\\\d into the regex source \d. The regex parser then treats \d as a digit. A text literal from a user goes through Pattern.quote, which is the decision that keeps their * from becoming "anything, repeated."
The near miss that explodes
(a+)+$ asks for one or more groups of one or more a characters, then the end of the input. On a string of as followed by !, the $ fails. The engine gives characters back and tries another way to split the run into groups. Each extra a doubles the splits it is willing to try. Twenty characters already hurt. Forty can look like a hung request. The pattern is small. The retry tree is not.
Pattern risky = Pattern.compile("(a+)+quot;);
// A long run of a's ending in ! forces exponential backtracking.
System.out.println(risky.matcher("aaaaaaaaaaaaaaaaaaaa!").matches());
Pattern linear = Pattern.compile("a+quot;);
System.out.println(linear.matcher("aaaaaaaaaaaaaaaaaaaa!").matches()); // false, quickly
Pattern possessive = Pattern.compile("(a++)++quot;);
System.out.println(possessive.matcher("aaaaaaaaaaaaaaaaaaaa!").matches()); // false, no retreata+$ has one way to eat the run, sees the !, and fails. The possessive form a++ consumes the run and will not return characters for another split, so the nested group cannot fan out either. Reluctant quantifiers change where the engine starts. They do not, by themselves, remove a nested retry. The decision on untrusted input is to refuse nested quantifiers, to compile a pattern you reviewed, and to keep a user's search text inside Pattern.quote. Java's matcher has no built-in time limit. A bad pattern spends the thread until it finishes.
A shape check such as a simple email pattern has the same kind of limit. It can reject strings that are obviously not addresses, and it will also reject real addresses and accept nonsense, because an address is a grammar with quotes, comments, and international names. matches answers "does this whole string fit this shape?" A parser or a purpose-built validator answers "is this an address?"
The pattern (a+)+$ is tested against thirty a's and a trailing exclamation mark. matches() sits there. The string is obvious failure. What is the engine doing?
It is enumerating ways to split the as into groups of one or more, then discovering that $ still sees !. Each split is a fresh attempt. The number of splits grows exponentially with the length of the run, which is catastrophic backtracking. Replace the pattern with a+$, or with a possessive quantifier that cannot return characters, and the same input fails in one pass.