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

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

  • автор:

Операторы объединения в Visual Basic

Операторы объединения объединяют несколько строк в одну. Существует два оператора объединения: + и & . Оба они выполняют базовую операцию объединения, как показано в следующем примере.

Dim x As String = "Mic" & "ro" & "soft" Dim y As String = "Mic" + "ro" + "soft" ' The preceding statements set both x and y to "Microsoft". 

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

Dim a As String = "abc" Dim d As String = "def" Dim z As String = a & d Dim w As String = a + d ' The preceding statements set both z and w to "abcdef". 

Различия между двумя операторами объединения

Оператор +имеет основную цель добавления двух чисел. Однако он также может объединять числовые операнды со строковыми. Оператор + имеет сложный набор правил, определяющий, следует ли выполнять добавление, объединение, сигнализировать об ошибке компилятора или выдавать исключение времени выполнения InvalidCastException.

Оператор и определяется только для String операндов, и он всегда расширяет его операнды String , независимо от параметра Option Strict . Оператор & рекомендуется использовать для объединения строк, так как он определен исключительно для строк и снижает шансы создания непреднамеренного преобразования.

Производительность: String и StringBuilder

Если вы выполняете множество операций со строкой, таких как объединения, удаления и замены, использование класса StringBuilder из пространства имен System.Text может оказать положительное влияние на производительность. Для создания и инициализации объекта StringBuilder требуется дополнительная инструкция, кроме того, еще одна инструкция необходима для преобразования итогового значения в String , однако это время можно скомпенсировать высокой скоростью выполнения StringBuilder.

См. также

  • Оператор Option Strict
  • Типы методов обработки строк в Visual Basic
  • Арифметические операторы в Visual Basic
  • Comparison Operators in Visual Basic
  • Логические и битовые операторы в Visual Basic

Совместная работа с нами на GitHub

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

Конкатенация строк и производительность

Советы от 5 февраля 2002 «Запись методов toString » (источник и перевод на JavaGu.ru) включали следующее предложение:

Обратите внимание, что использование «+» в toString для построения возвращаемого значения не всегда является самым эффективным подходом. Возможно, вы захотите использовать вместо этого StringBuffer .

Читатель технических советов отметил, что в документации по Java говорится о том, что для фактической реализации оператора + применяется StringBuffer . Поэтому возникает вопрос, какой выигрыш в производительности, если он существует, вы получите при явном использовании StringBuffer в ваших программах? В этой статье делается попытка ответить на этот вопрос.

Для начала рассмотрим пример, в котором строка формируется путем повторения одного и того же символа:

class MyTimer <
private final long start;

public MyTimer () start = System.currentTimeMillis () ;
>

public long getElapsed () return System.currentTimeMillis () — start;
>
>

public class AppDemo1 static final int N = 47500 ;

public static void main ( String args [])

// создать строку при помощи оператора +

