StringBuilder & StringBuffer

Mutable Alternatives to String for Better Performance

CPCS203 - Module 2 | Lesson 4

1 The Problem: Why We Need Mutable Strings

Remember from our previous lessons that String is immutable. Every modification creates a new object:

String result = ""; for (int i = 0; i < 1000; i++) { result = result + i; // Creates 1000 new String objects! }
The Problem
  • Each + creates a new String object
  • The old String becomes garbage
  • This is very slow and wastes memory
  • With 1000 iterations: ~1000 unnecessary objects created!

The Solution: Use StringBuilder or StringBuffer - they are mutable, meaning they can be modified without creating new objects!

2 Comparison: String vs StringBuilder vs StringBuffer

String
IMMUTABLE
  • Cannot modify content
  • Creates new objects
  • Thread-safe (content never changes)
  • Good for constants
StringBuilder
MUTABLE
FAST
  • Content can be changed
  • No new objects created
  • NOT thread-safe
  • Best for single-threaded
StringBuffer
MUTABLE
THREAD-SAFE
  • Content can be changed
  • No new objects created
  • IS thread-safe
  • Slightly slower (due to locking)
Key Difference: Thread Safety

The only difference between StringBuilder and StringBuffer is thread safety:

  • StringBuilder = Faster, but NOT safe for multiple threads
  • StringBuffer = Slightly slower, but SAFE for multiple threads

3 Understanding Multi-Threading

Before we understand thread safety, let's first understand what threads are.

What is a Thread?

A thread is a lightweight process - a sequence of instructions that can run independently within a program. Think of it as a separate "worker" doing a task.

Example 1: Single-Threaded (Sequential Execution)

In a normal program, code runs sequentially - one line after another:

public class SingleThreadDemo { public static void main(String[] args) { // Loop 1: Print "Java" 3 times for (int i = 0; i < 3; i++) { System.out.println("Java"); } // Loop 2: Print "HTML" 3 times for (int i = 0; i < 3; i++) { System.out.println("HTML"); } } }
What is the output?

The output is predictable - Loop 1 finishes completely, then Loop 2 starts:

Java
Java
Java
HTML
HTML
HTML

Example 2: Multi-Threaded (Parallel Execution)

With threads, we can run both loops at the same time:

public class MultiThreadDemo { public static void main(String[] args) { // Thread t1: Will print "Java" Thread t1 = new Thread(() -> { for (int i = 0; i < 3; i++) { System.out.println("Java"); } }); // Thread t2: Will print "HTML" Thread t2 = new Thread(() -> { for (int i = 0; i < 3; i++) { System.out.println("HTML"); } }); // Start both threads - they run simultaneously! t1.start(); t2.start(); } }
What is the output?

The output is UNPREDICTABLE! Both threads run at the same time, so the output can be different each time:

// Run 1:
Java
HTML
Java
HTML
Java
HTML
// Run 2:
HTML
Java
Java
HTML
HTML
Java
// Run 3:
Java
Java
HTML
Java
HTML
HTML
How CPU Handles Multiple Threads
💻
CPU

Switches between threads very fast

t1: print("Java") → pause → print("Java") → ...
t2: print("HTML") → pause → print("HTML") → ...

The CPU rapidly switches between t1 and t2, giving the illusion of parallel execution.

Note for Students:

Multi-threading is a complex topic covered in detail in:

  • Concurrency courses
  • Operating Systems courses

For now, just understand the basic concept: threads allow code to run simultaneously, but the order of execution is unpredictable!

4 What is Thread Safety?

When multiple threads access the same data at the same time, problems can occur if they try to modify it simultaneously.

The Problem: Race Condition

Imagine two threads both trying to append to the same StringBuilder:

  • Thread 1 wants to append "Java"
  • Thread 2 wants to append "HTML"
  • They both read the current state simultaneously
  • They both write at the same time
  • Result: Data corruption! Maybe "JaHTvMaL"
How StringBuffer Solves This: Locking
🔒

StringBuffer uses synchronization (locking):

  1. Thread 1 arrives, acquires the lock
  2. Thread 2 arrives, must wait (locked)
  3. Thread 1 finishes, releases the lock
  4. Thread 2 acquires the lock and proceeds

This ensures only one thread modifies at a time.

Thread-Safe = Safe to use with multiple threads

  • StringBuffer: Thread-safe (has locking) - slightly slower
  • StringBuilder: NOT thread-safe (no locking) - faster

5 Using StringBuilder

// Creating a StringBuilder StringBuilder sb = new StringBuilder(); // Or with initial content StringBuilder sb2 = new StringBuilder("Hello"); // append() - Add to the end sb.append("Hello"); sb.append(" "); sb.append("World"); System.out.println(sb); // Hello World // Method chaining (returns same object) sb.append("!").append(" Java"); System.out.println(sb); // Hello World! Java // insert() - Insert at specific position sb.insert(0, "*** "); System.out.println(sb); // *** Hello World! Java // delete() - Remove characters sb.delete(0, 4); // Remove "*** " System.out.println(sb); // Hello World! Java // replace() - Replace range with new string sb.replace(0, 5, "Hi"); System.out.println(sb); // Hi World! Java // reverse() - Reverse the content sb.reverse(); System.out.println(sb); // avaJ !dlroW iH // Convert to String when done String result = sb.toString();
Key StringBuilder Methods
Method Description
append(x) Add x to the end
insert(pos, x) Insert x at position
delete(start, end) Remove characters from start to end-1
replace(start, end, str) Replace range with str
reverse() Reverse the content
toString() Convert to String

6 Performance Comparison: String vs StringBuilder

Let's see the dramatic difference in performance. Copy and run this code:

public class PerformanceTest { public static void main(String[] args) { int iterations = 100000; // Test String concatenation long startTime = System.nanoTime(); String str = ""; for (int i = 0; i < iterations; i++) { str = str + "a"; } long stringTime = System.nanoTime() - startTime; // Test StringBuilder startTime = System.nanoTime(); StringBuilder sb = new StringBuilder(); for (int i = 0; i < iterations; i++) { sb.append("a"); } long builderTime = System.nanoTime() - startTime; // Print results in milliseconds System.out.println("String time: " + stringTime / 1000000 + " ms"); System.out.println("StringBuilder time: " + builderTime / 1000000 + " ms"); System.out.println("StringBuilder is " + (stringTime / builderTime) + "x faster!"); } }
Typical Results (100,000 iterations)
String:
~5000 ms
StringBuilder:
~5 ms

StringBuilder is ~1000x faster!

7 Decision Guide: Which One to Use?

Quick Decision Guide

Use String

  • Content rarely or never changes
  • Simple, short operations
  • Few concatenations (1-5)
  • Constants and literals
String name = "Java";

Use StringBuilder

  • Content changes frequently
  • Building strings in loops
  • Single-threaded application
  • Maximum performance needed
StringBuilder sb = ...

Use StringBuffer

  • Content changes frequently
  • Multiple threads access it
  • Thread safety is required
  • Shared mutable state
StringBuffer sbuf = ...

Simple Rule of Thumb:

  • For most cases: Use StringBuilder (99% of the time)
  • Only use StringBuffer when you specifically need thread safety
  • Use String for constants and simple cases

8 Summary

Key Takeaways
  1. String is immutable - modifications create new objects (slow for many changes)
  2. StringBuilder is mutable - modifies in place (fast, single-threaded)
  3. StringBuffer is mutable - modifies in place with locking (thread-safe)
  4. Multi-threading = multiple tasks running simultaneously
  5. Thread-safe = safe to use with multiple threads (uses locking)
  6. StringBuilder is ~1000x faster than String for many modifications
  7. Use StringBuilder for most cases when you need to modify strings frequently

Congratulations!

You now understand the differences between String, StringBuilder, and StringBuffer! In the next lesson, you'll practice these concepts with exercises.