03.08 StringBuffer und StringBuilder

Wie Sie wissen, sind Strings immutable und somit unveränderbar. Dies hat zur Folge, dass bei jeder Verknüpfung von Strings mit dem + Operator ein neuer String erzeugt wird, was in den meisten Fällen nicht gerade performant ist. Zwei Alternativen lernen Sie in diesem Kapitel kennen.

Ein Nachteil von Strings

Betrachten wir das oben angesprochene Problem noch einmal in der Praxis. Wenn Sie viele Zeichen einer Zeichenkette in einer Schleife hinzufügen, wird die Geschwindigkeit der Anwendung sehr schnell einbrechen. Führen Sie hierzu folgende Beispielanwendung aus:

package de.jbb;

public class Test {

  public static void main(String[] args) {
		
    String str = "";
    for (int i = 0; i < 100000; i++) {
      str += i;
    }
  }
}

Sie werden feststellen, dass das Programm ungewöhnlich lange läuft. Dies liegt wie bereits erwähnt daran, dass bei jedem Schleifendurchlauf ein neuer String erzeugt wird.

Die Lösung

Verwenden Sie allerdings anstelle eines Strings einen StringBuilder oder einen StringBuffer, verschwindet das Geschwindigkeits-Problem.

package de.jbb;

public class Test {

  public static void main(String[] args) {
		
    StringBuilder str = new StringBuilder();
    for (int i = 0; i < 100000; i++) {
      str.append(i);
    }
  }
}

Einsatzorte von String, StringBuilder und StringBuffer

Aber sollten Sie deshalb immer einen StringBuilder oder StringBuffer anstelle eines Strings verwenden? Nein! Denn Strings werden vom Compiler automatisch in StringBuffer bzw. StringBuilder umgewandelt. Dies funktioniert aber nicht immer, wie Sie beim ersten Beispiel in diesem Kapitel festgestellt haben. Aus

String str = "Simpler";
str += " String";
String str2 = "";
for (int i = 0; i < 10; i++) {
  str2 += str2.length();
}

macht der Compiler automatisch bspw. das hier:

String s = "Simpler";
s = (new StringBuilder()).append(s).append(" String").toString();
String s1 = "";
for(int i = 0; i < 10; i++)
  s1 = (new StringBuilder()).append(s1).append(s1.length()).toString();

Sie sehen, dass der Compiler die Verbindung von Strings, die außerhalb einer Schleife, also nicht dynamisch zusammengebaut werden, soweit akzeptabel löst. In der Schleife wird allerdings grober Unfug erzeugt, der garantiert nicht für die Performance förderlich ist.

Deshalb sollten Sie immer dann auf diese beiden Alternativen ausweichen, wenn es wichtig ist, dass Ihre Zeichenkette dynamisch veränderbar (z. B. in einer Schleife) ist, oder wenn Sie auf deren zusätzlichen Methoden wie z. B. insert (zusätzlichen Inhalt an eine beliebigen Stelle einfügen), setCharAt (ein Zeichen an einer bestimmten Stelle ersetzen), oder reverse (umkehren der Zeichenkette) zugreifen müssen/möchten. Die genauen Methodenaufrufe finden Sie in der Dokumentation zum StringBuilder bzw. StringBuffer. Bei der Entscheidung, ob Sie einen StringBuilder oder einen StringBuffer verwenden, sollten Sie beachten, dass ein StringBuffer im Gegensatz zum StringBuilder synchronisiert ist (mehr dazu lernen Sie im Kapitel über Threads), und dass es den StringBuilder erst seit Java 5 gibt (StringBuffer seit Java 1.0).

StringBuilder und StringBuffer in Kombination mit String

StringBuilder, StringBuffer und String sind nicht miteinander verwandt. Dadurch ist ein Vergleich, wie Sie ihn aus Kapitel 03.02 Strings vergleichen kennen, nicht mehr möglich. Wenn Sie StringBuilder und StringBuffer miteinander oder mit einem gewöhnlichen String vergleichen wollen, müssen Sie diese vorher in einen String mit der toString-Methode umwandeln.

StringBuilder build = new StringBuilder("123");
String str = "123";
StringBuffer buff = new StringBuffer("123");
System.out.println(build.equals(buff)); // false
System.out.println(buff.equals(str)); // false
System.out.println(buff.toString().equals(build.toString())); // true
System.out.println(str.equals(buff.toString())); // true

Dies gilt übrigens auch für scheinbar gleiche Instanzen selben Typs:

StringBuffer buffer = new StringBuffer("123");
StringBuffer buffer2 = new StringBuffer("123");
System.out.println(buffer.equals(buffer2)); // false

StringBuilder builder = new StringBuilder("123");
StringBuilder builder2 = new StringBuilder("123");
System.out.println(builder.equals(builder2)); // false

Über die String-Methode contentEquals können Sie aber zumindest den Inhalt eines Strings direkt mit dem eines StringBuilders oder StringBuffers vergleichen. Wenn Sie einen StringBuffer mit einem StringBuilder vergleichen möchten, müssen Sie trotzdem noch mindestens einen von beiden in einen gewöhnlichen String umwandeln.

