Skip to main content
DevHivesBlog

Search the blog

Find a post by title, tag or topic

String vs StringBuilder vs StringBuffer in Java (With Code)

String is immutable, StringBuilder is fast and mutable, StringBuffer is mutable and thread safe. Compare all three with compiled Java examples and a simple rule.

Arnab Biswas5 min read
WhatsApp
Three bars of different heights, one tall and two short, comparing the cost of three ways to build a Java string

Say you write a loop that joins ten thousand words into one sentence using +=. It works on your laptop. Then your teacher tries a hundred thousand words and your program crawls. Nothing is wrong with the logic. The cause is how Java treats String.

Java gives you three classes for text: String, StringBuilder and StringBuffer. This post shows what separates them, with code that compiles, and a simple rule for choosing.

Use String for text that does not change. Use StringBuilder when you build or edit text in a loop. Use StringBuffer only when several threads must share one text object, which is rare. StringBuilder and StringBuffer have the same methods, and they differ in thread safety.

String vs StringBuilder vs StringBuffer: comparison table

PointStringStringBuilderStringBuffer
Can you change it?No (immutable)Yes (mutable)Yes (mutable)
Thread safe?Yes, because it never changesNoYes (synchronized methods)
Speed for many editsSlowFastestSlower than StringBuilder
Stored in the string pool?Literals areNoNo
equals() comparesThe textThe reference onlyThe reference only
Typical useNames, keys, constantsLoops, building outputShared text edited by threads

The class pages in the Oracle Javadoc confirm the key facts: StringBuilder is described as a mutable sequence of characters with no synchronization guarantee, and StringBuffer is the thread-safe version.

String is immutable

Immutable means the object cannot change after it is created. A method such as concat does not edit the text. It returns a new String, and the old one stays as it was.

public class Immutable {
    public static void main(String[] args) {
        String s = "Dev";
        s.concat("Hives");              // result thrown away
        System.out.println(s);          // Dev
        s = s.concat("Hives");          // new object, reassigned
        System.out.println(s);          // DevHives
        StringBuilder sb = new StringBuilder("Dev");
        sb.append("Hives");             // same object changes
        System.out.println(sb);         // DevHives
    }
}

The first print shows Dev, because the result of concat was never saved. This is one of the most common beginner mistakes. The StringBuilder at the bottom changes in place, so no reassignment is needed.

The string pool and why == surprises people

Java keeps a pool of string literals. Two literals with the same text point to the same object, while new String(...) forces a separate one. Always compare text with equals, not ==.

public class Pool {
    public static void main(String[] args) {
        String a = "java";
        String b = "java";
        String c = new String("java");
        System.out.println(a == b);        // true  (same pooled object)
        System.out.println(a == c);        // false (different object)
        System.out.println(a.equals(c));   // true  (same text)
        StringBuilder x = new StringBuilder("java");
        StringBuilder y = new StringBuilder("java");
        System.out.println(x.equals(y));                       // false
        System.out.println(x.toString().equals(y.toString())); // true
    }
}

Note the last two lines. StringBuilder does not override equals, so two builders holding the same text are not equal. Convert both with toString() before comparing.

StringBuilder methods you will use

StringBuilder edits the same object again and again. The methods below cover most beginner needs.

public class Methods {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder("hello");
        sb.append(" world");
        sb.insert(0, ">> ");
        sb.reverse();
        sb.reverse();
        sb.setCharAt(3, 'H');
        sb.deleteCharAt(sb.length() - 1);
        System.out.println(sb);              // >> Hello worl
        System.out.println(sb.length());     // 13
        String result = sb.toString();
        System.out.println(result.toUpperCase());
    }
}

The program prints >> Hello worl, then 13, then >> HELLO WORL. Call toString() at the end when you need a normal String back.

  1. Create

    new StringBuilder() or start from text.

  2. Edit

    Use append, insert, delete or reverse.

  3. Finish

    Call toString() to get a String.

Speed: why += in a loop is slow

Each s += "a" creates a new String and copies all the old characters into it. Doing that many times copies the text again and again. StringBuilder keeps one growing buffer instead.

public class Speed {
    public static void main(String[] args) {
        int n = 100_000;
        long t1 = System.nanoTime();
        String s = "";
        for (int i = 0; i < n; i++) {
            s += "a";
        }
        long stringMs = (System.nanoTime() - t1) / 1_000_000;
        long t2 = System.nanoTime();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < n; i++) {
            sb.append("a");
        }
        long builderMs = (System.nanoTime() - t2) / 1_000_000;
        System.out.println("String +=      : " + stringMs + " ms, length " + s.length());
        System.out.println("StringBuilder  : " + builderMs + " ms, length " + sb.length());
    }
}

