Вопрос 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 является иммутабельным]]