Java Tutorial
🔍

Java String vs StringBuilder vs StringBuffer

Java String vs StringBuilder vs StringBuffer

Three classes. All three work with text. All three live in java.lang. A developer writing their first Java program uses String without thinking twice. A developer working on a report generator that builds a ten-thousand-line output discovers that using + in a loop is unbearably slow and switches to StringBuilder. A developer building a shared logging system for a multi-threaded application learns that StringBuilder is not safe to share between threads and reaches for StringBuffer.

Each class exists because the previous one could not handle a specific real-world need. Understanding when to use each — and why — separates code that works from code that works well.

The Core Distinction in One Place

String           — Immutable.  Every change creates a new object.
                   Safe for sharing. Safe for equality comparison.
                   Use for values you do not build iteratively.

StringBuilder    — Mutable.    Modifies the same buffer in place.
                   NOT thread-safe. Fastest for building strings.
                   Use inside loops and single-threaded builders.

StringBuffer     — Mutable.    Same API as StringBuilder.
                   Thread-safe — every method is synchronised.
                   Use only when multiple threads share one buffer.

Performance order (fastest to slowest for building):
  StringBuilder  >  StringBuffer  >  String (+)
  (no sync)         (sync overhead)   (new obj each concat)

1 — String: Immutable, Safe, Everywhere

Every String operation returns a new String. The original is never modified. This is not a limitation — it is a deliberate design that makes Strings safe to share between threads without any synchronisation, safe to use as HashMap keys, and eligible for pool reuse.

Java
1// File: StringBehaviour.java 2 3public class StringBehaviour { 4 5 public static void main(String[] args) { 6 7 String city = "mumbai"; 8 9 // Every method returns a NEW String — original unchanged 10 String upper = city.toUpperCase(); 11 String trimmed = " hello ".trim(); 12 String replaced = city.replace("mumbai", "delhi"); 13 14 System.out.println("Original : " + city); // mumbai — unchanged 15 System.out.println("Upper : " + upper); // MUMBAI 16 System.out.println("Replaced : " + replaced); // delhi 17 System.out.println("city still: " + city); // mumbai — still unchanged 18 19 System.out.println(); 20 21 // String concatenation with + in a simple expression — fine 22 String name = "Priya"; 23 String role = "Developer"; 24 String profile = "Name: " + name + " | Role: " + role; // OK — small, fixed 25 System.out.println(profile); 26 27 System.out.println(); 28 29 // String concatenation with + in a LOOP — problem 30 String result = ""; 31 for (int i = 1; i <= 5; i++) { 32 result = result + "item" + i + ","; 33 // Each iteration: old result + new content = NEW String object 34 // Old string is discarded. For 100,000 iterations, 100,000 discarded objects. 35 } 36 System.out.println("Loop result: " + result); 37 38 System.out.println(); 39 40 // When String is the RIGHT choice 41 String orderId = "ORD-2024-001"; // fixed value — no building needed 42 String status = "PLACED"; // constant-like 43 boolean isPlaced = "PLACED".equals(status); // safe comparison 44 System.out.println("Is placed: " + isPlaced); 45 } 46}
Output:
Original  : mumbai
Upper     : MUMBAI
Replaced  : delhi
city still: mumbai

Name: Priya | Role: Developer

Loop result: item1,item2,item3,item4,item5,

Is placed: true

2 — StringBuilder: Mutable, Fast, Single-Threaded

StringBuilder maintains a single internal buffer. Every append(), insert(), delete(), and replace() modifies the same buffer. No new objects are created until toString() is called. It is not synchronised — safe and fast in a single thread, undefined behaviour if shared across threads.

Java
1// File: StringBuilderBehaviour.java 2 3public class StringBuilderBehaviour { 4 5 public static void main(String[] args) { 6 7 // Same loop as before — now with StringBuilder 8 StringBuilder sb = new StringBuilder(); 9 for (int i = 1; i <= 5; i++) { 10 sb.append("item").append(i).append(","); 11 // No new objects — same buffer modified every iteration 12 } 13 System.out.println("Loop result: " + sb.toString()); 14 15 System.out.println(); 16 17 // All the modification methods 18 StringBuilder demo = new StringBuilder("Hello World"); 19 demo.insert(5, ","); // Hello, World 20 demo.replace(7, 12, "Java"); // Hello, Java 21 demo.delete(0, 7); // Java 22 demo.reverse(); // avaJ 23 demo.append("!!!"); // avaJ!!! 24 System.out.println("Modified: " + demo); 25 26 System.out.println(); 27 28 // Chaining — all methods return 'this' 29 String result = new StringBuilder() 30 .append("Order: ORD-001") 31 .append(" | Customer: Priya") 32 .append(" | Amount: Rs.1299") 33 .append(" | Status: PLACED") 34 .toString(); 35 System.out.println(result); 36 37 System.out.println(); 38 39 // StringBuilder is NOT thread-safe — do NOT share between threads 40 // Local variables are fine — they are never seen by other threads 41 StringBuilder localOnly = new StringBuilder(); // safe — local to this method 42 localOnly.append("This is safe to use here."); 43 System.out.println(localOnly); 44 } 45}
Output:
Loop result: item1,item2,item3,item4,item5,

