Łączenie ciągów: operator konkat () vs operator „+”


499

Zakładając ciąg A i B:

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

Czy pod maską są takie same?

Oto konkat dekompilowany jako odniesienie. Chciałbym również móc dekompilować +operatora, aby zobaczyć, co to robi.

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
Nie jestem pewien, czy +można go zdekompilować.
Galen Nare,

1
Użyj javap, aby zdemontować plik klasy Java.
Hot Licks,

Z powodu „niezmienności” prawdopodobnie powinieneś używać StringBufferlub StringBuilder- (wątek jest niebezpieczny, a więc szybszy, zamiast tego
Ujjwal Singh

Odpowiedzi:


560

Nie do końca.

Po pierwsze, istnieje niewielka różnica w semantyce. Jeśli atak null, to a.concat(b)rzuca a, NullPointerExceptionale a+=bpotraktuje oryginalną wartość atak, jakby była null. Ponadto concat()metoda akceptuje tylko Stringwartości, podczas gdy +operator po cichu przekształci argument na ciąg znaków (używając toString()metody dla obiektów). Zatem concat()metoda jest bardziej rygorystyczna pod względem tego, co akceptuje.

Aby zajrzeć pod maską, napisz prostą lekcję a += b;

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

Teraz zdemontuj za pomocą javap -c(zawartego w Sun JDK). Powinieneś zobaczyć listę zawierającą:

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

Więc a += bjest odpowiednikiem

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

concatMetoda powinna być szybsza. Jednak przy większej liczbie ciągów StringBuildermetoda wygrywa, przynajmniej pod względem wydajności.

Kod źródłowy Stringi StringBuilder(i jego podstawowa klasa pakietu) jest dostępny w src.zip Sun JDK. Możesz zobaczyć, że budujesz tablicę char (zmieniając rozmiar w razie potrzeby), a następnie wyrzucasz ją podczas tworzenia finału String. W praktyce przydzielanie pamięci jest zaskakująco szybkie.

Aktualizacja: Jak zauważa Paweł Adamski, wydajność zmieniła się w najnowszym HotSpot. javacnadal generuje dokładnie ten sam kod, ale kompilator kompilatora bajtów oszukuje. Proste testowanie całkowicie kończy się niepowodzeniem, ponieważ cały kod jest wyrzucany. Sumowanie System.identityHashCode(nie String.hashCode) pokazuje, że StringBufferkod ma niewielką przewagę. Zastrzega się możliwość zmian po wydaniu kolejnej aktualizacji lub w przypadku korzystania z innej maszyny JVM. Od @lukaseder , listy intrinsics HotSpot JVM .


4
@HyperLink Możesz zobaczyć kod javap -cw skompilowanej klasie, która go używa. (Och, jak w odpowiedzi. Musisz tylko zinterpretować demontaż kodu bajtowego, co nie powinno być takie trudne.)
Tom Hawtin - tackline

1
Możesz zapoznać się ze specyfikacją JVM, aby zrozumieć poszczególne kody bajtowe. Rzeczy, do których chciałbyś się odnieść, znajdują się w rozdziale 6. Trochę niejasne, ale można dość łatwo zrozumieć ich sedno.
Hot Licks,

1
Zastanawiam się, dlaczego kompilator Java używa StringBuildernawet podczas łączenia dwóch ciągów? Jeśli Stringuwzględnione zostaną statyczne metody konkatenacji do czterech łańcuchów lub wszystkich łańcuchów w String[], kod może dołączyć do czterech łańcuchów z dwoma przydziałami obiektów (wynik Stringi jego podstawa char[], bez jednego nadmiarowego) oraz dowolną liczbą ciągów z trzema przydziałami ( String[]wynik String, a podkład char[], a tylko pierwsze z nich są zbędne). Jak to jest, za pomocą StringBuilderwoli w najlepszym wymagają cztery przydziały, i będzie wymagać kopiowania każdy znak dwukrotnie.
supercat

To wyrażenie a + = b. Czy to nie znaczy: a = a + b?
najbardziej czcigodny panie

3
Sytuacja się zmieniła od czasu utworzenia tej odpowiedzi. Proszę przeczytać moją odpowiedź poniżej.
Paweł Adamski,

90

Niyaz ma rację, ale warto również zauważyć, że specjalny operator + może zostać przekształcony w coś bardziej wydajnego przez kompilator Java. Java ma klasę StringBuilder, która reprezentuje nie-bezpieczny dla wątków, modyfikowalny ciąg. Podczas wykonywania szeregu konkatenacji ciągów kompilator Java konwertuje po cichu

String a = b + c + d;

w

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

co w przypadku dużych łańcuchów jest znacznie bardziej wydajne. O ile mi wiadomo, nie dzieje się tak, gdy używasz metody concat.

Jednak metoda konkat jest bardziej wydajna podczas łączenia pustego ciągu z istniejącym ciągiem. W takim przypadku JVM nie musi tworzyć nowego obiektu String i może po prostu zwrócić istniejący. Zobacz dokumentację konkat, aby to potwierdzić.

Jeśli więc bardzo martwisz się wydajnością, powinieneś użyć metody concat podczas konkatenacji możliwie pustych łańcuchów, a także użyć + w przeciwnym razie. Jednak różnica w wydajności powinna być znikoma i prawdopodobnie nie powinieneś się o to martwić.


concat infact tego nie robi. Zredagowałem swój post dekompilacją metody
konkat

10
w rzeczywistości to robi. Spójrz na pierwsze wiersze kodu konkat. Problem z concat polega na tym, że zawsze generuje nowy String ()
Marcio Aguiar

2
@MarcioAguiar: może masz na myśli, że + zawsze generuje nowy String- jak mówisz, concatma jeden wyjątek, gdy konkatujesz pusty String.
Blaisorblade

45

Przeprowadziłem podobny test jak @marcio, ale z następującą pętlą:

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
}

