Zeichenfolgenverkettung: concat () vs "+" Operator

499

Angenommen, String a und b:

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

Sind sie unter der Haube dasselbe?

Hier wird concat als Referenz dekompiliert. Ich möchte auch den +Operator dekompilieren können, um zu sehen, was das bewirkt.

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
quelle
3
Mögliches Duplikat von StringBuilder vs String-Verkettung in toString () in Java
Greg Mattes
3
Ich bin nicht sicher, ob +dekompiliert werden kann.
Galen Nare
1
Verwenden Sie javap , um eine Java- Klassendatei zu zerlegen.
Hot Licks
Durch ‚Unveränderlichkeit‘ sollten Sie wahrscheinlich verwenden StringBufferoder StringBuilder- (Thread unsichere so schneller statt
Ujjwal Singh

Antworten:

560

Nein, nicht ganz.

Erstens gibt es einen kleinen Unterschied in der Semantik. Wenn dies der Fall aist null, wird a a.concat(b)ausgelöst, NullPointerExceptionaber a+=bder ursprüngliche Wert von wird so behandelt, aals wäre er es null. Darüber hinaus concat()akzeptiert die Methode nur StringWerte, während der +Operator das Argument stillschweigend in einen String konvertiert (unter Verwendung der toString()Methode für Objekte). Die concat()Methode ist also strenger in dem, was sie akzeptiert.

Um unter die Haube zu schauen, schreiben Sie eine einfache Klasse mit a += b;

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

Jetzt zerlegen mit javap -c(im Sun JDK enthalten). Sie sollten eine Liste sehen, die Folgendes enthält:

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

Also a += bist das Äquivalent von

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

Die concatMethode sollte schneller sein. Mit mehr Strings StringBuildergewinnt die Methode jedoch zumindest in Bezug auf die Leistung.

Der Quellcode von Stringund StringBuilder(und seine paketprivate Basisklasse) ist in src.zip des Sun JDK verfügbar. Sie können sehen, dass Sie ein char-Array erstellen (Größe nach Bedarf ändern) und es dann wegwerfen, wenn Sie das Finale erstellen String. In der Praxis ist die Speicherzuweisung überraschend schnell.

Update: Wie Pawel Adamski feststellt, hat sich die Leistung im neueren HotSpot geändert. javacproduziert immer noch genau den gleichen Code, aber der Bytecode-Compiler betrügt. Einfache Tests schlagen völlig fehl, weil der gesamte Code weggeworfen wird. Summieren System.identityHashCode(nicht String.hashCode) zeigt, dass der StringBufferCode einen leichten Vorteil hat. Änderungen vorbehalten, wenn das nächste Update veröffentlicht wird oder wenn Sie eine andere JVM verwenden. Von @lukaseder , eine Liste der HotSpot JVM-Eigenschaften .

Tom Hawtin - Tackline
quelle
4
@HyperLink Sie können den Code javap -cin einer kompilierten Klasse sehen, die ihn verwendet. (Oh, wie in der Antwort. Sie müssen nur die Bytecode-Demontage interpretieren, was nicht so schwierig sein sollte.)
Tom Hawtin - Tackline
1
Sie können die JVM-Spezifikation konsultieren , um die einzelnen Bytecodes zu verstehen. Das Zeug, auf das Sie verweisen möchten, befindet sich in Kapitel 6. Ein bisschen dunkel, aber Sie können das Wesentliche ziemlich leicht verstehen.
Hot Licks
1
Ich frage mich, warum der Java-Compiler auch dann verwendet StringBuilder, wenn zwei Zeichenfolgen verbunden werden. Wenn Stringstatische Methoden zum Verketten von bis zu vier Zeichenfolgen oder aller Zeichenfolgen in a enthalten sind String[], kann Code bis zu vier Zeichenfolgen mit zwei Objektzuordnungen (das Ergebnis Stringund seine Unterstützung char[], keine redundante) und eine beliebige Anzahl von Zeichenfolgen mit drei Zuordnungen anhängen ( das String[], das Ergebnis Stringund das Backing char[], wobei nur das erste überflüssig ist). Für die Verwendung StringBuildersind bestenfalls vier Zuordnungen erforderlich, und jedes Zeichen muss zweimal kopiert werden.
Supercat
Dieser Ausdruck a + = b. Bedeutet das nicht: a = a + b?
ehrwürdigster Herr
3
Die Dinge haben sich geändert, seit diese Antwort erstellt wurde. Bitte lesen Sie meine Antwort unten.
Paweł Adamski
90

Niyaz ist richtig, aber es ist auch erwähnenswert, dass der Operator special + vom Java-Compiler in etwas effizienteres konvertiert werden kann. Java verfügt über eine StringBuilder-Klasse, die einen nicht threadsicheren, veränderbaren String darstellt. Wenn Sie eine Reihe von String-Verkettungen ausführen, konvertiert der Java-Compiler unbeaufsichtigt

String a = b + c + d;

in

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

was für große Saiten deutlich effizienter ist. Soweit ich weiß, geschieht dies nicht, wenn Sie die Concat-Methode verwenden.

Die concat-Methode ist jedoch effizienter, wenn eine leere Zeichenfolge mit einer vorhandenen Zeichenfolge verknüpft wird. In diesem Fall muss die JVM kein neues String-Objekt erstellen und kann einfach das vorhandene zurückgeben. Weitere Informationen finden Sie in der Concat-Dokumentation .

Wenn Sie also große Bedenken hinsichtlich der Effizienz haben, sollten Sie die Concat-Methode verwenden, wenn Sie möglicherweise leere Strings verketten, und andernfalls + verwenden. Der Leistungsunterschied sollte jedoch vernachlässigbar sein und Sie sollten sich wahrscheinlich nie darum kümmern.

Eli Courtwright
quelle
concat infact macht das nicht. Ich habe meinen Beitrag mit einer Dekompilierung der Concat-Methode
bearbeitet
10
infact es tut. Schauen Sie sich die ersten Zeilen Ihres Concat-Codes an. Das Problem mit concat ist, dass es immer einen neuen String () generiert
Marcio Aguiar
2
@MarcioAguiar: Vielleicht meinst du, dass + immer eine neue generiert String- wie du sagst, concathat eine Ausnahme, wenn du eine leere konzentrierst String.
Blaisorblade
45

Ich habe einen ähnlichen Test wie @marcio durchgeführt, aber stattdessen mit der folgenden Schleife:

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
}