Modified: avaJ!!!

Order: ORD-001 | Customer: Priya | Amount: Rs.1299 | Status: PLACED

This is safe to use here.

3 — StringBuffer: Mutable, Thread-Safe, Shared

StringBuffer is identical to StringBuilder in every method — same append(), insert(), delete(), replace(), reverse(). The only difference: every method carries synchronized. One thread at a time. Correct for concurrent access, slower than StringBuilder due to locking.

Java
1// File: StringBufferBehaviour.java 2 3import java.util.concurrent.CountDownLatch; 4 5public class StringBufferBehaviour { 6 7 public static void main(String[] args) throws InterruptedException { 8 9 // Show that StringBuffer is safe when shared across threads 10 StringBuffer shared = new StringBuffer(); 11 CountDownLatch latch = new CountDownLatch(3); 12 13 // Three threads all appending to the same buffer 14 for (int t = 1; t <= 3; t++) { 15 final int threadNum = t; 16 new Thread(() -> { 17 for (int i = 1; i <= 3; i++) { 18 shared.append("T").append(threadNum) 19 .append("-MSG").append(i).append(" | "); 20 } 21 latch.countDown(); 22 }).start(); 23 } 24 25 latch.await(); // wait for all threads to finish 26 27 String result = shared.toString(); 28 System.out.println("StringBuffer — all messages present:"); 29 System.out.println("Message count : " + result.split("\\| ").length); 30 System.out.println("Contains T1 : " + result.contains("T1-MSG1")); 31 System.out.println("Contains T2 : " + result.contains("T2-MSG1")); 32 System.out.println("Contains T3 : " + result.contains("T3-MSG1")); 33 System.out.println("No data loss : " + (result.split("\\| ").length >= 9)); 34 35 System.out.println(); 36 37 // All the same methods as StringBuilder 38 StringBuffer buf = new StringBuffer("Java StringBuffer"); 39 System.out.println("Original : " + buf); 40 buf.insert(5, " is"); 41 System.out.println("insert : " + buf); 42 buf.delete(0, 5); 43 System.out.println("delete : " + buf); 44 buf.replace(0, 2, "IS"); 45 System.out.println("replace : " + buf); 46 buf.reverse(); 47 System.out.println("reverse : " + buf); 48 } 49}
Output:
StringBuffer — all messages present:
Message count : 9
Contains T1   : true
Contains T2   : true
Contains T3   : true
No data loss  : true

Original  : Java StringBuffer
insert    : Java is StringBuffer
delete    : is StringBuffer
replace   : IS StringBuffer
reverse   : reffuBgnirtS SI

Side-by-Side Performance Comparison

