skip to content

For high-throughput replacement, when should you use Matcher.replaceAll / appendReplacement instead of String.replaceAll, and why?

level: seniorimportance: should knowfreq 40%

answer

  1. String.replaceAll = Pattern.compile every call
  2. Precompile to static final Pattern; reuse Matcher
  3. Pattern is thread-safe; Matcher is NOT
  4. Computed replacement: appendReplacement + appendTail
  5. Java 9+: matcher.replaceAll(Function<MatchResult,String>)

basics

~10 s

String.replaceAll recompiles the regex on every call. In hot paths, compile the pattern once with Pattern.compile and reuse a Matcher (matcher.replaceAll). For replacements computed from each match, use appendReplacement/appendTail or replaceAll with a Function.

solid answer

~40 s

Each call to String.replaceAll/replaceFirst/split internally does Pattern.compile, which parses and builds the regex automaton every time — wasteful in loops or hot services. Precompiling with a static final Pattern and reusing it via Matcher avoids that cost; create a fresh Matcher per input (Matchers are not thread-safe) or call pattern.matcher(input).replaceAll(repl). When the replacement is not a fixed template but must be computed from each match (e.g. uppercase the captured group, look up a value), the classic API is the appendReplacement/appendTail loop, which streams the result into a StringBuilder. Modern Java offers Matcher.replaceAll(Function<MatchResult,String>) for the same thing more concisely. Remember the replacement string still honors $/\, so wrap dynamic computed text in Matcher.quoteReplacement.

go deeper

for a junior

Can use String.replaceAll and knows it works but not its cost.

for a middle

Knows to precompile a Pattern when reusing the same regex repeatedly.

for a senior

Explains the compile cost, Pattern vs Matcher thread-safety, and uses appendReplacement/appendTail or replaceAll(Function) for per-match computed replacements, with quoteReplacement where needed.

for a principal

Sets performance and concurrency conventions (static final Patterns, per-thread Matchers), profiles regex hotspots, and weighs regex vs simpler string ops or catastrophic-backtracking risk.

## The hidden cost of String.replaceAll `"...".replaceAll(regex, repl)` is a convenience method. Under the hood it does roughly: ```java return Pattern.compile(regex).matcher(this).replaceAll(repl); ``` The expensive part is **`Pattern.compile`**: it parses the regex text and builds an internal matching structure. Doing that once is cheap; doing it on every iteration of a loop, or on every request in a server, is pure waste. ### Fix: precompile and reuse ```java private static final Pattern WS = Pattern.compile("\\s+"); String collapse(String s) { return WS.matcher(s).replaceAll(" "); } ``` The `Pattern` is immutable and **thread-safe**, so a `static final` field is ideal. A **`Matcher` is mutable and NOT thread-safe**, so create a new one per input (`WS.matcher(s)`), or guard reuse with `matcher.reset(newInput)` on a single thread. ## When the replacement depends on each match A fixed template (`$1-$2`) is handled by `replaceAll`. But sometimes you need to *compute* the replacement from the matched text — e.g. uppercase it, decode it, or look it up in a map. Two tools: ### 1. The classic appendReplacement / appendTail loop ```java Pattern p = Pattern.compile("\\d+"); Matcher m = p.matcher("a1b22c"); StringBuilder sb = new StringBuilder(); while (m.find()) { int doubled = Integer.parseInt(m.group()) * 2; m.appendReplacement(sb, Matcher.quoteReplacement(String.valueOf(doubled))); } m.appendTail(sb); // "a2b44c" ``` `appendReplacement` copies the text between matches plus your replacement into the buffer; `appendTail` copies the final remainder. Note `quoteReplacement` — the computed text could contain `$`/`\`. ### 2. Modern functional form (Java 9+) ```java String out = Pattern.compile("\\d+") .matcher("a1b22c") .replaceAll(mr -> String.valueOf(Integer.parseInt(mr.group()) * 2)); ``` `Matcher.replaceAll(Function<MatchResult,String>)` calls your function per match and uses its return value as the (already-literal) replacement — no `$`/`\` interpretation, which is usually what you want for computed text. ## Decision guide - One-off, fixed pattern, fixed replacement → `String.replaceAll` is fine (readability wins). - Repeated/hot, fixed pattern → **precompile** to `static final Pattern`, reuse via `Matcher`. - Replacement computed per match → **appendReplacement/appendTail** (classic) or **replaceAll(Function)** (modern). - Computed replacement may contain `$`/`\` → use `Matcher.quoteReplacement` (the classic loop) — the `Function` overload already treats its result as literal.

  • Why is a Pattern safe to keep in a static field but a Matcher is not?
    Pattern is immutable and thread-safe; Matcher holds per-input mutable state (match positions, groups), so concurrent use corrupts it. Create a Matcher per input or per thread.
  • Does Matcher.replaceAll(Function) interpret $1 in the function's returned string?
    No. The Function overload treats the returned string as a literal replacement, so $ and \ are inserted as-is, unlike the String-replacement overload.

saying these in an interview costs you the question

  • Sharing a single Matcher across threads (not thread-safe)
  • Recompiling the same regex in a loop
  • Forgetting quoteReplacement for computed text in appendReplacement
  • Premature optimization: precompiling a one-off call where readability matters more

context