Nur zum guten Teil habe ich auch reingeworfen StringBuilder.append(). Jeder Test wurde 10 Mal ausgeführt, mit 100.000 Wiederholungen für jeden Lauf. Hier sind die Ergebnisse:

  • StringBuildergewinnt zweifellos. Das Ergebnis der Uhrzeit war für die meisten Läufe 0 und die längste dauerte 16 ms.
  • a += b dauert ungefähr 40000 ms (40 s) für jeden Lauf.
  • concat erfordert nur 10000 ms (10 s) pro Lauf.

Ich habe die Klasse noch nicht dekompiliert, um die Interna zu sehen oder sie durch den Profiler zu führen, aber ich vermute, dass ich a += bviel Zeit damit verbringe, neue Objekte zu erstellen StringBuilderund sie dann wieder zu konvertieren String.

ckpwong
quelle
4
Die Zeit für die Objekterstellung ist wirklich wichtig. Aus diesem Grund verwenden wir in vielen Situationen StringBuilder direkt, anstatt den StringBuilder hinter + zu nutzen.
Coolcfan
1
@coolcfan: Wenn +für zwei Zeichenfolgen verwendet wird, gibt es Fälle, in denen die Verwendung StringBuilderbesser ist als es wäre String.valueOf(s1).concat(s2)? Irgendeine Idee, warum Compiler Letzteres nicht verwenden würden [oder den valueOfAufruf in Fällen weglassen , in denen s1bekannt ist, dass er nicht null ist]?
Supercat
1
@ Supercat Entschuldigung, ich weiß es nicht. Vielleicht sind die Leute, die hinter diesem Zucker stehen, die besten, um dies zu beantworten.
Coolcfan
25