StringBuilder build = new StringBuilder("123");
String str = "123";
StringBuffer buff = new StringBuffer("123");
System.out.println(str.contentEquals(buff)); // true
System.out.println(build.toString().contentEquals(buff)); // true

6 Replies to “03.08 StringBuffer und StringBuilder”

  1. Michael

    Hallo,

    ein kleiner Verbesserungsvorschlag, ich finde man könnte bei den Vergleichen noch erwähnen, dass wenn man 2 unterschiedliche Instanzen von StringBuilder/StringBuffer vergleicht auch false rauskommt !!obowhl!! gleichen Inhaltes.

    
            StringBuilder builder = new StringBuilder("123");
            StringBuilder builder2 = new StringBuilder("123");
            
            if(builder.equals(builder2)) // false
    
  2. Marc

    Hallo,

    ich hätte da mal eine kleine Frage. Also d.h. außerhalb einer Schleife ist ein StringBuilder egal wieviel Strings ich aneinanderreihe total überflüßig(außer ich benötige die Methoden)?

    Ist es nicht so wenn ich den default StringBuilder () benutze er nach 16 zeichen einen neuen String erzeugt und diesen dann einfügen muss. Darum sollte doch vorher abgeschätzt werden wie groß der Builder werden kann , sonst ist der Vorteil geringer?z.B. new StringBuilder(255);

    Ich freu mich auf Ihre Antwort :)!!!

    Gruß Marc

  3. Stefan Kiesel

    Hallo Marc,

    völlig egal ist es nicht. Wie am obigen Beispiel gesehen werden kann, würde der Compiler aus dem hier:

    String str = "Simpler";
    str += " String";

    das hier machen

    String s = "Simpler";
    s = (new StringBuilder()).append(s).append(" String").toString();

    Noch performanter wäre es natürlich, wenn man von Anfang an das hier schreiben würde

    StringBuilder s = new StringBuilder("Simpler");
    s.append(" String");

    Aber bei so etwas würde ich die Lesbarkeit (die bei der Verwendung von String deutlich höher sein sollte) über den minimalen Performancevorteil stellen.

    Um die zweite Frage zu beantworte, werfen wir am Besten einen Blick in den Java Sourcecode. Was passiert, wenn ein weiterer String an den StringBuilder via append angehängt wird? Die Lösung befindet sich im Sourcecode der Klasse AbstractStringBuilder von welcher der StringBuilder abgeleitet ist.

    public AbstractStringBuilder append(String str) {
      if (str == null) str = "null";
      int len = str.length();
      if (len == 0) return this;
      int newCount = count + len;
      if (newCount > value.length)
        expandCapacity(newCount);
      str.getChars(0, len, value, count);
      count = newCount;
      return this;
    }

    Für die Frage ist nur die Zeile

    if (newCount > value.length)
      expandCapacity(newCount);

    relevant. Was paassiert, wenn die neue Größe des StringBuilders die Alte übersteigt?

    void expandCapacity(int minimumCapacity) {
      int newCapacity = (value.length + 1) * 2;
      if (newCapacity < 0) {
        newCapacity = Integer.MAX_VALUE;
      } else if (minimumCapacity > newCapacity) {
        newCapacity = minimumCapacity;
      }
      value = Arrays.copyOf(value, newCapacity);
    }

    Die Kapazität wird um das Doppelte erhöht. Wird der Wertebereich des Integers überschritten, ist die neue Kapazität gleich der maximalen Größe eines Integers. Außerdem wird noch überprüft, ob die Verdoppelung der Kapazität für die neue Größe ausreicht. Ist dies nicht der Fall, wird die aktuelle Größe gleich der neuen Größe gesetzt. Abschließend wird das alte Array, welches den Inhalt des StringBuilders beinhaltet, in ein neues, an die neue Größe angepasstes, Array kopiert.

    Der "+16"-Mythos resultiert daraus, dass beim Anlegen eines neuen StringBuilders automatisch die Kapazität auf "Länge des Initial-Strings" + 16 gesetzt wird.

    Dennoch ist es optimal, die ungefähre Kapazität des StringBuilders im Voraus abzuschätzen - sofern möglich.

    Ich hoffe ich konnte helfen. Ansonsten einfach noch einmal nachfragen.

    Gruß
    Stefan

  4. Marc

    Vielen Dank für die schnelle Antwort.
    Der Joshua Bloch schreibt, dass man den SB verwenden soll sobald mehr als 4 Strings verknüpft werden.
    Zu der Kapazität noch eine Frage, falls man die Größe nicht einschätzen kann. Ist es besser einen „leeren“ StringBuilder anzulegen oder einen vielleicht zu großen z.B. 255???

  5. Sebastian Würkner

    Hallo Marc,

    aus Sicht der Performance ist es besser einen „zu großen“ StringBuilder anzulegen, da später kein aufwendiges Arrays.copyOf() statt finden muss. Aus Sicht der Ressourcen belegt ein StringBuilder dann aber auch sofort ein char Array der entsprechenden Initialgröße (hier 255). Dies ist aber vernachlässigbar.

    Im Code zu AbstractStringBuilder kann man sich den Konstruktor auch anschauen:

    char value[];
    ...
    
    AbstractStringBuilder(int capacity) {
      value = new char[capacity];
    }

    Gruß
    Sebastian

Schreibe einen Kommentar

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.