Java
1// File: PerformanceComparison.java 2 3public class PerformanceComparison { 4 5 static final int ITERATIONS = 100_000; 6 7 // Method 1 — String with + 8 static long testStringConcat() { 9 long start = System.currentTimeMillis(); 10 String result = ""; 11 for (int i = 0; i < ITERATIONS; i++) { 12 result = result + i + ","; 13 } 14 return System.currentTimeMillis() - start; 15 } 16 17 // Method 2 — StringBuilder 18 static long testStringBuilder() { 19 long start = System.currentTimeMillis(); 20 StringBuilder sb = new StringBuilder(); 21 for (int i = 0; i < ITERATIONS; i++) { 22 sb.append(i).append(","); 23 } 24 String result = sb.toString(); 25 return System.currentTimeMillis() - start; 26 } 27 28 // Method 3 — StringBuffer 29 static long testStringBuffer() { 30 long start = System.currentTimeMillis(); 31 StringBuffer sbf = new StringBuffer(); 32 for (int i = 0; i < ITERATIONS; i++) { 33 sbf.append(i).append(","); 34 } 35 String result = sbf.toString(); 36 return System.currentTimeMillis() - start; 37 } 38 39 public static void main(String[] args) { 40 41 // Warm up JVM first 42 testStringBuilder(); 43 testStringBuffer(); 44 45 long stringTime = testStringConcat(); 46 long sbTime = testStringBuilder(); 47 long sbfTime = testStringBuffer(); 48 49 System.out.println("Performance comparison (" + ITERATIONS + " iterations):"); 50 System.out.println("─".repeat(45)); 51 System.out.printf("%-20s : %5d ms%n", "String (+)", stringTime); 52 System.out.printf("%-20s : %5d ms%n", "StringBuilder", sbTime); 53 System.out.printf("%-20s : %5d ms%n", "StringBuffer", sbfTime); 54 System.out.println("─".repeat(45)); 55 56 if (sbTime > 0) { 57 System.out.printf("String is ~%dx slower than StringBuilder%n", 58 stringTime / sbTime); 59 } 60 if (sbfTime > 0 && sbTime > 0) { 61 System.out.printf("StringBuffer is ~%dx slower than StringBuilder%n", 62 sbfTime / Math.max(sbTime, 1)); 63 } 64 } 65}
Output:
Performance comparison (100000 iterations):
─────────────────────────────────────────────
String (+)           :  2843 ms
StringBuilder        :     7 ms
StringBuffer         :    18 ms
─────────────────────────────────────────────
String is ~406x slower than StringBuilder
StringBuffer is ~2x slower than StringBuilder

StringBuilder is the fastest. StringBuffer is around 2x slower due to the synchronisation overhead. String + in a loop is hundreds of times slower because it creates a new String object on every iteration — for 100,000 iterations, the total amount of data copied grows quadratically.

Complete Comparison Table

AspectStringStringBuilderStringBuffer
MutableNo — every operation returns a new StringYes — modifies buffer in placeYes — modifies buffer in place
Thread-safeYes — immutability makes it inherently safeNo — not synchronisedYes — every method is synchronized
Performance in loopsSlowest — O(n²) character copiesFastest — O(n) character copiesFast but slower than SB — sync overhead
When to useFixed values, constants, parameters, comparisonsBuilding strings in a loop — single threadBuilding strings shared across multiple threads
+ operatorYes — compiles to StringBuilder behind the scenesNo — use append()No — use append()
String PoolYes — literals are pooledNo — toString() creates a heap objectNo — toString() creates a heap object
Return type of methodsNew String (immutable result)this — chainablethis — chainable
equals() behaviourCompares character contentInherits from Object — reference onlyInherits from Object — reference only
hashCode()Based on content — stableBased on identity — unstable for HashMap keysBased on identity — unstable for HashMap keys
Null appendN/AAppends "null" safelyAppends "null" safely
Default capacityN/A16 characters16 characters
IntroducedJava 1.0Java 1.5Java 1.0
Packagejava.langjava.langjava.lang

Decision Guide — Which One to Use

START: Do you need to BUILD a string by combining parts?
         |
        NO → Use String
              Examples:
              — "ORDER_PLACED" (constant)
              — comparing: "admin".equals(role)
              — method parameters: void process(String name)
         |
        YES → Is the building happening INSIDE A LOOP
              or does it need insert/delete/replace?
                  |
                 NO → Small, fixed number of + concatenations
                       Compiler handles it → String + is fine
                       "Name: " + name + " | Age: " + age
                  |
                 YES → Will the buffer be SHARED BETWEEN
                       MULTIPLE THREADS simultaneously?
                           |
                          NO → Use StringBuilder
                                — local variable in a method
                                — building a report / email
                                — constructing a SQL query
                                — assembling a JSON string
                           |
                          YES → Use StringBuffer
                                — shared log buffer
                                — concurrent event collector
                                — multi-thread message aggregator

When Each One Is Correct — Real Examples

