Автор оригинала: Gábor László Hajba.
Я видел быструю подсказку здесь, в Codementor, и я хочу объяснить ее более подробно — чтобы вы знали, почему это лучшая практика.
Сам Быстрый Совет
Давайте посмотрим, какую лучшую практику я пересмотрю: “Если вы хотите объединить строки, не используйте”+”, а строковый конструктор, потому что каждый раз, когда вы используете”+”, создается новый объект, и в памяти у вас много неиспользуемых объектов”.
Вы можете прочитать об этой передовой практике в Кратком руководстве Суреша Атты здесь .
Ну, на самом деле, что он имеет в виду под “создается новый объект”?
Пример
Давайте начнем с примера из этого краткого совета. Я немного изменил его, чтобы тоже было немного контента:
String myString = ""; for(int i = 0; i < 1_000_000; i++) { myString += i; } System.out.println(myString);
Как вы можете видеть, я начинаю с пустой строки, а затем создаю цикл из миллиона чисел ( 1_000_000
это способ, которым вы можете написать 1000000 в Java 8-и на самом деле я нахожу его лучше и читабельнее).
Давайте скомпилируем и не запускаем приложение! Это требует времени, очень много времени! Но почему?
Как упоминает Суреш, этот код создаст миллион объектов впустую и уничтожит вашу память. Честно говоря, это создает около 2000000 объектов-намного больше, чем он думает.
Чтобы убедиться в этом, давайте посмотрим на байт-код сгенерированного файла .class
, однако я не хочу, чтобы вы понимали, что здесь происходит, я включаю его для краткости:
public static void main(java.lang.String...); Code: 0: ldc #2 // String 2: astore_1 3: iconst_0 4: istore_2 5: iload_2 6: ldc #3 // int 1000000 8: if_icmpge 36 11: new #4 // class java/lang/StringBuilder 14: dup 15: invokespecial #5 // Method java/lang/StringBuilder."":()V 18: aload_1 19: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 22: iload_2 23: invokevirtual #7 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 26: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 29: astore_1 30: iinc 2, 1 33: goto 5 36: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream; 39: aload_1 40: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 43: return
Это байтовый код, который Java создает при компиляции приложения, вы можете посмотреть на него с помощью
javap -c YourClass.class
команда.
Интересная часть находится между строками 5 и 33 . Это цикл для
в примере. Если вы посмотрите внимательно, вы увидите, что в теле цикла создается новый StringBuilder
(строка 11 ) каждый раз в теле цикла с текущим содержимым myString
(строка 19 ), а затем текущее значение i
(строка 23 ) также добавляется в конструктор. Затем текущее значение StringBuilder
преобразуется Строка
и myString
получает это присвоенное значение (строка 26 ).
И это не действие по исполнению. Создание объекта требует времени и ресурсов, поэтому приведенный выше пример кода занимает слишком много времени.
Решение с Быстрыми Подсказками
Теперь пришло время взглянуть на краткий совет, который является нашей лучшей практикой с тех пор:
StringBuilder sb = new StringBuilder(); for(int i = 0; i < 1_000_000; i++) { sb.append(i); } System.out.println(sb.toString());
Здесь вы создаете StringBuilder
вне цикла for, добавляете значение я
обращаюсь к строителю, и в конце вы распечатываете результат. Вы можете запустить это приложение, потому что оно завершается в кратчайшие сроки (единственное, что отнимает много времени,-это распечатать результат на консоли, потому что это операция ввода-вывода).
Теперь давайте также посмотрим на байт-код и сравним решения:
public static void main(java.lang.String...); Code: 0: new #2 // class java/lang/StringBuilder 3: dup 4: invokespecial #3 // Method java/lang/StringBuilder."":()V 7: astore_1 8: iconst_0 9: istore_2 10: iload_2 11: ldc #4 // int 1000000 13: if_icmpge 28 16: aload_1 17: iload_2 18: invokevirtual #5 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 21: pop 22: iinc 2, 1 25: goto 10 28: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream; 31: aload_1 32: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 35: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 38: return }
Цикл для
здесь находится между строками 10 и 25 . Как вы можете видеть здесь, код добавляется только в онлайн-строку 4 создано StringBuilder
без создания чего-либо нового. После завершения цикла содержимое StringBuilder
преобразуется в строку и отображается на стандартном out
.
Это означает, что мы убедились, что эта старая “лучшая практика” все еще верна, и мы знаем, почему следует придерживаться этой практики.
Вне цикла
Прочитав эту статью, я задаюсь вопросом, как насчет объединения строк вне циклов? Должны ли мы использовать там Строковый конструктор
или использует +
нормально?
Давайте посмотрим, что говорит нам код.
Прежде всего, это простой блок кода, который объединяет некоторые строки в предложение:
String greeting = "Hello" + " " + "World" + "!"; System.out.println(greeting);
Этот пример очень прост: мы объединяем эти четыре простые строки. Что происходит после компиляции?
public static void main(java.lang.String...); Code: 0: ldc #2 // String Hello World! 2: astore_1 3: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 6: aload_1 7: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 10: return
На самом деле никакой магии: код просто хранит значение “Привет, мир!” в виде одной строки. Итак, давайте назначим “Привет” и “Мир” переменным:
String hello = "Hello"; String world = "World"; String greeting = hello + " " + world + "!"; System.out.println(greeting);
Теперь это позволяет компилятору творить чудеса:
public static void main(java.lang.String...); Code: 0: ldc #2 // String Hello 2: astore_1 3: ldc #3 // String World 5: astore_2 6: new #4 // class java/lang/StringBuilder 9: dup 10: invokespecial #5 // Method java/lang/StringBuilder."":()V 13: aload_1 14: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 17: ldc #7 // String 19: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 22: aload_2 23: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 26: ldc #8 // String ! 28: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 31: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 34: astore_3 35: getstatic #10 // Field java/lang/System.out:Ljava/io/PrintStream; 38: aload_3 39: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 42: return
Как вы можете видеть, компилятор создает StringBuilder
и добавляет четыре строительных блока, прежде чем преобразовать его в приветствие
Строку. Поэтому в данном случае не имеет значения, создадим ли мы StringBuilder
сами или объединим с +
.
Как насчет того, чтобы быть немного более хитрым и делать то же самое, что мы делали в цикле, и объединять приветствие шаг за шагом?
String hello = "Hello"; String world = "World"; String greeting = hello; greeting += " "; greeting += world; greeting += "!"; System.out.println(greeting);
Теперь я действительно противен — и компилятор не видит моего трюка, и мы сталкиваемся с той же проблемой, что и с циклом: мы получаем много StringBuilder
созданных, которые потребляют память и время:
public static void main(java.lang.String...); Code: 0: ldc #2 // String Hello 2: astore_1 3: ldc #3 // String World 5: astore_2 6: aload_1 7: astore_3 8: new #4 // class java/lang/StringBuilder 11: dup 12: invokespecial #5 // Method java/lang/StringBuilder."":()V 15: aload_3 16: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 19: ldc #7 // String 21: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 27: astore_3 28: new #4 // class java/lang/StringBuilder 31: dup 32: invokespecial #5 // Method java/lang/StringBuilder." ":()V 35: aload_3 36: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 39: aload_2 40: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 43: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 46: astore_3 47: new #4 // class java/lang/StringBuilder 50: dup 51: invokespecial #5 // Method java/lang/StringBuilder." ":()V 54: aload_3 55: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 58: ldc #9 // String ! 60: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 63: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 66: astore_3 67: getstatic #10 // Field java/lang/System.out:Ljava/io/PrintStream; 70: aload_3 71: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 74: return
Естественно, мы не видим никакого влияния на производительность этого небольшого приложения, но представьте себе более масштабный рабочий процесс, в котором вы добавляете значения в строку здесь и там, чтобы получить конечный результат (если вы спросите меня, я бы упомянул в качестве примера конструктор запросов, в котором вы можете создавать SQL-запрос на основе различных критериев).
Линии 8 , 28 и 47 создайте новый StringBuilder
объекты так для каждого +=
идет строительство нового объекта.
Java 8 и присоединяйтесь
Java 8 представила новый строковый метод: join
. Если мы этим занимаемся, давайте посмотрим, как join
выполняет свою работу за кулисами.
String greeting = String.join("", "Hello", " ", "World", "!"); System.out.println(greeting);
Это опять же очень простой пример, где я использую пустую строку в качестве разделителя и добавляю символ пробела в качестве строки в список для объединения. Результат на консоли будет таким, как ожидалось Привет, мир!
.
Теперь давайте взглянем на байт-код:
public static void main(java.lang.String...); Code: 0: ldc #2 // String 2: iconst_4 3: anewarray #3 // class java/lang/CharSequence 6: dup 7: iconst_0 8: ldc #4 // String Hello 10: aastore 11: dup 12: iconst_1 13: ldc #5 // String 15: aastore 16: dup 17: iconst_2 18: ldc #6 // String World 20: aastore 21: dup 22: iconst_3 23: ldc #7 // String ! 25: aastore 26: invokestatic #8 // Method java/lang/String.join:(Ljava/lang/CharSequence;[Ljava/lang/CharSequence;)Ljava/lang/String; 29: astore_1 30: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream; 33: aload_1 34: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 37: return
Ну, это немного кода для такой короткой программы. Давайте проанализируем, что происходит!
На линии 0 он инициализирует строку без значения (это разделитель) в строке 8 , 13 , 18 и 23 он создает строки для строительных блоков приветствия. После этого значения объединяются в объект приветствие
, который затем отображается.
Вывод
Некоторым лучшим практикам стоит доверять, однако вы должны знать, почему вы их используете. Если вы “просто делаете”, то подумайте о том, чтобы задать вопрос о причинах этого, потому что может случиться так, что это старая практика, и компилятор с некоторых пор изменил поведение вашего кода.
В этой статье я показал вам различные подходы к объединению строк и то, как они могут повлиять на производительность, если вы используете их чрезмерно. Кроме того, я доказал, что лучшая практика по-прежнему верна, и объяснил причины соответствующего быстрого совета.
Оригинал: “https://www.codementor.io/@ghajba/string-concatenation-revised-du1082cil”