Die meisten Antworten stammen aus dem Jahr 2008. Es sieht so aus, als hätten sich die Dinge im Laufe der Zeit geändert. Meine neuesten Benchmarks mit JMH zeigen, dass Java 8 +etwa doppelt so schnell ist wie concat.

Mein Benchmark:

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

Ergebnisse:

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
Paweł Adamski
quelle
Ich frage mich, warum Java Stringnie eine statische Funktion zum Bilden einer Zeichenfolge durch Verketten der Elemente von a eingefügt hat String[]. Die Verwendung +zum Verketten von 8 Zeichenfolgen mit einer solchen Funktion würde das Konstruieren und spätere Verlassen erfordern String[8], aber dies wäre das einzige Objekt, das aufgegeben werden StringBuildermüsste , während das Verwenden einer StringBuilderInstanz das Konstruieren und Verlassen einer Instanz und mindestens eines char[]Sicherungsspeichers erfordern würde .
Supercat
@supercat Einige statische String.join()Methoden wurden in Java 8 als schnelle Syntax-Wrapper für die java.util.StringJoinerKlasse hinzugefügt .
Ti Strga
@TiStrga: Hat sich das Handling +geändert, um solche Funktionen zu nutzen?
Supercat
@supercat Das würde die binäre Abwärtskompatibilität beeinträchtigen, also nein. Es war nur eine Antwort auf Ihren Kommentar "Warum String nie eine statische Funktion enthielt": Jetzt gibt es eine solche Funktion. Der Rest Ihres Vorschlags (Refactoring +, um ihn zu verwenden) würde leider mehr erfordern, als die Java-Entwickler bereit sind, zu ändern.
Ti Strga
@TiStrga: Gibt es eine Möglichkeit, wie eine Java-Bytecode-Datei "Wenn die Funktion X verfügbar ist, rufen Sie sie auf, andernfalls etwas anderes tun" auf eine Weise anzeigt, die beim Laden einer Klasse behoben werden könnte? Das Generieren von Code mit einer statischen Methode, die entweder mit der statischen Methode von Java verkettet werden kann oder einen Stringbuilder verwendet, wenn dieser nicht verfügbar ist, scheint die optimale Lösung zu sein.
Superkatze
22

Tom beschreibt zu Recht genau, was der Operator + tut. Es erstellt eine temporäre StringBuilderDatei, hängt die Teile an und endet mittoString() .

Alle bisherigen Antworten ignorieren jedoch die Auswirkungen von HotSpot-Laufzeitoptimierungen. Insbesondere werden diese temporären Vorgänge als allgemeines Muster erkannt und zur Laufzeit durch effizienteren Maschinencode ersetzt.

@marcio: Sie haben einen Mikro-Benchmark erstellt . Bei modernen JVMs ist dies keine gültige Methode zum Profilieren von Code.

Der Grund, warum die Laufzeitoptimierung wichtig ist, ist, dass viele dieser Unterschiede im Code - einschließlich der Objekterstellung - nach dem Start von HotSpot völlig unterschiedlich sind. Die einzige Möglichkeit, dies sicher zu wissen, besteht darin, Ihren Code vor Ort zu profilieren .

Schließlich sind alle diese Methoden in der Tat unglaublich schnell. Dies kann ein Fall vorzeitiger Optimierung sein. Wenn Sie Code haben, der Zeichenfolgen häufig verkettet, hat der Weg zur Maximierung der Geschwindigkeit wahrscheinlich nichts mit den von Ihnen ausgewählten Operatoren und dem von Ihnen verwendeten Algorithmus zu tun!

Jason Cohen
quelle
Ich denke, mit "diesen temporären Operationen" meinen Sie die Verwendung der Escape-Analyse, um "Heap" -Objekte auf dem Stapel zuzuweisen, sofern dies nachweislich korrekt ist. Obwohl Escape-Analyse in HotSpot vorhanden ist (nützlich zum Entfernen von Synchronisation), glaube ich nicht, ist es zum Zeitpunkt des Schreibens, u
Tom Hawtin - Tackline
21