Java
1// File: WhenToUseEach.java 2 3import java.util.List; 4 5public class WhenToUseEach { 6 7 // ── Use String ──────────────────────────────────────────────────── 8 // Reason: Fixed values, no iterative building, needs equals/hashCode 9 static final String DEFAULT_STATUS = "PENDING"; 10 11 public static boolean isOrderPlaced(String status) { 12 return "PLACED".equals(status); // immutable String — correct 13 } 14 15 // ── Use StringBuilder ───────────────────────────────────────────── 16 // Reason: Building in a loop — single thread — local variable 17 public static String buildCsvRow(String[] fields) { 18 StringBuilder sb = new StringBuilder(); 19 for (int i = 0; i < fields.length; i++) { 20 sb.append(fields[i]); 21 if (i < fields.length - 1) sb.append(","); 22 } 23 return sb.toString(); 24 } 25 26 public static String buildHtmlList(List<String> items) { 27 StringBuilder html = new StringBuilder("<ul>"); 28 for (String item : items) { 29 html.append("<li>").append(item).append("</li>"); 30 } 31 html.append("</ul>"); 32 return html.toString(); 33 } 34 35 // ── Use StringBuffer ────────────────────────────────────────────── 36 // Reason: Shared instance field accessed by multiple threads 37 static class SharedEventLog { 38 private final StringBuffer log = new StringBuffer(); // shared — needs sync 39 40 public void record(String event) { 41 log.append("[").append(System.currentTimeMillis()).append("] ") 42 .append(event).append("\n"); 43 } 44 45 public String dump() { return log.toString(); } 46 } 47 48 public static void main(String[] args) { 49 50 // String use 51 System.out.println("=== String ==="); 52 System.out.println("Is PLACED: " + isOrderPlaced("PLACED")); 53 System.out.println("Is PLACED: " + isOrderPlaced("PENDING")); 54 System.out.println("Default : " + DEFAULT_STATUS); 55 56 System.out.println(); 57 58 // StringBuilder use 59 System.out.println("=== StringBuilder ==="); 60 String[] orderFields = {"ORD-001", "Priya", "1299.50", "PLACED"}; 61 System.out.println("CSV: " + buildCsvRow(orderFields)); 62 63 List<String> items = List.of("Laptop", "Mouse", "Keyboard"); 64 System.out.println("HTML: " + buildHtmlList(items)); 65 66 System.out.println(); 67 68 // StringBuffer use 69 System.out.println("=== StringBuffer ==="); 70 SharedEventLog eventLog = new SharedEventLog(); 71 eventLog.record("User login: Priya"); 72 eventLog.record("Order placed: ORD-001"); 73 eventLog.record("Payment processed: PAY-789"); 74 75 System.out.println("Log has content: " + (eventLog.dump().length() > 0)); 76 System.out.println("Log line count : " + eventLog.dump().split("\n").length); 77 } 78}
Output:
=== String ===
Is PLACED: true
Is PLACED: false
Default  : PENDING

=== StringBuilder ===
CSV: ORD-001,Priya,1299.50,PLACED
HTML: <ul><li>Laptop</li><li>Mouse</li><li>Keyboard</li></ul>

=== StringBuffer ===
Log has content: true
Log line count : 3

The Compiler and String +

A common misconception is that String + is always bad. The Java compiler is smarter than that. For simple, non-loop expressions, it automatically converts + to StringBuilder internally.

Java
1// File: CompilerOptimisationDemo.java 2 3public class CompilerOptimisationDemo { 4 5 public static void main(String[] args) { 6 7 String name = "Priya"; 8 int age = 24; 9 String role = "Developer"; 10 11 // This line: 12 String result = "Name: " + name + " | Age: " + age + " | Role: " + role; 13 14 // Compiles to something equivalent to: 15 // String result = new StringBuilder() 16 // .append("Name: ").append(name) 17 // .append(" | Age: ").append(age) 18 // .append(" | Role: ").append(role) 19 // .toString(); 20 21 System.out.println(result); 22 23 System.out.println(); 24 25 // BUT — in a loop, the compiler does NOT carry the StringBuilder across iterations 26 String loop1 = ""; 27 for (int i = 0; i < 5; i++) { 28 loop1 = loop1 + i; // compiler creates a NEW StringBuilder PER ITERATION 29 // effectively: loop1 = new StringBuilder(loop1).append(i).toString(); 30 } 31 System.out.println("Loop with + : " + loop1); // 01234 32 33 // With explicit StringBuilder — same buffer used across ALL iterations 34 StringBuilder sb = new StringBuilder(); 35 for (int i = 0; i < 5; i++) { 36 sb.append(i); // same buffer — no temporary object 37 } 38 System.out.println("Loop with SB: " + sb); // 01234 39 40 System.out.println(); 41 System.out.println("Same output, very different performance for large loops."); 42 System.out.println("Compiler optimises simple expressions, NOT loops."); 43 } 44}
Output:
Name: Priya | Age: 24 | Role: Developer

Loop with + : 01234
Loop with SB: 01234

Same output, very different performance for large loops.
Compiler optimises simple expressions, NOT loops.

The two outputs are identical, but the performance is not. The + loop creates a new StringBuilder and a new String object on every single iteration. For 100,000 iterations, that is 100,000 StringBuilder and 100,000 String objects created and discarded. The explicit StringBuilder loop uses one buffer throughout.

