В работе программиста довольно часто некоторые задачи или их составляющие могут повторяться. Поэтому сегодня хотелось бы затронуть тему, которая часто встречается в повседневной работе любого Java-разработчика. Replace string в Java - 1Предположим, что вам из некоторого метода приходит некоторая строка. И всё в ней вроде бы хорошо, но есть какая-то мелочь, которая вас не устраивает. Например, не подходит разделитель, и вам нужен какой-то другой (или вовсе не нужен). Что можно сделать в такой ситуации? Естественно, воспользоваться методами replace класса String.

Java string replace

У объекта типа String есть четыре вариации метода замены — replace:
  • replace(char, char);
  • replace(CharSequence, CharSequence);
  • replaceFirst(String, String);
  • replaceAll(String, String).
Предназначение всех этих методов одно — замена части строки на другую строку. Давайте рассмотрим их подробнее. 1. replace(char, char) String replace(char oldChar, char newChar) — заменяет все вхождения символа первого аргумента oldChar вторым — newChar. В этом примере мы заменим запятую на точку с запятой:

String value = "In JavaRush, Diego the best, Diego is Java God".replace(',', ';');
System.out.println(value);
Вывод в консоль:
In JavaRush; Diego the best; Diego is Java God
2. replace(CharSequence, CharSequence) Заменяет каждую подстроку строки, которая соответствует указанной последовательности символов, на последовательности символов замены.

String value = "In JavaRush, Diego the best, Diego is Java God".replace("Java", "Rush");
System.out.println(value);
Вывод:
In RushRush, Diego the best, Diego is Rush God
3. replaceFirst(String, String) String replaceFirst(String regex, String replacement) — заменяет первую подстроку, которая соответствует указанному регулярному выражению, замещающей строкой. При использовании недопустимого регулярного выражения можно словить PatternSyntaxException (что не есть гуд). В этом примере давайте заменим имя робота-чемпиона:

String value = "In JavaRush, Diego the best, Diego is Java God".replaceFirst("Diego", "Amigo");
System.out.println(value);
Вывод в консоль:
In JavaRush, Amigo the best, Diego is Java God
Как мы видим, изменилось только первое вхождение "Diego", ну а дальнейшие остались за бортом — то есть, нетронутыми. 4. replaceAll() в Java String replaceAll(String regex, String replacement) — данный метод заменяет в строке все вхождения подстроки regex на replacement. В качестве первого аргумента regex возможно использование регулярного выражения. В качестве примера попробуем выполнить предыдущую замену с именами, но уже новым методом:

String value = "In JavaRush, Diego the best, Diego is Java God".replaceAll("Diego", "Amigo");
System.out.println(value);
Вывод в консоль:
In JavaRush, Amigo the best, Amigo is Java God
Как мы видим, произошла полная замена всех символов на необходимые. Думаю, Амиго будет доволен =)

Регулярные выражения

Выше было сказано, что есть возможность замены по регулярному выражению. Для начала проясним для себя, что же такое регулярное выражение? Регулярные выражения — это формальный язык для поиска и осуществления манипуляций с подстроками в тексте, основанный на использовании метасимволов (символов-джокеров). Проще говоря, это шаблон, состоящий из символов и метасимволов, задающий правило поиска. Например: \D — шаблон, описывающий любой нецифровой символ; \d — определяет любой цифровой символ, который также можно описать как [0-9]; [a-zA-Z] — шаблон, описывающий латинские символы от a до z, без учёта регистра; Рассмотрим применение в методе replaceAll класса String:

String value = "In JavaRush, Diego the best, Diego is Java God".replaceAll("\\s[a-zA-Z]{5}\\s", " Amigo ");
System.out.println(value);
Вывод в консоль:
In JavaRush, Amigo the best, Amigo is Java God
\\s[a-zA-Z]{5}\\s — описывает слово из 5 латинских символов, окруженное пробелами. Соответственно, этот шаблон и заменяется на переданную нами строку.

Java regex replace

В основном для использования регулярных выражений в Java применяют возможности пакета java.util.regex. Ключевыми классами являются:
  1. Pattern — класс, предоставляющий скомпилированный вариант регулярного выражения.
  2. Matcher — данный класс интерпретирует шаблон и определяет совпадения в пришедшей ему строке строке.
