Replace string в Java
В работе программиста довольно часто некоторые задачи или их составляющие могут повторяться. Поэтому сегодня хотелось бы затронуть тему, которая часто встречается в повседневной работе любого Java-разработчика. Предположим, что вам из некоторого метода приходит некоторая строка. И всё в ней вроде бы хорошо, но есть какая-то мелочь, которая вас не устраивает. Например, не подходит разделитель, и вам нужен какой-то другой (или вовсе не нужен). Что можно сделать в такой ситуации? Естественно, воспользоваться методами replace класса String .
Java string replace
- replace(char, char);
- replace(CharSequence, CharSequence);
- replaceFirst(String, String);
- replaceAll(String, String).
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 — определяет любой цифровой символ, который также можно описать как 5 ; [a-zA-Z] — шаблон, описывающий латинские символы от a до z, без учёта регистра; Рассмотрим применение в методе replaceAll класса String :
String value = "In JavaRush, Diego the best, Diego is Java God".replaceAll("\\s[a-zA-Z]\\s", " Amigo "); System.out.println(value);
In JavaRush, Amigo the best, Amigo is Java God
\\s[a-zA-Z]<5>\\s — описывает слово из 5 латинских символов, окруженное пробелами. Соответственно, этот шаблон и заменяется на переданную нами строку.5>
Java regex replace
- Pattern — класс, предоставляющий скомпилированный вариант регулярного выражения.
- Matcher — данный класс интерпретирует шаблон и определяет совпадения в пришедшей ему строке строке.
Pattern pattern = Pattern.compile("\\s[a-zA-Z]\\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 весьма впечатляющие, но нельзя не учитывать тот факт, что String — immutable объект, то есть он не может быть изменен после своего создания. Поэтому когда мы заменяем некоторые части строки с помощью методов replace , мы не меняем объект String , а каждый раз создаем новый, с необходимым содержимым. Но каждый раз создавать новый объект довольно долго, не правда ли? Особенно когда вопрос не в паре объектов, а в паре сотен, а то и тысяч. Волей-неволей начинаешь задумываться об альтернативах. И какие у нас есть альтернативы?Хм. Когда речь заходит о 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);
Как вы видите, мы указываем промежуток, в который хотим записать строку, и записываем подстроку поверх то, что имеется в промежутке. Так вот, с помощью 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 из общей строки.
Запускаем цикл, который должен отработать количество раз, равное длине общей строки (но, скорее всего, этого никогда не случится). С помощью метода класса StringBuilder — indexOf — узнаем индекс первого вхождения интересующей нас подстроки. С сожалением, хотелось бы отметить, что indexOf не работает с регулярными выражениями, поэтому наш итоговый метод будет работать только с вхождениями строк(( Если этот индекс у нас равен -1 , то данных вхождений в текущем объекте StringBuilder больше нет, поэтому выходим из метода с интересующим результатом: он содержится в нашем StringBuilder , который мы преобразуем к String , c помощью toString . Если у нас индекс равен -1 в первой же итерации цикла, значит подстроки, которую нужно заменить, не было в общей строке изначально. Поэтому в такой ситуации просто вернем общую строку. Далее у нас и идёт использование вышеописанного метода replace для StringBuilder с использованием найденного индекса вхождения для обозначения координат заменяемой подстроки. Данный цикл отработает столько раз, сколько будет найдено подстрок, которые нужно заменить. Если строка состоит только из символа, который нужно заменить, то только в таком случае у нас цикл отработает полностью и мы получим результат StringBuilder преобразованный в строку. Нужно проверить корректность работы данного метода, не так ли? Напишем тест, который проверяет работу метода в различных ситуациях:
@Test public void customReplaceAllTest()
Можно разбить на 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 раз!