Real-World Example — Order Report Generation Service

The Business Problem

A reporting service at a platform like Meesho or Flipkart generates three types of output from the same order data: a plain text summary for admin (fixed strings), a CSV export for finance (loop-built with StringBuilder), and a shared concurrent audit log for compliance (multi-threaded with StringBuffer). Three data types, three correct tool choices, all in the same service.

Java
1// File: OrderReport.java 2 3public class OrderReport { 4 private final String orderId; 5 private final String customerName; 6 private final String productName; 7 private final double amount; 8 private final String status; 9 private final String date; 10 11 public OrderReport(String orderId, String customerName, 12 String productName, double amount, 13 String status, String date) { 14 this.orderId = orderId; 15 this.customerName = customerName; 16 this.productName = productName; 17 this.amount = amount; 18 this.status = status; 19 this.date = date; 20 } 21 22 public String getOrderId() { return orderId; } 23 public String getCustomerName() { return customerName; } 24 public String getProductName() { return productName; } 25 public double getAmount() { return amount; } 26 public String getStatus() { return status; } 27 public String getDate() { return date; } 28}
Java
1// File: ReportService.java 2 3import java.util.List; 4 5public class ReportService { 6 7 // ── String — fixed status messages, comparisons, constants ──────── 8 private static final String HEADER_DIVIDER = "═".repeat(60); 9 private static final String STATUS_PLACED = "PLACED"; 10 private static final String STATUS_DELIVERED = "DELIVERED"; 11 12 public static String getStatusMessage(String status) { 13 // String.equals() — correct content comparison 14 if (STATUS_DELIVERED.equals(status)) return "Order completed successfully."; 15 if (STATUS_PLACED.equals(status)) return "Order is being processed."; 16 return "Order status: " + status; 17 } 18 19 // ── StringBuilder — building CSV in a loop (single-threaded) ────── 20 public static String buildCsvExport(List<OrderReport> orders) { 21 StringBuilder csv = new StringBuilder(orders.size() * 80); // estimate capacity 22 23 // Header row 24 csv.append("OrderId,Customer,Product,Amount,Status,Date\n"); 25 26 // Data rows — loop with no intermediate String objects 27 for (OrderReport order : orders) { 28 csv.append(order.getOrderId()) .append(",") 29 .append(order.getCustomerName()).append(",") 30 .append(order.getProductName()) .append(",") 31 .append(order.getAmount()) .append(",") 32 .append(order.getStatus()) .append(",") 33 .append(order.getDate()) .append("\n"); 34 } 35 return csv.toString(); 36 } 37 38 // ── StringBuilder — building a formatted text report (single-threaded) ── 39 public static String buildTextReport(List<OrderReport> orders) { 40 StringBuilder report = new StringBuilder(1024); 41 double grandTotal = 0; 42 43 report.append(HEADER_DIVIDER).append("\n") 44 .append(" ORDER REPORT — MEESHO PLATFORM\n") 45 .append(HEADER_DIVIDER).append("\n"); 46 report.append(String.format("%-14s %-16s %-20s %10s %10s%n", 47 "Order ID", "Customer", "Product", "Amount", "Status")); 48 report.append("─".repeat(60)).append("\n"); 49 50 for (OrderReport order : orders) { 51 report.append(String.format("%-14s %-16s %-20s Rs.%7.2f %10s%n", 52 order.getOrderId(), 53 order.getCustomerName(), 54 order.getProductName(), 55 order.getAmount(), 56 order.getStatus())); 57 grandTotal += order.getAmount(); 58 } 59 60 report.append("─".repeat(60)).append("\n") 61 .append(String.format("%-50s Rs.%7.2f%n", "GRAND TOTAL", grandTotal)) 62 .append(HEADER_DIVIDER); 63 64 return report.toString(); 65 } 66 67 // ── StringBuffer — shared audit log across multiple threads ─────── 68 static class ComplianceAuditLog { 69 private final StringBuffer buffer = new StringBuffer(2048); 70 71 public void logAccess(String userId, String reportType) { 72 buffer.append("[AUDIT] User=").append(userId) 73 .append(" accessed ").append(reportType) 74 .append(" at ").append(System.currentTimeMillis()) 75 .append("\n"); 76 } 77 78 public String getAuditTrail() { return buffer.toString(); } 79 public int getEntryCount() { return buffer.toString().split("\n").length; } 80 } 81}
Java
1// File: ReportServiceDemo.java 2 3import java.util.List; 4import java.util.concurrent.ExecutorService; 5import java.util.concurrent.Executors; 6import java.util.concurrent.TimeUnit; 7 8public class ReportServiceDemo { 9 10 public static void main(String[] args) throws InterruptedException { 11 12 List<OrderReport> orders = List.of( 13 new OrderReport("ORD-001", "Priya Sharma", "Wireless Headphones", 2499.0, "DELIVERED", "2024-01-10"), 14 new OrderReport("ORD-002", "Rohan Mehta", "Laptop Stand", 1299.0, "PLACED", "2024-01-11"), 15 new OrderReport("ORD-003", "Sneha Rao", "USB-C Hub", 899.0, "DELIVERED", "2024-01-12"), 16 new OrderReport("ORD-004", "Karan Singh", "Mechanical Keyboard", 3999.0, "PLACED", "2024-01-13"), 17 new OrderReport("ORD-005", "Ananya Iyer", "LED Monitor", 18500.0, "DELIVERED", "2024-01-14") 18 ); 19 20 System.out.println("╔══════════════════════════════════════════╗"); 21 System.out.println("║ REPORT GENERATION SERVICE ║"); 22 System.out.println("╚══════════════════════════════════════════╝\n"); 23 24 // 1 — String for status messages 25 System.out.println("=== [String] Status Messages ==="); 26 System.out.println(ReportService.getStatusMessage("DELIVERED")); 27 System.out.println(ReportService.getStatusMessage("PLACED")); 28 System.out.println(ReportService.getStatusMessage("CANCELLED")); 29 30 System.out.println(); 31 32 // 2 — StringBuilder for text report 33 System.out.println("=== [StringBuilder] Text Report ==="); 34 System.out.println(ReportService.buildTextReport(orders)); 35 36 System.out.println(); 37 38 // 3 — StringBuilder for CSV 39 System.out.println("=== [StringBuilder] CSV Export (first 120 chars) ==="); 40 String csv = ReportService.buildCsvExport(orders); 41 System.out.println(csv.substring(0, Math.min(120, csv.length()))); 42 System.out.println("... (" + csv.split("\n").length + " rows total)"); 43 44 System.out.println(); 45 46 // 4 — StringBuffer for concurrent audit logging 47 System.out.println("=== [StringBuffer] Concurrent Audit Log ==="); 48 ReportService.ComplianceAuditLog auditLog = new ReportService.ComplianceAuditLog(); 49 50 ExecutorService pool = Executors.newFixedThreadPool(3); 51 String[] users = {"finance-user-1", "admin-user-2", "compliance-user-3"}; 52 53 for (String user : users) { 54 pool.submit(() -> { 55 auditLog.logAccess(user, "ORDER_REPORT"); 56 auditLog.logAccess(user, "CSV_EXPORT"); 57 }); 58 } 59 60 pool.shutdown(); 61 pool.awaitTermination(5, TimeUnit.SECONDS); 62 63 System.out.println("Audit entries logged: " + auditLog.getEntryCount()); 64 System.out.println("All 6 entries safe : " + (auditLog.getEntryCount() >= 6)); 65 } 66}
Output:
╔══════════════════════════════════════════╗
║      REPORT GENERATION SERVICE          ║
╚══════════════════════════════════════════╝

