Unterschied zwischen StringBuilder, StringBuffer und String in Java

In der Programmiersprache Java gibt es verschiedene Möglichkeiten, Texte zu manipulieren und zu bearbeiten. Dazu gehören die Klassen StringBuilder, StringBuffer und String. Obwohl sie alle für die Arbeit mit Zeichenketten verwendet werden, gibt es einige wichtige Unterschiede zwischen ihnen. In diesem Artikel werden wir uns genauer mit diesen Unterschieden befassen.

Der wesentliche Unterschied zwischen String, StringBuilder und StringBuffer in Java liegt in ihrer "immutability" (Veränderbarkeit) und Thread-Sicherheit.

String: Strings in Java sind unveränderlich (immutable), d.h. ihr Wert kann nicht geändert werden, sobald sie erstellt wurden. Das macht ihre Verwendung in Multi-Thread-Umgebungen sicher, kann aber in Bezug auf die Speichernutzung weniger effizient sein, da jedes Mal, wenn Sie eine Zeichenkette ändern, ein neues String-Objekt erstellt wird.

StringBuilder: StringBuilder ist eine veränderbare Version von String, die für die Verwendung in Single-Thread-Umgebungen konzipiert ist (also nicht thread-safe!). Er bietet Methoden zum Anhängen, Einfügen oder Löschen von Zeichen aus einer Zeichenkette, so dass Sie Zeichenketten dynamisch erstellen können. StringBuilder ist für Single-Thread-Anwendungen schneller als StringBuffer, da er nicht synchronisiert ist.

StringBuffer: StringBuffer ist ähnlich wie StringBuilder, aber es ist thread-safe, so dass es geeignet für den Einsatz in Multi-Thread-Umgebungen. StringBuffer bietet Methoden zum Anhängen, Einfügen oder Löschen von Zeichen aus einer Zeichenkette, genau wie StringBuilder, aber es ist langsamer als StringBuilder aufgrund des Overheads für die Synchronisation.

Noch ein Beispiel für StringBuilder vs. StringBuffer beim Zugriff aus mehreren Threads:

public class StringBuilderVsStringBuffer {
    
    private static StringBuilder builder = new StringBuilder();
    private static StringBuffer buffer = new StringBuffer();
    
    public static void main(String[] args) {
       
        // Hier wird einfach 
        Runnable runnable = () -> {
            for (int i = 0; i < 1000; i++) {
                builder.append("s");
                buffer.append("s");
            }
        };
        
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        
        thread1.start();
        thread2.start();
        
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println("StringBuilder Länge: " + builder.length()); // unvorhersehbar
        System.out.println("StringBuffer Länge: " + buffer.length());   // immer 2000
    }
}

In diesem Beispiel haben wir zwei Threads, die gleichzeitig den Buchstaben "s" an ein StringBuilder- und ein StringBuffer-Objekt anhängen. Da StringBuffer thread-safe ist, können wir erwarten, dass der sich in einer Multi-Thread-Umgebung korrekt verhält (die Länge des StringBuffer-Objekts immer 2000 beträgt - 1000 jeweils von jedem Thread). Beim StringBuilder können die beiden Therads das Objekt gleichzeitig verändern, deswegen können wir am Ende die Länge nicht vorhersagen.

String-Verkettung und Performance

Auch bei String-Konkatenation gibt es wesentliche Unterschiede zwischen StringBuilder, StringBuffer und der String-Verkettung mit dem "+=" Operator:

Dazu eine kleine Demonstration:

public class StringConcatenationPerformance {
    private static final int COUNT = 1000000;

    public static void main(String[] args) {

        // StringBuilder performance test
        long startTime = System.nanoTime();
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < COUNT; i++) {
            stringBuilder.append("s");
        }
        String resultStringBuilder = stringBuilder.toString();
        long endTime = System.nanoTime();
        double durationStringBuilder = (endTime - startTime) / 1e9;
        System.out.println("StringBuilder: " + durationStringBuilder + " sec");

        // StringBuffer performance test
        startTime = System.nanoTime();
        StringBuffer stringBuffer = new StringBuffer();
        for (int i = 0; i < COUNT; i++) {
            stringBuffer.append("s");
        }
        String resultStringBuffer = stringBuffer.toString();
        endTime = System.nanoTime();
        double durationStringBuffer = (endTime - startTime) / 1e9;
        System.out.println("StringBuffer: " + durationStringBuffer + " sec");

        // String concatenation performance test
        startTime = System.nanoTime();
        String concatenatedString = "";
        for (int i = 0; i < COUNT; i++) {
            concatenatedString += "s";
        }
        endTime = System.nanoTime();
        double durationString = (endTime - startTime) / 1e9;
        System.out.println("String Concatenation: " + durationString + " sec");
    }
}

Auf einem MacBook M1 bekommen wir als Resultat mit COUNT = 1000000:
StringBuilder: 0.008037541 sec
StringBuffer: 0.011136042 sec
String Concatenation: 21.508547375 sec