Strings in Java
Strings in Java
Learn about String class, its methods, and string manipulation in Java.
1. String Immutability
In Java, Strings are immutable, meaning once a String object is created, its value cannot be changed. Any operation that appears to modify a String actually creates a new String object. This is a fundamental concept in Java with important implications for performance and security.
// Example 1: Basic immutability
String s1 = "Hello";
String s2 = s1; // Both s1 and s2 refer to the same "Hello" in string pool
s1 = s1 + " World"; // Creates a new String object, s1 now points to "Hello World"
System.out.println(s1); // "Hello World"
System.out.println(s2); // "Hello" (original unchanged)
// Example 2: String methods return new objects
String s3 = "Java";
s3.toUpperCase(); // Creates new String but we're not assigning it
System.out.println(s3); // Still "Java"
s3 = s3.toUpperCase(); // Now we're assigning the new String
System.out.println(s3); // "JAVA"
// Example 3: String literals vs new String()
String a = "Test";
String b = "Test";
String c = new String("Test");
String d = c.intern();
System.out.println(a == b); // true (same object in string pool)
System.out.println(a == c); // false (c is a new object in heap)
System.out.println(a == d); // true (d is from string pool)
System.out.println(a.equals(c)); // true (content comparison)
Why Strings are Immutable in Java?
1. Security
Strings are widely used in security-sensitive operations like:
- Network connections (URLs, hostnames)
- File paths and system properties
- Class loading mechanisms
- Database connection strings and SQL queries
Immutability prevents malicious code from modifying these critical strings after they've been validated.
2. Thread Safety
Immutable objects are inherently thread-safe, which is crucial because:
- No synchronization is needed when sharing strings between threads
- Eliminates race conditions and thread interference
- Enables safe caching and sharing of string literals
3. Performance Optimization
Immutability enables several performance optimizations:
- String Pooling: Reuse of string literals reduces memory usage
- Hash Caching: Strings cache their hash code after first calculation
- Safe Caching: Strings can be safely cached without defensive copies
4. Class Loading
String immutability is critical for Java's class loading mechanism:
- Class names are stored as strings
- Ensures class loading is consistent and secure
- Prevents spoofing of class names after verification
5. Hash-based Collections
Strings are commonly used as keys in HashMap and HashSet:
- Immutable keys ensure hash codes remain constant
- Prevents corruption of hash-based collections
- Enables efficient caching of hash codes
Important Note on String Concatenation
Because strings are immutable, concatenation in loops can be inefficient. Each concatenation creates a new string object. For example:
// Inefficient - creates multiple intermediate strings
String result = "";
for (int i = 0; i < 1000; i++) {
result += i; // Creates new String in each iteration
}
// Efficient - uses single StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append(i);
}
String finalResult = sb.toString();
2. Memory Storage (String Pool vs Heap)
Java manages String objects in memory using two main areas, each with specific behaviors and performance implications:
// String Pool (String Constant Pool) - Special area in the Heap
String s1 = "Hello"; // Goes to String Pool
String s2 = "Hello"; // Reuses from String Pool (same object as s1)
String s3 = new String("Hello"); // Creates new object in regular Heap
String s4 = s3.intern(); // Adds to String Pool if not exists, returns reference
// Reference comparison
System.out.println(s1 == s2); // true (same object in pool)
System.out.println(s1 == s3); // false (different objects)
System.out.println(s1 == s4); // true (s4 references pooled "Hello")
// Content comparison (always use for String content comparison)
System.out.println(s1.equals(s3)); // true (same content)
// More examples
String s5 = new String("Hello").intern(); // Explicitly interned
String s6 = "Hello";
System.out.println(s1 == s5); // true (both in pool)
System.out.println(s1 == s6); // true (both reference same pooled object)
Understanding String Pool
The String Pool is a special area in the heap memory that stores unique string literals. It offers several advantages:
1. Memory Efficiency
- String literals with the same content share the same memory location
- Reduces memory footprint for duplicate strings
- Example: Multiple occurrences of "Hello" in code use the same object
2. String Interning
The intern() method adds a string to the pool if it's not already present:
String s1 = new String("Hello").intern();
String s2 = "Hello";
System.out.println(s1 == s2); // true
Useful when you have many duplicate strings and want to save memory.
3. When Strings are Added to the Pool
- String literals are automatically added:
String s = "text"; - When
intern()is called on a String object - At class loading time for class and interface names, method names, etc.
Heap Memory for Strings
Strings created with the new operator are stored in the regular heap memory:
// Creates new String object in heap, not in pool
String s1 = new String("Hello");
String s2 = new String("Hello");
System.out.println(s1 == s2); // false (different objects)
System.out.println(s1.equals(s2)); // true (same content)
// Adding to pool explicitly
String s3 = s1.intern();
String s4 = "Hello";
System.out.println(s3 == s4); // true (both reference pooled string)
Performance Considerations
- Memory Usage: String Pool helps reduce memory usage by reusing common strings
- Garbage Collection: Strings in the pool are garbage collected when no longer referenced
- Manual Interning: Use
intern()judiciously as it can lead to memory leaks if overused - String Concatenation: Results of compile-time constant expressions are interned, runtime concatenations are not
String Pool in Action
// Compile-time constants are interned
String s1 = "Hello" + " World"; // Interned at compile time
String s2 = "Hello World"; // Same as s1
System.out.println(s1 == s2); // true
// Runtime concatenation creates new objects
String s3 = "Hello";
String s4 = s3 + " World"; // Creates new String at runtime
System.out.println(s1 == s4); // false
// Using final makes it a compile-time constant
final String s5 = "Hello";
String s6 = s5 + " World"; // Now interned at compile time
System.out.println(s1 == s6); // true
3. String Concatenation Best Practices
String concatenation in Java requires careful consideration of performance and memory usage. Here's a comprehensive guide to doing it right:
1. The Problem with + in Loops
// Inefficient - creates a new String object in each iteration
String result = "";
for (int i = 0; i < 1000; i++) {
result += i; // Equivalent to: result = new StringBuilder().append(result).append(i).toString();
}
// Creates 1000 StringBuilder objects and 1000 String objects!
2. Using StringBuilder (Recommended for Most Cases)
// Efficient - uses a single StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append(i);
}
String result = sb.toString(); // Single String creation
// Pre-allocate capacity for better performance
StringBuilder sb2 = new StringBuilder(4000); // Estimated size
for (int i = 0; i < 1000; i++) {
sb2.append(i);
}
String result2 = sb2.toString();
3. When to Use Different Concatenation Methods
| Method | When to Use | Example | Performance |
|---|---|---|---|
+ operator |
Single expressions, compile-time constants | String s = "a" + "b"; |
Best (compiler optimizes) |
StringBuilder |
Multiple concatenations in a loop | for(...) { sb.append(i); } |
Very good |
StringBuffer |
Multiple threads accessing same buffer | synchronized(buffer) { buffer.append(i); } |
Good (thread-safe) |
String.join() |
Joining collections/arrays with delimiter | String.join(", ", list); |
Good for its purpose |
4. Advanced Concatenation Techniques
a. Using StringJoiner (Java 8+)
// For joining with delimiter
StringJoiner joiner = new StringJoiner(", ", "[", "]");
joiner.add("Apple").add("Banana").add("Cherry");
String result = joiner.toString(); // "[Apple, Banana, Cherry]"
// With streams
List fruits = Arrays.asList("Apple", "Banana", "Cherry");
String joined = fruits.stream()
.collect(Collectors.joining(", ", "[", "]"));
b. String.format() for Complex Formatting
String name = "Alice";
int age = 30;
String message = String.format("Name: %s, Age: %d", name, age);
// "Name: Alice, Age: 30"
// Multi-line with format
String sql = String.format("""
SELECT * FROM users
WHERE name = '%s'
AND age > %d
""", name, 18);
c. Text Blocks (Java 15+)
String html = """
Hello, %s!
""".formatted("World");
5. Performance Tips
Pre-size StringBuilder
Estimate the final size to avoid internal array resizing:
// Bad - starts with default capacity (16)
StringBuilder sb = new StringBuilder();
// Good - pre-allocate expected size
StringBuilder sb = new StringBuilder(estimatedLength);
Chain Method Calls
Chaining is more readable and efficient:
// Less efficient
StringBuilder sb = new StringBuilder();
sb.append("Hello");
sb.append(" ");
sb.append("World");
// More efficient and readable
String message = new StringBuilder()
.append("Hello")
.append(" ")
.append("World")
.toString();
Use String.join() for Collections
Cleaner than manual concatenation:
List names = Arrays.asList("Alice", "Bob", "Charlie");
String result = String.join(", ", names); // "Alice, Bob, Charlie"
Key Takeaways
- Use
+for simple, single expressions (compiler optimizes these) - Use
StringBuilderfor multiple concatenations in loops - Pre-size
StringBuilderwhen you can estimate the final size - For thread-safety, use
StringBufferor external synchronization - Use
String.join()andStringJoinerfor joining collections - Consider
String.format()for complex formatting - Use text blocks (Java 15+) for multi-line strings
4. String vs StringBuffer vs StringBuilder
| Feature | String | StringBuffer | StringBuilder |
|---|---|---|---|
| Mutability | Immutable | Mutable | Mutable |
| Thread Safety | Yes (immutable) | Yes (synchronized) | No |
| Performance | Slow for modifications | Slower than StringBuilder | Fastest for modifications |
| When to Use | When immutability is required | When thread safety is needed | Single-threaded environments |
5. Common String Methods with Examples
String text = " Java Programming ";
// 1. Length and Empty Checks
int length = text.length(); // 19
boolean isEmpty = text.isEmpty(); // false
boolean isBlank = text.isBlank(); // false (Java 11+)
// 2. Case Conversion
String upper = text.toUpperCase(); // " JAVA PROGRAMMING "
String lower = text.toLowerCase(); // " java programming "
// 3. Trimming and Stripping
String trimmed = text.trim(); // "Java Programming"
String stripped = text.strip(); // "Java Programming" (Java 11+)
String leading = text.stripLeading(); // "Java Programming " (Java 11+)
String trailing = text.stripTrailing(); // " Java Programming" (Java 11+)
// 4. Substring and Splitting
String sub1 = text.substring(5); // "Programming "
String sub2 = text.substring(5, 12); // "Program"
String[] words = text.trim().split(" "); // ["Java", "Programming"]
String joined = String.join("-", "Java", "is", "awesome"); // "Java-is-awesome"
// 5. Searching
boolean contains = text.contains("Java"); // true
boolean startsWith = text.startsWith(" Java"); // true
boolean endsWith = text.endsWith(" "); // true
int index = text.indexOf("Pro"); // 7
int lastIndex = text.lastIndexOf("m"); // 14
// 6. Character Access
char firstChar = text.charAt(2); // 'J'
char[] chars = text.toCharArray(); // Convert to char array
// 7. Replacement
String replaced = text.replace("Java", "Python"); // " Python Programming "
String regexReplaced = text.replaceAll("\\s+", " ").trim(); // "Java Programming"
// 8. Comparison
boolean equals1 = "java".equals("Java"); // false
boolean equals2 = "java".equalsIgnoreCase("Java"); // true
int compare = "apple".compareTo("banana"); // negative value
// 9. Formatting
String formatted = String.format("Name: %s, Age: %d", "Alice", 30);
6. Creating an Immutable Class
Here's how to create your own immutable class in Java:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public final class ImmutablePerson {
// 1. Make fields private and final
private final String name;
private final int age;
private final List hobbies;
// 2. Initialize all fields in constructor
public ImmutablePerson(String name, int age, List hobbies) {
this.name = name;
this.age = age;
// 3. Create defensive copies of mutable objects
this.hobbies = new ArrayList<>(hobbies);
}
// 4. No setters (only getters)
public String getName() {
return name;
}
public int getAge() {
return age;
}
// 5. Return defensive copies of mutable objects
public List getHobbies() {
return new ArrayList<>(hobbies);
// Or return an unmodifiable list:
// return Collections.unmodifiableList(hobbies);
}
// 6. Methods that modify state return new instances
public ImmutablePerson withAge(int newAge) {
return new ImmutablePerson(this.name, newAge, this.hobbies);
}
@Override
public String toString() {
return "Person{" + "name='" + name + '\'' +
", age=" + age +
", hobbies=" + hobbies + '}';
}
}
// Usage
List hobbies = new ArrayList<>();
hobbies.add("Reading");
hobbies.add("Hiking");
ImmutablePerson person = new ImmutablePerson("Alice", 30, hobbies);
System.out.println(person);
// Try to modify the original list
hobbies.add("Swimming");
System.out.println(person); // Original person is unchanged
// Create new instance with modified state
ImmutablePerson olderPerson = person.withAge(31);
System.out.println(olderPerson);
Key Points for Immutable Classes:
- Make the class
finalto prevent extension. - Make all fields
privateandfinal. - Don't provide setter methods.
- Make defensive copies of mutable objects in constructors and getters.
- For collections, return unmodifiable views or copies.
- Methods that modify state should return new instances.