=== [String] Status Messages ===
Order completed successfully.
Order is being processed.
Order status: CANCELLED

=== [StringBuilder] Text Report ===
════════════════════════════════════════════════════════════
  ORDER REPORT — MEESHO PLATFORM
════════════════════════════════════════════════════════════
Order ID       Customer         Product              Amount     Status
────────────────────────────────────────────────────────────
ORD-001        Priya Sharma     Wireless Headphones Rs.2499.00  DELIVERED
ORD-002        Rohan Mehta      Laptop Stand        Rs.1299.00  PLACED
ORD-003        Sneha Rao        USB-C Hub           Rs. 899.00  DELIVERED
ORD-004        Karan Singh      Mechanical Keyboard Rs.3999.00  PLACED
ORD-005        Ananya Iyer      LED Monitor         Rs.18500.00 DELIVERED
────────────────────────────────────────────────────────────
GRAND TOTAL                                         Rs.27196.00
════════════════════════════════════════════════════════════

=== [StringBuilder] CSV Export (first 120 chars) ===
OrderId,Customer,Product,Amount,Status,Date
ORD-001,Priya Sharma,Wireless Headphones,2499.0,DELIVERED,2024-01-10
ORD-002,Rohan Mehta,Laptop Stand,1299.0,P
... (6 rows total)

=== [StringBuffer] Concurrent Audit Log ===
Audit entries logged: 6
All 6 entries safe  : true

