Skip to content
Java strings 6 min read

Why String is Immutable

In Java, a String object cannot be changed after it is created — every operation that appears to “modify” a string actually produces a brand-new String object. This design is deliberate, and understanding it will save you from subtle bugs and help you write faster, safer code.

String object memory layout

What Immutability Means

When you write s = s + "!", you are not appending to the original string. Java creates a new String object and reassigns the variable s to point to it. The old object is left unchanged (and eventually garbage-collected if nothing else holds a reference to it).

public class ImmutabilityDemo {
    public static void main(String[] args) {
        String s = "Hello";
        String t = s;           // both variables point to the same object

        s = s + " World";       // s now points to a NEW object

        System.out.println(s);  // Hello World
        System.out.println(t);  // Hello  — unchanged!
    }
}

Output:

Hello World
Hello

t still holds the reference to the original "Hello" — no code can mutate that object through s.

Note: The variable s is mutable (you can reassign it), but the String object it points to is immutable. These are two different things.

Five Reasons Java Made String Immutable

1. String Pool Efficiency

Java maintains a special region of the heap called the String Pool (part of the method area / metaspace). When you write a string literal, the JVM checks whether that value already exists in the pool. If it does, it returns the same object rather than creating a new one.

String a = "Java";
String b = "Java";

System.out.println(a == b);      // true — same object from the pool
System.out.println(a.equals(b)); // true

This sharing is only safe because strings cannot change. If a could mutate "Java" to "Python", b would silently see "Python" too — a nightmare scenario. Immutability makes the pool trustworthy.

2. Thread Safety

An immutable object can be freely shared across threads without synchronization. No thread can alter the object’s state, so there is no risk of one thread seeing a half-updated string written by another.

// Safe to share across many threads — no locks needed
public class Config {
    public static final String DB_URL = "jdbc:mysql://localhost/mydb";
}

If String were mutable, every access to DB_URL across concurrent threads would require explicit locking.

3. Security

Strings carry sensitive data — file paths, usernames, passwords, class names for reflection. If they were mutable, an attacker could obtain a reference to a validated string (say, a file path that passed a security check) and then mutate it after the check but before it is used.

// Hypothetical MUTABLE scenario — DO NOT DO THIS
// String path = "/safe/resource";
// securityCheck(path);         // passes
// path.mutate("/etc/passwd");  // changes the object in place!
// openFile(path);              // now opens /etc/passwd

Because strings are immutable, the value you checked is guaranteed to be the value used. This is why the JVM itself relies on immutable String for class loading — a class name cannot be swapped for a malicious one between load and verification.

4. Hashcode Caching

String caches its hashCode() result after the first computation. Because the content can never change, the cached value is always valid.

String key = "username";

// First call: computes and caches the hash
int h1 = key.hashCode();

// Subsequent calls: returns the cached value instantly
int h2 = key.hashCode();

System.out.println(h1 == h2); // true, and the second call is O(1)

This makes strings extremely efficient as HashMap keys — a very common use case. A mutable string would require recomputation on every call (or risk returning stale hashes after mutation).

5. Class Loading Integrity

The JVM uses String to identify classes, packages, and resources. Immutability guarantees that once the JVM resolves a class name to a Class object, nothing can alter that string reference and redirect class loading to a different class — a critical security boundary.

Under the Hood

How the JVM Stores Strings

Internally, a String holds:

  • A byte[] array (since Java 9; char[] before Java 9) containing the encoded characters.
  • An int hash field (0 until first hashCode() call).
  • A byte coder flag (Java 9+) indicating whether Latin-1 or UTF-16 encoding is used.

The value array is declared private final, and String exposes no method that modifies it. There is no setCharAt or similar mutator.

+------------------+
|  String object   |
|  value: byte[]   |  ──► [ 'H','e','l','l','o' ]  (private final)
|  hash:  int      |      (cached, lazily computed)
|  coder: byte     |
+------------------+

Compact Strings (Java 9+)

Before Java 9, every character occupied 2 bytes (UTF-16). Java 9 introduced Compact Strings: if every character fits in Latin-1 (code point ≤ 255), the array uses 1 byte per character, cutting memory usage roughly in half for ASCII-heavy applications. This optimization is transparent and possible only because strings are immutable — the encoding cannot change after construction.

Reflection Can Break Immutability (Don’t Do It)

It is technically possible to mutate a String’s backing array via reflection, but this is undefined behavior, violates the Java specification, and can corrupt the string pool:

// WARNING: Never do this in real code — included only to illustrate the internals
import java.lang.reflect.Field;

String s = "Hello";
Field value = String.class.getDeclaredField("value");
value.setAccessible(true);
byte[] bytes = (byte[]) value.get(s);
bytes[0] = (byte) 'J'; // corrupts the pool entry!
System.out.println(s); // may print "Jello" — undefined and dangerous

Warning: Mutating a String through reflection is undefined behavior. Because "Hello" may be interned in the String Pool, this mutation can silently corrupt every part of your program that holds a reference to the same interned string.

Practical Takeaways

ScenarioRecommendation
Building a string in a loopUse StringBuilder — it is mutable and fast
Thread-safe string buildingUse StringBuffer — synchronized
Read-only shared constantString is perfect
HashMap keyString is ideal due to cached hashcode
Sensitive data (password)Consider char[] — can be explicitly zeroed after use

Tip: If you find yourself concatenating strings in a tight loop with +, switch to StringBuilder. The compiler optimizes simple cases, but complex loops benefit from explicit StringBuilder use. See String Concatenation for the full story.

A Quick Experiment

You can verify immutability yourself by comparing object identities:

public class IdentityCheck {
    public static void main(String[] args) {
        String original = "Java";
        String modified = original.toUpperCase(); // returns a NEW object

        System.out.println(original);                      // Java
        System.out.println(modified);                      // JAVA
        System.out.println(original == modified);          // false — different objects
        System.out.println(System.identityHashCode(original));
        System.out.println(System.identityHashCode(modified)); // different codes
    }
}

Output:

Java
JAVA
false
(two different integers)

toUpperCase(), toLowerCase(), trim(), replace() — every String method that “transforms” the string returns a new object. The original is untouched.

Last updated June 13, 2026
Was this helpful?