🔤 Section 12 · Question #16

Difference Between replace() vs replaceAll()

Both replace() and replaceAll() replace all matching occurrences within a string with a replacement sequence.


🟢 Junior Level

Both replace() and replaceAll() replace all matching occurrences within a string with a replacement sequence.

The fundamental distinction lies in how the search target is evaluated:

  • replace() performs a literal (exact character) replacement. It searches for exact character or substring matches. Metacharacters (such as ., *, +, ?, [, ], (, )) are treated as ordinary text.
  • replaceAll() performs replacement based on a Regular Expression (Regex). The target string is compiled into a regex pattern, and characters like $ and \ in the replacement string have special group-referencing semantics.

In a Nutshell (30 Seconds)

The most common interview misconception is assuming that replace() replaces only the first occurrence while replaceAll() replaces all of them. Both methods replace ALL occurrences! If you only need to replace the first matching occurrence, use replaceFirst().

String text = "a.b.c.d";

// 1. Literal replacement: dot '.' is treated as an exact character
System.out.println(text.replace(".", "/")); 
// Output: "a/b/c/d"

// 2. Regex replacement: dot '.' matches ANY character in Regex!
System.out.println(text.replaceAll(".", "/")); 
// Output: "///////" (every single character replaced by '/')

// 3. Replace only the first occurrence:
System.out.println(text.replaceFirst("\\.", "/")); 
// Output: "a/b.c.d"

🟡 Middle Level

Method Signatures and Performance Characteristics

The java.lang.String class provides four replacement methods:

Method Target Parameter Replacement Parameter Scope Performance & Mechanism
replace(char, char) Primitive char Primitive char All occurrences ⚡ Fastest: Single-pass primitive byte array scan
replace(CharSequence, CharSequence) Literal text Literal text All occurrences 🚀 Very fast: Direct index scan, no regex engine in Java 9+
replaceAll(String, String) Regular Expression Replacement string ($ groups) All occurrences 🐢 Slower: Compiles Pattern and traverses Matcher
replaceFirst(String, String) Regular Expression Replacement string ($ groups) First occurrence 🐢 Slower: Compiles Pattern, stops after first match

Traps When Using replaceAll()

  1. Unescaped Regex Metacharacters in Target: Calling str.replaceAll("100$", "200$") will not find the literal string "100$", because in regex syntax $ signifies the end of a line. For literal matching with replaceAll, the target must be escaped using Pattern.quote("100$").

  2. Crashes Caused by $ in the Replacement String:

    String price = "Cost: PRICE";
    // 💥 RUNTIME CRASH: Throws IllegalArgumentException: No group with name 100!
    String result = price.replaceAll("PRICE", "$100"); 
    

    In replaceAll, the $ symbol designates back-references to captured regex groups ($1, $2, etc.). When followed by digits, the regex engine expects corresponding capturing parentheses in the search pattern. Solution: When inserting arbitrary text that may contain $ or \, sanitize the replacement string via Matcher.quoteReplacement("$100").


🔴 Senior Level

Implementation Evolution: Java 8 vs Java 9+

Prior to Java 9, replace(CharSequence, CharSequence) carried hidden regex overhead:

  • Internally, it delegated to: Pattern.compile(target.toString(), Pattern.LITERAL).matcher(this).replaceAll(Matcher.quoteReplacement(replacement.toString())) Even though matching was literal (Pattern.LITERAL), the JVM still created Pattern and Matcher instances and evaluated matches through a finite state automaton.

Starting with Java 9 (including Java 17 and 21): replace(CharSequence, CharSequence) was completely rewritten without using java.util.regex:

  1. Fast Failure Check: Checks for the first occurrence using indexOf(target).
  2. Zero-Allocation Short-Circuit: If the target substring is not found, the method immediately returns this without allocating a single byte on the heap!
  3. Exact Allocation Sizing: If matches exist, it calculates the exact capacity needed for the resulting array: \(\text{newLength} = \text{oldLength} + \text{matchCount} \times (\text{replacementLength} - \text{targetLength})\)
  4. Allocates a single byte[] array of the exact required length (accounting for Latin-1 or UTF-16 coder) and copies segments directly using System.arraycopy. As a result, replace() in modern Java is 5 to 10 times faster than replaceAll(), generating substantially less GC pressure.
// Simplified OpenJDK String.java implementation
public String replace(CharSequence target, CharSequence replacement) {
    String trgStr = target.toString();
    String replStr = replacement.toString();
    int i = indexOf(trgStr);
    if (i < 0) {
        return this; // Zero allocations when target is absent!
    }
    // Direct byte-level replacement without Pattern or Matcher
    return isLatin1()
        ? StringLatin1.replace(value, trgStr.value, replStr.value)
        : StringUTF16.replace(value, trgStr.value, replStr.value);
}