One service, three classes, three correct decisions: String for comparison and constants, StringBuilder for building CSV and report tables in loops, StringBuffer for the shared concurrent audit log.

Best Practices

Default to String for values you read but do not build. Configuration values, method parameters, comparison targets, HashMap keys, and return values that come from a single expression are all best represented as String. Immutability means safe sharing, safe use as map keys, and no defensive copying needed.

Default to StringBuilder for anything you build iteratively. Any time you write a for loop or while loop that builds a string piece by piece, use StringBuilder. The performance difference over String + grows from negligible at 5 iterations to hundreds of times faster at 100,000 iterations.

Use StringBuffer only when you have confirmed multi-threaded shared access. The synchronisation overhead is real — StringBuffer is about twice as slow as StringBuilder. Introducing that overhead in single-threaded code is a performance regression with no correctness benefit.

For simple fixed concatenations outside loops, String + is fine. The Java compiler converts "Hello, " + name + "!" to StringBuilder internally. You gain nothing by writing new StringBuilder().append("Hello, ").append(name).append("!").toString() for two or three parts outside a loop.

Common Mistakes

Mistake 1 — String + in a Loop

Java
1// Creates thousands of temporary objects — very slow for large n 2String result = ""; 3for (String item : largeList) { 4 result = result + item + ", "; // new String object every iteration 5} 6 7// Fix 8StringBuilder sb = new StringBuilder(); 9for (String item : largeList) { 10 sb.append(item).append(", "); 11} 12String result = sb.toString();

Mistake 2 — StringBuilder in a Multi-Threaded Shared Context

Java
1// WRONG — StringBuilder shared across threads is not safe 2class Logger { 3 private StringBuilder log = new StringBuilder(); // shared — race condition! 4 5 public void write(String msg) { 6 log.append(msg).append("\n"); // two threads can corrupt this 7 } 8} 9 10// Fix 11class Logger { 12 private StringBuffer log = new StringBuffer(); // synchronised — safe 13 14 public void write(String msg) { 15 log.append(msg).append("\n"); 16 } 17}

Mistake 3 — Comparing StringBuilder or StringBuffer With equals()

Java
1StringBuilder sb1 = new StringBuilder("hello"); 2StringBuilder sb2 = new StringBuilder("hello"); 3 4System.out.println(sb1.equals(sb2)); // false! — StringBuilder inherits Object.equals() 5// equals() for StringBuilder/StringBuffer is reference comparison — not content 6 7// Fix — convert to String first 8System.out.println(sb1.toString().equals(sb2.toString())); // true — String.equals()

Neither StringBuilder nor StringBuffer overrides equals(). Calling equals() on them compares object references, not content. Always call toString() before comparing.

Mistake 4 — Using StringBuilder as a HashMap Key

Java
1Map<StringBuilder, String> map = new HashMap<>(); 2StringBuilder key = new StringBuilder("user-001"); 3map.put(key, "Priya"); 4 5key.append("-updated"); // mutates the key! 6System.out.println(map.get(key)); // null — hash changed, key no longer found 7 8// Fix — use String as HashMap keys, always 9Map<String, String> map = new HashMap<>(); 10map.put("user-001", "Priya");

StringBuilder's hashCode() is based on its object identity — not its content. Even if it were content-based, mutating the key after insertion would corrupt the map. Always use immutable String as HashMap keys.

Interview Questions

Q1. What are the main differences between String, StringBuilder, and StringBuffer?

String is immutable — every modification creates a new object. It is thread-safe by design, eligible for String Pool reuse, and correct for equality comparison via equals(). StringBuilder is mutable — it modifies its internal buffer in place, is not thread-safe, and is the fastest option for building strings iteratively in a single thread. StringBuffer is mutable with the same API as StringBuilder but every method is synchronised, making it thread-safe at the cost of performance overhead. Use String for fixed values, StringBuilder for loops, StringBuffer for multi-threaded shared buffers.

Q2. When should you use StringBuilder over String concatenation with +?

Whenever string building happens inside a loop or involves a variable number of parts. The Java compiler converts simple multi-part + expressions outside loops into StringBuilder automatically — so "Hello, " + name + "!" is already efficient. But in a loop, each iteration creates a new StringBuilder and discards it, producing O(n²) character copy work. An explicit StringBuilder declared outside the loop reuses the same buffer for all iterations, reducing copy work to O(n). The difference is negligible for 5 iterations and hundreds of times for 100,000.

Q3. Is StringBuffer fully thread-safe for compound operations?

