Java StringBuilder
Veränderliche Strings effizient in Java mit der StringBuilder-Klasse aufbauen — append, insert, reverse und mehr.
Ein String ist unveränderlich; ihn mit += in einer Schleife zu vergrößern ist quadratisch. StringBuilder ist die Antwort der Standardbibliothek: ein einzelnes Objekt mit einem veränderbaren internen Puffer, an den man anhängen, einfügen, löschen und umkehren kann – und der am Ende einmalig in einen unveränderlichen String umgewandelt wird. Es ist das Arbeitstier hinter jedem modernen JDK-String-Aufbaumuster und die richtige Wahl, sobald man Text in einer Schleife oder über mehrere Methodenaufrufe hinweg sammelt.
Einen Builder anlegen
StringBuilder sb = new StringBuilder(); // empty, capacity 16
StringBuilder withCap = new StringBuilder(1024); // empty, preallocated capacity
StringBuilder fromText = new StringBuilder("hi "); // capacity = length + 16Der No-Arg-Konstruktor startet mit einer Kapazität von 16, was für kurze Ergebnisse ausreicht, für lange aber eine pessimistische Wahl ist. Wenn man ungefähr weiß, wie groß der finale String sein wird, sollte man die Kapazität von Anfang an übergeben — jedes vermiedene Wachstum bedeutet eine Speicherzuweisung weniger und ein arraycopy weniger. new StringBuilder(estimatedLength) ist die einzeln wirksamste Mikrooptimierung in diesem ganzen Kapitel.
Verkettung: Jede mutierende Methode gibt this zurück
Jede mutierende Methode von StringBuilder gibt den Builder selbst zurück, sodass Aufrufe zu einem einzigen Ausdruck zusammengefasst werden können:
String greeting = new StringBuilder()
.append("Hello, ")
.append(name)
.append('!')
.append('\n')
.toString();Das ist Konvention, kein Magie; man könnte es auch auf mehrere Anweisungen aufteilen, ohne funktionalen Unterschied. Der verkettete Stil spiegelt wider, wie der Compiler +-Ketten intern umschreibt – was selbst der Grund ist, warum die verkettete Form in Java natürlich wirkt.
Die Methoden-Familie
StringBuilder hat eine kleine, fokussierte Oberfläche:
append— fügt Text oder beliebige Primitives am Ende an. Überladen für alle Primitives,char[],CharSequenceundObject(rufttoStringauf).insert(offset, ...)— dieselben Überladungen, aber an einer beliebigen Position.delete(start, end),deleteCharAt(i)— entfernt einen Bereich oder ein einzelnes Zeichen.replace(start, end, replacement)— ersetzt einen Bereich durch einen neuen Teilstring; die Längen dürfen unterschiedlich sein.reverse()— dreht den Puffer an Ort und Stelle um.setCharAt(i, ch)— überschreibt ein einzelnes Zeichen.setLength(n)— kürzt (oder füllt mit) aufnZeichen auf.
Diese Methoden bearbeiten den Puffer; sie geben keinen neuen String zurück. Um einen Schnappschuss des Inhalts als unveränderlichen String zu erhalten, ruft man toString() auf.
Inspizieren und Konvertieren
length()— aktuelle Zeichenanzahl.capacity()— aktuelle Größe des internen Arrays. Immer ≥length().charAt(i),substring(start)/substring(start, end)— Lesezugriff, identisch zuString.indexOf(s),lastIndexOf(s)— sucht einen Teilstring.toString()— erzeugt einen unveränderlichenString. Einmal am Ende aufrufen; wiederholtes Aufrufen während des Aufbaus ist unnötige Speicherverschwendung.ensureCapacity(n)— vergrößert den Puffer vorab auf mindestensn.trimToSize()— verkleinert den Puffer auf die aktuelle Inhaltsgröße. Wird selten benötigt.
Wie der Puffer wächst
Intern hält StringBuilder ein byte[] (oder char[] in älteren JDKs). Wenn ein append den Puffer überlaufen würde, wird er auf ungefähr 2 × oldCapacity + 2 neu zugeteilt und der alte Inhalt wird kopiert. Jedes Wachstum ist O(n) bezogen auf die aktuelle Größe, aber das Verdopplungsmuster macht die Gesamtkosten von n Appends zu O(n) amortisiert — ganz im Gegensatz zu wiederholter String-Konkatenation, bei der dieselbe Schleife O(n²) ist.
append "a" — capacity 16, length 1
... 15 more — capacity 16, length 16
append "b" — grow to 34, length 17
... 17 more — capacity 34, length 34
append "c" — grow to 70, length 35Wenn man die finale Länge kennt, umgeht man all diese Neuzuweisungen, indem man den Builder direkt mit dieser Kapazität erstellt.
StringBuilder vs. den +-Operator
Bei kurzen, statisch bekannten String-Zusammensetzungen macht der Compiler von sich aus das Richtige. "Hello, " + name + "!" wird zur Kompilierzeit entweder in eine einzelne StringBuilder-Kette oder in einen Aufruf von StringConcatFactory.makeConcatWithConstants (Java 9+) umgeschrieben. Beides ist effizient. Man muss diese Ausdrücke nicht manuell optimieren.
Das zu vermeidende Muster ist += innerhalb einer Schleife über eine unbekannte Anzahl:
// O(n²) — every += allocates a new String holding everything seen so far
String out = "";
for (String token : tokens) {
out += token + "|";
}
// O(n) — one buffer, one final String
StringBuilder sb = new StringBuilder();
for (String token : tokens) {
sb.append(token).append('|');
}
String out = sb.toString();Wenn die Schleife nur ein paar Mal läuft, ist der Unterschied unsichtbar. Bei einigen tausend Tokens ist es ein messbares Problem. Bei einer Million ist die erste Form ein Hänger, die zweite Millisekunden.
StringBuilder ist nicht thread-sicher
StringBuilder verzichtet bewusst auf Synchronisation, um im überwältigend häufigen Single-Thread-Betrieb schnell zu sein. Wenn zwei Threads gleichzeitig an denselben Builder anhängen, sind die Ergebnisse undefiniert: verlorene Schreibvorgänge, überschriebene Zeichen oder eine ArrayIndexOutOfBoundsException aus dem Wachstumspfad. Für den seltenen Fall, dass ein Builder thread-übergreifend geteilt wird, verwendet man stattdessen den synchronisierten Zwilling — StringBuffer. In der Praxis teilt man einen Builder fast nie; jeder Thread baut seinen eigenen auf.
Ein ausgearbeitetes Beispiel
Das folgende Programm verwendet jeden relevanten Teil der Oberfläche für den alltäglichen Code: verkettetes append, ein explizites insert, ein replace, das die Länge eines Bereichs ändert, ein reverse und ein abschließendes toString. Die Kapazität wird am Anfang und Ende ausgegeben, um das Pufferwachstum sichtbar zu machen.
Beachten Sie die Kapazitätszahlen in der Ausgabe (capacity=32 am Anfang, capacity=66 am Ende). Jeder Append in der Kette passt in die anfängliche Kapazität von 32 — der aufgebaute String ist nur 24 Zeichen lang. Das insert und replace schieben die Länge dann auf 40, was 32 überschreitet und genau ein Wachstum auslöst (auf 2 × 32 + 2 = 66). Eine genauere Vorgröße — hier new StringBuilder(48) — hätte diese einzelne Neuzuweisung vollständig vermieden. Das ist das ganze Spiel: Je besser die Kapazitätsschätzung, desto weniger Kopier-bei-Wachstum-Ereignisse.
Was kommt als Nächstes
StringBuilder ist schnell, weil er nicht thread-sicher ist. Sein synchronisierter Zwilling ist die richtige Antwort im (seltenen) Fall, dass man tatsächlich einen Puffer zwischen Threads teilen möchte — gleiche API, Methoden mit Sperrung. Weiter zu Java StringBuffer.