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 StringBuilder for multiple concatenations in loops
  • Pre-size StringBuilder when you can estimate the final size
  • For thread-safety, use StringBuffer or external synchronization
  • Use String.join() and StringJoiner for 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 final to prevent extension.
  • Make all fields private and final.
  • 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.