Wie wäre es mit einfachen Tests? Verwendete den folgenden Code:

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);
  • Die "a + b"Version in 2500ms ausgeführt .
  • Die a.concat(b)in 1200ms ausgeführt .

Mehrmals getestet. Die concat()Ausführung der Version dauerte durchschnittlich die Hälfte der Zeit.

Dieses Ergebnis hat mich überrascht, weil die concat()Methode immer eine neue Zeichenfolge erstellt (sie gibt ein " new String(result)" zurück. Es ist bekannt, dass:

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

Warum war der Compiler nicht in der Lage, die Zeichenfolgenerstellung in "a + b" -Code zu optimieren, da er wusste, dass immer dieselbe Zeichenfolge erstellt wurde? Es könnte eine neue Zeichenfolgenerstellung vermeiden. Wenn Sie der obigen Aussage nicht glauben, testen Sie sich selbst.

Marcio Aguiar
quelle
Ich habe auf Java jdk1.8.0_241 Ihren Code getestet. Für mich liefert der "a + b" -Code optimierte Ergebnisse. Mit concat (): 203 ms und mit "+": 113 ms . Ich denke in der vorherigen Version war es nicht so optimiert.
Akki
6

Grundsätzlich gibt es zwei wichtige Unterschiede zwischen + und der concatMethode.

  1. Wenn Sie die concat- Methode verwenden, können Sie nur Zeichenfolgen verketten, während Sie im Fall des Operators + die Zeichenfolge auch mit einem beliebigen Datentyp verketten können.

    Zum Beispiel:

    String s = 10 + "Hello";

    In diesem Fall sollte die Ausgabe 10Hallo sein .

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

    In diesem Fall müssen Sie zwei obligatorische Zeichenfolgen angeben.

  2. Der zweite und Hauptunterschied zwischen + und concat ist folgender:

    Fall 1: Angenommen, ich konzentriere auf diese Weise dieselben Zeichenfolgen mit dem concat- Operator

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

    In diesem Fall beträgt die Gesamtzahl der im Pool erstellten Objekte 7 wie folgt:

    I
    am
    good
    boy
    Iam
    Iamgood
    Iamgoodboy

    Fall 2:

    Jetzt werde ich die gleichen Zeichenfolgen über den Operator + zusammenfassen

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

    Im obigen Fall beträgt die Gesamtzahl der erstellten Objekte nur 5.

    Wenn wir die Zeichenfolgen über den Operator + zusammenfassen , wird eine StringBuffer-Klasse verwaltet, um dieselbe Aufgabe wie folgt auszuführen: -

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

    Auf diese Weise werden nur fünf Objekte erstellt.

Also Leute, das sind die grundlegenden Unterschiede zwischen + und der Concat- Methode. Genießen :)

Deepak Sharma
quelle
Meine Liebe, Sie wissen sehr gut, dass jedes String-Literal selbst als String-Objekt behandelt wird, das im String-Pool gespeichert ist. In diesem Fall haben wir also 4 String-Literale. Offensichtlich sollten mindestens 4 Objekte im Pool erstellt werden.
Deepak Sharma
1
Ich glaube nicht: String s="I"+"am"+"good"+"boy"; String s2 = "go".concat("od"); System.out.println(s2 == s2.intern());Drucke true, was bedeutet "good", dass sie vor dem Aufruf nicht im String-Pool warenintern()
fabian
Ich spreche nur über diese Zeile String s = "Ich" + "bin" + "gut" + "Junge"; In diesem Fall werden alle 4 Zeichenfolgenliterale in einem Pool gespeichert. Daher sollten 4 Objekte im Pool erstellt werden.
Deepak Sharma
4

Der Vollständigkeit halber wollte ich hinzufügen, dass die Definition des Operators '+' im JLS SE8 15.18.1 zu finden ist :

