И снова здравствуйте. Как мы уже писали, на следующей неделе стартует новая группа обучения по курсу «Разработчик Java», по устоявшейся традиции делимся с вами переводом интересного материала по теме.

Начиная с JDK 9 конкатенация строк претерпела значительные изменения.

JEP 280 («Indify String Concatenation») был реализован в рамках JDK 9 и, в соответствии с разделом «Summary»: «Изменяет статическую последовательность байт-кода конкатенации строк, сгенерированную javac, для использования вызовов invokedynamic к функциям библиотеки JDK». Влияние, которое это оказывает на конкатенацию строк в Java, легче всего заметить, посмотрев на javap-вывод классов, использующих конкатенацию строк, которые скомпилированы в JDK до JDK 9 и после JDK 9.



Для первой демонстрации будет использоваться простой Java-класс с именем «HelloWorldStringConcat».

package dustin.examples;
import static java.lang.System.out;
public class HelloWorldStringConcat
{
  public static void main(final String[] arguments)
  {
     out.println("Hello, " + arguments[0]);
  }
}

Ниже показано сопоставление различий для -verbose вывода javap для метода main(String) класса HelloWorldStringConcat при компиляции с JDK 8 (AdoptOpenJDK) и JDK 11 (Oracle OpenJDK). Я выделил несколько ключевых различий.

JDK 8 javap-вывод

Classfile /C:/java/examples/helloWorld/classes/dustin/examples/HelloWorldStringConcat.class
 Last modified Jan 28, 2019; size 625 bytes
 MD5 checksum 3e270bafc795b47dbc2d42a41c8956af
 Compiled from "HelloWorldStringConcat.java"
