В чому особливість методу 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) мав прихований оверхед:
- Усередині він викликав:
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().