Почему не рекомендуются множественные конкатенации string
Перейти к содержимому

Почему не рекомендуются множественные конкатенации string

Эффективная конкатенация строк в .NET

Для программистов на платформе .NET одним из первых советов, направленных на повышение производительности их программ, является «Используй StringBuilder для конкатенации строк». Как и «Использование исключений затратно», утверждение о конкатенации часто неправильно понимается и превращается в догму. К счастью, оно не столь деструктивно, как миф о производительности исключений, но встречается заметно чаще.

Было бы неплохо, если бы вы перед прочтением данной статьи прочли мою предыдущую статью о строках в .NET. И, во имя удобочитаемости, дальше я буду обозначать строки в .NET просто строками, а не «string» или «System.String».

Я включил эту статью в список статей, посвящённых .NET Framework в общем, а не в список C#-специфичных статей, так как полагаю, что все языки на платформе .NET под капотом содержат один и тот же механизм конкатенации строк.

Проблема, которую пытаются решить

Проблема конкатенации большого массива строк, при которой результирующая строка очень быстро и сильно растёт, очень реальна, и совет использовать StringBuilder для конкатенации очень правильный. Вот пример:

using System; public class Test < static void Main() < DateTime start = DateTime.Now; string x = ""; for (int i=0; i < 100000; i++) < x += "!"; >DateTime end = DateTime.Now; Console.WriteLine ("Time taken: ", end-start); > > 

На моём относительно быстром ноутбуке выполнение данной программы заняло около 10 секунд. Если удвоить количество итераций, то время выполнения возрастёт до минуты. На .NET 2.0 beta 2 результаты несколько лучше, но не так уж и сильно. Проблема низкой производительности в том, что строки неизменяемы (immutable), и поэтому при применении оператора « += » строка на следующей итерации не добавляется в конец первой. На самом деле выражение x += «!»; абсолютно эквивалентно выражению x = x+»!»; . Здесь конкатенация — это создание полностью новой строки, для которой выделяется нужный объём памяти, в который копируется содержимое существующего значения x , а потом копируется содержимое конкатенируемой строки ( «!» ). По мере того, как результирующая строка растёт, возрастает и количество данных, которые всё время копируются туда-сюда, и именно поэтому когда я увеличил количество итераций вдвое, время выросло больше, чем в два раза.

Данный алгоритм конкатенации определённо неэффективен. Ведь если кто-то попросит вас добавить что-то в список покупок, вы же не будете перед добавлением копировать весь список, правда? Вот так мы и подходим к StringBuilder.

Используем StringBuilder

А вот эквивалент (эквивалент в смысле идентичного конечного значения x ) вышеприведённой программы, который намного-намного быстрее:

using System; using System.Text; public class Test < static void Main() < DateTime start = DateTime.Now; StringBuilder builder = new StringBuilder(); for (int i=0; i < 100000; i++) < builder.Append("!"); >string x = builder.ToString(); DateTime end = DateTime.Now; Console.WriteLine ("Time taken: ", end-start); > > 

На моём ноутбуке данный код выполняется настолько быстро, что тот механизм замера времени, который я использую, неэффективен и не даёт удовлетворительных результатов. При увеличении количества итераций до одного миллиона (т.е. в 10 раз больше от изначального количества, при котором первая версия программы выполнялась 10 секунд) время выполнения вырастает до 30-40 миллисекунд. Причём время выполнения растёт приблизительно линейно количеству итераций (т.е. удвоив количество итераций, время выполнения также удвоится). Такой скачок производительности достигается благодаря устранению ненужной операции копирования — копируются только те данные, которые присоединяются к результирующей строке. StringBuilder содержит и обслуживает свой внутренний буфер и при добавлении строки копирует её содержимое в буфер. Когда новые присоединяемые строки не вмещаются в буфер, он копируется со всем своим содержимым, но уже с большим размером. По сути, внутренний буфер StringBuilder — это та же самая обычная строка; строки неизменяемы лишь с точки зрения своих публичных интерфейсов, но изменяемы со стороны сборки mscorlib . Можно было бы сделать данный код ещё более производительным, указав конечный размер (длину) строки (ведь в данном случае мы можем вычислить размер строки ещё до начала конкатенации) в конструкторе StringBuilder, благодаря чему внутренний буфер StringBuilder’а был бы создан с точно подходящим для результирующей строки размером, и в процессе конкатенации ему бы не прошлось увеличиваться через копирование. В данной ситуации вы можете определить длину результирующей строки до конкатенации, но даже если и не можете, то не беда — при заполнении буфера и его копировании StringBuilder удваивает размер новой копии, поэтому заполнений и копирований буфера не будет так уж и много.