Na wszelki wypadek też wrzuciłem StringBuilder.append(). Każdy test był uruchamiany 10 razy, z 100 000 powtórzeń dla każdego uruchomienia. Oto wyniki:

  • StringBuilderwygrywa ręce w dół. Wynik czasu zegara wynosił 0 dla większości przebiegów, a najdłuższy trwał 16ms.
  • a += b zajmuje około 40000 ms (40 s) dla każdego uruchomienia.
  • concat wymaga tylko 10000ms (10s) na przebieg.

Nie zdekompilowałem jeszcze klasy, aby zobaczyć elementy wewnętrzne lub uruchomić ją za pomocą profilera, ale podejrzewam, że a += bspędza dużo czasu na tworzeniu nowych obiektów, StringBuildera następnie konwertowaniu ich z powrotem String.


4
Czas tworzenia obiektu naprawdę ma znaczenie. Dlatego w wielu sytuacjach używamy StringBuilder bezpośrednio, zamiast korzystać z StringBuilder za +.
coolcfan

1
@coolcfan: Kiedy +używa się dwóch ciągów, czy są jakieś przypadki, w których użycie StringBuilderjest lepsze niż byłoby String.valueOf(s1).concat(s2)? Masz pojęcie, dlaczego kompilatory nie wykorzystałyby tego ostatniego [lub zrezygnowałyby z valueOfwywołania w przypadkach, w których s1wiadomo, że nie ma wartości null]?
supercat

1
@ supercat przepraszam nie wiem. Być może ludzie, którzy stoją za tym cukrem, są najlepszymi, aby na to odpowiedzieć.
coolcfan

25

Większość odpowiedzi pochodzi z 2008 roku. Wygląda na to, że z czasem wszystko się zmieniło. Moje najnowsze testy porównawcze wykonane przy użyciu JMH pokazują, że na Javie 8 +jest około dwa razy szybsza niż concat.

Mój punkt odniesienia:

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

Wyniki:

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

Zastanawiam się, dlaczego Java Stringnigdy nie zawierała funkcji statycznej do utworzenia łańcucha przez połączenie elementów String[]. Użycie +do konkatenacji 8 łańcuchów przy użyciu takiej funkcji wymagałoby skonstruowania i późniejszego porzucenia String[8], ale byłby to jedyny obiekt, który musiałby zostać skonstruowany porzucony, podczas gdy użycie StringBuilderwymagałoby skonstruowania i porzucenia StringBuilderinstancji oraz co najmniej jednego char[]magazynu zaplecza.
supercat

@ superuper Niektóre String.join()metody statyczne zostały dodane w Javie 8, jako szybkie opakowania składni w całej java.util.StringJoinerklasie.
Ti Strga

@TiStrga: Czy obsługa +zmieniła się, aby korzystać z takich funkcji?
supercat

@ supercat To złamałoby binarną kompatybilność wsteczną, więc nie. To była tylko odpowiedź na twój komentarz „dlaczego String nigdy nie zawierał funkcji statycznej”: teraz jest taka funkcja. Reszta twojej propozycji (refaktoryzacja, +aby z niej skorzystać) wymagałaby niestety tego, co deweloperzy Java są gotowi zmienić, niestety.
Ti Strga

