З'єднання рядків: оператор concat () проти "+"


499

Припускаючи рядок a і b:

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

Під кришкою вони одне й те саме?

Ось стислий текст, декомпільований як посилання. Я хотів би мати можливість декомпілювати +оператор, щоб побачити, що це робить.

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);
    }
}


3
Я не впевнений, що +можна декомпілювати.
Гален Наре

1
Використовуйте javap, щоб розібрати файл класу Java.
Гарячі лизання

Через "незмінність" ви, ймовірно, повинні використовувати StringBufferабо StringBuilder- (нитка небезпечна таким чином швидше, замість цього
Ujjwal Singh

Відповіді:


560

Ні, не зовсім.

По-перше, є незначна різниця в семантиці. Якщо aє null, то a.concat(b)кидає , NullPointerExceptionале a+=bбуде ставитися до первісної вартості , aяк якщо б воно було null. Крім того, concat()метод приймає лише Stringзначення, тоді як +оператор мовчки перетворює аргумент у String (використовуючи toString()метод для об'єктів). Тож concat()метод суворіший у тому, що він приймає.

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

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

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

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. Ви можете бачити, що ви створюєте масив char (змінюючи розмір за необхідності), а потім викидаєте його під час створення фіналу String. На практиці розподіл пам’яті напрочуд швидко.

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


4
@HyperLink Код можна побачити за допомогою javap -cкомпільованого класу, який його використовує. (О, як у відповіді. Вам просто потрібно інтерпретувати розбирання байт-коду, що не повинно бути таким складним.)
Том Хоутін - таклін

1
Ви можете проконсультуватися зі специфікацією JVM, щоб зрозуміти окремі байт-коди. Те, про що ви хочете посилатись, знаходиться у главі 6. Трохи незрозуміло, але ви можете зрозуміти суть його досить легко.
Гарячі лизи

1
Цікаво, чому компілятор Java використовує StringBuilderнавіть при з'єднанні двох рядків? Якщо Stringвключені статичні методи об'єднання до чотирьох рядків або всіх рядків у a String[], код може додавати до чотирьох рядків з двома виділеннями об'єктів (результат Stringта його резервування char[], ні один зайвий), так і будь-яку кількість рядків з трьома виділеннями ( String[], результат String, а підкладка char[], тільки перший є зайвим). Як це, використовуючи StringBuilderволю в кращому випадку буде потрібно чотири розподілу, і зажадають копіювання кожного персонажа двічі.
supercat

Цей вираз, a + = b. Чи це не означає: a = a + b?
найповажніший сер

3
Все змінилося з моменту створення цієї відповіді. Прочитайте нижче мою відповідь.
Paweł Adamski

90

Niyaz вірно, але також варто зазначити, що спеціальний + оператор може бути перетворений на щось більш ефективне компілятором Java. У Java є клас StringBuilder, який представляє собою безпечний, не змінюваний потік, String String. Виконуючи купу струнних конкатенацій, компілятор Java мовчки перетворює

String a = b + c + d;

в

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

що для великих струн значно ефективніше. Наскільки мені відомо, це не відбувається, коли ви використовуєте метод concat.

Однак метод concat є більш ефективним при об'єднанні порожньої рядка в існуючу струну. У цьому випадку JVM не потрібно створювати новий об'єкт String і може просто повернути існуючий. Для підтвердження цього див. Документацію .

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


concat infact цього не робить. Я відредагував свою публікацію з декомпіляцією методу concat
shsteimer

10
фактично це робить. Подивіться перші рядки вашого короткого коду. Проблема з concat полягає в тому, що він завжди генерує нову String ()
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 к. Повторень за кожний пробіг. Ось результати:

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

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


4
Час створення об’єкта дійсно має значення. Ось чому у багатьох ситуаціях ми використовуємо StringBuilder безпосередньо, а не використовуючи StringBuilder поза +.
coolcfan

1
@coolcfan: Якщо +використовується для двох рядків, чи є випадки, коли використання StringBuilderкраще, ніж було б String.valueOf(s1).concat(s2)? Будь-яка ідея, чому компілятори не використовуватимуть останнє [або пропускають valueOfвиклик у випадках, коли, s1як відомо, немає значення]?
supercat

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], але це був би єдиний об'єкт, який повинен бути побудований покинутим, тоді як використання a StringBuilderвимагає побудови та відмови від StringBuilderекземпляра та принаймні одного char[]резервного сховища.
Supercat

@supercat У String.join()Java 8 були додані деякі статичні методи як швидкі обгортки синтаксису навколо java.util.StringJoinerкласу.
Ti Strga

@TiStrga: Чи +змінився режим обробки таких функцій?
supercat

@supercat Це порушило б бінарну сумісність, тому ні. Це було лише у відповідь на ваш коментар "чому String ніколи не включав статичну функцію": тепер є така функція. Решта вашої пропозиції (рефакторинг +на її використання) потребуватиме більше, ніж те, що на жаль Java готові змінити, на жаль.
Ti Strga

@TiStrga: Чи може який-небудь файл байт-коду Java може вказати "Якщо функція X доступна, зателефонуйте їй; інакше зробіть щось інше" таким чином, що можна було б вирішити в процесі завантаження класу? Генерування коду статичним методом, який може або ланцюг статичного методу Java, або використовувати інсталятор струн, якщо цього немає в наявності, здавалося б оптимальним рішенням.
supercat

22

Том правильно описує, що саме робить + оператор. Він створює тимчасовий StringBuilder, додає деталі і закінчує toString().

Однак усі відповіді поки що ігнорують ефекти оптимізації виконання HotSpot. Зокрема, ці тимчасові операції визнаються загальною схемою і замінюються більш ефективними машинними кодами під час виконання.

@marcio: Ви створили мікро-орієнтир ; У сучасних JVM це не допустимий спосіб кодування профілю.

Причина оптимізації часу роботи полягає в тому, що багато з цих відмінностей у коді - навіть у тому числі і створення об’єктів - абсолютно різні, коли HotSpot починає працювати. Єдиний спосіб точно знати - це профілювання коду in situ .

Нарешті, всі ці методи насправді неймовірно швидкі. Це може бути випадком передчасної оптимізації. Якщо у вас є код, який багато об'єднує рядки, спосіб отримання максимальної швидкості, ймовірно, не має нічого спільного з тими операторами, які ви обираєте, а замість алгоритму, який ви використовуєте!


Я маю на увазі, що під "цими тимчасовими операціями" ви маєте на увазі використання аналізу втечі для виділення "купи" об'єктів на стеку, де можна підтвердити правильність. Хоча аналіз евакуації присутній у HotSpot (корисно для видалення деякої синхронізації), я не вірю, є на момент написання, u
Том Хоутін - 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", знаючи, що це завжди призводило до тієї ж строки? Це могло б уникнути створення нового рядка. Якщо ви не вірите твердженню вище, перевірте себе.


Я перевірив ваш код jdk1.8.0_241 ваш код, для мене код "a + b" дає оптимізовані результати. З concat (): 203ms та з "+": 113ms . Я думаю, що в попередньому випуску це було не так оптимізовано.
Аккі

6

В основному існують дві важливі відмінності між + та concatметодом.

  1. Якщо ви використовуєте метод concat, то ви зможете об'єднати рядки лише тоді, коли у випадку оператора + ви також можете об'єднати рядок з будь-яким типом даних.

    Наприклад:

    String s = 10 + "Hello";

    У цьому випадку вихід повинен бути 10Hello .

    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 pool. Отже, у цьому випадку у нас є 4 рядкові літерали. Тому, очевидно, принаймні 4 об’єкти повинні бути створені в пулі.
Діпак Шарма

1
Я не думаю, що так: String s="I"+"am"+"good"+"boy"; String s2 = "go".concat("od"); System.out.println(s2 == s2.intern());друкується true, а значить, "good"не було в пулу рядків перед викликомintern()
fabian

Я говорю лише про цей рядок 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 або подібної методики" різні компілятори можуть створювати різні байт-коди.


2

Оператор + може працювати між рядком і рядком, знаком char, integer, подвійним або плаваючим типом даних. Він просто перетворює значення в його рядкове подання перед конкатенацією.

Оператор concat може бути виконаний тільки на рядках і з ними. Він перевіряє сумісність типів даних і видає помилку, якщо вони не відповідають.

Крім цього, код, який ви надали, робить те саме.


2

Я не думаю, що так.

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

Я думаю, що +=це реалізовано за допомогою +та аналогічним чином оптимізовано.


7
"В даний час + реалізовано за допомогою StringBuffer" False It StringBuilder. StringBuffer - безпечний потік імпульсу StringBuilder.
Фредерік Морін

1
Раніше він був StringBuffer до java 1.5, як це була версія, коли StringBuilder був вперше представлений.
ccpizza

0

При використанні + швидкість зменшується у міру збільшення довжини струни, але при використанні concat швидкість є стабільнішою, а найкращим варіантом є використання класу StringBuilder, який має стабільну швидкість для цього.

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


1
використання оператора + еквівалентно використанню StringBuilder ( docs.oracle.com/javase/specs/jls/se8/html/… )
ihebiheb
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.