Значит, при конкатенации я должен всегда использовать StringBuilder?

Кратко говоря — нет. Всё вышеприведённое разъясняет, почему утверждение «Используй StringBuilder для конкатенации строк» в некоторых ситуациях бывает правильным. Вместе с тем, некоторые люди принимают данное утверждение за догму, не разобравшись в основах, и вследствие этого начинают переделывать такой код:

string name = firstName + " " + lastName; Person person = new Person (name); 

вот в такое:

// Bad code! Do not use! StringBuilder builder = new StringBuilder(); builder.Append (firstName); builder.Append (" "); builder.Append (lastName); string name = builder.ToString(); Person person = new Person (name); 

И всё это во имя производительности. Если взглянуть на проблему в общем, то даже если вторая версия была бы более быстрой, нежели первая, то, очевидно, она не была бы намного быстрее, ведь конкатенаций всего несколько. Смысл в использовании второй версии может быть только в случае, если данный кусок кода вызывается очень, очень большое количество раз. Ухудшение удобочитаемости кода (а я думаю, вы все согласитесь, что вторая версия намного менее удобочитаемая, нежели первая) ради микроскопической прибавки производительности — это очень плохая идея.

Более того, на самом деле вторая версия, со StringBuilder’ом, менее производительна, нежели первая, хотя и не намного. И если бы вторая версия была более легко воспринимаемой, нежели первая, то вслед за аргументацией из предыдущего абзаца я бы сказал — используйте её; но когда версия со StringBuilder’ом и менее удобочитаемая, и менее производительная, то использовать её — это просто бред.

Если предположить, что firstName и lastName являются «настоящими» переменными, а не константами (об этом будет ниже), то первая версия будет скомпилирована в вызов String.Concat, как-то так:

string name = String.Concat (firstName, " ", lastName); Person person = new Person (name); 

Метод String.Concat принимает на вход набор строк (или объектов) и «склеивает» их в одну новую строку, просто и чётко. String.Concat имеет разные перегрузки — некоторые принимают несколько строк, некоторые — несколько переменных типа Object (которые при конкатенации конвертируются в строки), а некоторые принимают массивы строк или массивы Object. Все перегрузки делают одно и то же. Перед собственно началом процесса конкатенации String.Concat считывает длины всех переданных ему строк (по крайней мере, если вы передали ему строки — если вы передали переменные типа Object , то String.Concat для каждой такой переменной создаст новую временную (промежуточную) строку и будет конкатенировать уже её). Благодаря этому на момент конкатенации String.Concat точно «знает» длину результирующей строки, благодаря чему выделяет для неё точно подходящий по размерам буфер, а поэтому нет никаких лишних операций копирования и т.д.

Сравните этот алгоритм со второй StringBuilder-версией. На момент своего создания StringBuilder не знает размер результирующей строки (и мы ему этот размер не «сказали»; а если бы и сказали, то сделали бы код ещё менее понятным), а это значит, что, скорее всего, размер стартового буфера будет превышен, и StringBuilder’у придётся его увеличивать посредством создания нового и копированием содержимого. Более того, как мы помним, StringBuilder увеличивает буфер в два раза, а это значит, что, в конечном счёте, буфер окажется намного большим, нежели того требует результирующая строка. Кроме этого, не следует забывать о накладных расходах, связанных с созданием дополнительного объекта, которого нет в первой версии (этим объектом и есть StringBuilder). Так чем же вторая версия лучше?

Важным различием между примером из этого раздела и примером из начала статьи является то, что в этом мы сразу имеем в наличии все строки, которые надо конкатенировать, и поэтому можем все их передать в String.Concat, который, в свою очередь, выдаст результат максимально эффективно, без всяких промежуточных строк. В раннем же примере мы не имеем доступ ко всем строкам сразу, и поэтому нуждаемся во временном хранилище промежуточных результатов, на роль которого наилучше подходит именно StringBuilder. Т.е., обобщая, StringBuilder эффективен как контейнер с промежуточным результатом, так как позволяет избавиться от внутреннего копирования строк; если же все строки доступны сразу и промежуточных результатов нет, то от StringBuilder’а не будет никакой пользы.

Константы

Ситуация накаляется ещё сильнее, когда дело доходит до констант (я говорю о строковых литералах, объявленных как const string ). Как вы думаете, во что будет скомпилировано выражение string x = «hello» + » » + «there»; ? Логично предположить, что будет произведён вызов String.Concat, но это не так. На самом деле данное выражение будет скомпилировано вот в такое: string x = «hello there»; . Компилятор знает, что все составные части строки x являются константами времени компиляции, и поэтому все они будут конкатенированы ещё в момент компиляции программы, и в скомпилированном коде будет храниться строка x со значением «hello there» . Перевод подобного кода под StringBuilder неэффективен и в аспекте потребляемой памяти, и в аспекте ресурсов CPU, не говоря уже об удобочитаемости.