@TiStrga: Czy jest jakiś sposób, by plik kodu bajtowego Java mógł wskazywać „Jeśli funkcja X jest dostępna, wywołaj ją; w przeciwnym razie zrób coś innego” w sposób, który można rozwiązać podczas ładowania klasy? Generowanie kodu za pomocą metody statycznej, która może albo połączyć się z metodą statyczną Java, albo użyć konstruktora łańcuchów, jeśli nie jest to możliwe, wydaje się optymalnym rozwiązaniem.
supercat

22

Tom ma rację, opisując dokładnie, co robi operator +. Tworzy tymczasowe StringBuilder, dołącza części i kończy z toString().

Jednak wszystkie dotychczasowe odpowiedzi ignorują efekty optymalizacji środowiska uruchomieniowego HotSpot. W szczególności te tymczasowe operacje są rozpoznawane jako wspólny wzorzec i są zastępowane bardziej wydajnym kodem maszynowym w czasie wykonywania.

@marcio: Utworzyłeś mikro-benchmark ; w przypadku nowoczesnych maszyn JVM nie jest to prawidłowy sposób profilowania kodu.

Przyczyną optymalizacji czasu pracy jest to, że wiele z tych różnic w kodzie - nawet w zakresie tworzenia obiektów - jest zupełnie innych, gdy HotSpot się uruchomi. Jedynym sposobem, aby się upewnić, jest profilowanie kodu na miejscu .

Wreszcie wszystkie te metody są w rzeczywistości niezwykle szybkie. Może to być przypadek przedwczesnej optymalizacji. Jeśli masz kod, który często łączy łańcuchy, sposób na uzyskanie maksymalnej prędkości prawdopodobnie nie ma nic wspólnego z tym, których operatorów wybierasz, a zamiast tego używasz algorytmu!


Sądzę, że przez „te operacje tymczasowe” rozumiesz użycie analizy ucieczki do przydzielenia obiektów „sterty” na stosie, jeśli jest to możliwe do udowodnienia. Chociaż analiza ucieczki jest obecna w HotSpot (przydatna do usuwania synchronizacji), nie wierzę, że jest w momencie pisania, u
Tom Hawtin - tackline

21

Co powiesz na proste testy? Użyłem kodu poniżej:

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"Wersja wykonana w 2500ms .
  • a.concat(b)Wykonywane 1200ms .

Testowane kilka razy. Theconcat() wersji zajęło średnio połowę czasu.

Ten wynik mnie zaskoczył, ponieważ concat()metoda zawsze tworzy nowy ciąg znaków (zwraca „ new String(result)”. Dobrze wiadomo, że:

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

Dlaczego kompilator nie był w stanie zoptymalizować tworzenia łańcucha w kodzie „a + b”, wiedząc, że zawsze zapewniał ten sam ciąg? Mogłoby to uniknąć tworzenia nowego ciągu. Jeśli nie wierzysz powyższemu stwierdzeniu, sprawdź sam.


Testowałem na java jdk1.8.0_241 twój kod, dla mnie kod „a + b” daje zoptymalizowane wyniki. Z concat (): 203ms i z „+”: 113ms . Wydaje mi się, że w poprzednim wydaniu nie było tak zoptymalizowane.
Akki

6

Zasadniczo istnieją dwie ważne różnice między + a concatmetodą.

  1. Jeśli używasz metody concat , wówczas możesz łączyć tylko łańcuchy, podczas gdy w przypadku operatora + możesz także łączyć łańcuch z dowolnym typem danych.

    Na przykład:

    String s = 10 + "Hello";

    W takim przypadku wyjście powinno wynosić 10 Cześć .

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

    W powyższym przypadku musisz podać dwa ciągi obowiązkowe.

  2. Druga i główna różnica między + a concat polega na tym, że:

    Przypadek 1: Załóżmy, że łączę te same ciągi z operatorem konkat w ten sposób

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

    W tym przypadku całkowita liczba obiektów utworzonych w puli wynosi 7 takich jak to:

    I
    am
    good
    boy
    Iam
    Iamgood
    Iamgoodboy

    Przypadek 2:

    Teraz zamierzam skonkatować te same łańcuchy za pomocą operatora +

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

    W powyższym przypadku całkowita liczba utworzonych obiektów to tylko 5.

    W rzeczywistości, gdy łączymy łańcuchy za pomocą operatora + , wówczas utrzymuje on klasę StringBuffer, aby wykonać to samo zadanie w następujący sposób:

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

    W ten sposób utworzy tylko pięć obiektów.

To są podstawowe różnice między + a metodą konkat . Cieszyć się :)