I ran this on JDK 21. The String version took far longer than the StringBuilder version, by a large factor. Your times will differ with your computer, so run it yourself. The gap grows as the loop gets longer.

A single + in one statement, like "Hello " + name, is fine. The Java compiler handles it well. The problem is repeated concatenation inside a loop.

Thread safety: where StringBuffer fits

StringBuffer makes each method synchronized. That means only one thread can run a method on it at a time. StringBuilder skips that protection, so two threads writing to one builder can lose updates.

public class Threads {
    public static void main(String[] args) throws InterruptedException {
        StringBuilder builder = new StringBuilder();
        StringBuffer buffer = new StringBuffer();
        Runnable task = () -> {
            for (int i = 0; i < 10_000; i++) {
                builder.append("x");
                buffer.append("x");
            }
        };
        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("Expected 20000");
        System.out.println("StringBuffer  : " + buffer.length());
        System.out.println("StringBuilder : " + builder.length());
    }
}

The StringBuffer length is always 20000. The StringBuilder length is often lower, and it can even throw an exception. In my run it printed 19237, but the number changes every time, because the loss depends on timing.

Even so, StringBuffer is rarely the best answer today. When threads share text, many programmers keep one builder per thread and join the results at the end, which avoids locking altogether.

Which one should you pick?

Pick StringBuilder when

  • You build text in a loop
  • Only one thread touches the text
  • You need the fastest option

Pick StringBuffer when

  • Several threads must write to the same object
  • You maintain older code that already uses it
  • Safety matters more than the small speed cost

For plain text that never changes, use String. It is the safest and the simplest. If you are working through the basics of Java in order, the free Java beginner syllabus shows where strings fit.

Interview answers in one line each

  • Why is String immutable? Immutable objects are safe to share and reuse, which makes the string pool and use as map keys safe.
  • Which is faster, StringBuilder or StringBuffer? StringBuilder, because it has no synchronization.
  • How do you reverse a string? new StringBuilder(s).reverse().toString().
  • Does StringBuilder override equals? No. Compare with toString().equals(...).

A real example: building a report line

Say you have a list of student names and you want one comma-separated line for a file. A loop with StringBuilder is the classic way. Java also offers String.join, which does the same job in one call and uses a builder inside.

import java.util.List;
public class Report {
    public static void main(String[] args) {
        List<String> names = List.of("Asha", "Rohan", "Meera");
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < names.size(); i++) {
            if (i > 0) {
                sb.append(", ");
            }
            sb.append(names.get(i));
        }
        System.out.println(sb);                        // Asha, Rohan, Meera
        System.out.println(String.join(", ", names));  // Asha, Rohan, Meera
    }
}

Use String.join when you already have a list. Use the builder when each piece needs logic, such as skipping blanks or adding a prefix.

How a StringBuilder grows

A StringBuilder holds an internal array of characters. The Javadoc says the no-argument constructor starts with room for 16 characters. When you add more than fits, Java allocates a bigger array and copies the old text across. That copy is cheap compared with copying on every single append, but it is not free.

If you already know the rough size, tell the builder at the start. This avoids repeated growth when you are building very large text.

public class Capacity {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder(1000);   // room for 1000 chars
        System.out.println(sb.length());              // 0, nothing added yet
        System.out.println(sb.capacity());            // 1000
        sb.append("ready");
        System.out.println(sb.length());              // 5
    }
}

Length is how many characters you have added. Capacity is how many fit before the next growth. Beginners often mix the two up.

Common mistakes

  • Ignoring the return value. s.toUpperCase(); alone does nothing. Save the result.
  • Using == on strings. It compares references, not text.
  • Using += in a long loop. Switch to StringBuilder.
  • Using StringBuffer everywhere. It adds locking cost that most programs never need.

What to do next

Copy the Speed program and run it on your own machine. Then change n to a smaller and a larger number and watch how the gap moves. Save your results in a README, using the layout from the GitHub README guide for beginners.

ShareWhatsAppLinkedInX

Picked from the tags and topic of this post.

A large question mark, symbolizing the uncertainty about which free Java course to choose in 2026.
study tips9 min read

Best Free Java Course for Beginners in 2026: Honest Picks

As of October 2026, the top-ranked Java MOOC is officially legacy. Here's the truly free course to start with, plus which "free" offers hide a certificate fee, and a five-step path to your first Java project.

Arnab Biswas