Security Implications: ReDoS (Regular Expression Denial of Service)

Passing untrusted user input directly into replaceAll() introduces a severe ReDoS vulnerability:

  • If a malicious user supplies a regex pattern with catastrophic backtracking (e.g., (a+)+$), the worker thread will hang at 100% CPU utilization for minutes or hours, leading to server thread pool exhaustion.
  • The replace() method is completely immune to ReDoS attacks because it executes a deterministic linear string scan running in $O(N)$ time.

4 Tricky Questions

1. Why does str.replaceAll("PRICE", "$100") throw IllegalArgumentException and how do you fix it?

Answer: In replaceAll(), the second argument (replacement) is parsed for group capture references: the $ symbol indicates a captured group. When the regex engine encounters $100, it attempts to look up group number 100. Because the search pattern "PRICE" contains no capturing groups, the JVM throws java.lang.IllegalArgumentException: No group with name 100 (or IndexOutOfBoundsException). Solutions:

  1. Use str.replace("PRICE", "$100") if regex capabilities are not required (safe, literal substitution).
  2. If regex matching is required, wrap the replacement string in Matcher.quoteReplacement("$100").

2. Does str.replace("target", "replacement") allocate a new object on the heap if "target" is not present in the string?

Answer: In modern Java (Java 9, 11, 17, 21), no, it does not allocate any new object. The method first checks indexOf(trgStr). If no match is found (i < 0), it returns the original instance reference this. Evaluating str == str.replace("missing", "new") yields true, incurring zero heap allocation and zero GC overhead. Contrast: str.replaceAll("missing", "new") always compiles a Pattern and instantiates a Matcher, allocating garbage on the heap even when zero matches occur.

3. How do you replace ONLY THE FIRST occurrence of a literal substring without treating it as a regular expression?

Answer: The String class does not provide a replaceFirst(CharSequence, CharSequence) overload — replaceFirst(String regex, String replacement) always parses its first argument as a regex. Two recommended approaches:

  1. Quoting both arguments:
    String result = str.replaceFirst(
        Pattern.quote(target), 
        Matcher.quoteReplacement(replacement)
    );
    
  2. Manual indexOf slicing (Fastest, zero regex overhead):
    public static String replaceFirstLiteral(String source, String target, String replacement) {
        int index = source.indexOf(target);
        if (index == -1) return source;
        return source.substring(0, index) + replacement + source.substring(index + target.length());
    }
    

4. What happens when executing text.replaceAll("(?i)apple", "orange")?

Answer: It performs a case-insensitive replacement of every variant of the word apple ("Apple", "APPLE", "aPple") with "orange". The embedded flag expression (?i) enables Pattern.CASE_INSENSITIVE for the remainder of the regex. The replace() method cannot achieve case-insensitive replacements natively because it performs strict byte-for-byte literal matching.


🎯 Interview Cheat Sheet

30-Second Summary

“Both replace() and replaceAll() replace all occurrences in a string. The difference is that replace() treats inputs as literal strings, whereas replaceAll() compiles the first parameter into a regular expression (Pattern). In replaceAll(), the replacement string treats $ and \ as group references, requiring Matcher.quoteReplacement() for raw strings. Since Java 9, replace() has been rewritten without regex, returning this when no matches are found and running 5–10x faster than replaceAll().”

Key Takeaways for Senior Interviews

  • Both Replace All: Neither method stops at the first occurrence (use replaceFirst() for single substitutions).
  • Java 9+ Engine Redesign: replace(CharSequence, CharSequence) no longer compiles Pattern.LITERAL; it uses a direct byte scan with exact buffer pre-allocation.
  • Fast Return: replace() returns this if no match is found (zero heap allocation).
  • Replacement Traps: replaceAll() crashes on unquoted $ in replacements (IllegalArgumentException); use Matcher.quoteReplacement().
  • Security: replaceAll() with untrusted input is vulnerable to ReDoS; replace() runs in deterministic $O(N)$ time.
  • Fastest Option: replace(char, char) performs a single-pass primitive array copy and should always be preferred for single-character swaps.

Red Flags to Avoid

  • ❌ Saying that “replace() only replaces the first occurrence.” (The classic junior mistake).
  • ❌ Using replaceAll() to substitute simple static characters (e.g., replacing dots with slashes).
  • ❌ Passing raw, untrusted user inputs into replaceAll() without Pattern.quote().
  • ❌ Believing replaceAll() is faster than replace().