🚀 StringBuilder and StringBuffer in Java: Complete Guide
📚 Introduction to Java StringBuilder and StringBuffer Classes
In Java, strings are immutable - once created, they cannot be changed. While this immutability has advantages, it can lead to performance issues when you need to modify strings frequently. This is where StringBuilder
and StringBuffer
come into play.
These two classes provide mutable sequences of characters, allowing you to modify string content without creating new objects each time. They are essential tools in a Java developer's toolkit, especially when dealing with string manipulation in loops or building complex strings dynamically.
🔍 What You'll Learn in This Guide
- The fundamental differences between String, StringBuilder, and StringBuffer
- How StringBuilder and StringBuffer work internally
- When and why to use these classes
- Performance considerations and best practices
- Common pitfalls to avoid
- Practical examples and exercises
🧩 The Problem with String Immutability
Before diving into StringBuilder and StringBuffer, let's understand why we need them in the first place:
String result = "";
for (int i = 0; i < 10000; i++) {
result += i; // Creates 10,000 String objects!
}
In this simple loop, each iteration creates a new String object because strings are immutable. For large loops, this leads to:
- Excessive memory usage
- Poor performance due to garbage collection
- Unnecessary CPU cycles
This is where StringBuilder and StringBuffer shine - they provide a mutable alternative that solves these problems.
🔄 Understanding Java StringBuilder and StringBuffer
📋 Key Characteristics of Java StringBuilder and StringBuffer
Both StringBuilder and StringBuffer:
- Provide mutable character sequences
- Have similar APIs with methods like
append()
,insert()
,delete()
, etc. - Allow you to modify string content without creating new objects
- Convert to String using the
toString()
method
🔄 hread Safety in Java: StringBuilder vs StringBuffer
The primary difference between StringBuilder and StringBuffer is thread safety:
Feature | StringBuilder | StringBuffer |
---|---|---|
Thread Safety | ❌ Not thread-safe | ✅ Thread-safe |
Synchronization | No synchronization | All methods are synchronized |
Performance | Faster (no synchronization overhead) | Slower due to synchronization |
Introduction | Java 5 (2004) | Java 1.0 (1996) |
Use Case | Single-threaded environments | Multi-threaded environments |
Memory Overhead | Lower | Higher due to synchronization |
🧠 How Java StringBuilder and StringBuffer Work Internally
Both classes extend AbstractStringBuilder
and maintain:
- A character array (
char[]
) that stores the characters - A count field that tracks the number of characters
- The array grows automatically when needed
// Simplified internal representation
private char[] value; // Character array to store data
private int count; // Number of characters used
When you append or modify the content:
- The operation checks if the internal array has enough capacity
- If needed, the array is expanded (typically doubling in size)
- The new characters are added or existing ones modified
- The count is updated
💻 Creating and Using StringBuilder and StringBuffer
🏗️ Constructors
Both classes offer similar constructors:
// Default constructor (initial capacity of 16 characters)
StringBuilder sb1 = new StringBuilder();
StringBuffer sb2 = new StringBuffer();
// Constructor with initial capacity
StringBuilder sb3 = new StringBuilder(100);
StringBuffer sb4 = new StringBuffer(100);
// Constructor with initial content
StringBuilder sb5 = new StringBuilder("Hello");
StringBuffer sb6 = new StringBuffer("Hello");
🛠️ Common Methods
Both classes share the same core methods:
StringBuilder builder = new StringBuilder("Hello");
// Append (most commonly used)
builder.append(" World"); // "Hello World"
builder.append(123); // "Hello World123"
builder.append(true); // "Hello World123true"
// Insert at specific position
builder.insert(5, " Java"); // "Hello Java World123true"
// Delete characters
builder.delete(11, 16); // "Hello Java true"
// Replace a range of characters
builder.replace(6, 10, "Python"); // "Hello Python true"
// Reverse the characters
builder.reverse(); // "eurt nohtyP olleH"
// Set length (truncate or expand with null characters)
builder.setLength(10); // "eurt nohty"
// Get character at index
char c = builder.charAt(0); // 'e'
// Get substring
String sub = builder.substring(0, 4); // "eurt"
// Convert to String
String result = builder.toString(); // "eurt nohty"
🔄 Chaining Methods
One of the powerful features is method chaining:
String result = new StringBuilder()
.append("Hello")
.append(" ")
.append("World")
.append("!")
.toString(); // "Hello World!"
📊 Performance Comparison: String vs StringBuilder vs StringBuffer in Java
Let's compare the performance of these classes with a simple benchmark:
public class StringPerformanceTest {
public static void main(String[] args) {
int iterations = 100000;
// Test with String concatenation
long startTime1 = System.currentTimeMillis();
String result1 = "";
for (int i = 0; i < iterations; i++) {
result1 += "a";
}
long endTime1 = System.currentTimeMillis();
// Test with StringBuilder
long startTime2 = System.currentTimeMillis();
StringBuilder builder = new StringBuilder();
for (int i = 0; i < iterations; i++) {
builder.append("a");
}
String result2 = builder.toString();
long endTime2 = System.currentTimeMillis();
// Test with StringBuffer
long startTime3 = System.currentTimeMillis();
StringBuffer buffer = new StringBuffer();
for (int i = 0; i < iterations; i++) {
buffer.append("a");
}
String result3 = buffer.toString();
long endTime3 = System.currentTimeMillis();
// Print results
System.out.println("String concatenation time: " + (endTime1 - startTime1) + " ms");
System.out.println("StringBuilder time: " + (endTime2 - startTime2) + " ms");
System.out.println("StringBuffer time: " + (endTime3 - startTime3) + " ms");
}
}
Typical results might look like:
- String concatenation: 5000+ ms
- StringBuilder: 5-10 ms
- StringBuffer: 10-20 ms
This demonstrates the massive performance advantage of StringBuilder and StringBuffer over String concatenation in loops.
🔍 Detailed Java StringBuilder and StringBuffer Examples
Example 1: Building a Simple Text Processor
public class TextProcessor {
public static void main(String[] args) {
String input = "This is a sample text with UPPERCASE and lowercase letters.";
// Process the text using StringBuilder
StringBuilder processed = new StringBuilder();
// Add a title
processed.append("PROCESSED TEXT:\n");
processed.append("---------------\n");
// Convert to lowercase and add line numbers
String[] lines = input.split("\\.");
for (int i = 0; i < lines.length; i++) {
if (!lines[i].trim().isEmpty()) {
processed.append(i + 1)
.append(". ")
.append(lines[i].trim().toLowerCase())
.append(".\n");
}
}
// Add a summary
processed.append("\nSUMMARY:\n");
processed.append("--------\n");
processed.append("Number of lines: ").append(lines.length).append("\n");
processed.append("Total characters: ").append(input.length()).append("\n");
// Print the result
System.out.println(processed.toString());
}
}
This example demonstrates:
- Using StringBuilder to build a formatted text output
- Appending different types of data (strings, numbers)
- Building a multi-line output efficiently
Example 2: Employee Report Generator
public class EmployeeReportGenerator {
public static void main(String[] args) {
// Sample employee data
String[] names = {"John Smith", "Mary Johnson", "David Lee", "Sarah Williams", "Michael Brown"};
int[] ages = {32, 45, 28, 39, 51};
String[] departments = {"Engineering", "Marketing", "Finance", "HR", "Operations"};
double[] salaries = {75000.00, 82500.50, 65000.75, 71200.25, 95000.00};
// Create a report using StringBuilder
StringBuilder report = new StringBuilder();
// Add report header
report.append("EMPLOYEE SUMMARY REPORT\n");
report.append("======================\n\n");
report.append(String.format("%-15s %-10s %-15s %-15s\n", "Name", "Age", "Department", "Salary"));
report.append("------------------------------------------------\n");
// Add employee data
double totalSalary = 0;
int totalAge = 0;
for (int i = 0; i < names.length; i++) {
report.append(String.format("%-15s %-10d %-15s $%-14.2f\n",
names[i], ages[i], departments[i], salaries[i]));
totalSalary += salaries[i];
totalAge += ages[i];
}
// Add summary
report.append("------------------------------------------------\n");
report.append(String.format("Average Age: %.1f years\n", (double)totalAge / names.length));
report.append(String.format("Total Salary: $%.2f\n", totalSalary));
report.append(String.format("Average Salary: $%.2f\n", totalSalary / names.length));
// Print the report
System.out.println(report.toString());
}
}
This example demonstrates:
- Using StringBuilder to build a complex, multi-line report
- Combining StringBuilder with String.format() for formatted output
- Accumulating data while building the string
- The clean, readable approach to constructing a complex string
Example 3: Thread Safety Demonstration
This example illustrates the thread safety difference between StringBuilder and StringBuffer:
public class ThreadSafetyDemo {
public static void main(String[] args) throws InterruptedException {
// Test with StringBuilder (not thread-safe)
testWithStringBuilder();
// Test with StringBuffer (thread-safe)
testWithStringBuffer();
}
private static void testWithStringBuilder() throws InterruptedException {
StringBuilder builder = new StringBuilder();
// Create multiple threads that append to the same StringBuilder
Thread[] threads = new Thread[100];
for (int i = 0; i < threads.length; i++) {
final int threadNumber = i;
threads[i] = new Thread(() -> {
for (int j = 0; j < 100; j++) {
builder.append(threadNumber);
}
});
}
// Start all threads
for (Thread thread : threads) {
thread.start();
}
// Wait for all threads to complete
for (Thread thread : threads) {
thread.join();
}
// Check the result
System.out.println("StringBuilder length: " + builder.length());
System.out.println("Expected length: " + (100 * 100));
System.out.println("StringBuilder is " +
(builder.length() == 100 * 100 ? "correct" : "corrupted"));
}
private static void testWithStringBuffer() throws InterruptedException {
StringBuffer buffer = new StringBuffer();
// Create multiple threads that append to the same StringBuffer
Thread[] threads = new Thread[100];
for (int i = 0; i < threads.length; i++) {
final int threadNumber = i;
threads[i] = new Thread(() -> {
for (int j = 0; j < 100; j++) {
buffer.append(threadNumber);
}
});
}
// Start all threads
for (Thread thread : threads) {
thread.start();
}
// Wait for all threads to complete
for (Thread thread : threads) {
thread.join();
}
// Check the result
System.out.println("StringBuffer length: " + buffer.length());
System.out.println("Expected length: " + (100 * 100));
System.out.println("StringBuffer is " +
(buffer.length() == 100 * 100 ? "correct" : "corrupted"));
}
}
This example demonstrates:
- How StringBuilder can produce corrupted results in a multi-threaded environment
- How StringBuffer maintains data integrity through synchronization
- The practical implications of thread safety in real-world scenarios
When you run this code, you'll likely see that the StringBuilder version produces inconsistent or corrupted results (the length may not match the expected value), while the StringBuffer version consistently produces the correct result.
🎯 Why StringBuilder and StringBuffer Matter in Java: Use Cases
Understanding when to use StringBuilder and StringBuffer is crucial for writing efficient Java applications. Here are some common use cases where these classes shine:
1. String Concatenation in Loops
// Inefficient way
String result = "";
for (int i = 0; i < 1000; i++) {
result += i; // Creates 1000 String objects
}
// Efficient way
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 1000; i++) {
builder.append(i); // Modifies the same object
}
String result = builder.toString();
2. Building Complex Strings
// Building HTML content
StringBuilder html = new StringBuilder();
html.append("<!DOCTYPE html>\n")
.append("<html>\n")
.append(" <head>\n")
.append(" <title>").append(pageTitle).append("</title>\n")
.append(" <meta charset=\"UTF-8\">\n")
.append(" </head>\n")
.append(" <body>\n")
.append(" <h1>").append(pageTitle).append("</h1>\n");
// Add content dynamically
for (String paragraph : paragraphs) {
html.append(" <p>").append(paragraph).append("</p>\n");
}
// Close tags
html.append(" </body>\n")
.append("</html>");
String htmlContent = html.toString();
3. Parsing and Tokenizing
public List<String> parseCSV(String line) {
List<String> tokens = new ArrayList<>();
StringBuilder currentToken = new StringBuilder();
boolean inQuotes = false;
for (char c : line.toCharArray()) {
if (c == '"') {
inQuotes = !inQuotes;
} else if (c == ',' && !inQuotes) {
tokens.add(currentToken.toString());
currentToken.setLength(0); // Clear the builder
} else {
currentToken.append(c);
}
}
// Add the last token
tokens.add(currentToken.toString());
return tokens;
}
4. Dynamic SQL Query Building
public String buildQuery(String table, Map<String, Object> conditions) {
StringBuilder query = new StringBuilder();
query.append("SELECT * FROM ").append(table);
if (!conditions.isEmpty()) {
query.append(" WHERE ");
boolean first = true;
for (Map.Entry<String, Object> entry : conditions.entrySet()) {
if (!first) {
query.append(" AND ");
}
query.append(entry.getKey()).append(" = ");
// Format the value based on its type
Object value = entry.getValue();
if (value instanceof String) {
query.append("'").append(value).append("'");
} else {
query.append(value);
}
first = false;
}
}
return query.toString();
}
5. Log Message Formatting
public String formatLogMessage(String level, String message, Map<String, Object> context) {
StringBuilder log = new StringBuilder();
// Add timestamp
log.append("[").append(new Date()).append("] ");
// Add log level
log.append("[").append(level.toUpperCase()).append("] ");
// Add message
log.append(message);
// Add context if available
if (context != null && !context.isEmpty()) {
log.append(" - Context: {");
boolean first = true;
for (Map.Entry<String, Object> entry : context.entrySet()) {
if (!first) {
log.append(", ");
}
log.append(entry.getKey()).append("=").append(entry.getValue());
first = false;
}
log.append("}");
}
return log.toString();
}
🛠️ Best Practices for Using StringBuilder and StringBuffer in Java
To get the most out of these classes, follow these best practices:
1. Choose the Right Class for Your Needs
// Single-threaded environment: Use StringBuilder
StringBuilder builder = new StringBuilder();
// Multi-threaded environment where the same instance is shared: Use StringBuffer
StringBuffer buffer = new StringBuffer();
// Multi-threaded but each thread has its own instance: StringBuilder is still better
// Each thread creates its own StringBuilder
Thread t1 = new Thread(() -> {
StringBuilder localBuilder = new StringBuilder();
// Use localBuilder...
});
2. Set Initial Capacity When Possible
// If you know the approximate size, set the initial capacity
// This avoids multiple resizing operations
StringBuilder builder = new StringBuilder(1000);
// For example, when converting a large collection to a string
List<String> items = getVeryLargeList(); // Assume this returns 1000 items
int estimatedSize = items.size() * 10; // Assume average item length is 10
StringBuilder builder = new StringBuilder(estimatedSize);
for (String item : items) {
builder.append(item).append(", ");
}
3. Reuse StringBuilder Instances
// In methods that are called frequently, consider reusing a StringBuilder
public class StringUtils {
// ThreadLocal allows each thread to have its own StringBuilder
private static final ThreadLocal<StringBuilder> builder =
ThreadLocal.withInitial(() -> new StringBuilder(256));
public static String join(List<String> items, String delimiter) {
StringBuilder sb = builder.get();
sb.setLength(0); // Clear the builder
boolean first = true;
for (String item : items) {
if (!first) {
sb.append(delimiter);
}
sb.append(item);
first = false;
}
return sb.toString();
}
}
4. Use setLength(0) to Clear Instead of Creating New Instances
// Inefficient: Creating new instances
for (int i = 0; i < 1000; i++) {
StringBuilder builder = new StringBuilder();
// Use builder...
String result = builder.toString();
}
// Efficient: Reusing the same instance
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 1000; i++) {
builder.setLength(0); // Clear the builder
// Use builder...
String result = builder.toString();
}
5. Use Method Chaining for Readability
// Without chaining
StringBuilder builder = new StringBuilder();
builder.append("Hello");
builder.append(" ");
builder.append("World");
String result = builder.toString();
// With chaining (more readable)
String result = new StringBuilder()
.append("Hello")
.append(" ")
.append("World")
.toString();
6. Avoid Unnecessary toString() Calls
// Inefficient: Converting to String multiple times
StringBuilder builder = new StringBuilder("Start");
String temp = builder.toString();
temp = temp + " middle";
builder = new StringBuilder(temp);
builder.append(" end");
String result = builder.toString();
// Efficient: Keep using StringBuilder
StringBuilder builder = new StringBuilder("Start");
builder.append(" middle").append(" end");
String result = builder.toString();
7. Use StringBuilder for Simple Concatenations in Modern Java
// In modern Java, the compiler optimizes simple concatenations
String result = "Hello" + " " + "World" + "!";
// This is automatically optimized to use StringBuilder behind the scenes
// But for loops or conditional concatenations, explicitly use StringBuilder
StringBuilder dynamic = new StringBuilder();
for (int i = 0; i < 10; i++) {
if (i % 2 == 0) {
dynamic.append(i);
}
}
8. Use StringBuffer for Thread-Safe Concatenations
// If you need thread safety, use StringBuffer
StringBuffer buffer = new StringBuffer();
buffer.append("Hello").append(" ").append("World");
String result = buffer.toString();
9. Use StringBuilder for Performance-Critical Operations
// If performance is critical, use StringBuilder
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 1000; i++) {
builder.append(i);
}
String result = builder.toString();
10. Use StringBuilder for Dynamic Content
// If you need to build a string dynamically, use StringBuilder
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 1000; i++) {
builder.append(i).append(", ");
}
String result = builder.toString();
11. Use StringBuilder for String Formatting
// If you need to format strings, use StringBuilder
StringBuilder builder = new StringBuilder();
builder.append("Name: ").append(name)
.append(", Age: ").append(age);
String result = builder.toString();
12. Use StringBuilder for String Manipulation
// If you need to manipulate strings, use StringBuilder
StringBuilder builder = new StringBuilder("Hello World");
builder.insert(5, " there");
builder.delete(11, 16);
String result = builder.toString();
13. Use StringBuilder for String Concatenation
// If you need to concatenate strings, use StringBuilder
StringBuilder builder = new StringBuilder();
builder.append("Hello").append(" ").append("World");
String result = builder.toString();
14. Use StringBuilder for String Tokenization
// If you need to tokenize strings, use StringBuilder
StringBuilder builder = new StringBuilder();
builder.append("Hello,World,Java");
String[] tokens = builder.toString().split(",");
🚫 Common Pitfalls When Using Java StringBuilder and StringBuffer
When working with StringBuilder and StringBuffer, be aware of these common mistakes:
1. Thread Safety Confusion
// WRONG: Using StringBuilder in a multi-threaded context
public class SharedStringBuilder {
// This is dangerous - shared across threads!
private static StringBuilder sharedBuilder = new StringBuilder();
public static void appendToShared(String text) {
sharedBuilder.append(text); // Not thread-safe!
}
}
// RIGHT: Using StringBuffer for shared access
public class SharedStringBuffer {
private static StringBuffer sharedBuffer = new StringBuffer();
public static void appendToShared(String text) {
sharedBuffer.append(text); // Thread-safe
}
}
2. Capacity vs Length Confusion
StringBuilder builder = new StringBuilder(100);
System.out.println(builder.length()); // Prints 0, not 100!
// The capacity is the size of the internal buffer
System.out.println(builder.capacity()); // Prints 100
// Length is the number of characters currently stored
builder.append("Hello");
System.out.println(builder.length()); // Prints 5
3. Forgetting to Call toString()
// WRONG: Trying to use StringBuilder directly where a String is expected
Map<String, String> map = new HashMap<>();
StringBuilder key = new StringBuilder("user");
StringBuilder value = new StringBuilder("John");
map.put(key, value); // Doesn't work as expected!
// RIGHT: Convert to String first
map.put(key.toString(), value.toString());
4. Unnecessary StringBuilder Creation
// WRONG: Creating StringBuilder for simple concatenation
String name = "John";
String greeting = new StringBuilder("Hello, ").append(name).append("!").toString();
// RIGHT: For simple cases, use the + operator
String greeting = "Hello, " + name + "!";
// The compiler optimizes this to use StringBuilder behind the scenes
5. Ignoring Return Values
// WRONG: Ignoring the return value
StringBuilder builder = new StringBuilder("Hello");
builder.replace(0, 5, "Hi"); // Returns the StringBuilder
builder.append(" World"); // This works
// WRONG: This doesn't modify the original builder
StringBuilder builder = new StringBuilder("Hello");
builder.replace(0, 5, "Hi").toString(); // toString() returns a String, not a StringBuilder
builder.append(" World"); // Still appends to "Hello World", not "Hi World"
// RIGHT: Chain methods properly
StringBuilder builder = new StringBuilder("Hello");
String result = builder.replace(0, 5, "Hi").append(" World").toString();
6. Memory Leaks with Large Buffers
// WRONG: Keeping large buffers around
public class LogProcessor {
private StringBuilder logBuffer = new StringBuilder(1000000); // 1MB initial capacity
public void processLog(String logEntry) {
logBuffer.append(logEntry).append("\n");
// Process the log...
// WRONG: Not clearing the buffer after processing
// This keeps consuming memory
}
// RIGHT: Clear the buffer after use
public void processLogCorrectly(String logEntry) {
logBuffer.append(logEntry).append("\n");
// Process the log...
// Clear the buffer to free memory
logBuffer.setLength(0);
}
}
7. Excessive Synchronization
// WRONG: Using StringBuffer when not needed
public String formatName(String firstName, String lastName) {
StringBuffer buffer = new StringBuffer(); // Unnecessary synchronization overhead
buffer.append(lastName).append(", ").append(firstName);
return buffer.toString();
}
// RIGHT: Use StringBuilder for non-shared contexts
public String formatName(String firstName, String lastName) {
StringBuilder builder = new StringBuilder();
builder.append(lastName).append(", ").append(firstName);
return builder.toString();
}
🔍 Advanced Topics
🧵 Custom Thread-Safe StringBuilder
Sometimes you need more control over synchronization than StringBuffer provides:
public class CustomThreadSafeStringBuilder {
private final StringBuilder builder;
private final Object lock = new Object();
public CustomThreadSafeStringBuilder() {
builder = new StringBuilder();
}
public CustomThreadSafeStringBuilder(int capacity) {
builder = new StringBuilder(capacity);
}
public CustomThreadSafeStringBuilder append(String str) {
synchronized(lock) {
builder.append(str);
return this;
}
}
public CustomThreadSafeStringBuilder append(Object obj) {
synchronized(lock) {
builder.append(obj);
return this;
}
}
// Add other methods as needed...
public String toString() {
synchronized(lock) {
return builder.toString();
}
}
}
🔄 StringBuilder in Java 9+: New Methods
Java 9 introduced some useful new methods:
// Java 9+ StringBuilder enhancements
StringBuilder builder = new StringBuilder("Hello World");
// compareTo method
StringBuilder other = new StringBuilder("Hello Java");
int result = builder.compareTo(other); // Compare lexicographically
// chars() method returns an IntStream of the characters
IntStream charStream = builder.chars();
charStream.forEach(c -> System.out.print((char)c));
// codePoints() returns an IntStream of Unicode code points
IntStream codePointStream = builder.codePoints();
🧠 Memory Management and Performance Tuning
For performance-critical applications, consider these advanced techniques:
// 1. Pre-sizing with exact capacity
String[] words = new String[1000]; // Assume this is filled with data
int exactSize = 0;
for (String word : words) {
exactSize += word.length();
}
// Add space for delimiters
exactSize += words.length - 1;
StringBuilder builder = new StringBuilder(exactSize);
boolean first = true;
for (String word : words) {
if (!first) {
builder.append(' ');
}
builder.append(word);
first = false;
}
// 2. Using ensureCapacity for growing builders
StringBuilder builder = new StringBuilder(100);
// Later, if we need more capacity:
builder.ensureCapacity(500); // More efficient than automatic growth
// 3. Trimming excess capacity
StringBuilder builder = new StringBuilder(1000);
builder.append("Hello");
// Trim to size (not directly supported, but can be simulated)
String trimmed = builder.toString();
builder = new StringBuilder(trimmed);
📝 Key Takeaways: Mastering Java StringBuilder and StringBuffer
📋 StringBuilder vs StringBuffer Comparison Table
Feature | StringBuilder | StringBuffer |
---|---|---|
Thread Safety | ❌ Not thread-safe | ✅ Thread-safe |
Performance | ⚡ Faster | 🐢 Slower due to synchronization |
Use Case | Single-threaded environments | Multi-threaded shared access |
Java Version | Introduced in Java 5 | Available since Java 1.0 |
Memory Efficiency | More efficient | Less efficient due to synchronization |
Method Synchronization | None | All methods synchronized |
API | append(), insert(), delete(), replace(), etc. | Same as StringBuilder |
Default Capacity | 16 characters | 16 characters |
Growth Strategy | Doubles capacity when needed | Doubles capacity when needed |
🔑 Key Points to Remember
-
Performance Matters: Use StringBuilder instead of String concatenation in loops and for complex string building.
-
Thread Safety: Use StringBuffer only when you need thread safety (shared access from multiple threads).
-
Modern Java Optimization: For simple concatenations, the Java compiler automatically uses StringBuilder.
-
Method Chaining: Take advantage of method chaining for cleaner, more readable code.
-
Initial Capacity: Set an appropriate initial capacity when you know the approximate size of the result.
-
Reuse Instances: Reuse StringBuilder instances with setLength(0) instead of creating new ones.
-
toString() Conversion: Only call toString() when you need the final String result, not during intermediate operations.
-
Memory Management: Be mindful of memory usage with very large builders.
🏋️ Exercises and Mini-Projects
Exercise 1: CSV Builder
Create a CSV builder that can generate CSV content from a list of objects:
public class CSVBuilder {
private final StringBuilder builder;
private final String delimiter;
private boolean headerAdded;
public CSVBuilder() {
this(",");
}
public CSVBuilder(String delimiter) {
this.builder = new StringBuilder();
this.delimiter = delimiter;
this.headerAdded = false;
}
public CSVBuilder addHeader(String... headers) {
if (headerAdded) {
throw new IllegalStateException("Header already added");
}
appendRow(headers);
headerAdded = true;
return this;
}
public CSVBuilder addRow(String... values) {
appendRow(values);
return this;
}
private void appendRow(String... values) {
boolean first = true;
for (String value : values) {
if (!first) {
builder.append(delimiter);
}
// Escape values containing delimiter or quotes
if (value.contains(delimiter) || value.contains("\"") || value.contains("\n")) {
builder.append("\"")
.append(value.replace("\"", "\"\""))
.append("\"");
} else {
builder.append(value);
}
first = false;
}
builder.append("\n");
}
public String build() {
return builder.toString();
}
// Example usage
public static void main(String[] args) {
CSVBuilder csv = new CSVBuilder();
csv.addHeader("ID", "Name", "Email", "Age")
.addRow("1", "John Smith", "john@example.com", "32")
.addRow("2", "Jane Doe", "jane@example.com", "28")
.addRow("3", "Bob Johnson", "bob@example.com", "45");
System.out.println(csv.build());
}
}
Now try it yourself! Extend this class to add features like:
- Reading from a List of objects using reflection
- Adding formatting options for numeric values
- Supporting different line endings (Windows, Unix)
Exercise 2: Log Message Formatter
Create a log message formatter that builds structured log messages:
public class LogFormatter {
private static final ThreadLocal<StringBuilder> builderCache =
ThreadLocal.withInitial(() -> new StringBuilder(256));
public static String format(String level, String message, Map<String, Object> context) {
StringBuilder builder = builderCache.get();
builder.setLength(0);
// Add timestamp
builder.append('[')
.append(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date()))
.append("] ");
// Add log level with padding
builder.append('[');
padRight(builder, level.toUpperCase(), 5);
builder.append("] ");
// Add message
builder.append(message);
// Add context if available
if (context != null && !context.isEmpty()) {
builder.append(" {");
boolean first = true;
for (Map.Entry<String, Object> entry : context.entrySet()) {
if (!first) {
builder.append(", ");
}
builder.append(entry.getKey())
.append('=');
Object value = entry.getValue();
if (value instanceof String) {
builder.append('"').append(value).append('"');
} else {
builder.append(value);
}
first = false;
}
builder.append('}');
}
return builder.toString();
}
private static void padRight(StringBuilder builder, String s, int width) {
builder.append(s);
for (int i = s.length(); i < width; i++) {
builder.append(' ');
}
}
// Example usage
public static void main(String[] args) {
Map<String, Object> context = new HashMap<>();
context.put("userId", 12345);
context.put("action", "login");
context.put("ip", "192.168.1.1");
String logMessage = LogFormatter.format("INFO", "User logged in", context);
System.out.println(logMessage);
// Output example:
// [2023-04-15 14:23:45.123] [INFO ] User logged in {userId=12345, action="login", ip="192.168.1.1"}
}
}
Your turn! Enhance this formatter with:
- Color coding for different log levels
- Support for nested context objects
- Customizable date/time formats
- Log rotation capabilities
Exercise 3: Template Engine
Build a simple template engine using StringBuilder:
public class SimpleTemplateEngine {
public String render(String template, Map<String, Object> context) {
StringBuilder result = new StringBuilder(template.length() * 2);
int pos = 0;
while (pos < template.length()) {
int startMarker = template.indexOf("{{", pos);
if (startMarker == -1) {
// No more markers, append the rest of the template
result.append(template.substring(pos));
break;
}
// Append text before the marker
result.append(template.substring(pos, startMarker));
int endMarker = template.indexOf("}}", startMarker);
if (endMarker == -1) {
// Unclosed marker, treat as text
result.append("{{");
pos = startMarker + 2;
continue;
}
// Extract variable name and trim whitespace
String variable = template.substring(startMarker + 2, endMarker).trim();
// Replace with context value if available
Object value = context.get(variable);
if (value != null) {
result.append(value);
}
pos = endMarker + 2;
}
return result.toString();
}
// Example usage
public static void main(String[] args) {
String template = "Hello, {{name}}! Welcome to {{company}}.\n" +
"Your account balance is {{balance}}.\n" +
"Thank you for your business.";
Map<String, Object> context = new HashMap<>();
context.put("name", "John Smith");
context.put("company", "Acme Corp");
context.put("balance", "$1,250.00");
SimpleTemplateEngine engine = new SimpleTemplateEngine();
String output = engine.render(template, context);
System.out.println(output);
}
}
Challenge yourself! Extend this template engine to support:
- Conditional blocks (if/else)
- Loops (for each)
- Nested variables
- Filters (e.g., uppercase, lowercase, date formatting)
🎓 Final Thoughts
StringBuilder and StringBuffer are essential tools in Java development, offering significant performance improvements over regular String concatenation. By understanding when and how to use them effectively, you can write more efficient and maintainable code.
Remember these key principles:
- Use StringBuilder by default for string manipulation
- Only use StringBuffer when thread safety is required
- Set an appropriate initial capacity when possible
- Take advantage of method chaining for cleaner code
- Reuse instances when appropriate
With these tools in your toolkit, you'll be well-equipped to handle any string manipulation task efficiently in Java.
Happy coding! 🚀