Wenn nur ein Operandenausdruck vom Typ String ist, wird die String-Konvertierung (§5.1.11) für den anderen Operanden durchgeführt, um zur Laufzeit einen String zu erzeugen.

Das Ergebnis der Zeichenfolgenverkettung ist eine Referenz auf ein Zeichenfolgenobjekt, bei dem es sich um die Verkettung der beiden Operandenzeichenfolgen handelt. Die Zeichen des linken Operanden stehen vor den Zeichen des rechten Operanden in der neu erstellten Zeichenfolge.

Das String-Objekt wird neu erstellt (§12.5), es sei denn, der Ausdruck ist ein konstanter Ausdruck (§15.28).

Über die Implementierung sagt das JLS Folgendes:

Eine Implementierung kann sich dafür entscheiden, die Konvertierung und Verkettung in einem Schritt durchzuführen, um zu vermeiden, dass ein Zwischenzeichenfolgenobjekt erstellt und dann verworfen wird. Um die Leistung der wiederholten Verkettung von Zeichenfolgen zu erhöhen, kann ein Java-Compiler die StringBuffer-Klasse oder eine ähnliche Technik verwenden, um die Anzahl der zwischengeschalteten Zeichenfolgenobjekte zu verringern, die durch Auswertung eines Ausdrucks erstellt werden.

Bei primitiven Typen kann eine Implementierung auch die Erstellung eines Wrapper-Objekts optimieren, indem sie direkt von einem primitiven Typ in eine Zeichenfolge konvertiert.

Nach dem "Ein Java-Compiler kann die StringBuffer-Klasse oder eine ähnliche Technik zum Reduzieren verwenden" zu urteilen, könnten verschiedene Compiler unterschiedlichen Bytecode erzeugen.

Dingalapadum
quelle
2

Der Operator + kann zwischen einer Zeichenfolge und einem Datentypwert für Zeichenfolge, Zeichen, Ganzzahl, Doppel oder Gleitkomma arbeiten. Der Wert wird lediglich vor der Verkettung in seine Zeichenfolgendarstellung konvertiert.

Der Concat-Operator kann nur mit und mit Strings ausgeführt werden. Es prüft die Datentypkompatibilität und gibt einen Fehler aus, wenn sie nicht übereinstimmen.

Abgesehen davon macht der von Ihnen bereitgestellte Code dasselbe.

Niyaz
quelle
2

Das glaube ich nicht.

a.concat(b)ist in String implementiert und ich denke, die Implementierung hat sich seit frühen Java-Maschinen nicht viel geändert. Die +Operationsimplementierung hängt von der Java-Version und dem Compiler ab. Derzeit +wird mit implementiert StringBuffer, um den Vorgang so schnell wie möglich zu gestalten. Vielleicht wird sich dies in Zukunft ändern. In früheren Versionen war die Java- +Operation auf Strings viel langsamer, da sie zu Zwischenergebnissen führte.

Ich denke, das +=wird mit implementiert +und ähnlich optimiert.

Bartosz Bierkowski
quelle
7
"Derzeit ist + mit StringBuffer implementiert" False It's StringBuilder. StringBuffer ist das threadsichere Gerät von StringBuilder.
Frederic Morin
1
Vor Java 1.5 war es StringBuffer, da dies die Version war, als StringBuilder zum ersten Mal eingeführt wurde.
ccpizza
0

Bei Verwendung von + nimmt die Geschwindigkeit mit zunehmender Länge der Zeichenfolge ab. Bei Verwendung von concat ist die Geschwindigkeit jedoch stabiler. Die beste Option ist die Verwendung der StringBuilder-Klasse, die dafür eine stabile Geschwindigkeit aufweist.

Ich denke du kannst verstehen warum. Der beste Weg, um lange Strings zu erstellen, ist die Verwendung von StringBuilder () und append (). Beide Geschwindigkeiten sind nicht akzeptabel.

iamreza
quelle
1
Die Verwendung des Operators + entspricht der Verwendung des StringBuilder ( docs.oracle.com/javase/specs/jls/se8/html/… )
ihebiheb