Эмпирические правила конкатенации
  • Определённо используйте StringBuilder, когда вы конкатенируете строки в нетривиальном цикле, и особенно, когда вы не знаете (на момент компиляции), сколько именно итераций будет произведено. К примеру, чтение содержимого текстового файла путём считывания по одному символу внутри одной итерации в цикле, и конкатенация этого символа через оператор += предположительно «убьёт» ваше приложение в плане производительности.
  • Определённо используйте оператор += , если вы можете указать все необходимые для конкатенации строки в одном утверждении. Если вам нужно конкатенировать массив строк, используйте явный вызов String.Concat, а если между этими строками нужен разделитель — используйте String.Join.
  • Не бойтесь в коде разбивать литералы на несколько частей и связывать их через + — результат будет тот же самый. Если у вас в коде содержится длинная литеральная строка, то, разбив её на несколько подстрок, вы тем самым улучшите удобочитаемость кода.
  • Если промежуточные результаты конкатенации нужны вам где-нибудь ещё, кроме собственно быть промежуточными результатами (т.е. служить временным хранилищем строк, изменяющимся на каждой итерации), то StringBuilder вам не поможет. К примеру, если вы создаёте полное имя путём конкатенации имени и фамилии, а потом добавляете третий элемент (к примеру, логин) в конец строки, то StringBuilder будет полезен, только если вам не нужно использовать строку (имя + фамилия) саму по себе, без логина, где-нибудь ещё (как мы это делали в примере, создавая экземпляр Person на основании имени и фамилии).
  • Если вам нужно конкатенировать несколько подстрок, и вы не можете их конкатенировать в одном утверждении через String.Concat, то выбор «классической»- или StringBuilder-конкатенации не будет играть особой роли. Здесь скорость будет зависеть от количества участвующих в конкатенации строк, от их длины, а также от порядка, в котором строки будут конкатенироваться. Если вы полагаете, что конкатенация является «бутылочным горлышком» производительности и непременно хотите использовать быстрейший способ, то замерьте производительность обеих способов и только тогда выберите быстрейший из них.

Объединение строк в Java

Java предоставляет значительное количество методов и классов, предназначенных для объединения строк.

В этом уроке мы рассмотрим некоторые из них, а также обрисуем в общих чертах некоторые распространенные ловушки и плохие практики.

2. Построитель строк ​

Прежде всего, это скромный StringBuilder. Этот класс предоставляет набор утилит для построения строк , упрощающих работу со строками .

Давайте создадим быстрый пример конкатенации строк, используя класс StringBuilder :

 StringBuilder stringBuilder = new StringBuilder(100);   stringBuilder.append("ForEach");  stringBuilder.append(" is");  stringBuilder.append(" awesome");    assertEquals("ForEach is awesome", stringBuilder.toString()); 

Внутри StringBuilder поддерживает изменяемый массив символов. В нашем примере кода мы объявили начальный размер 100 с помощью конструктора StringBuilder . Из-за этого объявления размера StringBuilder может быть очень эффективным способом объединения строк .

Также стоит отметить, что класс StringBuffer является синхронизированной версией StringBuilder .

Хотя синхронизация часто является синонимом безопасности потоков, ее не рекомендуется использовать в многопоточных приложениях из -за шаблона построителя StringBuffer . В то время как отдельные вызовы синхронизированного метода являются потокобезопасными, множественные вызовы — нет .

3. Оператор сложения​

Далее следует оператор сложения (+). Это тот же самый оператор, который приводит к сложению чисел и перегружается для конкатенации при применении к строкам.

Давайте кратко рассмотрим, как это работает:

 String myString = "The " + "quick " + "brown " + "fox. ";    assertEquals("The quick brown fox. ", myString); 

На первый взгляд это может показаться гораздо более лаконичным, чем вариант StringBuilder . Однако при компиляции исходного кода символ + транслируется в цепочки вызовов StringBuilder.append() . Из-за этого смешивание « метода конкатенации StringBuilder и + считается плохой практикой .

Кроме того, следует избегать конкатенации строк с помощью оператора + внутри цикла . Поскольку объект String неизменяем, каждый вызов конкатенации приведет к созданию нового объекта String .

4. Строковые методы​

Сам класс String предоставляет целый набор методов для объединения строк.

4.1. String.concat ​

