Конкатенация строк: concat() против оператора "+"
Предполагая строку 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);
}
}
12 ответов
Нет не совсем
Во-первых, есть небольшая разница в семантике. Если 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.
Нияз прав, но стоит также отметить, что специальный оператор + может быть преобразован в нечто более эффективное с помощью компилятора 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 при конкатенации, возможно, пустых строк, и использовать + в противном случае. Однако разница в производительности должна быть незначительной, и вам, вероятно, не стоит беспокоиться об этом.
Я запустил такой же тест, как @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 раз, с 100000 повторений для каждого запуска. Вот результаты:
StringBuilder
выигрывает руки вниз Результат времени был равен 0 для большинства запусков, а самый длинный - 16 мс.a += b
занимает около 40000 мс (40 с) для каждого запуска.concat
требуется только 10000 мс (10 с) за цикл.
Я еще не декомпилировал класс, чтобы увидеть внутреннее устройство или запустить его через профилировщик, но я подозреваю, что a += b
тратит большую часть времени на создание новых объектов StringBuilder
а затем преобразовать их обратно в String
,
Большинство ответов здесь с 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
Том прав, точно описав, что делает оператор +. Это создает временный StringBuilder
, добавляет части, и заканчивается toString()
,
Тем не менее, все ответы до сих пор игнорируют эффекты оптимизации во время выполнения HotSpot. В частности, эти временные операции распознаются как общий шаблон и заменяются более эффективным машинным кодом во время выполнения.
@marcio: вы создали микро-тест; в современных JVM это недопустимый способ профилировать код.
Причиной оптимизации во время выполнения является то, что многие из этих различий в коде - даже в том числе создание объектов - после запуска HotSpot полностью различаются. Единственный способ узнать наверняка - это профилировать ваш код на месте.
Наконец, все эти методы на самом деле невероятно быстры. Это может быть случай преждевременной оптимизации. Если у вас есть код, который объединяет строки много, способ получить максимальную скорость, вероятно, не имеет ничего общего с тем, какие операторы вы выбираете, а вместо этого алгоритм, который вы используете!
Как насчет простого тестирования? Использовал код ниже:
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"
версия выполнена за 2500мс.a.concat(b)
выполнено в 1200мс.
Проверено несколько раз. concat()
Исполнение версии занимало в среднем половину времени.
Этот результат удивил меня, потому что concat()
метод всегда создает новую строку (он возвращает "new String(result)
". Хорошо известно, что:
String a = new String("a") // more than 20 times slower than String a = "a"
Почему компилятор не был способен оптимизировать создание строки в коде "a + b", зная, что это всегда приводит к одной и той же строке? Это могло бы избежать создания новой строки. Если вы не верите утверждению выше, проверьте себя.
В принципе, есть два важных различия между + и concat
метод.
Если вы используете метод concat, то вы сможете конкатенировать только строки, а в случае оператора + вы можете конкатенировать строку с любым типом данных.
Например:
String s = 10 + "Hello";
В этом случае на выходе должно быть 10 Hello.
String s = "I"; String s1 = s.concat("am").concat("good").concat("boy"); System.out.println(s1);
В приведенном выше случае вы должны предоставить две строки обязательно.
Второе и главное отличие между + и 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. Наслаждаться:)
Для полноты картины я хотел бы добавить, что определение оператора '+' можно найти в JLS SE8 15.18.1:
Если только одно выражение операнда имеет тип String, то преобразование строки (§5.1.11) выполняется для другого операнда для получения строки во время выполнения.
Результатом конкатенации строк является ссылка на объект String, который является конкатенацией двух строк операндов. Символы левого операнда предшествуют символам правого операнда во вновь создаваемой строке.
Объект String создается заново (§12.5), если выражение не является константным выражением (§15.28).
О реализации JLS говорит следующее:
Реализация может выбрать выполнение преобразования и объединения за один шаг, чтобы избежать создания, а затем отбрасывания промежуточного объекта String. Чтобы повысить производительность многократной конкатенации строк, компилятор Java может использовать класс StringBuffer или аналогичный метод для уменьшения числа промежуточных объектов String, которые создаются путем вычисления выражения.
Для примитивных типов реализация также может оптимизировать создание объекта-обертки путем прямого преобразования из примитивного типа в строку.
Таким образом, исходя из того, что "компилятор Java может использовать класс StringBuffer или аналогичный метод для сокращения", разные компиляторы могут создавать разные байт-коды.
Я так не думаю.
a.concat(b)
реализован в String, и я думаю, что реализация не сильно изменилась с ранних Java-машин. +
Реализация операции зависит от версии Java и компилятора. В настоящее время +
реализуется с использованием StringBuffer
сделать операцию как можно быстрее. Возможно, в будущем это изменится. В более ранних версиях Java +
работа со строками была намного медленнее, поскольку она давала промежуточные результаты.
я думаю что +=
реализуется с использованием +
и так же оптимизированы.
Оператор + может работать между строкой и значением типа данных string, char, integer, double или float. Он просто конвертирует значение в свое строковое представление перед конкатенацией.
Оператор concat может выполняться только со строками. Он проверяет совместимость типов данных и выдает ошибку, если они не совпадают.
Кроме того, код, который вы предоставили, делает то же самое.
При использовании + скорость уменьшается с увеличением длины строки, но при использовании concat скорость более стабильна, и лучшим вариантом для этого является использование класса StringBuilder, который имеет стабильную скорость.
Я думаю, вы можете понять, почему. Но самый лучший способ создания длинных строк - это использование StringBuilder() и append(), любая скорость будет недопустимой.
Обратите внимание, что
s.concat("hello");
приведет к
NullPointereException
когда s равно нулю. В Java поведение оператора + обычно определяется левым операндом:
System.out.println(3 + 'a'); //100
Однако строки являются исключением. Если любой из операндов является строкой, ожидается, что результатом будет строка. По этой причине null преобразуется в "null", даже если вы ожидаете, что
RuntimeException
.