В чём особенность метода replace() vs replaceAll()
Оба метода — и replace(), и replaceAll() — заменяют все найденные вхождения в строке на новое значение.
🟢 Junior Level
Оба метода — и replace(), и replaceAll() — заменяют все найденные вхождения в строке на новое значение.
Главное отличие между ними заключается в том, как именно интерпретируется искомый шаблон:
replace()выполняет литеральную (текстовую) замену. Он ищет точное совпадение символов или подстроки. Спецсимволы (точка, звездочка, скобки) воспринимаются как обычный текст.replaceAll()выполняет замену на основе регулярного выражения (Regex). Искомый аргумент компилируется как шаблон регулярного выражения, а в строке замены символы$и\имеют специальное значение (ссылки на группы).
Суть в 30 секундах
Самое коварное заблуждение на собеседованиях — думать, что replace() меняет только первое вхождение, а replaceAll() — все. Оба метода меняют ВСЕ вхождения! Если вам требуется заменить только первое вхождение, для этого существует отдельный метод replaceFirst().
String text = "a.b.c.d";
// 1. Литеральная замена: точка — это просто символ '.'
System.out.println(text.replace(".", "/"));
// Выведет: "a/b/c/d"
// 2. Замена по регулярному выражению: точка — это "любой символ" в Regex!
System.out.println(text.replaceAll(".", "/"));
// Выведет: "///////" (каждый символ заменен на '/')
// 3. Замена только первого вхождения:
System.out.println(text.replaceFirst("\\.", "/"));
// Выведет: "a/b.c.d"
🟡 Middle Level
Сигнатуры и различия методов
В классе java.lang.String представлены 4 метода для замены:
| Метод | Что принимает на поиск | Что принимает на замену | Что заменяет | Производительность |
|---|---|---|---|---|
replace(char, char) |
Символ char |
Символ char |
Все вхождения | ⚡ Самый быстрый (прямой скан массива байт) |
replace(CharSequence, CharSequence) |
Литеральная строка | Литеральная строка | Все вхождения | 🚀 Очень быстрый (без regex-движка в Java 9+) |
replaceAll(String, String) |
Регулярное выражение | Строка подстановки (с группами $) |
Все вхождения | 🐢 Медленный (компиляция Pattern/Matcher) |
replaceFirst(String, String) |
Регулярное выражение | Строка подстановки (с группами $) |
Первое вхождение | 🐢 Медленный (компиляция Pattern/Matcher) |
Ловушки метода replaceAll()
- Неэкранированные спецсимволы в regex:
Вызов
str.replaceAll("100$", "200$")не найдет строку"100$", потому что в регулярных выражениях знак$означает конец строки. Для литерального поиска требуется экранирование:Pattern.quote("100$"). - Падение при символе
$в строке подстановки (replacement):String price = "Cost: PRICE"; // ОШИБКА: выбросит IllegalArgumentException: No group with name 100! String result = price.replaceAll("PRICE", "$100");В
replaceAllсимвол$используется для подстановки захваченных групп ($1,$2). Если за знаком$идет число, движок ищет соответствующую regex-группу. Решение: Для безопасной вставки произвольного текста вreplaceAllстроку замены оборачивают вMatcher.quoteReplacement("$100").
🔴 Senior Level
Эволюция реализации: как устроен replace() в Java 8 против Java 9+
До Java 9 метод replace(CharSequence, CharSequence) имел скрытый overhead:
- Внутри он вызывал:
Pattern.compile(target.toString(), Pattern.LITERAL).matcher(this).replaceAll(Matcher.quoteReplacement(replacement.toString()))Хотя поиск был литеральным (Pattern.LITERAL), JVM создавала объектыPatternиMatcher, тратя память и время на прогон через абстрактный конечный автомат.
Начиная с Java 9 (включая Java 17 и 21):
Метод replace(CharSequence, CharSequence) был полностью переписан без использования java.util.regex:
- Проверяет первое вхождение через быстрый поиск
indexOf(target). - Оптимизация отсутствия совпадений: Если подстрока не найдена, метод немедленно возвращает
thisбез единой аллокации в куче! - Точный расчет размера: Если совпадения есть, метод заранее вычисляет точную длину итогового массива байтов: \(\text{newLength} = \text{oldLength} + \text{matchCount} \times (\text{replacementLength} - \text{targetLength})\)
- Выделяет ровно один массив
byte[]нужного размера с учетомcoder(Latin-1 / UTF-16) и напрямую копирует нетронутые фрагменты и строки подстановки. В результатеreplace()в современных версиях Java работает в 5–10 раз быстрее, чемreplaceAll().
// OpenJDK String.java (упрощенно)
public String replace(CharSequence target, CharSequence replacement) {
String trgStr = target.toString();
String replStr = replacement.toString();
int i = indexOf(trgStr);
if (i < 0) {
return this; // Быстрый возврат без аллокаций!
}
// Прямой расчет и сборка массива байт без Pattern/Matcher
return isLatin1()
? StringLatin1.replace(value, trgStr.value, replStr.value)
: StringUTF16.replace(value, trgStr.value, replStr.value);
}
Безопасность: уязвимость ReDoS (Regular Expression Denial of Service)
Использование replaceAll() с динамическим пользовательским вводом в качестве шаблона поиска несет критическую уязвимость ReDoS:
- Если злоумышленник передаст специально сформированное регулярное выражение с катастрофическим откатом (Catastrophic Backtracking), например
(a+)+$, поток сервера зависнет со 100% утилизацией ядра CPU на несколько минут или часов. - Метод
replace()принципиально защищен от ReDoS, так как выполняет детерминированный линейный поиск за время $O(N)$.
4 Tricky Questions
1. Почему вызов str.replaceAll("PRICE", "$100") выбрасывает IllegalArgumentException и как это исправить?
Ответ:
В методе replaceAll второй аргумент (replacement) не является простым текстом. Символ $ зарезервирован для механизма обратных ссылок на группы захвата (Capturing Groups).
Когда движок регулярных выражений встречает $100, он пытается найти в regex захваченную круглыми скобками группу с номером 100. Поскольку в регулярном выражении "PRICE" скобок и групп нет, JVM выбрасывает java.lang.IllegalArgumentException: No group with name 100 или IndexOutOfBoundsException.
Способы решения:
- Если не требуется regex — использовать
str.replace("PRICE", "$100")(безопасная литеральная замена). - Если regex необходим — экранировать замену через
str.replaceAll("PRICE", Matcher.quoteReplacement("$100")).
2. Создает ли str.replace("target", "replacement") новый объект в куче, если подстрока "target" не была найдена в исходной строке?
Ответ:
В современных версиях Java (Java 9, 11, 17, 21) — нет, не создает.
Метод replace() первым делом запускает поиск через indexOf(target). Если индекс первого совпадения < 0, метод возвращает ссылку this (исходный объект). Проверка str == str.replace("absent", "new") вернет true, а потребление памяти кучи равно 0 байт.
Примечание: Метод replaceAll(), напротив, создает объекты Pattern и Matcher даже в том случае, если ни одного совпадения не будет найдено.
3. Как заменить ТОЛЬКО ПЕРВОЕ вхождение обычной текстовой строки без интерпретации ее как регулярного выражения?
Ответ:
В классе String нет перегрузки replaceFirst(CharSequence, CharSequence) — метод replaceFirst(String regex, String replacement) принимает только регулярное выражение.
Есть два надежных пути:
- Экранирование обоих аргументов:
String result = str.replaceFirst( Pattern.quote(target), Matcher.quoteReplacement(replacement) ); - Ручная замена через
indexOf(наиболее производительный вариант без аллокаций regex):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. Что произойдет при вызове text.replaceAll("(?i)apple", "orange")?
Ответ:
Произойдет регистронезависимая замена слова apple во всех вариантах написания (Apple, APPLE, aPple и т.д.) на слово orange.
Префикс (?i) в синтаксисе Java Regex включает флаг Pattern.CASE_INSENSITIVE (регистронезависимый режим) для всего последующего шаблона. Метод replace() реализовать подобное одной строкой не способен, так как работает строго с литералами чувствительно к регистру.
🎯 Шпаргалка для интервью
30-секундный ответ
«И
replace(), иreplaceAll()заменяют все найденные совпадения в строке. Разница в типе поиска:replace()выполняет быстрый литеральный поиск точного текста без использования регулярных выражений, аreplaceAll()компилирует первый аргумент в регулярное выражение (Pattern). Кроме того, во втором аргументеreplaceAll()символы$и\служат для обратных ссылок на regex-группы и требуют экранирования черезMatcher.quoteReplacement(). Для замены одного вхождения используетсяreplaceFirst()».
Ключевые тезисы для Senior-интервью
- Оба меняют ВСЁ:
replaceменяет не первое вхождение, а абсолютно все вхождения. - Java 9+ переписан: В Java 9+
replace(CharSequence, CharSequence)работает безPattern.LITERAL, используя прямой скан массиваbyte[], и возвращаетthisпри 0 совпадений. - Спецсимволы в
replacement: ВreplaceAllсимвол$означает ссылку на группу; передача сырых строк (например, цен$50) безMatcher.quoteReplacement()приводит к падению в рантайме. - Защита от ReDoS:
replace()детерминирован и неуязвим к атакам на отказ в обслуживании через регулярные выражения. - Fast Char Replacement: Метод
replace(char, char)— самый быстрый способ замены одиночных символов (однопроходной цикл по массиву байт).
Красные флаги (Чего говорить категорически нельзя)
- ❌ Утверждать: «
replace()меняет только первое вхождение, аreplaceAll()— все» (главный индикатор некомпетентности). - ❌ Использовать
replaceAll()для замены простых фиксированных строк (например, точек или слэшей), забывая об оверхеде компиляции regex и экранировании. - ❌ Передавать непроверенный пользовательский ввод напрямую в
replaceAll()(риск ReDoS-атаки). - ❌ Полагать, что
replace()медленнееreplaceAll().