Each individual method call is thread-safe — only one thread can execute any given method at a time. But a sequence of calls is not atomic. If Thread A reads sb.length() and then calls sb.insert(length, value), Thread B could modify the buffer between those two calls. The result of Thread A's insert would then target a position that no longer reflects what it read. For compound read-then-modify operations, callers must wrap the sequence in a synchronized (buffer) block themselves.

Q4. Does the Java compiler optimise String + automatically?

Yes, for simple expressions. The compiler converts "a" + b + "c" to equivalent StringBuilder code. However, this optimisation does not apply across loop iterations. Each iteration of result = result + item compiles to a new StringBuilder that wraps the current result, appends item, and calls toString() — creating a new String each time. For loops, explicit StringBuilder outside the loop is required to get the performance benefit.

Q5. Why can you not use StringBuilder as a HashMap key?

StringBuilder does not override hashCode() or equals(). Its hashCode() is based on object identity — the memory address — not on content. If you put a StringBuilder as a key and then mutate it (which is easy since it is mutable), the hash code changes and the map can no longer find the key. HashMap stores entries by initial hash — after mutation, lookups compute a different hash, land in a different bucket, and find nothing. Always use immutable String as map keys — its content-based hashCode() is stable.

Q6. What is the output of new StringBuilder("hello").equals(new StringBuilder("hello"))?

false. StringBuilder inherits equals() from Object, which compares references — not content. Two different StringBuilder objects with the same content are not equal by equals(). To compare content, call toString() first: sb1.toString().equals(sb2.toString()). This is a common interview trap — unlike String, which overrides equals() to compare characters, StringBuilder and StringBuffer do not.

FAQs

Can you convert between String, StringBuilder, and StringBuffer?

Yes. new StringBuilder(str) creates a StringBuilder from a String. sb.toString() converts a StringBuilder back to a String. new StringBuffer(sb.toString()) converts from StringBuilder to StringBuffer. All three also accept CharSequence in their constructors — the interface that all three implement — so conversion is always straightforward.

Does String.format() use StringBuilder internally?

Yes. String.format() builds the result using a Formatter which internally uses StringBuilder. So String.format("Name: %s | Age: %d", name, age) is efficient — no multiple String objects are created in the process. For formatting a single structured output string, String.format() is clean and readable. For building many formatted strings in a loop, pre-allocate a StringBuilder and use Formatter or String.format() per item, appending results.

Which should you use in a Spring Boot REST controller?

For building response bodies, JSON strings, or HTML in a controller method — use StringBuilder. These methods run in a single thread per request (the request-handling thread). The StringBuilder is local to the method — it is never shared. For any shared state that multiple request threads write to concurrently (like a shared in-memory log), use StringBuffer or better: a ConcurrentLinkedQueue or logging framework.

Do modern Java features replace StringBuilder?

For many common cases, yes. Java 8 String.join() replaces delimiter-separated list building. Streams with Collectors.joining() replace loop-based aggregation. Java 15 text blocks replace multi-line string construction with +. For complex dynamic building — conditional appends, template filling, HTML generation — StringBuilder is still the clearest tool.

Is there any case where String + is faster than StringBuilder?

For a single concatenation — "Hello, " + name — the compiler-generated StringBuilder code and manual StringBuilder produce identical bytecode. For two or three fixed parts outside a loop, there is no performance difference. String + only becomes significantly slower when it is inside a loop where the compiler cannot carry the same StringBuilder across iterations.

Summary

String, StringBuilder, and StringBuffer each exist because the others could not handle a specific real-world need efficiently and correctly.

String is the default — immutable, safe to share, eligible for pool reuse, correct for equality comparison. Use it for everything that is not iteratively built.

StringBuilder is the loop workhorse — single mutable buffer, fastest possible building, not thread-safe. Use it any time you assemble a string in a loop or through multiple conditional appends in one thread.

StringBuffer is the concurrent option — same methods as StringBuilder with synchronisation on every call. Use it when one buffer object is written by multiple threads simultaneously.

The decision is almost never difficult in practice: String for values, StringBuilder for building, StringBuffer only when the building is concurrent.

What to Read Next

TopicLink
How StringBuilder methods work in detail — append, insert, delete, reverseJava StringBuilder →
How String immutability and the pool explain the performance cost of String +Java String Pool →
How String methods provide text processing without mutabilityJava String Methods →
How thread safety works and why StringBuffer needs synchronisationJava Synchronization →
How String comparison with equals() and == works correctlyJava String Comparison →
Java String vs StringBuilder vs StringBuffer | DevStackFlow