public class dustin.examples.HelloWorldStringConcat
 minor version: 0
 major version: 52
    . . .
 public static void main(java.lang.String[]);
   descriptor: ([Ljava/lang/String;)V
   flags: ACC_PUBLIC, ACC_STATIC
   Code:
     stack=4, locals=1, args_size=1
        0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        3: new           #3                  // class java/lang/StringBuilder
        6: dup
        7: invokespecial #4                  // Method java/lang/StringBuilder."10: ldc           #5                  // String Hello,
       12: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;

       15: aload_0
       16: iconst_0
       17: aaload
":()V 18: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 21: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 24: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 27: return
// Метод java/io/PrintStream.println:(Ljava/lang/String;)V 27: return

JDK 11 javap-вывод

Classfile /C:/java/examples/helloWorld/classes/dustin/examples/HelloWorldStringConcat.class
 Last modified Jan 28, 2019; size 908 bytes
 MD5 checksum 0e20fe09f6967ba96124abca10d3e36d
 Compiled from "HelloWorldStringConcat.java"
public class dustin.examples.HelloWorldStringConcat
 minor version: 0
 major version: 55
    . . .
 public static void main(java.lang.String[]);
   descriptor: ([Ljava/lang/String;)V
   flags: (0x0009) ACC_PUBLIC, ACC_STATIC
   Code:
     stack=3, locals=1, args_size=1
        0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        3: aload_0
        4: iconst_0
        5: aaload
        6: invokedynamic #3,  0              // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
       11: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       14: return

Раздел «Description» в JEP 280 описывает это различие: «Идея состоит в том, чтобы заменить весь танец присоединения StringBuilder простым вызовом invokedynamic к java.lang.invoke.StringConcatFactory, который будет принимать значения, требующие объединения». В этом же разделе показано аналогичное сравнение скомпилированного вывода для аналогичного примера конкатенации строк.

Скомпилированный вывод с JDK 11 для простой конкатенации — это не просто меньшее количество строк, чем в выводе с JDK 8; у него также меньше «дорогих» операций. Потенциальное улучшение производительности может быть достигнуто за счет того, что нет необходимости в обертывании примитивных типов и не требуется создавать множество дополнительных объектов. Одним из основных мотивов этого изменения было «заложить основу для создания оптимизированных обработчиков конкатенации строк, реализуемых без необходимости изменения компилятора Java-to-bytecode» и «включить будущие оптимизации конкатенации строк без дополнительных изменений в байт-коде генерируемом javac. "

Есть интересное следствие этого с точки зрения использования StringBuffer (которому мне в любом случае сложно найти хорошее применение) и StringBuilder. В JEP 280 в “Non-Goal” было заявлено не «вводить какие-либо новые API-интерфейсы для String и/или StringBuilder, которые могли бы помочь в создании более эффективных стратегий перевода». В связи с этим для простой конкатенации строк, такой как в примере в начале этого поста, явное использование StringBuilder и StringBuffer фактически исключает для компилятора возможность использовать фичу, представленную в JEP 280, которую мы обсуждаем в этом посте.

Следующие два листинга показывают аналогичные реализации простого приложения, показанного выше, но вместо конкатенации строк они используют StringBuilder и StringBuffer соответственно. Когда javap -verbose выполняется для этих классов после того, как они скомпилированы с JDK 8 и с JDK 11, в main(String []) методах нет существенных различий.

Явное использование StringBuilder в JDK 8 и JDK 11 одинаково

package dustin.examples;
import static java.lang.System.out;
public class HelloWorldStringBuilder
{
  public static void main(final String[] arguments)
  {
     out.println(new StringBuilder().append("Hello, ").append(arguments[0]).toString());
  }
}

Classfile /C:/java/examples/helloWorld/classes/dustin/examples/HelloWorldStringBuilder.class
 Last modified Jan 28, 2019; size 627 bytes
 MD5 checksum e7acc3bf0ff5220ba5142aed7a34070f
 Compiled from "HelloWorldStringBuilder.java"
public class dustin.examples.HelloWorldStringBuilder
 minor version: 0
 major version: 52
    . . .
 public static void main(java.lang.String[]);
   descriptor: ([Ljava/lang/String;)V
   flags: ACC_PUBLIC, ACC_STATIC
   Code:
     stack=4, locals=1, args_size=1
        0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
// Поле java/lang/System.out:Ljava/io/PrintStream;

        3: new           #3                  // class java/lang/StringBuilder
// Класс java/lang/StringBuilder

        6: dup
        7: invokespecial #4                  // Method java/lang/StringBuilder."":()V 10: ldc #5 // String Hello, 12: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 15: aload_0 16: iconst_0 17: aaload 18: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 21: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 24: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 27: return

Classfile /C:/java/examples/helloWorld/classes/dustin/examples/HelloWorldStringBuilder.class
 Last modified Jan 28, 2019; size 627 bytes
 MD5 checksum d04ee3735ce98eb6237885fac86620b4
 Compiled from "HelloWorldStringBuilder.java"
public class dustin.examples.HelloWorldStringBuilder
 minor version: 0
 major version: 55
    . . .
 public static void main(java.lang.String[]);
   descriptor: ([Ljava/lang/String;)V
   flags: (0x0009) ACC_PUBLIC, ACC_STATIC
   Code:
     stack=4, locals=1, args_size=1
        0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        3: new           #3                  // class java/lang/StringBuilder
        6: dup
        7: invokespecial #4                  // Method java/lang/StringBuilder."":()V 10: ldc #5 // String Hello, 12: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 15: aload_0 16: iconst_0 17: aaload 18: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 21: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 24: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 27: return

Явное использование StringBuffer в JDK 8 и JDK 11 одинаково

package dustin.examples;
import static java.lang.System.out;
public class HelloWorldStringBuffer
{
  public static void main(final String[] arguments)
  {
     out.println(new StringBuffer().append("Hello, ").append(arguments[0]).toString());
  }
}

Classfile /C:/java/examples/helloWorld/classes/dustin/examples/HelloWorldStringBuffer.class
 Last modified Jan 28, 2019; size 623 bytes
 MD5 checksum fdfb90497db6a3494289f2866b9a3a8b
 Compiled from "HelloWorldStringBuffer.java"
public class dustin.examples.HelloWorldStringBuffer
 minor version: 0
 major version: 52
    . . .
 public static void main(java.lang.String[]);
   descriptor: ([Ljava/lang/String;)V
   flags: ACC_PUBLIC, ACC_STATIC
   Code:
     stack=4, locals=1, args_size=1
        0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        3: new           #3                  // class java/lang/StringBuffer
        6: dup
        7: invokespecial #4                  // Method java/lang/StringBuffer."":()V 10: ldc #5 // String Hello, 12: invokevirtual #6 // Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer; 15: aload_0 16: iconst_0 17: aaload 18: invokevirtual #6 // Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer; 21: invokevirtual #7 // Method java/lang/StringBuffer.toString:()Ljava/lang/String; 24: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 27: return

Classfile /C:/java/examples/helloWorld/classes/dustin/examples/HelloWorldStringBuffer.class
 Last modified Jan 28, 2019; size 623 bytes
 MD5 checksum e4a83b6bb799fd5478a65bc43e9af437
 Compiled from "HelloWorldStringBuffer.java"
public class dustin.examples.HelloWorldStringBuffer
 minor version: 0
 major version: 55
    . . .
 public static void main(java.lang.String[]);
   descriptor: ([Ljava/lang/String;)V
   flags: (0x0009) ACC_PUBLIC, ACC_STATIC
   Code:
     stack=4, locals=1, args_size=1
        0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        3: new           #3                  // class java/lang/StringBuffer
        6: dup
        7: invokespecial #4                  // Method java/lang/StringBuffer."":()V 10: ldc #5 // String Hello, 12: invokevirtual #6 // Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer; 15: aload_0 16: iconst_0 17: aaload 18: invokevirtual #6 // Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer; 21: invokevirtual #7 // Method java/lang/StringBuffer.toString:()Ljava/lang/String; 24: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 27: return

JDK 8 и JDK 11 Обработка зацикленных конкатенаций строк

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

package dustin.examples;
import static java.lang.System.out;
public class HelloWorldStringConcatComplex
{
  public static void main(final String[] arguments)
  {
     String message = "Hello";
     for (int i=0; i<25; i++)
     {
        message += i;
     }
     out.println(message);
  }
}

Classfile /C:/java/examples/helloWorld/classes/dustin/examples/HelloWorldStringConcatComplex.class
 Last modified Jan 30, 2019; size 766 bytes
 MD5 checksum 772c4a283c812d49451b5b756aef55f1
 Compiled from "HelloWorldStringConcatComplex.java"
public class dustin.examples.HelloWorldStringConcatComplex
 minor version: 0
 major version: 52
    . . .
 public static void main(java.lang.String[]);
   descriptor: ([Ljava/lang/String;)V
   flags: ACC_PUBLIC, ACC_STATIC
   Code:
     stack=2, locals=3, args_size=1
        0: ldc           #2                  // String Hello
        2: astore_1
        3: iconst_0
        4: istore_2
        5: iload_2
        6: bipush        25
        8: if_icmpge     36
       11: new           #3                  // class java/lang/StringBuilder
       14: dup
       15: invokespecial #4                  // Method java/lang/StringBuilder."19: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
       22: iload_2
       23: invokevirtual #6                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
       26: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
       29: astore_1
       30: iinc          2, 1
":()V 18: aload_1 33: goto 5 36: getstatic #8 // Field java/lang/System.out:Ljava/io/PrintStream; 39: aload_1 40: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 43: return

 Classfile /C:/java/examples/helloWorld/classes/dustin/examples/HelloWorldStringConcatComplex.class
 Last modified Jan 30, 2019; size 1018 bytes
 MD5 checksum 967fef3e7625965ef060a831edb2a874
 Compiled from "HelloWorldStringConcatComplex.java"
public class dustin.examples.HelloWorldStringConcatComplex
 minor version: 0
 major version: 55
    . . .
 public static void main(java.lang.String[]);
   descriptor: ([Ljava/lang/String;)V
   flags: (0x0009) ACC_PUBLIC, ACC_STATIC
   Code:
     stack=2, locals=3, args_size=1
        0: ldc           #2                  // String Hello
        2: astore_1
        3: iconst_0
        4: istore_2
        5: iload_2
        6: bipush        25
        8: if_icmpge     25
       11: aload_1
       12: iload_2
       13: invokedynamic #3,  0              // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;I)Ljava/lang/String;
       18: astore_1
       19: iinc          2, 1
       22: goto          5
       25: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
       28: aload_1
       29: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       32: return
 

В презентации «Enough java.lang.String to Hang Ourselves ...», доктор Хайнц М. Кабуц (Heinz M. Kabutz) и Дмитрий Вязеленко (Dmitry Vyazelenko) обсуждают внесенные изменения в конкатенацию строк Java и кратко их обобщают, “+ больше не компилируется в StringBuilder”. На слайде «Lessons from Today» они заявляют: «Используйте + вместо StringBuilder, где это возможно» и «Перекомпилируйте классы для Java 9+».

Изменения, реализованные в JDK 9 с JEP 280, «позволят в будущем оптимизировать конкатенацию строк, не требуя дополнительных изменений в байт-коде, генерируемом javac». Интересно, что недавно было объявлено, что JEP 348 («Java Compiler Intrinsics for JDK APIs») теперь кандидат в JEP, и его целью является использование аналогичного подхода для компиляции методов String::format и Objects::hash.

Как считаете, полезная статья? Ждем ваши комментарии и приглашаем всех на день открытых дверей по курсу «Разработчик Java», который уже 25 марта проведет генеральный директор компании ОТУС — Виталий Чибриков.

Комментарии (10)


  1. dougrinch
    22.03.2019 22:08
    -1

    Что-то поздновато про 9 рассказывать. Ну а вообще, лучше про подобные фичи смотреть доклады от их автора, а не какой-то сомнительный обзор.


    1. UbuRus
      23.03.2019 00:38
      +1

      Большинство джавистов сидит на JDK8, ведь 9 и 10 это не LTS релизы, на который не было смысла переходить. Так что ложка все еще к обеду


    1. Petrelevich
      23.03.2019 00:58

      Об интересных и малоизвестных особенностях никогда не поздно узнать.


  1. million
    23.03.2019 13:25

    Не знаю что они изменили в JDK 9, но тесты куска кода

    String message = "Hello";
         for (int i=0; i<25; i++)
         {
            message += i;
         }
         out.println(message);

    Что в 8 что в 11 работают одинаково медленно и жрут очень много памяти. То что немного убыстрили в 11 — это есть и в основном связано что в JDK9 сделали хранение строк либо в байтах (если ASCII строка) либо в символах (для строк в других кодировках). Если проводить тест с ASCII символами, то памяти меньше расходуется, если с русскими — то так же как и в JDK8


    1. Petrelevich
      23.03.2019 18:58

      Как вы измеряли сторость и потребление памяти?


      1. million
        23.03.2019 23:54
        -1

        Классически: System.currentTimeMillis() и через Runtime.


        1. Petrelevich
          24.03.2019 00:03

          Такие тесты без применения JMH кажутся странными.


          1. million
            24.03.2019 00:06

            Почему? javap дает более точный результат чем реальное выполнение циклов?
            В чем подвох? Неправильно работают внутренние процедуры/функции Java?


            1. Petrelevich
              24.03.2019 00:10

              Подвов в методике проведения микробенчмарков.
              Что реально даст подобное измерение на цикле в 25 итераций?
              Посмотрите доклады Шепелева про методики оценки производительности кода.


              1. million
                24.03.2019 01:06

                Ну если мне ловить микросекунды, то да, наверное не правильный метод. Но если я запускаю цикл в 10 тысяч итераций, то этот метод нормально отображает неправильность поведения JVM при работе со строками. Мне не нужна точность, мне нужен показатель, что так делать нельзя.
                В нормальном профилировании кода, все равно видно что где ты накосячил. А для показа, так делать нельзя, классический тупой подход подходит.
                Просто в статье приводится пример что работает через StringBuffer, а в JDK 11 через какую то другую хрень, то я лично не увидел, что в JDK 11 это сработает быстрее.
                Но вот классический пример с конкатенацией в цикле и с использованием StringBuilder наглядно показывает, что в JDK8 что в JDK11 лучше использовать StringBuilder в цикле, чем конкатенацию через "+" либо String.concat().
                И ничего не поменялось в JDK11, и это видно из статьи. Да на микросекунду стало быстрей!