Конкатенация строк в Java: лучший способ? (+Примеры)

Java предоставляет различные способы объединения двух строк для формирования новой строки, такие как конкатенация строк, классы StringBuilder и StringBuffer, API MessageFormat, поток Java 8 и даже шаблоны строк и т. д. В этой статье рассматриваются различия между этими способами и подчеркивается, как Java оптимизирует байт-код для ускорения конкатенации строк.

1. Различные способы составления строк

1.1.Конкатенация строк

В Java конкатенация строк означает объединение нескольких строк для формирования новой строки. Самый простой метод — использование оператора +. При таком подходе каждый раз, когда мы конкатенируем две строки, Java внутренне создает новый литерал в пуле строковых констант.

var name = "Alex";var time = LocalTime.now();var greeting = "Hello " + name + ", how are you?\nThe current time is " + time + " now!";

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

1.2. StringBuilder и StringBuffer

Класс StringBuilder — более эффективный способ манипулирования строками при конкатенации нескольких значений. В отличие от базовой конкатенации строк с использованием оператора +, StringBuilder изменяет содержимое строки, не создавая каждый раз новый объект.

Такой подход более эффективен с точки зрения использования памяти, чем использование оператора + для конкатенации при работе с большим количеством конкатенаций.

StringBuilder greetingBuilder = new StringBuilder();greetingBuilder.append("Hello ").append(name).append(", how are you?\n");greetingBuilder.append("The current time is ").append(time).append(" now!");System.out.println( builder.toString() ); 

Хотя StringBuilder эффективен, он не является потокобезопасным. В многопоточной среде, если два или более потоков одновременно попытаются изменить содержимое StringBuilder, могут возникнуть неожиданные результаты. Для решения этой проблемы Java предоставляет другой класс, называемый StringBuffer.

StringBuffer sb = new StringBuffer();sb.append("Hello ").append(name).append(", how are you?\n");sb.append("The current time is ").append(time).append(" now!");System.out.println( sb.toString() ); 

Однако эта синхронизация идет за счет производительности. Для однопоточных приложений StringBuilder обычно предпочтительнее из-за его более высокой производительности.

1.3.Шаблоны строк

Начиная с Java 21, мы можем создавать шаблоны строк, содержащие встроенные выражения(оцениваемые во время выполнения). Мы встраиваем переменные в строку, и значения переменных разрешаются во время выполнения. Таким образом, строки шаблонов выдают разные результаты для разных значений переменных.

String greeting = STR."Hello \{name}, how are you?\nThe current time is \{time} now!";

2. Оптимизация компилятора для конкатенации строк

2.1 До Java 8

В Java 8 компилятор выполнил определенные оптимизации, когда дело дошло до конкатенации строк с использованием оператора +. Для простых операций, где было объединено фиксированное количество строк, компилятор автоматически преобразовал код в реализацию StringBuilder для лучшей производительности.

Рассмотрим следующую программу:

import java.time.LocalTime;public class MainClass {public static void main(String[] args) throws Exception {var name = "Alex";var time = LocalTime.now();String greeting = "Hello " + name + ", how are you?\nThe current time is " + time + " now!";System.out.println(greeting);}}

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

String name = "Alex";LocalTime time = LocalTime.now();StringBuilder stringBuilder = new StringBuilder();stringBuilder.append("Hello ").append(name).append(", how are you?\nThe current time is ").append(time).append(" now!");String greeting = stringBuilder.toString();System.out.println(greeting);

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

Мы можем убедиться в этом, посмотрев на сгенерированный байт-код:

public static void main(java.lang.String[]) throws java.lang.Exception;descriptor:([Ljava/lang/String;)Vflags:(0x0009) ACC_PUBLIC, ACC_STATICCode:stack=3, locals=4, args_size=10: ldc #2 // String Alex2: astore_13: invokestatic #3 // Method java/time/LocalTime.now:()Ljava/time/LocalTime;6: astore_27: new #4 // class java/lang/StringBuilder10: dup11: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V14: ldc #6 // String Hello16: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;19: aload_120: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;23: ldc #8 // String , how are you?25: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;28: ldc #9 // String \nThe current time is30: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;33: aload_234: invokevirtual #10 // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;37: ldc #11 // String now!39: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;42: invokevirtual #12 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;45: astore_346: getstatic #13 // Field java/lang/System.out:Ljava/io/PrintStream;49: aload_350: invokevirtual #14 // Method java/io/PrintStream.println:(Ljava/lang/String;)V53: returnException table:from to target type7 46 46 Class java/lang/Exception

2.2 Java 9 и более поздние версии

Начиная с Java 9 [ JEP-280 ], компилятор использует класс StringConcatFactory для выбора подходящей стратегии конкатенации строк с помощью вызова Invoke Dynamic(также известного как Indy) [ JSR-292 ].

Начиная с Java 9, «+» больше не компилируется в StringBuilder.

Вызов invokedynamic для java.lang.invoke.StringConcatFactory предлагает возможности для ленивой компоновки, предоставляя средства для начальной загрузки цели вызова один раз, во время первоначального вызова. Во время выполнения метод начальной загрузки(BSM) запускается и связывается с фактическим кодом, выполняющим конкатенацию.

Начиная с Java 9, конкатенация строк — это решение времени выполнения, а не времени компиляции. Это означает, что после того, как вы кодируете Java 9(или более позднюю версию), он может изменить базовую реализацию так, как ему заблагорассудится, без необходимости повторной компиляции.

Рассмотрим следующую программу Java:

public class StringConcatenationBenchmark {public static void main(String[] args) throws Exception {String string1 = "Hello ";String string2 = "World!";String concatResult = string1 + string2;System.out.println(concatResult);}}

Когда мы компилируем и видим сгенерированный байт-код в Java 21, мы можем увидеть использование invokedynamic. Посмотрите, насколько код минимален и короток.

 public static void main(java.lang.String[]) throws java.lang.Exception;descriptor:([Ljava/lang/String;)Vflags:(0x0009) ACC_PUBLIC, ACC_STATICCode:stack=2, locals=4, args_size=10: ldc #7 // String Alex2: astore_13: invokestatic #9 // Method java/time/LocalTime.now:()Ljava/time/LocalTime;6: astore_27: aload_18: aload_29: invokestatic #15 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;12: invokedynamic #21, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;17: astore_318: getstatic #25 // Field java/lang/System.out:Ljava/io/PrintStream;21: aload_322: invokevirtual #31 // Method java/io/PrintStream.println:(Ljava/lang/String;)V25: returnLineNumberTable:line 69: 0line 70: 3line 72: 7line 73: 18line 79: 25LocalVariableTable:Start Length Slot Name Signature0 26 0 args [Ljava/lang/String;3 23 1 name Ljava/lang/String;7 19 2 time Ljava/time/LocalTime;18 8 3 greeting Ljava/lang/String;Exceptions:throws java.lang.Exception}

Теперь на основе строковых переменных и констант, которые необходимо добавить, вызов invokedynamic может выбрать одну из 6 возможных стратегий, которые указывают на фактическую логику конкатенации.

private enum Strategy {/*** Bytecode generator, calling into {@link java.lang.StringBuilder}.*/BC_SB,/*** Bytecode generator, calling into {@link java.lang.StringBuilder};* but trying to estimate the required storage.*/BC_SB_SIZED,/*** Bytecode generator, calling into {@link java.lang.StringBuilder};* but computing the required storage exactly.*/BC_SB_SIZED_EXACT,/*** MethodHandle-based generator, that in the end calls into {@link java.lang.StringBuilder}.* This strategy also tries to estimate the required storage.*/MH_SB_SIZED,/*** MethodHandle-based generator, that in the end calls into {@link java.lang.StringBuilder}.* This strategy also estimate the required storage exactly.*/MH_SB_SIZED_EXACT,/*** MethodHandle-based generator, that constructs its own byte[] array from* the arguments. It computes the required storage exactly.*/MH_INLINE_SIZED_EXACT}

Стратегия по умолчанию — MH_INLINE_SIZE_EXACT. Однако мы можем изменить эту стратегию с помощью системного свойства -Djava.lang.invoke.stringConcat=<strategyName>.

3. Конкатенация строк против шаблонов строк

Начиная с Java 21, шаблоны String предоставляют возможности интерполяции для строк Java. Мы можем помещать переменные и выражения в Strings, которые оцениваются и заменяются во время выполнения.

Во время выполнения конкатенация строк и шаблоны Sprint генерируют в основном схожий байт-код.

Оказывается, шаблоны строк — это скорее синтаксический сахар, и по сути они похожи только на конкатенацию строк.

public static void main(String[] args) throws Exception {var name = "Alex";var time = LocalTime.now();// 1- String concatenationString greeting = "Hello " + name + ", how are you?\nThe current time is " + time + " now!";System.out.println(greeting);// 2- String TemplateString greetingTemplate = STR."Hello \{name}, how are you?\nThe current time is \{time} now!";System.out.println(greetingTemplate);}

Давайте проверим сгенерированный байт-код обеих техник с помощью команды javap -c -v. Обратите внимание, что команды, сгенерированные для обоих наборов методов, почти одинаковы.

public static void main(java.lang.String[]) throws java.lang.Exception;descriptor:([Ljava/lang/String;)Vflags:(0x0009) ACC_PUBLIC, ACC_STATICCode:stack=2, locals=5, args_size=10: ldc #7 // String Alex2: astore_13: invokestatic #9 // Method java/time/LocalTime.now:()Ljava/time/LocalTime;6: astore_27: aload_18: aload_29: invokestatic #15 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;12: invokedynamic #21, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;17: astore_318: getstatic #25 // Field java/lang/System.out:Ljava/io/PrintStream;21: aload_322: invokevirtual #31 // Method java/io/PrintStream.println:(Ljava/lang/String;)V25: aload_126: aload_227: invokestatic #15 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;30: invokedynamic #21, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;35: astore 437: getstatic #25 // Field java/lang/System.out:Ljava/io/PrintStream;40: aload 442: invokevirtual #31 // Method java/io/PrintStream.println:(Ljava/lang/String;)V45: return

4. Заключение

В этой статье Java мы изучили различные способы составления строк Java до Java 21 и проверили их байт-коды. Мы рассмотрели, как среда выполнения Java применяет оптимизацию производительности, чтобы заставить код работать быстрее для разных входных данных.

Исходный код на Github

Прокрутить вверх