Конкатенация строк: concat () против оператора «+»

499

Предполагая строку a и b:

a += b
a = a.concat(b)

Под капотом они одно и то же?

Здесь concat декомпилирован как ссылка. Я хотел бы иметь возможность декомпилировать +оператора, чтобы увидеть, что это делает.

public String concat(String s) {

    int i = s.length();
    if (i == 0) {
        return this;
    }
    else {
        char ac[] = new char[count + i];
        getChars(0, count, ac, 0);
        s.getChars(0, i, ac, count);
        return new String(0, count + i, ac);
    }
}
shsteimer
источник
3
возможный дубликат StringBuilder против конкатенации строк в toString () в Java
Грег Мэттес
3
Я не уверен, что +можно декомпилировать.
Гален Наре
1
Используйте javap для дизассемблирования файла классов Java.
Hot Licks
Из-за «неизменности» вы, вероятно, должны использовать StringBufferили StringBuilder- (поток небезопасен, поэтому быстрее, вместо этого
Ujjwal Singh

Ответы:

560

Нет не совсем

Во-первых, есть небольшая разница в семантике. Если aесть null, то a.concat(b)бросает, NullPointerExceptionно a+=bбудет обрабатывать исходное значение, aкак если бы это было null. Кроме того, concat()метод принимает только Stringзначения, в то время как +оператор автоматически преобразует аргумент в строку (используя toString()метод для объектов). Таким образом, concat()метод более строг в том, что он принимает.

Чтобы заглянуть под капот, напишите простой класс с a += b;

public class Concat {
    String cat(String a, String b) {
        a += b;
        return a;
    }
}

Теперь разберите с javap -c(входит в Sun JDK). Вы должны увидеть список, включающий:

java.lang.String cat(java.lang.String, java.lang.String);
  Code:
   0:   new     #2; //class java/lang/StringBuilder
   3:   dup
   4:   invokespecial   #3; //Method java/lang/StringBuilder."<init>":()V
   7:   aload_1
   8:   invokevirtual   #4; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   11:  aload_2
   12:  invokevirtual   #4; //Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   15:  invokevirtual   #5; //Method java/lang/StringBuilder.toString:()Ljava/lang/    String;
   18:  astore_1
   19:  aload_1
   20:  areturn

Итак, a += bявляется эквивалентом

a = new StringBuilder()
    .append(a)
    .append(b)
    .toString();

concatМетод должен быть быстрее. Однако при большем количестве строк StringBuilderметод побеждает, по крайней мере, с точки зрения производительности.

Исходный код Stringи StringBuilder(и его закрытый для пакета базовый класс) доступен в src.zip Sun JDK. Вы можете видеть, что вы создаете массив символов (изменяя размер по мере необходимости), а затем выбрасываете его, когда создаете финал String. На практике распределение памяти происходит на удивление быстро.

Обновление: как отмечает Павел Адамски, производительность изменилась в более позднем HotSpot. javacвсе еще производит точно такой же код, но компилятор байт-кода обманывает. Простое тестирование полностью терпит неудачу, потому что весь объем кода отбрасывается. Суммирование System.identityHashCode(не String.hashCode) показывает, что StringBufferкод имеет небольшое преимущество. Возможны изменения при выходе следующего обновления или при использовании другой JVM. От @lukaseder , список встроенных функций HotSpot JVM .

Том Хотин - Tackline
источник
4
@HyperLink Вы можете увидеть код javap -cв скомпилированном классе, который его использует. (О, как в ответе. Вам просто нужно интерпретировать разборку байт-кода, что не должно быть так сложно.)
Том Хотин - tackline
1
Вы можете обратиться к спецификации JVM, чтобы понять отдельные байтовые коды. Материал, на который вы хотите сослаться, находится в главе 6. Немного неясный, но вы можете понять его довольно легко.
Hot Licks
1
Интересно, почему компилятор Java использует StringBuilderдаже при соединении двух строк? Если Stringвключены статические методы для конкатенации до четырех строк или всех строк в String[]коде, можно добавить до четырех строк с двумя выделениями объектов (результат Stringи его поддержка char[], ни один с избыточностью), и любое количество строк с тремя выделениями ( String[], результат String, а подложка char[], только первый является излишним). Как это, используя StringBuilderволю в лучшем случае потребуется четыре распределения, и потребуют копирования каждого персонажа дважды.
суперкат
Это выражение, а + = б. Разве это не значит: a = a + b?
самый почтенный сэр
3
С тех пор, как этот ответ был создан, все изменилось. Пожалуйста, прочитайте мой ответ ниже.
Павел Адамский
90

Нияз прав, но также стоит отметить, что специальный оператор + может быть преобразован во что-то более эффективное компилятором Java. В Java есть класс StringBuilder, который представляет не поточнобезопасную изменяемую строку. При выполнении связки строк String, компилятор Java молча преобразует

String a = b + c + d;

в

String a = new StringBuilder(b).append(c).append(d).toString();

что для больших струн значительно эффективнее. Насколько я знаю, этого не происходит при использовании метода concat.

Однако метод concat более эффективен при объединении пустой строки в существующую строку. В этом случае JVM не нужно создавать новый объект String, и он может просто вернуть существующий. См. Документацию concat, чтобы подтвердить это.

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

Эли Кортрайт
источник
Concat Infact не делает этого. Я отредактировал свой пост декомпиляцией метода
concat
10
Infact это делает. Посмотрите первые строки вашего concat-кода. Проблема с concat заключается в том, что он всегда генерирует новую строку ()
Marcio Aguiar
2
@MarcioAguiar: возможно, вы имеете в виду, что + всегда генерирует новое String- как вы говорите, concatесть одно исключение, когда вы конкатанируете пустое String.
Blaisorblade
45

Я запустил такой же тест как @marcio, но вместо этого использовал следующий цикл:

String c = a;
for (long i = 0; i < 100000L; i++) {
    c = c.concat(b); // make sure javac cannot skip the loop
    // using c += b for the alternative
}

Просто для хорошей меры, я также добавил StringBuilder.append(). Каждый тест был выполнен 10 раз, с 100 000 повторений для каждого запуска. Вот результаты:

  • StringBuilderвыигрывает руки вниз Результат времени был равен 0 для большинства запусков, а самый длинный - 16 мс.
  • a += b занимает около 40000 мс (40 с) для каждого запуска.
  • concat требуется только 10000 мс (10 с) за цикл.

Я еще не декомпилировал класс, чтобы увидеть внутреннее устройство или запустить его через профилировщик, но я подозреваю, что a += bбольшую часть времени проводит, создавая новые объекты StringBuilderи затем преобразовывая их обратно String.

ckpwong
источник
4
Время создания объекта действительно имеет значение. Вот почему во многих ситуациях мы используем StringBuilder напрямую, а не пользуемся преимуществом StringBuilder за +.
coolcfan
1
@coolcfan: Когда +используется для двух строк, есть ли случаи, когда использование StringBuilderлучше, чем было бы String.valueOf(s1).concat(s2)? Любая идея, почему компиляторы не будут использовать последний [или иначе пропустить valueOfвызов в случаях, когда s1известно, что он не равен нулю]?
Суперкат
1
@supercat прости, я не знаю. Может быть, люди, которые стоят за этим сахаром, являются лучшими, чтобы ответить на это.
coolcfan
25

Большинство ответов здесь с 2008 года. Похоже, что со временем все изменилось. Мои последние тесты, сделанные с помощью JMH, показывают, что в Java 8 +примерно в два раза быстрее, чемconcat .

Мой тест:

@Warmup(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS)
@Measurement(iterations = 5, time = 200, timeUnit = TimeUnit.MILLISECONDS)
public class StringConcatenation {

    @org.openjdk.jmh.annotations.State(Scope.Thread)
    public static class State2 {
        public String a = "abc";
        public String b = "xyz";
    }

    @org.openjdk.jmh.annotations.State(Scope.Thread)
    public static class State3 {
        public String a = "abc";
        public String b = "xyz";
        public String c = "123";
    }


    @org.openjdk.jmh.annotations.State(Scope.Thread)
    public static class State4 {
        public String a = "abc";
        public String b = "xyz";
        public String c = "123";
        public String d = "!@#";
    }

    @Benchmark
    public void plus_2(State2 state, Blackhole blackhole) {
        blackhole.consume(state.a+state.b);
    }

    @Benchmark
    public void plus_3(State3 state, Blackhole blackhole) {
        blackhole.consume(state.a+state.b+state.c);
    }

    @Benchmark
    public void plus_4(State4 state, Blackhole blackhole) {
        blackhole.consume(state.a+state.b+state.c+state.d);
    }

    @Benchmark
    public void stringbuilder_2(State2 state, Blackhole blackhole) {
        blackhole.consume(new StringBuilder().append(state.a).append(state.b).toString());
    }

    @Benchmark
    public void stringbuilder_3(State3 state, Blackhole blackhole) {
        blackhole.consume(new StringBuilder().append(state.a).append(state.b).append(state.c).toString());
    }

    @Benchmark
    public void stringbuilder_4(State4 state, Blackhole blackhole) {
        blackhole.consume(new StringBuilder().append(state.a).append(state.b).append(state.c).append(state.d).toString());
    }

    @Benchmark
    public void concat_2(State2 state, Blackhole blackhole) {
        blackhole.consume(state.a.concat(state.b));
    }

    @Benchmark
    public void concat_3(State3 state, Blackhole blackhole) {
        blackhole.consume(state.a.concat(state.b.concat(state.c)));
    }


    @Benchmark
    public void concat_4(State4 state, Blackhole blackhole) {
        blackhole.consume(state.a.concat(state.b.concat(state.c.concat(state.d))));
    }
}

Результаты:

Benchmark                             Mode  Cnt         Score         Error  Units
StringConcatenation.concat_2         thrpt   50  24908871.258 ± 1011269.986  ops/s
StringConcatenation.concat_3         thrpt   50  14228193.918 ±  466892.616  ops/s
StringConcatenation.concat_4         thrpt   50   9845069.776 ±  350532.591  ops/s
StringConcatenation.plus_2           thrpt   50  38999662.292 ± 8107397.316  ops/s
StringConcatenation.plus_3           thrpt   50  34985722.222 ± 5442660.250  ops/s
StringConcatenation.plus_4           thrpt   50  31910376.337 ± 2861001.162  ops/s
StringConcatenation.stringbuilder_2  thrpt   50  40472888.230 ± 9011210.632  ops/s
StringConcatenation.stringbuilder_3  thrpt   50  33902151.616 ± 5449026.680  ops/s
StringConcatenation.stringbuilder_4  thrpt   50  29220479.267 ± 3435315.681  ops/s
Павел Адамский
источник
Интересно, почему Java Stringникогда не включал статическую функцию для формирования строки путем объединения элементов a String[]. Использование +для объединения 8 строк с использованием такой функции потребовало бы конструирования и последующего отказа String[8], но это был бы единственный объект, который нужно было бы создать оставленным, в то время как использование объекта StringBuilderпотребовало бы создания и отказа от StringBuilderэкземпляра и по крайней мере одного char[]резервного хранилища.
суперкат
@supercat Некоторые статические String.join()методы были добавлены в Java 8 как быстрые синтаксические обёртки вокруг java.util.StringJoinerкласса.
Ти Стрга
@TiStrga: изменилась ли обработка +для использования таких функций?
суперкат
@supercat Это нарушило бы двоичную обратную совместимость, так что нет. Это был просто в ответ на ваш «почему строка никогда не включала статическую функцию» комментарий: теперь есть такая функция. К +сожалению, остальная часть вашего предложения (рефакторинг для его использования) потребует большего, чем то, что разработчики Java готовы изменить.
Ti Strga
@TiStrga: Есть ли способ, которым файл байт-кода Java может указать «Если функция X доступна, вызовите ее; в противном случае сделайте что-нибудь еще» способом, который может быть решен в процессе загрузки класса? Генерация кода с помощью статического метода, который может либо связать со статическим методом Java, либо использовать построитель строк, если он недоступен, может показаться оптимальным решением.
суперкат
22

Том прав в описании того, что делает оператор +. Он создает временный StringBuilder, добавляет части и заканчивается toString().

Тем не менее, все ответы до сих пор игнорируют эффекты оптимизации во время выполнения HotSpot. В частности, эти временные операции распознаются как общий шаблон и заменяются более эффективным машинным кодом во время выполнения.

@marcio: вы создали микро-тест ; в современных JVM это недопустимый способ профилировать код.

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

Наконец, все эти методы на самом деле невероятно быстрые. Это может быть случай преждевременной оптимизации. Если у вас есть код, который объединяет строки много, способ получить максимальную скорость, вероятно, не имеет ничего общего с тем, какие операторы вы выбираете, а вместо этого алгоритм, который вы используете!

Джейсон Коэн
источник
Я предполагаю, что под «этими временными операциями» вы подразумеваете использование escape-анализа для выделения «кучных» объектов в стеке, где доказуемо правильно. Хотя в HotSpot есть анализ побега (полезен для удаления некоторой синхронизации), я не верю, что это на момент написания статьи, u
Tom Hawtin - tackline
21

Как насчет простого тестирования? Использовал код ниже:

long start = System.currentTimeMillis();

String a = "a";

String b = "b";

for (int i = 0; i < 10000000; i++) { //ten million times
     String c = a.concat(b);
}

long end = System.currentTimeMillis();

System.out.println(end - start);
  • "a + b"Версия выполнена в 2500ms .
  • a.concat(b)Выполнен в 1200ms .

Проверено несколько раз. concat()Исполнение версии взяла половину времени в среднем.

Этот результат меня удивил, потому что concat()метод всегда создает новую строку (он возвращает " new String(result)". Хорошо известно, что:

String a = new String("a") // more than 20 times slower than String a = "a"

Почему компилятор не был способен оптимизировать создание строки в коде "a + b", зная, что это всегда приводит к одной и той же строке? Это могло бы избежать создания новой строки. Если вы не верите утверждению выше, проверьте себя.

Марсио Агияр
источник
Я протестировал на java jdk1.8.0_241 ваш код, для меня код "a + b" дает оптимизированные результаты. С concat (): 203мс и с "+": 113мс . Я предполагаю, что в предыдущем выпуске это не было так оптимизировано.
Акки
6

В принципе, есть два важных различия между + и concatметодом.

  1. Если вы используете метод concat, то вы сможете конкатенировать только строки, а в случае оператора + вы можете также конкатенировать строку с любым типом данных.

    Например:

    String s = 10 + "Hello";

    В этом случае вывод должен быть Hello .

    String s = "I";
    String s1 = s.concat("am").concat("good").concat("boy");
    System.out.println(s1);

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

  2. Второе и основное отличие между + и concat заключается в том, что:

    Случай 1: Предположим, я конкатирую одни и те же строки с помощью оператора concat таким образом

    String s="I";
    String s1=s.concat("am").concat("good").concat("boy");
    System.out.println(s1);

    В этом случае общее количество объектов, созданных в пуле, равно 7:

    I
    am
    good
    boy
    Iam
    Iamgood
    Iamgoodboy

    Случай 2:

    Теперь я собираюсь объединить те же строки с помощью оператора +

    String s="I"+"am"+"good"+"boy";
    System.out.println(s);

    В приведенном выше случае общее количество созданных объектов составляет всего 5.

    На самом деле, когда мы объединяем строки с помощью оператора +, он поддерживает класс StringBuffer для выполнения той же задачи, как показано ниже:

    StringBuffer sb = new StringBuffer("I");
    sb.append("am");
    sb.append("good");
    sb.append("boy");
    System.out.println(sb);

    Таким образом, он будет создавать только пять объектов.

Так что, ребята, это основные различия между + и методом concat . Наслаждаться :)

Дипак Шарма
источник
Мой дорогой, Вы очень хорошо знаете, что любой строковый литерал обрабатывается как сам объект String, который хранится в пуле String. Так что в этом случае у нас есть 4 строковых литерала. Очевидно, что в пуле должно быть создано не менее 4 объектов.
Дипак Шарма
1
Я так не думаю: String s="I"+"am"+"good"+"boy"; String s2 = "go".concat("od"); System.out.println(s2 == s2.intern());печатает true, что означает, что "good"не было в пуле строк до вызоваintern()
Фабиан
Я говорю только об этой строке String s = "I" + "am" + "good" + "boy"; В этом случае все 4 строковых литерала хранятся в пуле. Поэтому 4 объекта должны быть созданы в пуле.
Дипак Шарма
4

Для полноты картины я хотел бы добавить, что определение оператора '+' можно найти в JLS SE8 15.18.1 :

Если только одно выражение операнда имеет тип String, тогда преобразование строки (§5.1.11) выполняется для другого операнда для получения строки во время выполнения.

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

Объект String создается заново (§12.5), если выражение не является константным выражением (§15.28).

О реализации JLS говорит следующее:

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

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

Таким образом, исходя из того, что «компилятор Java может использовать класс StringBuffer или подобный метод для сокращения», разные компиляторы могут создавать разные байт-коды.

dingalapadum
источник
2

Оператор + может работать между строкой и значением типа данных string, char, integer, double или float. Он просто преобразует значение в свое строковое представление перед объединением.

Оператор concat может выполняться только со строками. Он проверяет совместимость типов данных и выдает ошибку, если они не совпадают.

Кроме того, код, который вы предоставили, делает то же самое.

Нияз
источник
2

Я так не думаю.

a.concat(b)реализован в String, и я думаю, что реализация не сильно изменилась с ранних Java-машин. Реализация +операции зависит от версии Java и компилятора. В настоящее время +реализовано использование, StringBufferчтобы сделать операцию максимально быстрой. Возможно, в будущем это изменится. В более ранних версиях Java +работа со строками была намного медленнее, поскольку она давала промежуточные результаты.

Я предполагаю, что +=это реализовано с использованием +и аналогичным образом оптимизировано.

Бартош Берковский
источник
7
"В настоящее время + реализован с использованием StringBuffer" False It's StringBuilder. StringBuffer является потокобезопасным имплементом StringBuilder.
Фредерик Морен
1
Раньше это был StringBuffer до Java 1.5, так как это была версия, когда StringBuilder был впервые представлен.
ccpizza
0

При использовании + скорость уменьшается по мере увеличения длины строки, но при использовании concat скорость более стабильна, и лучшим вариантом для этого является использование класса StringBuilder, который имеет стабильную скорость.

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

iamreza
источник
1
использование оператора + эквивалентно использованию StringBuilder ( docs.oracle.com/javase/specs/jls/se8/html/… )
ihebiheb