5.6 Veränderbare Zeichenketten mit StringBuilder und StringBuffer
Zeichenketten, die in der virtuellen Maschine in String-Objekten gespeichert sind, haben die Eigenschaft, dass ihr Inhalt nicht mehr verändert werden kann. Anders verhalten sich die Exemplare der Klassen StringBuilder und StringBuffer, an denen sich Veränderungen vornehmen lassen. Die Veränderungen betreffen anschließend das StringBuilder/StringBuffer-Objekt selbst, und es wird kein neu erzeugtes Objekt als Ergebnis geliefert, wie zum Beispiel beim Plus-Operator und der concat(String)-Methode bei herkömmlichen String-Objekten. Sonst sind sich aber die Implementierung von String-Objekten und StringBuilder/StringBuffer-Objekten ähnlich. In beiden Fällen nutzen die Klassen ein internes Zeichen-Array.
Die Klasse StringBuilder bietet die gleichen Methoden wie StringBuffer, nur nicht synchronisiert. Bei nebenläufigen Programmen kann daher die interne Datenstruktur des StringBuilder-Objekts inkonsistent werden. StringBuilder ist nur bei nichtnebenläufigen Zugriffen ein wenig schneller.
Überblick
StringBuilder und StringBuffer sind schnell erklärt: Es gibt einen Konstruktor, der die Objekte aufbaut, Modifizierungsmethoden wie append(…) und eine toString()-Methode, die das Ergebnis als String liefert.
[»] Hinweis
Wegen der symmetrischen API von StringBuffer und StringBuilder werden wir im Folgenden immer nur von den Methoden von StringBuilder sprechen.
5.6.1 Anlegen von StringBuilder-Objekten
Mit mehreren Konstruktoren lassen sich StringBuilder-Objekte aufbauen:
final class java.lang.StringBuilder
implements Appendable, CharSequence, Comparable<StringBuilder>, Serializable
StringBuilder()
Legt ein neues Objekt an, das die leere Zeichenreihe enthält und Platz für (zunächst) bis zu 16 Zeichen bietet. Spätere Einfügeoperationen füllen den Puffer und vergrößern ihn automatisch weiter.StringBuilder(int length)
Wie oben, jedoch reicht die anfängliche Kapazität des Objekts für die angegebene Anzahl an Zeichen. Optimalerweise ist die Größe so zu setzen, dass sie der Endgröße der dynamischen Zeichenfolge nahekommt.StringBuilder(String str)
Baut ein Objekt, das eine Kopie der Zeichen aus str enthält. Zusätzlich plant der Konstruktor bereits Platz für 16 weitere Zeichen ein.StringBuilder(CharSequence seq)
Erzeugt ein neues Objekt aus einer CharSequence. Damit können auch die Zeichenfolgen anderer StringBuilder-Objekte Basis dieses Objekts werden.
Da nur String-Objekte von der Sprache bevorzugt werden, bleibt uns allein der explizite Aufruf eines Konstruktors, um StringBuilder-Exemplare anzulegen. Alle String-Literale in Anführungszeichen sind ja schon Exemplare der Klasse String.
[»] Hinweis
Weder in der Klasse String noch in StringBuilder existiert ein Konstruktor, der explizit ein char als Parameter zulässt, um aus dem angegebenen Zeichen eine Zeichenkette aufzubauen. Dennoch gibt es bei StringBuilder einen Konstruktor, der ein int annimmt, wobei die übergebene Ganzzahl die interne Startgröße des Puffers spezifiziert. Rufen wir den Konstruktor mit char auf – etwa einem '*' –, so konvertiert der Compiler automatisch das Zeichen in ein int. Das resultierende Objekt enthält kein Zeichen, sondern hat nur eine anfängliche Kapazität von 42 Zeichen, da 42 der ASCII-Code des Sternchens ist. Korrekt ist daher für den Aufbau einer veränderbaren Zeichenkette, gefüllt mit dem Startzeichen c, nur Folgendes: new StringBuilder(Character.toString(c)) oder new StringBuilder().append(c).
5.6.2 StringBuilder in andere Zeichenkettenformate konvertieren
StringBuilder werden in der Regel intern in Methoden eingesetzt, aber tauchen selten als Parameter- oder Rückgabetyp auf. Aus den Konstruktoren der Klassen konnten wir ablesen, wie bei einem Parametertyp String etwa ein StringBuilder aufgebaut wird, es fehlt aber der Weg zurück.
final class java.lang.StringBuilder
implements Appendable, CharSequence, Comparable<StringBuilder>, Serializable
String toString()
Erzeugt aus der aktuellen Zeichenkette ein String-Objekt.void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)
Kopiert einen gewünschten Ausschnitt in ein char-Array.
5.6.3 Zeichen(folgen) erfragen
Die bekannten Anfragemethoden aus String finden wir auch beim StringBuilder wieder. So verhalten sich charAt(int) und getChars(…) bei Exemplaren beider Klassen identisch. Auch substring(int start) und substring(int start, int end) sind aus der Klasse String bekannt. Wenn nur diese Methoden nötig sind, ist auch ein StringBuilder unnötig, und ein String-Objekt selbst reicht.
5.6.4 Daten anhängen
Die häufigste Anwendung von StringBuilder-Objekten ist das Zusammenfügen von Texten aus Daten unterschiedlichen Typs. Dazu deklarieren die Klassen eine Reihe von append(…)-Methoden, die mit unterschiedlichen Datentypen überladen sind. Die append(…)-Methoden von StringBuilder geben einen StringBuilder zurück. Die append(…)-Methoden hängen sich immer an das Ende an und vergrößern den internen Platz – ein internes char-Array –, falls es nötig ist. Ein neues StringBuilder-Objekt erzeugen sie nicht.
Nutzen wir dies für eine eigene Methode enumerate(…), die ein String-Array abläuft und eine Bildschirmaufzählung erzeugt:
public class Enumerator {
public static String enumerate( String... lines ) {
if ( lines == null || lines.length == 0 )
return "";
StringBuilder sb = new StringBuilder();
for ( int i = 0; i < lines.length; i++ ) {
sb.append( i + 1 );
sb.append( ". " );
sb.append( lines[i] ).append( System.lineSeparator() );
}
return sb.toString().trim();
}
public static void main( String[] args ) {
System.out.println( enumerate( "Aufstehen", "Frühstücken" ) );
}
}
Die Ausgabe ist:
1. Aufstehen
2. Frühstücken
Die Zusammenfassung listet alle append(…)-Methoden auf.
final class java.lang.StringBuilder
implements Appendable, CharSequence, Comparable<StringBuilder>, Serializable
StringBuilder append(boolean b)
StringBuilder append(char c)
StringBuilder append(char[] str)
StringBuilder append(char[] str, int offset, int len)
StringBuilder append(CharSequence s)
StringBuilder append(CharSequence s, int start, int end)
StringBuilder append(double d)
StringBuilder append(float f)
StringBuilder append(int i)
StringBuilder append(long lng)
StringBuilder append(Object obj)
StringBuilder append(String str)
StringBuilder append(StringBuilder sb)
Die Methoden append(char), append(CharSequence) und append(CharSequence, int, int) werden von der Schnittstelle Appendable vorgeschrieben.
Besonders nützlich ist in der Praxis append(CharSequence, int, int), da sich auf diese Weise Teile von String-, StringBuilder- und StringBuffer-Objekten anhängen lassen.
[»] Hinweis
Jede append(…)-Methode verändert den StringBuilder und liefert als Rückgabewert noch eine Referenz darauf. Das hat den großen Vorteil, dass sich Aufrufe der append(…)-Methoden einfach hintereinandersetzen (kaskadieren) lassen:
StringBuilder sb = new StringBuilder( "George Peppard" ).append(',');
sb.append(" Mr. T, ").append("Dirk Benedict, ").append("Dwight Schultz");
Die Auswertung erfolgt von links nach rechts, sodass das Ergebnis ist: »George Peppard, Mr. T, Dirk Benedict, Dwight Schultz«.
[+] Tipp
Den Plus-Operator innerhalb von append(…)-Strings zu nutzen, ist kontraproduktiv. Heißt es etwa sb.append(", " + value), sollte es besser sb.append(", ").append(value) heißen.
5.6.5 Zeichen(folgen) setzen, löschen und umdrehen
Da sich bei einem StringBuilder Zeichen verändern lassen, gibt es neben den append(…)-Methoden weitere Modifikationsmethoden, die in der Klasse String fehlen.
Einzelne Zeichen setzen
final class java.lang.StringBuilder
implements Appendable, CharSequence, Comparable<StringBuilder>, Serializable
void setCharAt(int index, char ch)
Setzt an die Stelle index das Zeichen ch und überschreibt das alte Zeichen.
[zB] Beispiel
Ändere das erste Zeichen im StringBuilder in einen Großbuchstaben:
StringBuilder sb = new StringBuilder( "spare Wasser und dusche mit dem Partner" );
char upperCharacter = Character.toUpperCase( sb.charAt(0) );
sb.setCharAt( 0, upperCharacter );
Das erste Argument 0 in setCharAt(int, char) steht für die Position des zu setzenden Zeichens.
Zeichenfolgen einfügen
Diverse insert(…)-Methoden fügen die Zeichenketten-Repräsentation eines Werts an eine bestimmte Stelle ein. Sie ähnelt der überladenen append(…)-Methode.
final class java.lang.StringBuilder
implements Appendable, CharSequence, Comparable<StringBuilder>, Serializable
StringBuilder insert(int offset, boolean b)
StringBuilder insert(int offset, char c)
StringBuilder insert(int offset, char[] str)
StringBuilder insert(int index, char[] str, int offset, int len)
StringBuilder insert(int dstOffset, CharSequence s)
StringBuilder insert(int dstOffset, CharSequence s, int start, int end)
StringBuilder insert(int offset, double d)
StringBuilder insert(int offset, float f)
StringBuilder insert(int offset, int i)
StringBuilder insert(int offset, long l)
StringBuilder insert(int offset, String str)
[zB] Beispiel *
Lies eine Datei ein, und drehe die Zeilen so um, dass die letzte Zeile der Datei oben steht und die erste Zeile der Datei unten. Das Ergebnis auf der Konsole soll ein String sein, der keinen Weißraum zu Beginn und am Ende aufweist:
InputStream resource = ReverseFile.class.getResourceAsStream( "EastOfJava.txt" );
try ( Scanner input = new Scanner( resource ) ) {
StringBuilder result = new StringBuilder();
while ( input.hasNextLine() )
result.insert( 0, input.nextLine() + System.lineSeparator() );
System.out.println( result.toString().trim() );
}
Die Konstruktion mit dem try ist neu und wird in Abschnitt 8.7.1, »try mit Ressourcen«, näher erklärt.
Für char-Arrays existiert insert(…) in einer abgewandelten Art: insert(int index, char[] str, int offset, int len). Die Methode übernimmt nicht das komplette Array in den StringBuilder, sondern nur einen Ausschnitt.
Einzelnes Zeichen und Zeichenbereiche löschen
Eine Folge von Zeichen lässt sich durch delete(int start, int end) löschen. deleteCharAt(int index) löscht nur ein Zeichen. In beiden Fällen wird ein inkorrekter Index durch eine StringIndexOutOfBoundsException bestraft.
Zeichenbereiche ersetzen
Die Methode replace(int start, int end, String str) löscht zuerst die Zeichen zwischen start und end und fügt anschließend den neuen String str ab start ein. Dabei sind die Endpositionen wie immer exklusiv. Das heißt, sie geben das erste Zeichen hinter dem zu verändernden Ausschnitt an.
[zB] Beispiel
Ersetze den Teil-String an den Positionen 4 und 5 (also bis exklusive 6):
StringBuilder sb = new StringBuilder( "Sub-XX-Sens-O-Matic" );
// 0123456
System.out.println( sb.replace( 4, 6, "Etha" ) ) ; // Sub-Etha-Sens-O-Matic
Zeichenfolgen umdrehen
Eine weitere Methode reverse() dreht die Zeichenfolge um.
[zB] Beispiel
Teste unabhängig von der Groß-/Kleinschreibung, ob der String s ein Palindrom ist. Palindrome lesen sich von vorn genauso wie von hinten, etwa »Rentner«:
boolean isPalindrome =
new StringBuilder( s ).reverse().toString().equalsIgnoreCase( s );
5.6.6 Länge und Kapazität eines StringBuilder-Objekts *
Wie bei einem String lässt sich die Länge und die Anzahl der enthaltenen Zeichen mit der Methode length() erfragen. StringBuilder-Objekte haben jedoch auch eine interne Puffergröße, die sich mit capacity() erfragen lässt und die im Konstruktor wie beschrieben festgelegt wird. In diesem Puffer, der genauer gesagt ein Array vom Typ char ist, werden die Veränderungen wie das Ausschneiden oder Anhängen von Zeichen vorgenommen. Während length() die Anzahl der Zeichen angibt, ist capacity() immer größer oder gleich length() und sagt etwas darüber aus, wie viele Zeichen der Puffer noch aufnehmen kann, ohne dass intern ein neues, größeres Array benötigt würde.
[zB] Beispiel
StringBuilder sb = new StringBuilder( "www.tutego.de" );
System.out.println( sb.length() ); // 13
System.out.println( sb.capacity() ); // 29
Bei der Längenabfrage liefert sb.length() 13, aber sb.capacity() ergibt 13 + 16 = 29.
Die Startgröße sollte mit der erwarteten Größe initialisiert werden, um später ein internes Vergrößern zu vermeiden, das mehr Rechenzeit kostet. Falls der StringBuilder einen großen internen Puffer hat, aber auf lange Sicht nur wenig Zeichen besitzt, lässt er sich mit trimToSize() auf eine kleinere Größe schrumpfen.
Ändern der Länge
Soll der StringBuilder mehr Daten aufnehmen, so ändert setLength(int) die Länge in eine angegebene Anzahl von Zeichen. Der Parameter ist die neue Länge. Ist sie kleiner als length(), so wird der Rest der Zeichenkette einfach abgeschnitten. Die Größe des internen Puffers ändert sich dadurch nicht.
[zB] Beispiel
Hänge alle Strings eines Arrays mit einem Trenner zusammen, und schneide den letzten Trenner zum Schluss ab:
String[] elements = { "Manila", "Cebu", "Ipil" };
String separator = ", ";
StringBuilder sb = new StringBuilder();
for ( String elem : elements )
sb.append( elem ).append( separator );
sb.setLength( sb.length() - separator.length() );
System.out.println( sb ); // Manila, Cebu, Ipil
Ist setLength(int) größer, so vergrößert sich der Puffer, und die Methode füllt die übrigen Zeichen mit Nullzeichen '\0000' auf. Die Methode ensureCapacity(int) fordert, dass der interne Puffer für eine bestimmte Anzahl von Zeichen ausreicht. Wenn nötig, legt sie ein neues, vergrößertes char-Array an, verändert aber nicht die Zeichenfolge, die durch das StringBuilder-Objekt repräsentiert wird.
5.6.7 Vergleich von StringBuilder-Exemplaren und Strings mit StringBuilder
Zum Vergleichen von Strings bietet sich die bekannte equals(…)-Methode an. Diese ist aber bei StringBuilder nicht wie erwartet implementiert. Dazu gesellen sich andere Methoden, die zum Beispiel unabhängig von der Groß-/Kleinschreibung vergleichen.
equals(…) bei der String-Klasse
Die Klasse String implementiert die equals(Object)-Methode, sodass ein String mit einem anderen String verglichen werden kann. Allerdings vergleicht equals(Object) von String nur String/String-Paare. Die Methode beginnt erst dann den Vergleich, wenn das Argument auch vom Typ String ist. Das bedeutet, dass der Compiler alle Übergaben auch vom Typ StringBuilder bei equals(Object) zulässt, doch zur Laufzeit ist das Ergebnis immer false, da eben ein StringBuilder nicht vom Typ String ist. Ob die Zeichenfolgen dabei gleich sind, spielt keine Rolle.
contentEquals(…) beim String
Eine allgemeine Methode zum Vergleichen eines Strings mit entweder einem anderen String oder mit StringBuilder ist contentEquals(CharSequence). Die Methode liefert die Rückgabe true, wenn der String und die CharSequence den gleichen Zeicheninhalt haben. (String, StringBuilder und StringBuffer sind Klassen vom Typ CharSequence.) Die interne Länge des Puffers spielt keine Rolle. Ist das Argument null, wird eine NullPointerException ausgelöst.
[zB] Beispiel
Vergleiche einen String mit einem StringBuilder:
String s = "Elektrisch-Zahnbürster";
StringBuilder sb = new StringBuilder( "Elektrisch-Zahnbürster" );
System.out.println( s.equals(sb) ); // false
System.out.println( s.equals(sb.toString()) ); // true
System.out.println( s.contentEquals(sb) ); // true
Kein eigenes equals(…) bei StringBuilder
Wollen wir zwei StringBuilder-Objekte miteinander vergleichen, so geht das nicht mit der equals(…)-Methode. Es gibt zwar die übliche von Object geerbte Methode, doch das heißt, nur Objektreferenzen werden verglichen. Anders gesagt: StringBuilder überschreibt die equals(…)-Methode nicht. Wenn also zwei verschiedene StringBuilder-Objekte mit gleichem Inhalt mit equals(…) verglichen werden, kommt trotzdem immer false heraus.
[zB] Beispiel
Um den inhaltlichen Vergleich von zwei StringBuilder-Objekten zu realisieren, können wir sie erst mit toString() in Strings umwandeln und dann mit String-Methoden vergleichen:
StringBuilder sb1 = new StringBuilder( "The Ocean Cleanup" );
StringBuilder sb2 = new StringBuilder( "The Ocean Cleanup" );
System.out.println( sb1.equals( sb2 ) ); // false
System.out.println( sb1.toString().equals( sb2.toString() ) ); // true
System.out.println( sb1.toString().contentEquals( sb2 ) ); // true
Ab Java 11 gibt es bessere Lösungen.
Lexikografische Vergleiche (StringBuilder ist Comparable)
Seit Java 11 bietet StringBuilder eine Methode int compareTo(StringBuilder another), sodass lexikografische Vergleiche möglich sind. (StringBuilder implementiert die Schnittstelle Comparable<StringBuilder>.) Somit realisieren String und StringBuilder beide eine Ordnung, siehe den Abschnitt »Lexikografische Vergleiche mit Größer/Kleiner-Relation« in Abschnitt 5.5.7, »Gut, dass wir verglichen haben«.
Eine Begleiterscheinung ist die Tatsache, dass bei gleichen Zeichenfolgen die Rückgabe von compareTo(…) gleich 0 ist. Das ist deutlich besser, als erst den StringBuilder in einen String zu konvertieren.
Die compareTo(…)-Methode StringBuilder vergleicht nur mit anderen StringBuilder-Objekten. Flexibler ist da die in Java 11 hinzugekommene statische Methode compare(CharSequence, CharSequence) in CharSequence. Mit ihr ist ein lexikografischer Vergleich aller erdenklichen CharSequence-Exemplare möglich.
[zB] Beispiel
StringBuilder sb1 = new StringBuilder( "The Ocean Cleanup" );
StringBuilder sb2 = new StringBuilder( "The Ocean Cleanup" );
System.out.println( sb1.compareTo( sb2 ) == 0 ); // true
System.out.println( CharSequence.compare( sb1, sb2 ) == 0 ); // true
5.6.8 hashCode() bei StringBuilder *
Die obige Betrachtung zeigt, dass eine Methode equals(…), die den Inhalt von StringBuilder-Objekten vergleicht, nicht schlecht wäre. Dennoch besteht das Problem, wann StringBuilder-Objekte als gleich angesehen werden sollen. Das ist interessant, denn StringBuilder-Objekte sind nicht nur durch ihren Inhalt bestimmt, sondern auch durch die Größe ihres internen Puffers, also durch ihre Kapazität. Sollte equals(…) den Rückgabewert true haben, wenn die Inhalte gleich sind, oder nur dann, wenn Inhalt und Puffergröße gleich sind? Da jeder Entwickler andere Ansichten über die Gleichwertigkeit besitzt, bleibt es bei dem standardmäßigen Test auf identische Objektreferenzen. Eine ähnliche Argumentation gilt bei der hashCode()-Methode, die für alle inhaltsgleichen Objekte denselben, im Idealfall eindeutigen Zahlenwert liefert. Die Klasse String besitzt eine hashCode()-Methode, doch StringBuilder erbt die Implementierung aus der Klasse Object unverändert. Mit anderen Worten: Die Klassen selbst bieten keine Implementierung an.