Back to Blog
Java

Java String Immutability: What It Means and Why It Matters

Understand Java string immutability, its impact on memory and performance, and how it shapes everyday string handling.

JavaStringImmutabilityMemoryConcurrency
Illustration of a Java string object as a sealed box with a lock, representing immutability and thread safety, next to a memory pool diagram.

In Java, strings are immutable: once a String object is created, its value cannot be changed. This design decision affects how you compare, concatenate, and store strings, and it has direct consequences for memory usage and thread safety. Understanding Java string immutability is essential for writing efficient and correct Java code.

What Immutability Actually Means for a String Object

When we say a String is immutable, we mean that no method changes the characters in an existing instance. Operations that produce a different value return a new String object; the original remains untouched.

String original = "hello"; String upper = original.toUpperCase(); System.out.println(original); // prints "hello" System.out.println(upper); // prints "HELLO"

The String class stores its character data in a private final char[] in older JDKs, and in a private final byte[] in Java 9+ (the change that enables compact strings). It does not expose a setter that can modify that array. Even reflection is not a reliable way to change it, because doing so can break internal invariants. This is not an accident; it is a deliberate design choice that enables several important behaviors.

How the JVM Exploits Immutability: The String Pool

Because strings are immutable, the JVM can safely reuse identical strings. When you write a string literal, it is recorded in the class file and interned in the string pool (also called the interned string table) at runtime. Identical literals therefore refer to the same String instance.

String a = "hello"; String b = "hello"; System.out.println(a == b); // true, both refer to the same pooled instance

This pooling reduces memory usage when the same string value appears many times. However, it only works because strings cannot change. If a string could be modified, one reference would see changes made through another, causing unpredictable behavior. The intern() method explicitly adds a string to the pool, but you rarely need it unless you are dealing with many dynamically created strings that repeat frequently.

Performance Implications of Immutability

Immutability has a direct performance cost: every value-changing operation creates a new object. Consider concatenation:

String result = ""; for (int i = 0; i < 1000; i++) { result += i; }

Each += creates a new String, copies the existing content, and appends the new part. This is O(n²) in the number of concatenations. Optimizations for individual concatenations do not eliminate the repeated copying in this pattern, because each iteration still needs a new String result. The practical solution is to use StringBuilder explicitly when you need to build a string incrementally.

StringBuilder sb = new StringBuilder(); for (int i = 0; i < 1000; i++) { sb.append(i); } String result = sb.toString();

This avoids the repeated copying and is the standard approach for dynamic string construction. Immutability also affects implementation choices: older JDKs could share the underlying character array for substring(), while current JDKs copy the needed range to avoid keeping large backing arrays alive. In either case, be mindful of how many intermediate strings you create.

Concurrency and Thread Safety

Because a String cannot change after construction, it is inherently thread-safe. Multiple threads can read and share the same String instance without synchronization. This is a major reason why strings are so widely used as keys in hash maps, cache entries, and configuration values.

public class Config { private final String name; public Config(String name) { this.name = name; } public String getName() { return name; } }

If name were mutable, a thread could modify it while another thread reads it, leading to inconsistent state or visibility issues. With immutability, the object is safe to publish without additional synchronization. This is a concrete benefit that simplifies concurrent code.

Common Pitfalls and Misconceptions

A frequent mistake is using == to compare strings. Because of pooling, == may work for literals but fails for strings created at runtime.

String s1 = "hello"; String s2 = new String("hello"); System.out.println(s1 == s2); // false, different instances System.out.println(s1.equals(s2)); // true, same value

Always use .equals() for value comparison. Another pitfall is assuming that methods like trim() or toLowerCase() always allocate a new object. They often do, but if the operation does not change the string, some implementations return the same instance. Do not rely on object identity either way; use the returned value.

When Immutability Becomes a Problem

There are cases where immutability is inconvenient. If you need to modify a string frequently, the overhead of creating new objects can hurt performance and increase garbage collection pressure. For example, building a large XML document or a CSV file by repeated concatenation is inefficient. In those situations, use StringBuilder or StringBuffer (if you need thread safety).

Another scenario is when you want to use a string as a mutable buffer, such as in a parser. You might be tempted to manipulate a char[] directly, but that breaks the abstraction. Instead, use StringBuilder and convert to String only when the value is complete. This keeps your code clear and avoids accidental aliasing.

Working with Strings Efficiently in Practice

Understanding immutability helps you make better decisions about memory and performance. For example, when you have a method that returns a string, consider whether you can reuse a constant rather than creating a new one. Use String.valueOf() for conversions, and avoid + in loops. When you need to store many similar strings, rely on string literals, which are automatically interned, and use intern() sparingly.

If you are parsing a large text, remember that extracting many substrings can be costly in current JDKs because each substring() call copies data. Consider using CharSequence or a custom view if you need to avoid copying.

Finally, remember that immutability is a contract. When you design your own classes, you can choose to make them immutable as well, which brings the same benefits: thread safety, safe sharing, and simpler reasoning about state. The String class is a model example of how immutability can be used to create a robust, widely used API.

Java String Immutability: Practical Usage and Code Examples | RYUSLOG DEV