Moja droga, wiesz bardzo dobrze, że każdy literał łańcuchowy traktowany jako sam obiekt String, który przechowuje w puli String, więc w tym przypadku mamy 4 literały łańcuchowe. Oczywiście w puli powinny zostać utworzone co najmniej 4 obiekty.
Deepak Sharma

1
Nie sądzę: String s="I"+"am"+"good"+"boy"; String s2 = "go".concat("od"); System.out.println(s2 == s2.intern());odciski true, co oznacza, że "good"nie było w puli ciągów przed wywołaniemintern()
fabian

Mówię tylko o tej linii String s = "I" + "am" + "good" + "boy"; W tym przypadku wszystkie 4 literały łańcuchowe są przechowywane w puli, dlatego w puli powinny zostać utworzone 4 obiekty.
Deepak Sharma

4

Dla kompletności chciałem dodać, że definicja operatora „+” znajduje się w JLS SE8 15.18.1 :

Jeśli tylko jedno wyrażenie operandu jest typu String, wówczas konwersja łańcucha (§5.1.11) jest przeprowadzana na drugim operandzie w celu wygenerowania łańcucha w czasie wykonywania.

Rezultatem konkatenacji łańcuchów jest odwołanie do obiektu String, który jest konkatenacją dwóch łańcuchów argumentów. Znaki operandu po lewej stronie poprzedzają znaki operandu po prawej stronie w nowo utworzonym ciągu.

Obiekt String jest nowo utworzony (§12.5), chyba że wyrażenie jest wyrażeniem stałym (§15.28).

O implementacji JLS mówi:

Implementacja może wybrać konwersję i konkatenację w jednym kroku, aby uniknąć tworzenia, a następnie odrzucania pośredniego obiektu String. Aby zwiększyć wydajność powtarzania konkatenacji ciągów, kompilator Java może użyć klasy StringBuffer lub podobnej techniki w celu zmniejszenia liczby pośrednich obiektów String, które są tworzone przez ocenę wyrażenia.

W przypadku typów pierwotnych implementacja może również zoptymalizować tworzenie obiektu opakowania poprzez konwersję bezpośrednio z typu pierwotnego na ciąg.

Sądząc po „kompilatorze Java, można użyć klasy StringBuffer lub podobnej techniki w celu zmniejszenia”, różne kompilatory mogą generować inny kod bajtowy.


2

Operatora + mogą pracować między łańcucha i łańcuch, odbarwiającego, liczb całkowitych, podwójne lub pływaka wartość typu danych. Po prostu konwertuje wartość na reprezentację ciągu przed konkatenacją.

Operatora concat może być dokonana jedynie na i ze strun. Sprawdza zgodność typu danych i zgłasza błąd, jeśli nie są one zgodne.

Poza tym podany kod działa tak samo.


2

Nie wydaje mi się

a.concat(b)jest zaimplementowany w String i myślę, że implementacja niewiele się zmieniła od wczesnych maszyn Java. +Realizacja operacji zależy od wersji Java i kompilator. Obecnie +jest wdrażany za pomocą, StringBufferaby operacja była jak najszybsza. Może w przyszłości to się zmieni. We wcześniejszych wersjach java +operacja na Strings była znacznie wolniejsza, ponieważ dawała wyniki pośrednie.

Wydaje mi się, że +=jest implementowany za pomocą +i podobnie zoptymalizowany.


7
„Obecnie + jest implementowany przy użyciu StringBuffer” False It's StringBuilder. StringBuffer to bezpieczny wątek impl StringBuilder.
Frederic Morin

1
Kiedyś był to StringBuffer przed wersją java 1.5, ponieważ była to wersja, kiedy wprowadzono StringBuilder.
ccpizza

0

Gdy używasz +, prędkość maleje wraz ze wzrostem długości łańcucha, ale gdy używasz konkat, prędkość jest bardziej stabilna, a najlepszą opcją jest użycie klasy StringBuilder, która ma stabilną prędkość, aby to zrobić.

Chyba rozumiesz dlaczego. Ale najlepszym sposobem na tworzenie długich łańcuchów jest użycie StringBuilder () i append (), albo prędkość będzie niedopuszczalna.


1
użycie operatora + jest równoważne użyciu StringBuilder ( docs.oracle.com/javase/specs/jls/se8/html/… )
ihebiheb
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.