Back to Blog
Java

Java Immutable String: What It Means and Why It Matters

Explains why Java String objects are immutable, how the string pool relies on this design, and when to use StringBuilder instead.

String ImmutabilityString PoolStringBuilderThread SafetyJava Memory
Illustration of a sealed Java String object with arrows branching to new string instances, representing immutability.

In Java, strings are immutable. Once a String instance is created, its character sequence cannot be changed. Any operation that appears to modify a string—concatenation, replacement, substring extraction—leaves the original untouched. It may return a new String, or the original instance if no change is actually needed, but it never mutates the receiver.

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

The original greeting still holds "hello" after toUpperCase() is called. The method returns a new object rather than mutating the existing one.

How the String Pool Relies on Immutability

The JVM maintains a string pool to reuse identical string literals. When you write:

String a = "config"; String b = "config";

Both variables reference the same object from the pool. This deduplication is safe only because no code can modify the shared instance. If String were mutable, one variable's change would silently affect the other.

The intern() method also depends on this guarantee. Calling intern() on a string returns the pooled instance if one exists, or adds the current instance to the pool. Without immutability, pooled strings could be corrupted by any code holding a reference.

What Common String Operations Actually Do

No String method mutates an existing instance. Methods like concat(), replace(), substring(), trim(), and toLowerCase() return a new object when they produce a different value; if no change is needed, they may return the original instance. Either way, the receiver is not modified.

String path = "/api/users"; String trimmed = path.replace("/api", "/v2"); System.out.println(path); // /api/users System.out.println(trimmed); // /v2/users

The original path is unchanged. This has a practical consequence: if you need the modified value, you must assign the result to a variable. Failing to do so is a common mistake.

String name = "alice"; name.replace("a", "x"); // result discarded System.out.println(name); // alice

The replace result is discarded because the return value was not assigned. The original string remains "alice".

Thread Safety Without Synchronization

Immutable objects are inherently thread-safe. Multiple threads can read the same String instance without synchronization because no thread can alter its state. This removes an entire class of concurrency bugs for code that shares string values across threads.

public class Config { private final String endpoint; public Config(String endpoint) { this.endpoint = endpoint; } public String getEndpoint() { return endpoint; } }

The endpoint field can be safely published to multiple threads. No method can change it after construction, so readers never observe a partially updated value. The same reasoning applies to using String as a HashMap key: the hash code is stable because the content never changes.

Security Implications of Immutable Strings

Immutability provides a security boundary in code that handles sensitive values. When a String holds a path, a username, or a database connection parameter, no caller can mutate the value after it has been validated.

Consider a method that validates an input string and then passes it to a downstream component:

public void handleRequest(String input) { String sanitized = sanitize(input); process(sanitized); }

Because sanitize returns a new String, the original input is not modified by the sanitization logic. More importantly, the String object passed to process cannot be changed by another thread; String has no mutator methods, so its content is fixed. In a mutable design, a caller could hold a reference to the same object and modify it between validation and use.

This is also why String is used for class names, file paths, and reflection lookups. The JVM can safely cache and compare these values without worrying about mutations.

Performance Tradeoffs and StringBuilder

The main cost of immutability is allocation. Every operation that produces a modified value creates a new object, which becomes wasteful in loops that build strings incrementally.

String result = ""; for (int i = 0; i < 1000; i++) { result += i; // creates a new String on every iteration }

Each += allocates a new String and copies the previous content, producing quadratic copying behavior. For small loops this is irrelevant, but for larger iterations the overhead is measurable.

StringBuilder provides a mutable buffer for this case:

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

append writes into the builder's internal buffer instead of allocating a new object per operation. Use StringBuilder when you are assembling a string from multiple parts in a loop or across several method calls. Use plain String when the value is fixed or changed only once.

StringBuffer is the synchronized variant of StringBuilder. Its methods are thread-safe, but the synchronization overhead is unnecessary in single-threaded code. Prefer StringBuilder unless the buffer is shared across threads.

Where Immutability Assumptions Can Break

The immutability guarantee applies to the String object itself, not to references held by other code. A char[] passed into a constructor can still be modified externally if the constructor does not copy it. The String(char[]) constructor copies the array, so later changes to the original array are not visible through the String.

char[] secret = {'p', 'a', 's', 's'}; String password = new String(secret); secret[0] = 'x'; // password still contains "pass"

The String is unaffected because the constructor copied the array. However, if you pass a char[] to a method that stores it directly, that method can observe later changes. This is a general mutable-reference pitfall, not a violation of String immutability.

Another boundary: String immutability does not make a containing class immutable. A class with a String field is immutable only if the field is final, no setter or method can replace the reference, and subclassing cannot add mutable state. The String content is fixed, but a non-final reference can be reassigned.

public class Holder { public String value; // reference can be changed }

The String object is immutable, but the Holder instance is not. Making the field final and limiting updates to construction is a necessary step; if the class is intended to be immutable, make it final as well so subclasses cannot introduce mutable state.

Why Java Strings Are Immutable and When to Use StringBuilder | RYUSLOG DEV