Неудивительно, что метод String.concat является нашим первым портом захода при попытке конкатенации объектов String . Этот метод возвращает объект String , поэтому объединение методов в цепочку является полезной функцией.

 String myString = "Both".concat(" fickle")   .concat(" dwarves")   .concat(" jinx")   .concat(" my")   .concat(" pig")   .concat(" quiz");    assertEquals("Both fickle dwarves jinx my pig quiz", myString); 

В этом примере наша цепочка начинается с литерала String , затем метод concat позволяет нам объединять вызовы в цепочку для добавления дополнительных строк .

4.2. String.format ​

Далее следует метод String.format , который позволяет нам внедрять различные объекты Java в шаблон String .

Сигнатура метода String.format принимает одну строку , обозначающую наш шаблон . Этот шаблон содержит символы ‘%’ для обозначения того, где в нем должны быть размещены различные объекты « .

Как только наш шаблон объявлен, он принимает массив объектов varargs , который вводится « в шаблон.

Давайте посмотрим, как это работает, на быстром примере:

 String myString = String.format("%s %s %.2f %s %s, %s. ", "I",   "ate",   2.5056302,   "blueberry",   "pies",   "oops");    assertEquals("I ate 2.51 blueberry pies, oops. ", myString); 

Как мы видим выше, метод внедрил наши строки в правильный формат.

4.3. String.join (Java 8+)​

Если наше приложение работает на Java 8 или выше , мы можем воспользоваться методом String.join . При этом мы можем соединить массив строк с общим разделителем , гарантируя, что не будут пропущены пробелы.

 String[] strings = "I'm", "running", "out", "of", "pangrams!">;    String myString = String.join(" ", strings);    assertEquals("I'm running out of pangrams!", myString); 

Огромным преимуществом этого метода является то, что вам не нужно беспокоиться о разделителе между строками.

5. StringJoiner (Java 8+)​

StringJoiner абстрагирует всю функциональность String.join в простой в использовании класс. Конструктор принимает разделитель с необязательным префиксом и суффиксом . Мы можем добавлять строки , используя хорошо названный метод add .

 StringJoiner fruitJoiner = new StringJoiner(", ");   fruitJoiner.add("Apples");  fruitJoiner.add("Oranges");  fruitJoiner.add("Bananas");    assertEquals("Apples, Oranges, Bananas", fruitJoiner.toString()); 

Используя этот класс вместо метода String.join , мы можем добавлять строки во время работы программы ; Нет необходимости сначала создавать массив!

Перейдите к нашей статье о StringJoiner для получения дополнительной информации и примеров.

6. Массивы.toString ​

Что касается массивов, класс Array также содержит удобный метод toString , который прекрасно форматирует массив объектов. Массивы . Метод toString также вызывает метод toString любого вложенного объекта, поэтому нам нужно убедиться, что он определен.

 String[] myFavouriteLanguages = "Java", "JavaScript", "Python">;    String toString = Arrays.toString(myFavouriteLanguages);    assertEquals("[Java, JavaScript, Python]", toString); 

К сожалению, массивы. Метод toString не настраивается и выводит только строку , заключенную в квадратные скобки.

7. Collectors.joining (Java 8+)​

Наконец, давайте взглянем на метод Collectors.joining , который позволяет нам направлять вывод Stream в одну строку.

 ListString> awesomeAnimals = Arrays.asList("Shark", "Panda", "Armadillo");    String animalString = awesomeAnimals.stream().collect(Collectors.joining(", "));    assertEquals("Shark, Panda, Armadillo", animalString); 

Использование потоков открывает доступ ко всем функциям, связанным с Java 8 Stream API , таким как фильтрация, сопоставление, итерация и многое другое.

8. Подведение итогов​

В этой статье мы подробно рассмотрели множество классов и методов, используемых для объединения строк « в языке Java.