Как правило эти два класса работают в связке. Итак, как же будет выглядеть наш предыдущий объект, но уже с помощью Matcher и Pattern:

Pattern pattern = Pattern.compile("\\s[a-zA-Z]{5}\\s");
Matcher matcher = pattern.matcher("In JavaRush, Diego the best, Diego is Java God");
String value = matcher.replaceAll(" Amigo ");
System.out.println(value);
И вывод у нас будет всё тем же:
In JavaRush, Amigo the best, Amigo is Java God
Более детально с регулярными выражениями можно ознакомится в этой статье.

Альтернатива replaceAll

Спору нет, методы replace у String весьма впечатляющие, но нельзя не учитывать тот факт, что Stringimmutable объект, то есть он не может быть изменен после своего создания. Поэтому когда мы заменяем некоторые части строки с помощью методов replace, мы не меняем объект String, а каждый раз создаем новый, с необходимым содержимым. Но каждый раз создавать новый объект довольно долго, не правда ли? Особенно когда вопрос не в паре объектов, а в паре сотен, а то и тысяч. Волей-неволей начинаешь задумываться об альтернативах. И какие у нас есть альтернативы?Replace string в Java - 2Хм... Когда речь заходит о String и его свойстве immutable, сразу вспоминаешь об альтернативах, но не immutable, а именно о StringBuilder/StringBuffer. Как мы помним, эти классы фактически не различаются за исключением того, что StringBuffer оптимизирован под использование в многопоточной среде, поэтому в условиях однопоточного использования StringBuilder работает несколько быстрее. Исходя из этого, сегодня мы и будем использовать StringBuilder. У данного класса есть много интересных методов, но конкретно сейчас нас интересует replace. StringBuilder replace(int start, int end, String str) — данный метод заменяет символы в подстроке этой последовательности на символы в указанной строке. Подстрока начинается в указанном начале и продолжается до символа в конце индекса -1 или до конца последовательности, если такого символа не существует. Давайте рассмотрим пример:

StringBuilder strBuilder = new StringBuilder("Java Rush");
strBuilder.replace(5, 9, "God");
System.out.println(strBuilder);
Вывод:
Java God
Как вы видите, мы указываем промежуток, в который хотим записать строку, и записываем подстроку поверх то, что имеется в промежутке. Так вот, с помощью StringBuilder воссоздадим аналог метода replaceall java. Как это будет выглядеть:

public static String customReplaceAll(String str, String oldStr, String newStr) {

   if ("".equals(str) || "".equals(oldStr) || oldStr.equals(newStr)) {
       return str;
   }
   if (newStr == null) {
       newStr = "";
   }
   final int strLength = str.length();
   final int oldStrLength = oldStr.length();
   StringBuilder builder = new StringBuilder(str);

   for (int i = 0; i < strLength; i++) {
       int index = builder.indexOf(oldStr, i);

       if (index == -1) {
           if (i == 0) {
               return str;
           }
           return builder.toString();
       }
       builder = builder.replace(index, index + oldStrLength, newStr);

   }
       return builder.toString();
}
На первый взгляд страшно, но немного разобравшись можно понять, что всё не так уж и сложно и вполне себе логично.У нас есть три аргумента:
  • str — строка, в которой мы хотим заменить некоторые подстроки;
  • oldStr — представление подстрок, которые будем заменять;
  • newStr — то, на что мы будем заменять.
Первый if нам необходим, чтобы проверить входящие данные, и если строка str или oldStr пусты, или же новая подстрока newStr равна старой oldStr, то выполнение метода будет бессмысленным. Поэтому возвращаем первоначальную строку — str. Далее проверяем newStr на null, и если это так, то преобразуем в более удобный для нас формат пустой строки — "". После у нас идёт объявление необходимых нам переменных:
  • длины общей строки str;
  • длины подстроки oldStr;
  • объект StringBuilder из общей строки.
