Regular Expressions
Match, extract and replace text with Pattern and Matcher, reuse compiled patterns, and keep untrusted input from breaking or stalling them.
static final Pattern P = Pattern.compile("(?<id>\\d+)");
Matcher m = P.matcher(text);
if (m.matches()) { String id = m.group("id"); }
text.replaceAll("\\s+", " ")A regular expression describes a shape of text: "letters, a dash, then digits". Java's support lives in java.util.regex. Pattern is the compiled expression; Matcher applies it to one input and holds the position and groups of the current match.
static final Pattern ID = Pattern.compile("(?<id>\\d+)");
Matcher m = ID.matcher(input);
if (m.matches()) {
long id = Long.parseLong(m.group("id"));
}In Java source every backslash in a pattern must be doubled, so the regex \d+ is written "\\d+".
matches, find and lookingAt
matches()succeeds only if the whole input fits the pattern. Use it for validation.find()searches for the next match anywhere in the input. Call it in a loop, or useresults()(Java 9) for a stream of matches.lookingAt()requires a match at the start but not to the end.
Confusing matches and find is the most common bug: a validator using find accepts "abc123xyz" as a number because it contains one.
Compile once
String.matches, replaceAll and replaceFirst compile the pattern on every call. In a request handler that runs thousands of times a second, keep the Pattern in a static final field. A Pattern is immutable and safe to share between threads; a Matcher is not, so create one per use.
Groups and replacement
Parentheses capture parts of a match. Named groups, (?<name>...), keep code readable when patterns change. In a replacement String, $1 or ${name} inserts a group, which means a literal $ or \ in the replacement must be escaped with Matcher.quoteReplacement. If you do not need a pattern at all, plain replace treats both arguments as literal text.
Untrusted input
Never compile user input as a pattern. A search box value like price (USD throws PatternSyntaxException, and a crafted one can make matching extremely slow. Wrap literal text in Pattern.quote. Java's engine backtracks, so patterns with nested repetition such as (a+)+ or (\w|\d)* can take exponential time on certain inputs; that is a denial-of-service risk on any endpoint that matches request data. Keep patterns simple, anchor them, and limit input length before matching.
Regex is a good fit for path segments, IDs and log masking. It is a poor fit for HTML, JSON or full email validation; use a parser, or a loose check plus a confirmation email.
Example
When to use it
- Match /orders/{id}/items/{itemId} and pull out both path parameters in a hand-written router.
- Mask card numbers and email addresses in log lines before they are shipped to a log store.
- Validate that an incoming SKU or reference code has the expected format before querying the database.
More examples
matches versus find, and collecting every match
matches fails because the message is more than a ticket ID, while find locates one inside it. results() returns every match as a stream, which suits extracting references from commit messages or support tickets.
void main() {
Pattern ticket = Pattern.compile("[A-Z]{2,5}-\\d+");
String message = "Fixed in ABC-12 and WEB-7, see also abc-3";
IO.println(ticket.matcher(message).matches());
IO.println(ticket.matcher(message).find());
List<String> ids = ticket.matcher(message).results()
.map(MatchResult::group)
.toList();
IO.println(ids);
IO.println(ticket.matcher("ABC-12").matches());
}
// Output:
// false
// true
// [ABC-12, WEB-7]
// trueMasking values in log lines
$1 puts the captured group back into the result, keeping only the last four digits and the first letter of the email. Without quoteReplacement, the $5 would be read as a reference to group 5 and throw; plain replace avoids the question entirely.
void main() {
String log = "card=4111111111111111 [email protected]";
String masked = log
.replaceAll("\\d{12}(\\d{4})", "************$1")
.replaceAll("([^@\\s=])[^@\\s=]*@", "$1***@");
IO.println(masked);
String template = "Price: {amount}";
IO.println(template.replaceAll("\\{amount\\}", Matcher.quoteReplacement("$5.00")));
IO.println(template.replace("{amount}", "$5.00"));
}
// Output:
// card=************1111 email=a***@example.com
// Price: $5.00
// Price: $5.00Flags and literal user input
Pattern.quote turns the search text into a literal, so brackets and dots in it lose their special meaning. Compiling the raw text fails, and on a public endpoint that would be an unhandled exception for any user who types a bracket.
void main() {
Pattern image = Pattern.compile("jpe?g|png|webp", Pattern.CASE_INSENSITIVE);
IO.println(image.matcher("JPG").matches());
String userSearch = "price (USD";
Pattern literal = Pattern.compile(Pattern.quote(userSearch), Pattern.CASE_INSENSITIVE);
IO.println(literal.matcher("Total price (usd) 20").find());
try {
Pattern.compile(userSearch);
} catch (PatternSyntaxException e) {
IO.println("Invalid pattern: " + e.getDescription());
}
}
// Output:
// true
// true
// Invalid pattern: Unclosed group
Discussion