Как всегда, исходный код доступен на GitHub .

  • 1. Введение
  • 2. Построитель строк
  • 3. Оператор сложения
  • 4. Строковые методы
    • 4.1. String.concat
    • 4.2. String.format
    • 4.3. String.join (Java 8+)

    Java String. Вопросы к собеседованию и ответы на них, ч.1

    Java-университет

    Java String. Вопросы к собеседованию и ответы на них, ч.1 - 1

    Класс String один из наиболее широко используемых классов в Java. В статье приведены некоторые важные вопросы к собеседованию по Java String и ответы на них. Они будут очень полезны для получения полного представления о классе String и подготовят к любому вопросу, касающемуся класса String на собеседовании.

    1. Что такое String в Java? Какой это тип данных?

    String — это класс в Java, который прописан в пакете java.lang. Это не примитивный тип данных, как int и long. Класс String представляет строковый набор символов. Строки используются практически по всех Java-приложениях, и есть несколько фактов, которые мы должны знать о классе String. Это неизменяемый ( immutable ) и финализированный тип данных в Java и все объекты класса String виртуальная машина хранит в пуле строк. Еще одной особенностью String является способ получения объектов класса String используя двойные кавычки и перегружая оператор “+” для конкатенации.

    2. Какие есть способы создания объекта String?

    Мы можем создавать объекты используя оператор new, как и для любого другого класса в Java или мы можем использовать двойные кавычки для создания объекта String . Также есть несколько конструкторов класса String для получения строки из массива символов, массива байтов, а также используя StringBuffer или StringBuilder .

     String str = new String("abc"); String str1 = "abc"; 

    Когда мы создаем строку используя двойные кавычки, виртуальная машина Java ищет в пуле строк другую строку с таким же значением. Если строка найдена, то возвращается только ссылка на существующий объект класса String , иначе создается новый объект с полученным значением, и сохраняется в пул. Когда мы используем оператор new, виртуальная машина создает объект String , но не хранит его в пуле строк. Мы можем использовать метод intern() для сохранения строки в пуле строк, или получения ссылки, если такая строка уже находится в пуле.

    3. Напишите метод проверки, является ли строка палиндромом.

    Строка называется палиндромом, если она одинаково читается в обоих направлениях. К примеру “аба” является строкой-палиндромом. Класс String не предоставляет никакого метода для реверса строки, зато классы StringBuffer и StringBuilder имеют метод реверсирования, при помощи которого мы можем проверить, является ли наша строка палиндромом или нет.

     private static boolean isPalindrome(String str)

    Иногда собеседующий может попросить не использовать другие классы для этой проверки, в таком случае мы можем сравнивать символы в строке с обоих сторон для проверки на палиндром.

     private static boolean isPalindromeString(String str) < if (str == null) return false; int length = str.length(); System.out.println(length / 2); for (int i = 0; i < length / 2; i++) < if (str.charAt(i) != str.charAt(length - i - 1)) return false; >return true; > 

    4. Напишите метод удаления данного символа из строки.

    Мы можем использовать метод replaceAll для замены всех вхождений в строку другой строкой. Обратите внимание на то, что метод получает в качестве аргумента строку, поэтому мы используем класс Character для создания строки из символа, и используем её для замены всех символов на пустую строку.

     private static String removeChar(String str, char ch)

    5. Как мы можем перевести строку в верхний регистр или нижний регистр?

    Мы можем использовать методы класса String toUpperCase и toLowerCace для получения строки в верхнем и в нижнем регистре. Эти методы имеют перегруженный вариант, принимающий аргумент Locale , и используют его правила локализации для преобразования строки в верхний или нижний регистр.

    6. Что делает метод subSequence?

    Java 1.4 ввела интерфейс CharSequence , класс String наследует этот интерфейс, и это единственная причина реализации метода subSequence в классе String . Внутри он вызывает метод substring . Простой пример использования метода:

     public class StringSubsequence < public static void main(String[] args) < String str = "www.journaldev.com"; System.out.println("Last 4 char String: "+str.subSequence(str.length()-4, str.length())); System.out.println("First 4 char String: "+str.subSequence(0, 4)); System.out.println("website name: "+str.subSequence(4, 14)); //substring vs subSequence System.out.println("substring == subSequence ? " +(str.substring(4, 14) == str.subSequence(4, 14))); System.out.println("substring equals subSequence ? " +(str.substring(4, 14).equals(str.subSequence(4, 14)))); >> 

    На выходе программа покажет следующее:

     Last 4 char String: .com First 4 char String: www. website name: journaldev substring == subSequence ? false substring equals subSequence ? true 

    В идеале вы должны всегда использовать метод substring .

    7. Как сравнить две строки в Java?

    Класс String наследует интерфейс Comparable и имеет два варианта метода compareTo() . Метод compareTo(String anotherString) сравнивает объект String с полученным аргументом String лексикографически. Если текущая строка предшествует полученной строке, метод возвращает отрицательное значение типа integer, и если строка следует за полученным аргументом, то возвращает положительное значение integer . Если метод возвращает 0, значит строка имеет то же значение, в таком случае метод equals(String str) так же вернет true. compareToIgnoreCase(String str) : этот метод подобен предыдущему, за исключением того, что он игнорирует регистр символов. Он использует CASE_INSENSITIVE_ORDER Comparator для регистронезависимого сравнения. Если возвращаемое значение равно нулю, тогда метод equalsIgnoreCase(String str) так же вернет true. Давайте рассмотрим небольшой пример, объясняющий эти методы:

     public class StringCompareToExample < public static void main(String[] args) < String str = "ABC"; System.out.println(str.compareTo("DEF")); System.out.println(str.compareToIgnoreCase("abc")); >> 

    Программа выведет следующее:

    8. Как преобразовать строку в символ и обратно?

    Это вопрос с подвохом, поскольку строка – это последовательность символов, поэтому мы можем преобразовать её только в единичный символ. Мы можем использовать метод charAt для получения символа, находящегося в указанной позиции или мы можем использовать метод toCharArray() для преобразования строки в массив символов. Простой пример, показывающий как преобразовать строку в символ и символ в строку в Java.

     import java.util.Arrays; public class StringToCharToString < public static void main(String[] args) < //String to char array String str = "123"; char[] chArr = str.toCharArray(); System.out.println("String to char array: "+Arrays.toString(chArr)); //String to char char c = str.charAt(1); System.out.println("String to char: "+c); //char to String String s = Character.toString(c); System.out.println("char to String: "+s); //удалить все заданные символы из строки System.out.println("removing all chars from String: " +removeCharFromString("1ABCD12DW", '1')); >private static String removeCharFromString(String str, char c) < return str.replaceAll(Character.toString( c ), ""); >> 

    Программа выведет следующее:

     String to char array: [1, 2, 3] String to char: 2 char to String: 2 removing all chars from String: ABCD2DW 

    9. Как преобразовать строку в массив байтов и обратно?

    Мы можем использовать метод getBytes() для преобразования строки в массив байтов и мы можем использовать конструктор new String(byte[] arr) для преобразования массива байтов в строку.

     import java.util.Arrays; public class StringByteArray < public static void main(String[] args) < String str = "www.journaldev.com"; //преобразование String в byte array byte[] byteArr = str.getBytes(); System.out.println("String to byte array : "+Arrays.toString(byteArr)); //преобразование byte array и String String str1 = new String(byteArr); System.out.println("byte array to String : "+str1); //посмотрим, str и str1 одинаковые или нет System.out.println("str == str1? " + (str == str1)); System.out.println("str.equals(str1)? " + (str.equals(str1))); >> 

    Программа выведет следующее:

     String to byte array : [119, 119, 119, 46, 106, 111, 117, 114, 110, 97, 108, 100, 101, 118, 46, 99, 111, 109] byte array to String : www.journaldev.com str == str1? false str.equals(str1)? true 

    10. Можем ли мы использовать строку в конструкции switch.

    Этот хитрый вопрос, используется для проверки ваших знаний о текущем развитии языка. Java 7 расширяет возможности оператора switch для использования строк, ранние версии Java не поддерживают этого. Если вы реализуете условный поток для строк, вы можете использовать условия if-else и вы можете использовать оператор switch, если используете Java 7 или поздние версии. Небольшой пример использования строки в операторе switch и другой метод, который показывает такую же логику, используя условия if-else .

     public class SwitchStringExample < public static void main(String[] args) < printColorUsingSwitch("red"); printColorUsingIf("red"); // оператор switch регистрозависимый printColorUsingSwitch("RED"); printColorUsingSwitch(null); >private static void printColorUsingIf(String color) < if (color.equals("blue")) < System.out.println("BLUE"); >else if (color.equals("red")) < System.out.println("RED"); >else < System.out.println("INVALID COLOR CODE"); >> private static void printColorUsingSwitch(String color) < switch (color) < case "blue": System.out.println("BLUE"); break; case "red": System.out.println("RED"); break; default: System.out.println("INVALID COLOR CODE"); >> > 

    Программа выведет следующее:

     RED RED INVALID COLOR CODE Exception in thread "main" java.lang.NullPointerException at com.journaldev.util.SwitchStringExample.printColorUsingSwitch(SwitchStringExample.java:24) at com.journaldev.util.SwitchStringExample.main(SwitchStringExample.java:10) 
    • использование строк в конструкции switch делает код читабельнее, убирая множественные цепи условий if-else .
    • строки в switch чувствительны к регистру, пример выше показывает это.
    • оператор switch использует метод String.equals() для сравнения полученного значения со значениями case, поэтому добавьте проверку на NULL во избежание NullPointerException .
    • согласно документации Java 7 для строк в switch , компилятор Java формирует более эффективный байткод для строк в конструкции switch , чем для сцепленных условий if-else .
    • убедитесь, что это будет использоваться с Java 7 или поздней версии, иначе получите xception .

    11. Напишите программу, печатающую все перестановки строки.

    Это непростой вопрос, и мы должны использовать рекурсию для нахождения всех перестановок строки, например перестановками “AAB” могут быть “AAB”, “ABA” и “BAA”. Также нам необходимо использовать Set для того, чтобы убедиться, что у нас нет дубликатов строк. Для получения всех перестановок, мы для начала берем первый символ строки и переставляем оставшиеся символы. Если String = “ABC” Первый символ char = A и оставшиеся перестановки BC и CB. Теперь мы можем вставить первый символ в доступные позиции в перестановках. BC -> ABC, BAC, BCA CB -> ACB, CAB, CBA Пример программы:

     import java.util.HashSet; import java.util.Set; public class StringHelper < public static SetpermutationFinder(String str) < Setperm = new HashSet(); //Handling error scenarios if (str == null) < return null; >else if (str.length() == 0) < perm.add(""); return perm; >char initial = str.charAt(0); // первый символ String rem = str.substring(1); // полная строка без первого символа Set words = permutationFinder(rem); for (String strNew : words) < for (int i = 0;i<=strNew.length();i++)< perm.add(charInsert(strNew, initial, i)); >> return perm; > public static String charInsert(String str, char c, int j) < String begin = str.substring(0, j); String end = str.substring(j); return begin + c + end; >public static void main(String[] args) < String s = "AAC"; String s1 = "ABC"; String s2 = "ABCD"; System.out.println("\nPermutations for " + s + " are: \n" + permutationFinder(s)); System.out.println("\nPermutations for " + s1 + " are: \n" + permutationFinder(s1)); System.out.println("\nPermutations for " + s2 + " are: \n" + permutationFinder(s2)); >> 

    Вывод программы:

     Permutations for AAC are: [AAC, ACA, CAA] Permutations for ABC are: [ACB, ABC, BCA, CBA, CAB, BAC] Permutations for ABCD are: [DABC, CADB, BCAD, DBAC, BACD, ABCD, ABDC, DCBA, ADBC, ADCB, CBDA, CBAD, DACB, ACBD, CDBA, CDAB, DCAB, ACDB, DBCA, BDAC, CABD, BADC, BCDA, BDCA] 

    Сюрпризы конкатенации

    Вопрос в стиле головоломок с offline-конференций: Что выведет этот код при запуске?

    import java.util.concurrent.atomic.AtomicInteger; public class Disturbed < public static void main(String. args) < AtomicInteger counter = new AtomicInteger(1); System.out.println( "First two positive numbers: " + counter + ", " + counter.incrementAndGet() ); >>

    Помедитируйте немного над кодом и приходите за ответом под кат.

    Вероятно, что увидев код многие воскликнули «Это же элементарно, Ватсон!»
    Ответом, однако, будет фраза «Зависит от компилятора и параметров компиляции».

    Код, скомпилированный JDK 8 и более ранними выдаст ожидаемое:

    First two positive numbers: 1, 2

    Однако при компиляции в JDK 9 и более новых мы внезапно получим ответ:

    First two positive numbers: 2, 2

    Всё изложенное в данной заметке проверялось на компиляторах из Oracle JDK/OpenJDK, в других реализациях могут быть другие баги.

    Предпосылки

    Среди нововведений Java 9 был JEP 280, новый механизм конкатенации строк.

    Конкатена́ция (лат. concatenatio «присоединение цепями; сцепле́ние») — операция склеивания объектов линейной структуры, обычно строк. Например, конкатенация слов «микро» и «мир» даст слово «микромир».

    Целью было сделать возможной оптимизацию конкатенации строк без необходимости перекомпиляции программ из исходников. Обновил JDK — увеличил производительность. Магия!

    Традиционно, с самого начала времён, конкатенация строк транслировалась компилятором в создание экземпляра класса StringBuilder , серию вызовов StringBuilder::append() и преобразование результата в строку при помощи вызова StringBuilder::toString() в финале.

    Так, например, конструкция System.out.println(«Hello, » + name + «!»); превращалась в

    System.out.println( (new StringBuilder()) .append("Hello, ") .append(name) .append("!") .toString() );

    При новом подходе все манипуляции с StringBuilder исчезают и заменяются одной инструкцией invokedynamic . В качестве bootstrap-метода при этом используется один из методов класса java.lang.invoke.StringConcatFactory.

    Чистой Java это не передать, но javap -c -v покажет нам примерно такой байткод:

     0: getstatic #23 // Field java/lang/System.out:Ljava/io/PrintStream; 3: aload_0 4: invokedynamic #27, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String; 9: invokevirtual #31 // Method java/io/PrintStream.println:(Ljava/lang/String;)V . LocalVariableTable: Start Length Slot Name Signature 0 13 0 name Ljava/lang/String; . BootstrapMethods: 0: #50 REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite; Method arguments: #56 Hello, \u0001!

    В чём проблема?

    Само собой, предполагалось, что изменение никак не повлияет на поведение пользовательского кода. Но не всегда и не всё можно предусмотреть. Java 9 была выпущена в 2017 году, а в сентябре этого года был зарегистрирован баг JDK-8273914.

    Как обнаружилось, javac генерирует байткод, нарушающий JLS, пункт §15.7.1. Последний требует для бинарных операций чтобы левая часть выражения была полностью вычислена перед тем, как будет вычислена правая:

    15.7.1. Evaluate Left-Hand Operand First

    The left-hand operand of a binary operator appears to be fully evaluated before any part of the right-hand operand is evaluated.

    Это требование без всяких ухищрений выполняется при использовании старого-доброго StringBuilder , но не всегда выполняется при использовании новой стратегии.

    Сравним поведение на примере выражения из Кода Для Привлечения Внимания, предварявшего эту статью:

    StringBuilder

     // Создаём буфер для формирования результата конкатенации. (new StringBuilder()) // Добавляем к результату строку "First two positive numbers: " .append("First two positive numbers: ") // Разыменовываем ссылку на объект counter и переводим его в // строковое представление, неявно вызывая метод toString() .append(counter) // Добавляем к результату строку ", " .append(", ") // Увеличиваем значение счётчика на единицу и получаем новое значение как // целое число. Полученное число переводим в строковое предствление // и добавляем к результату. .append(counter.incrementAndGet()) // Получаем содержимое буфера в виде строки. .toString()

    Это ассемблер, но не пугайтесь, дальше будет псевдокод.

     // Помещаем ссылку на экземпляр счётчика на стек. // Сейчас его внутреннее состояние хранит значение равное единице, // но это ничего не значит. aload_1; // Разыменовываем ссылку на экземпляр счётчика и вызываем его метод incrementAndGet() // Состояние счётчика меняется с 1 на 2, новое значение в виде целого числа // типа int возвращается в качестве результата вызова и помещается на вершину // стека. aload_1; invokevirtual Method java/util/concurrent/atomic/AtomicInteger.incrementAndGet:"()I"; // Ссылка на экземпляр счётчика и его последнее значение приходят в качестве // параметров в метод, реализующий конкатенацию. Там они будут переведены в // строковое представление и подставлены в строку-шаблон. invokedynamic InvokeDynamic REF_invokeStatic :Method java/lang/invoke/StringConcatFactory.makeConcatWithConstants :"(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;":makeConcatWithConstants :"(Ljava/util/concurrent/atomic/AtomicInteger;I)Ljava/lang/String;" < // Строка-шаблон. Символами \u0001 обозначаются места, в которые будут // подставлены значения из параметров. String "First two positive numbers: \u0001, \u0001" >;

    Процитированный выше фрагмент можно представить в виде такого псевдокода:

     // Помещаем ссылку на экземпляр счётчика на стек. // Сейчас его внутреннее состояние хранит значение равное единице, // но это ничего не значит. AtomicInteger temp1 = counter; // Разыменовываем ссылку на экземпляр счётчика и вызываем его метод incrementAndGet() // Состояние счётчика меняется с 1 на 2, новое значение в виде целого числа // типа int возвращается в качестве результата вызова и помещается на вершину // стека. int temp2 = counter.incrementAndGet(); // Ссылка на экземпляр счётчика и его последнее значение приходят в качестве // параметров в метод, реализующий конкатенацию. Там они будут переведены в // строковое представление и подставлены в строку-шаблон. String result = makeConcatWithConstants( "First two positive numbers: \u0001, \u0001", temp1, temp2 ); . System.out.println(result);

    Другими словами, в метод makeConcatWithConstants() объект count придёт уже в изменённом состоянии и результат будет неверным. Мистерия раскрыта!

    Добиться стабильной работы нашего КДПВ можно просто заменив в выражении counter на counter.get() , а в более общем случае — явно приведя к строковому представлению все значения ссылочных типов, встречающиеся в выражении.

    Если этот баг вызывает у вас серьёзное беспокойство, то вы можете временно откатиться на использование старого способа конкатенации строк.

    Для это нужно при компиляции передать javac параметр -XDstringConcat=inline :

    javac -XDstringConcat=inline Disturbed.java

    Мораль

    Пишите хороший код, не пишите плохой и остерегайтесь побочных эффектов при конкатенации строк. Баги коварны и умеют ждать.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *