Питання 16 · Розділ 12

В чому особливість методу replace() vs replaceAll()?

Обидва методи замінюють всі знайдені входження. Різниця в тому, як вони шукають:

Мовні версії: English Russian Ukrainian

🟢 Junior Level

Обидва методи замінюють всі знайдені входження. Різниця в тому, як вони шукають:

replace() — шукає звичайний текст (літерал):

String s = "apple.banana.orange";
System.out.println(s.replace(".", "/"));
// "apple/banana/orange" — крапка шукається буквально

replaceAll() — шукає за регулярним виразом:

String s = "apple.banana.orange";
System.out.println(s.replaceAll(".", "/"));
// "/////////////////" — крапка в regex = "будь-який символ"!

Головна пастка: Назва replace оманлива — він замінює ВСІ входження, а не перше. Для заміни тільки першого є replaceFirst().


🟡 Middle Level

Порівняння методів

Метод Що шукає Замінює Швидкість
replace(char, char) Символ Всі Найшвидший
replace(CharSequence, CharSequence) Літерал (текст) Всі Швидкий
replaceAll(String regex, String replacement) Регулярний вираз Всі Повільний
replaceFirst(String regex, String replacement) Регулярний вираз Перше Повільний

Практичне застосування

// replace — для звичайного тексту
s.replace("old", "new");        // Літеральна заміна
s.replace('a', 'b');            // Заміна символів

// replaceAll — для патернів
s.replaceAll("\\d+", "#");      // Всі цифри → #
s.replaceAll("\\s+", " ");      // Множинні пробіли → один

// replaceFirst — тільки перший збіг
s.replaceFirst("\\d", "#");     // Тільки першу цифру

Типові помилки

  1. Помилка: replaceAll("user.name", "admin") — крапка = будь-який символ Рішення: replaceAll(Pattern.quote("user.name"), "admin") або replace("user.name", "admin")

  2. Помилка: Думати, що replace міняє тільки перше Рішення: replace міняє ВСІ. Для першого — replaceFirst

  3. Помилка: $ в replacement рядку для replaceAll Рішення: $1, $2 — посилання на групи. Для літерального $ використовуйте Matcher.quoteReplacement("$")


🔴 Senior Level

Internal Implementation

replace(CharSequence, CharSequence) — через Pattern.LITERAL replace(char, char) — окремий метод, прямий scan byte[], БЕЗ Pattern

public String replace(CharSequence target, CharSequence replacement) {
    // Перевірка Fast Path для single char
    if (target instanceof String && replacement instanceof String) {
        String tgt = (String) target;
        String repl = (String) replacement;
        if (tgt.length() == 1 && repl.length() == 1) {
            return replace(tgt.charAt(0), repl.charAt(0));
        }
    }
    // Через Pattern/Literal replacement — без regex engine
    return Pattern.compile(target.toString(), Pattern.LITERAL)
                  .matcher(this)
                  .replaceAll(Matcher.quoteReplacement(replacement.toString()));
}

replaceAll(String regex, String replacement):

public String replaceAll(String regex, String replacement) {
    return Pattern.compile(regex).matcher(this).replaceAll(replacement);
}

Ключова різниця: replace використовує Pattern.LITERAL (literal matching), replaceAll — повноцінний regex engine.

Архітектурні Trade-offs

replace (literal):

  • Плюси: Немає regex overhead, безпечний зі спецсимволами, передбачуваний
  • Мінуси: Немає патернів, груп, backreferences

replaceAll (regex):

  • Плюси: Повна потужність regex (групи, lookahead, backreferences)
  • Мінуси: Компіляція regex, алокації, спецсимволи потрібно екранувати

Performance Comparison

Метод Механізм Алокації 1M викликів
replace('a', 'b') Прямий прохід по byte[] 1 String ~10ms
replace("ab", "cd") indexOf + копіювання 1 String + Pattern ~50ms
replaceAll("a.b", "x") Regex compile + match Pattern + Matcher + String ~300ms
replaceAll(precompiled, "x") Тільки match Matcher + String ~80ms

// Цифри приблизні, залежать від JVM і довжини рядків.

Edge Cases

  1. Regex metacharacters в replaceAll:
    s.replaceAll("path\\to\\file", "/new"); // \t = tab, \f = form feed!
    s.replaceAll(Pattern.quote("path\\to\\file"), "/new"); // Літерально
    
  2. Replacement з $:
    "price: $100".replaceAll("\\$100", "$200"); // $2 — backreference, помилка!
    "price: $100".replaceAll("\\$100", Matcher.quoteReplacement("$200")); // OK
    
  3. Empty replacement:
    "abcabc".replace("b", ""); // "acac"
    "abcabc".replaceAll("b", ""); // "acac"
    
  4. No match:
    "hello".replace("x", "y"); // "hello" — без змін, новий об'єкт!
    

Production Experience

Сценарій: User input sanitization:

// ПОГАНО — користувач ввів "user[admin]" → regex syntax error
String sanitized = input.replaceAll(userInput, "[REDACTED]");

// ГАРАЗД — literal replacement
String sanitized = input.replace(userInput, "[REDACTED]");

// АБО — екранування
String sanitized = input.replaceAll(Pattern.quote(userInput), "[REDACTED]");

Сценарій 2: Log masking (100K log lines/sec):

  • replaceAll("\\b\\d{4}\\b", "XXXX") для маскування PIN → ~300ms на 100K
  • Pre-compiled pattern → ~80ms
  • Ручний парсинг → ~15ms (але складніше підтримувати)

Best Practices для Highload

  • Дефолт: replace() для літерального тексту — безпечніше і швидше
  • Для regex: pre-compiled Pattern + matcher.replaceAll()
  • Для спецсимволів в replacement: Matcher.quoteReplacement()
  • Для ultra-low-latency: ручна реалізація через indexOf + StringBuilder
  • Security: завжди Pattern.quote() для user input в regex

🎯 Шпаргалка для інтерв’ю

Обов’язково знати:

  • replace() — літеральна заміна (не regex), замінює ВСІ входження
  • replaceAll() — заміна за регулярним виразом, замінює ВСІ входження
  • replaceFirst() — заміна за regex, тільки перший збіг
  • Назва replace оманлива — він замінює все, а не одне входження
  • Спецсимволи в replacement для replaceAll: $1, $2 — backreference на групи
  • Для літерального $ в replacement: Matcher.quoteReplacement("$")

Часті уточнюючі запитання:

  • Чому replaceAll(".", "/") замінює все? — Крапка в regex = “будь-який символ”. Використовуйте replace(".", "/") для літеральної заміни.
  • Що швидше: replace() чи replaceAll()?replace() швидше — немає regex overhead. replace('a','b') — прямий прохід по масиву.
  • Чи міняє replace() тільки перше входження? — Ні, попри назву, replace() міняє ВСІ входження.
  • Як замінити тільки перше входження тексту?replaceFirst() — але це regex. Для літерального — indexOf + substring.

Червоні прапорці (НЕ говорити):

  • ❌ “replace() міняє тільки перше входження” — міняє ВСІ, попри назву
  • ❌ “replaceAll() швидший за replace()” — навпаки, regex повільніший
  • ❌ “Можна передавати user input напряму в replaceAll()” — regex injection, використовуйте Pattern.quote()
  • ❌ “$ в replacement — це просто символ” — це backreference на групи regex

Пов’язані теми:

  • [[15. Як працює метод split()]]
  • [[7. Що відбувається при конкатенації рядків через оператор +]]
  • [[4. Чому String є незмінним]]