Back to Blog
Java

Java Immutable String: What It Means and Why It Matters

java immutable string: Explains why Java String objects are immutable, how the string pool depends 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. The java immutable string guarantee means any operation that appears to modify a string—concatenation, replacement, substring extraction—actually creates a new String object and leaves the original untouched.

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

Every method on String that appears to change the value returns a new object. concat(), replace(), substring(), trim(), and toLowerCase() all follow this pattern.

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 thread can modify 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 cannot be altered by the sanitization logic. More importantly, no reference to sanitized can be changed by another thread while process is reading it. 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 modification 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 mutates the internal character array 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 static or modified 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, but code that retains the original array reference can still change it.

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 class immutable. A class with a String field is immutable only if the field is final and the class exposes no way to replace the reference. The String content is fixed, but the reference can still be reassigned.

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

The String object is immutable, but the Holder instance is not. Treating the field as final and providing only a getter is what makes the containing class immutable.

java immutable string: Practical Usage and Code Examples | RYUSLOG DEV