MyTimer mt = new MyTimer () ;
String str1 = «» ;
for ( int i = 1 ; i str1 = str1 + «*» ;
>
System.out.println ( «elapsed time #1 = » + mt.getElapsed ()) ;

// создать строку при помощи StringBuffer

mt = new MyTimer () ;
StringBuffer sb = new StringBuffer () ;
for ( int i = 1 ; i sb.append ( «*» ) ;
>
String str2 = sb.toString () ;
System.out.println ( «elapsed time #2 = » + mt.getElapsed ()) ;

// проверка на равенство

if ( !str1.equals ( str2 )) System.out.println ( «str1/str2 mismatch» ) ;
>
>
>

После выполнения этой программы вы должны получить примерно следующий результат:

elapsed time # 1 = 61890
elapsed time # 2 = 16

Подход №2 явно использует StringBuffer , тогда как подход №1 использует его неявно, как часть реализации оператора + . Вы можете исследовать байт-коды, использующиеся для реализации первого подхода при помощи команды:

javap -c -classpath . AppDemo1

Разница в «+» и StringBuffer

Откуда такая огромная разница между этими двумя подходами? Во втором подходе символы добавляются в StringBuffer , что довольно эффективно. А в первом подходе не используется этот метод? На самом деле нет. Выражение:

str1 = str1 + «*» ;

не добавляет символы к строке str1 . Это происходит из-за того, что Java-строки постоянны, они не изменяются после создания. Вот что происходит в действительности:

  • StringBuffer создается
  • str1 копируется в него
  • «*» добавляется в буфер
  • Результат преобразуется в строку
  • Ссылка str1 меняется для указания на эту строку
  • Старая строка, на которую ранее ссылалась переменная str1 , делается доступной для сборщика мусора.

Цикл проходит через N итераций, и на каждой итерации содержимое str1 (содержащей N-1 символов) должно быть скопировано в буфер. Такое поведение подразумевает, что первый подход имеет квадратичную или худшую производительность. «Квадратичная» означает, что время выполнения пропорционально квадрату N. Есть вероятность эффективно заморозить приложение при применении такого типа цикла.

В примере AppDemo1 демонстрируется ситуация, когда периодически присоединяется одна строка к другой, так что обе строки должны быть скопированы во временную область ( StringBuffer ), создана новая строка и, затем, ссылка на оригинальную строку заменяется ссылкой на новую строку.

Но что если вы не выполняете этот тип операции, а вместо этого, просто имеете некоторый код, похожий на следующий:

public String toString () <
return «X=» + x + » Y=» + y;
>

Здесь нет цикла или повторений и нет строки, которая становится все длиннее и длиннее. Есть какой-либо вред от применения + вместо StringBuffer в этом примере?

Поясняющий пример

В примере AppDemo1 демонстрируется ситуация, когда периодически присоединяется одна строка к другой, так что обе строки должны быть скопированы во временную область ( StringBuffer ), создана новая строка и, затем, ссылка на оригинальную строку заменяется ссылкой на новую строку.

Но что если вы не выполняете этот тип операции, а вместо этого, просто имеете некоторый код, похожий на следующий:

public String toString () <
return «X=» + x + » Y=» + y;
>

Здесь нет цикла или повторений и нет строки, которая становится все длиннее и длиннее. Есть какой-либо вред от применения + вместо StringBuffer в этом примере?

Для ответа на этот вопрос рассмотрим дополнительный код:

class MyPoint <
private final int x, y;
private final String cache;

public MyPoint ( int x, int y ) this .x = x;
this .y = y;
cache = «X=» + x + » Y=» + y;
>

public String toString1 () return «X=» + x + » Y=» + y;
>

public String toString2 () StringBuffer sb = new StringBuffer () ;
sb.append ( «X=» ) ;
sb.append ( x ) ;
sb.append ( » Y=» ) ;
sb.append ( y ) ;
return sb.toString () ;
>

public String toString3 () String s = «» ;
s = s + «X=» ;
s = s + x;
s = s + » Y=» ;
s = s + y;
return s;
>

public String toString4 () return cache;
>
>

class MyTimer private final long start;

public MyTimer () start = System.currentTimeMillis () ;
>

public long getElapsed () return System.currentTimeMillis () — start;
>
>

public class AppDemo2 static final int N = 1000000 ;

public static void main ( String args []) MyPoint mp = new MyPoint ( 37 , 47 ) ;
String s1 = null ;
String s2 = null ;
String s3 = null ;
String s4 = null ;

MyTimer mt = new MyTimer () ;
for ( int i = 1 ; i s1 = mp.toString1 () ;
>
System.out.println ( «toString1 » + mt.getElapsed ()) ;

mt = new MyTimer () ;
for ( int i = 1 ; i s2 = mp.toString2 () ;
>
System.out.println ( «toString2 » + mt.getElapsed ()) ;

mt = new MyTimer () ;
for ( int i = 1 ; i s3 = mp.toString3 () ;
>
System.out.println ( «toString3 » + mt.getElapsed ()) ;

mt = new MyTimer () ;
for ( int i = 1 ; i s4 = mp.toString4 () ;
>
System.out.println ( «toString4 » + mt.getElapsed ()) ;

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

if ( !s1.equals ( s2 ) || !s2.equals ( s3 ) || !s3.equals ( s4 )) System.out.println ( «check error» ) ;
>
>
>

В этой программе создается класс MyPoint , который используется для представления точек X,Y . В ней реализуются различные методы toString для класса. Результат выполнения программы может выглядеть примерно так:

toString1 2797
toString2 2703
toString3 5656
toString4 32

Расшифровка результатов

Первые два способа, использующие + и StringBuffer , имеют примерно одинаковую производительность. Поэтому вы можете сделать вывод, что эти два способа фактически идентичны. Генерируемый для toString1 байт-код указывает, что создается StringBuffer , а затем различные строки просто добавляются к нему. Полученный код очень похож на toString2 .

Но не все так просто. Первая проблема в том, что вы не всегда сможете сформулировать возвращаемое из toString значение в виде отдельного выражения. toString3 и toString1 показывают идентичные результаты. Но время работы toString3 в два раза больше по причине, описанной в примере AppDemo1 . Пример AppDemo2 демонстрирует ситуацию, когда надо создать возвращаемое значение за один раз. В этом случае toString2 , использующий явно StringBuffer , является более хорошим выбором.

Другая проблема касается высказывания, найденного в «Спецификации по языку программирования Java» в разделе 15.18.1.2, в котором говорится:

Реализация может выполнить преобразование и конкатенацию за один шаг, чтобы избежать создания и удаления промежуточного объекта String . Для увеличения производительности повторных конкатенаций строки компилятор Java может использовать класс StringBuffer или аналогичную технику для уменьшения количества промежуточных объектов String , создающихся при вычислении выражения.

Это утверждение говорит о том, что компилятор Java не обязательно оптимизирует такое выражение как:

str1 + str2 + str3 + .

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

Поэтому будьте осторожны при использовании оператора + , особенно для длинных строк или в циклах.

Отметим, что существует даже более быстрый способ реализации toString для этого примера. MyPoint является постоянным классом. Это означает, что его экземпляры не могут быть модифицированы после создания. Учитывая это, возвращаемое из toString значение всегда будет одним и тем же. Поскольку значения одинаковы, оно может быть вычислено один раз в конструкторе MyPoint и затем просто возвращено из toString4 .

Такой вид кэширования часто очень полезен, но есть и отрицательные стороны. Если класс является изменяемым, то кэширование может не иметь смысла. Тоже самое можно сказать и для ситуаций, когда вычисление значения кэша трудоемко, когда кэш занимает много памяти, или когда метод toString вызывается нечасто.

Ссылки

Дополнительная информация по этой теме находится в разделе 15.18.1 «Оператор конкатенации строк +» в «Спецификации по языку программирования Java, второе издание (http://java.sun.com/docs/books/jls/), и в разделе 5 «Строки» книги «Настройка производительности Java» Jack Shirazi.

A может Вас также заинтересует что-нибудь из этого:
  1. Разное → Теория и практика Java: Динамическая компиляция и измерение производительности
  2. Java сниппеты → Какой длины ваша строка?
  3. Java Standard Edition → Блокировки
  4. Java сниппеты → Блоки статической и объектной инициализации
  5. Java сниппеты → Методы для работы с переменным количеством аргументов
  6. Java Standard Edition → Производительность операций ввода/вывода в Java

Объединение строк в 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+)

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

    Вопрос в стиле головоломок с 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 не будет опубликован. Обязательные поля помечены *