Запускаем цикл, который должен отработать количество раз, равное длине общей строки (но, скорее всего, этого никогда не случится). С помощью метода класса StringBuilderindexOf — узнаем индекс первого вхождения интересующей нас подстроки. С сожалением, хотелось бы отметить, что indexOf не работает с регулярными выражениями, поэтому наш итоговый метод будет работать только с вхождениями строк(( Если этот индекс у нас равен -1, то данных вхождений в текущем объекте StringBuilder больше нет, поэтому выходим из метода с интересующим результатом: он содержится в нашем StringBuilder, который мы преобразуем к String, c помощью toString. Если у нас индекс равен -1 в первой же итерации цикла, значит подстроки, которую нужно заменить, не было в общей строке изначально. Поэтому в такой ситуации просто вернем общую строку. Далее у нас и идёт использование вышеописанного метода replace для StringBuilder с использованием найденного индекса вхождения для обозначения координат заменяемой подстроки. Данный цикл отработает столько раз, сколько будет найдено подстрок, которые нужно заменить. Если строка состоит только из символа, который нужно заменить, то только в таком случае у нас цикл отработает полностью и мы получим результат StringBuilder преобразованный в строку. Нужно проверить корректность работы данного метода, не так ли? Напишем тест, который проверяет работу метода в различных ситуациях:

@Test
public void customReplaceAllTest() {
   String str = "qwertyuiop__qwertyuiop__";

   String firstCase = Solution.customReplaceAll(str, "q", "a");
   String firstResult = "awertyuiop__awertyuiop__";
   assertEquals(firstCase, firstResult);

   String secondCase = Solution.customReplaceAll(str, "q", "ab");
   String secondResult = "abwertyuiop__abwertyuiop__";
   assertEquals(secondCase, secondResult);

   String thirdCase = Solution.customReplaceAll(str, "rtyu", "*");
   String thirdResult = "qwe*iop__qwe*iop__";
   assertEquals(thirdCase, thirdResult);

   String fourthCase = Solution.customReplaceAll(str, "q", "");
   String fourthResult = "wertyuiop__wertyuiop__";
   assertEquals(fourthCase, fourthResult);

   String fifthCase = Solution.customReplaceAll(str, "uio", "");
   String fifthResult = "qwertyp__qwertyp__";
   assertEquals(fifthCase, fifthResult);

   String sixthCase = Solution.customReplaceAll(str, "", "***");
   assertEquals(sixthCase, str);

   String seventhCase = Solution.customReplaceAll("", "q", "***");
   assertEquals(seventhCase, "");
}
Можно разбить на 7 отдельных тестов, каждый из которых будет отвечать за свой тестовый случай. Запустив его мы увидим, что он зелёный, то есть успешный. Ну вот, вроде и всё. Хотя постойте, выше мы говорили, что данный метод будет значительно быстрее, чем replaceAll у String. Что ж, давайте посмотрим:

String str = "qwertyuiop__qwertyuiop__";
long firstStartTime = System.nanoTime();

for (long i = 0; i < 10000000L; i++) {
   str.replaceAll("tyu", "#");
}

double firstPerformance = System.nanoTime() - firstStartTime;

long secondStartTime = System.nanoTime();

for (long i = 0; i < 10000000L; i++) {
   customReplaceAll(str, "tyu", "#");
}

double secondPerformance = System.nanoTime() - secondStartTime;

System.out.println("Performance ratio  - " +  firstPerformance / secondPerformance);
Далее, этот код был запущен трижды, и мы получили следующие результаты: Вывод в консоль:
Performance ratio  - 5.012148941181627
 
Performance ratio  - 5.320637176017641
 
Performance ratio  - 4.719192686500394
Как мы видим, в среднем наш метод производительнее, чем классический replaceAll класса String в 5 раз! Что ж и напоследок давайте запустим эту же проверку, но, так сказать, вхолостую. Другими словами, в том случае когда совпадения найдено не будет. Заменим строку для поиска с "tyu" на "--". При трёх запусках были получены следующие результаты: Вывод в консоль:
Performance ratio  - 8.789647093542246
 
Performance ratio  - 9.177105482660881
 
Performance ratio  - 8.520964375227406
В среднем производительность для случаев, когда совпадений не было найдено, выросла в 8.8 раз! Replace string в Java - 4