Why Strings Are Immutable in Java: Reasons and Examples

Learn why Strings are immutable in Java, how the final String class enforces it, and how the string pool, hash caching, threads and security depend on it.

Three steps showing that toUpperCase() creates a new String object and that reassignment moves the variable to another object while the old String stays unchanged

Strings are immutable in Java because the String class is final and keeps its characters in a private, final array that no method ever changes, so one String object can be shared safely by the string pool, many threads, hash maps and security checks. Every method that seems to modify a String, such as toUpperCase() or replace(), returns a new String instead.

We rely on String immutability every time we use a String as a HashMap key, pass a file name or host name to a method that validates it, or read a configuration value from several threads without locks. The design also lets the JVM store one copy of each literal and cache the hash code.

The following example shows that a method call returns a new String while the original keeps its value.

String env = "dev";
String upper = env.toUpperCase(Locale.ROOT);          // "DEV"
String original = env;                                // "dev"
boolean sameObject = upper == env;                    // false

Notice that env still refers to “dev” after the call. We look at what an immutable class is, how the string pool depends on immutability, how the String class enforces it in Java 25, the advantages and costs it brings, and a real-world example of sharing configuration values across threads.

1. What is an Immutable Class?

An immutable object is an object whose state cannot change after it is constructed. A variable that holds a reference to it can still be reassigned to another object, and that difference confuses many developers, because reassigning looks like changing the String.

Three steps showing that toUpperCase() creates a new String object and that reassignment moves the variable to another object while the old String stays unchanged
Methods create new String objects and assignments move references; the String object “dev” never changes
String name = "dev";
name.concat("-eu");                                   // result is discarded
String unchanged = name;                              // "dev"
name = name.concat("-eu");                            // name now points to a new object
String reassigned = name;                             // "dev-eu"

The second line creates “dev-eu” and throws it away, because nothing stores the result. Only the assignment on the fourth line moves name to the new object. If we want the variable itself to stay fixed, we declare it final, which is a separate rule from object immutability.

The JDK has many other immutable classes, such as the wrapper classes Integer and Long, BigDecimal, LocalDate and the collections returned by List.of(). The guide on how to create an immutable class shows the same rules applied to our own classes.

2. Strings are Stored in String Constant Pool

The String constant pool is a table inside the JVM that keeps one shared String object per literal. Since Java 7, the pooled Strings are stored in the normal heap (before that, in PermGen), so the garbage collector removes them when nothing refers to them any more.

The compiler and JVM put string literals and compile-time constants into the pool. Strings built at runtime, for example by StringBuilder, by reading a file or by new String(), are ordinary heap objects until we call intern() on them.

String first = "value";
String second = "value";
String built = new StringBuilder("val").append("ue").toString();
boolean pooled = first == second;                     // true
boolean runtime = first == built;                     // false
boolean interned = first == built.intern();           // true

Sharing one object between all variables with the same literal is safe only because nobody can change that object. If first could change “value” in place, second and every other class that used the literal “value” would see the change too.

Assigning another literal, as in first = “test”;, does not modify the pooled “value”. It makes first refer to the pooled “test” object, and “value” stays in the pool for the other variables.

3. How the String Class Enforces Immutability

The String class does not rely on a convention, because its code leaves no way for another class to change the characters. Running javap -p java.lang.String on JDK 25 prints the fields that matter. The value array holds the characters, coder says whether they are Latin-1 or UTF-16, and hash caches the hash code.

public final class java.lang.String implements java.io.Serializable, java.lang.Comparable<java.lang.String>, java.lang.CharSequence, java.lang.constant.Constable, java.lang.constant.ConstantDesc {
  private final byte[] value;
  private final byte coder;
  private int hash;
  private boolean hashIsZero;
  ...

Each part of the declaration closes one way to change a String.

  • The class is final, so nobody can write a subclass that overrides methods and adds mutable behavior.
  • The value array is private and final, so no other class can read it or replace it.
  • Constructors copy the array they receive, and methods such as toCharArray() return a copy, so no outside code ever holds a reference to value.
  • Every method that produces different text creates a new String with a new array.
char[] letters = {'d', 'e', 'v'};
String fromArray = new String(letters);
letters[0] = 'r';
String stillDev = fromArray;                          // "dev"
char[] copy = fromArray.toCharArray();
copy[0] = 'r';
String alsoDev = fromArray;                           // "dev"

The hash and hashIsZero fields are not final, but they only cache a value computed from the characters, so the visible state of the String never changes. Since Java 9, compact strings store Latin-1 text in one byte per character, and the coder field records which encoding the array uses.

Reflection used to be a back door into value. Since Java 17, strong encapsulation of JDK internals (JEP 403) blocks it, and the call fails unless the app starts with –add-opens java.base/java.lang=ALL-UNNAMED.

Field field = String.class.getDeclaredField("value");
field.setAccessible(true);                            // InaccessibleObjectException: module java.base does not "opens java.lang"

4. Why Strings Are Immutable in Java

Immutability is a trade of extra objects for code that can share Strings without defensive copies. The four advantages that follow are the reasons the Java designers gave String this behavior, and each one shows up in everyday code.

4.1. Application and Data Security

Many security decisions in Java take a String, such as a class name passed to Class.forName(), a file path, a host name or a user name. The JDK checks the value and uses it later, and immutability guarantees that the value used is the value that was checked.

Say a service accepts a host name from a request and allows only hosts from a list. The following example contrasts a mutable StringBuilder with the String copy that the service checks.

StringBuilder mutableHost = new StringBuilder("api.example.com");
String checkedHost = mutableHost.toString();
boolean allowed = Set.of("api.example.com").contains(checkedHost);   // true
mutableHost.replace(0, 3, "dev");
String builderNow = mutableHost.toString();           // "dev.example.com"
String stillChecked = checkedHost;                    // "api.example.com"

Changing the builder after the check has no effect on checkedHost. If the method had stored the StringBuilder instead, the caller could pass the check with one host and connect to another.

Immutability does not prevent SQL injection. A query built by concatenating user input is unsafe whether or not Strings are immutable, and the fix is a PreparedStatement with parameters.

4.2. Improved Performance

Immutability makes several JVM optimizations possible. The string pool shares one object per literal, and the hash code is computed once and cached, which speeds up every later lookup of the same key.

  • The string pool and intern() keep a single object for repeated text, such as the same status value in a million log records.
  • The JVM option -XX:+UseStringDeduplication lets the garbage collector point equal Strings at one shared array, which is safe only because no String changes its array.
  • Methods such as substring(0), strip() on text without spaces, or toLowerCase() on lowercase text can return the same object instead of a copy.

The flip side is that code which changes text in a loop creates many short-lived objects, as section 5 shows.

4.3. Thread Safety

A String object is thread-safe, because its fields are final or only cache derived values. The Java Memory Model guarantees that final fields are visible to every thread once the constructor finishes, so threads can share a String without synchronized.

The variable that holds the String is a different matter. When one thread replaces a shared String field and other threads read it, the field still needs volatile or an AtomicReference so the readers see the new reference.

AtomicReference<String> status = new AtomicReference<>("idle");
boolean changed = status.compareAndSet("idle", "running");   // true
String current = status.get();                        // "running"

4.4. Caching

A HashMap stores each entry in a bucket chosen by the key’s hash code. If a key changed after insertion, its hash would change too, and the map would look in the wrong bucket and miss the entry. Strings cannot change, so their hash stays valid and they make reliable keys for caches and maps.

The following example shows what happens with a mutable key, a List, compared with a String key.

Map<List<String>, Integer> byList = new HashMap<>();
List<String> listKey = new ArrayList<>(List.of("apple"));
byList.put(listKey, 5);
listKey.add("kiwi");
Integer lost = byList.get(listKey);                   // null
Map<String, Integer> byName = new HashMap<>();
byName.put("apple", 5);
Integer found = byName.get("apple");                  // 5

The entry for the list is still in the map, but no lookup can find it because the list has a different hash after the change. The guide on designing a good HashMap key lists the same rule for our own key classes.

5. The Cost of Immutability

Each change to a String creates a new object. That is cheap for a few operations, but building text with += in a loop copies the growing text on every pass. A StringBuilder is the mutable counterpart to use there, as the comparison of String concatenation methods explains.

StringBuilder csv = new StringBuilder();
for (String fruit : List.of("apple", "kiwi", "fig")) {
    if (!csv.isEmpty()) {
        csv.append(',');
    }
    csv.append(fruit);
}
String line = csv.toString();                         // "apple,kiwi,fig"

Immutability is also the reason the Java security guides recommend a char[] for passwords. A password String stays in memory until the garbage collector removes it, and we cannot clear it, whereas an array can be overwritten as soon as the password is checked. For this reason Console.readPassword() returns a char[].

char[] password = {'s', 'e', 'c', 'r', 'e', 't'};
Arrays.fill(password, '*');
String wiped = new String(password);                  // "******"

6. Real-World Example of Sharing Configuration Across Threads

A web app reads feature flags and settings, such as the payment mode, from a config server. Hundreds of request threads read these values on every request, and an admin can reload them while the app runs. Because Strings and the map returned by Map.copyOf() are both immutable, the reader threads need no locks.

The following example keeps the settings in an unmodifiable map and replaces the whole map on reload.

class Settings {
    private volatile Map<String, String> values = Map.of("payment.mode", "sandbox");

    String get(String key) {
        return values.get(key);
    }

    void reload(Map<String, String> fresh) {
        values = Map.copyOf(fresh);
    }
}
Settings settings = new Settings();
String before = settings.get("payment.mode");         // "sandbox"
settings.reload(Map.of("payment.mode", "live"));
String after = settings.get("payment.mode");          // "live"
String oldValue = before;                             // "sandbox"

A thread that read “sandbox” before the reload keeps a valid, unchanged value for the rest of its request, and the next request sees “live”. The volatile field publishes the new map, and immutability of the keys, values and map removes every other synchronization concern.

7. String Immutability FAQs

Immutability is a favorite interview topic, and the follow-up questions test whether the candidate knows the limits of the rule as well as the rule itself.

7.1. Why is the String class final?

A final class cannot be extended. Without it, a subclass could override methods such as equals() or hashCode(), or add mutable fields, and code that accepts a String could no longer trust that the value stays the same.

7.2. Can we change a String with reflection?

Not in Java 17 and later without extra flags. The call setAccessible(true) on the value field throws an InaccessibleObjectException unless the app opens java.lang with –add-opens. Even with that flag, changing a pooled String breaks every class that uses the same literal.

7.3. Is String thread-safe in Java?

Yes, a String object is thread-safe and can be shared without synchronization. A field that holds a String and gets reassigned still needs volatile, an AtomicReference or a lock.

7.4. Are StringBuilder and StringBuffer immutable?

No. Both are mutable buffers. A StringBuilder is meant for one thread, and StringBuffer synchronizes every method. Call toString() to get an immutable String when the text is complete.

7.5. What happens in memory when we concatenate two Strings?

The JVM creates a new String that holds both texts, and the two originals stay unchanged. When both parts are compile-time constants, such as “ab” + “cd”, the compiler joins them into one literal, which goes into the pool.

8. Conclusion

A String cannot change because the class is final, its byte[] is private and final, and every method that produces different text returns a new object. Reassigning a variable only moves the reference, and the old String stays as it was.

That design is what makes the string pool, cached hash codes, thread-safe sharing and trustworthy security checks possible. The cost is extra objects when text changes often, which StringBuilder solves, and passwords that cannot be wiped, which char[] solves. The Java String tutorial lists the methods that return these new Strings.

9. References

Happy Learning !!

Source Code on Github

Leave a Comment

  1. If String is immutable, then why its content can be modified ?

     
    public static void main (String[] args) {
    String st1= "hello";
    s1=s1+" India";
    } 
    
  2. Hi Lokesh,
    I have 1 query to ask, please help me out:

    Is there any way to find out, how many objects would create in my java program. For e.g

    String str= “abc”+”xyz”;

    • Perhaps you are confusing with singleton and immutability. Singleton prevents instance creation (allows only one). But immutability does not prevent creating new instances, it just prevents the state of instance once it is fully created.

  3. Hi, I didn’t understand this statement in above explainations about hacking , “Imagine for a minute, Strings were mutable, then anybody could have injected its own class-loading mechanism with very little effort and destroyed or hacked in any application in a minute” Also ” string pool is not a possibility without making String class immutable” please elaborate little more details on this discussions

    • OK. So i got two things to explain. Lets make it clear one by one.

      1) We know in java load classes using Class.forName(string). If Strings were mutable, then someone wrote the code to load “java.io.Writer”. This string variable will be in pool. Now, some evil code can access this pool and change the string to “com.MakeHavocWriter” and now when code gets executed, one can easily imagine the consequences.

      2) String pool is there so that one String instance can be referred from multiple reference variables. If string is mutable, then one object can change the string content and other will see this undesired value.

      See the fact is that String pool was designed for sake of performance, by minimizing the total count of string instances (not references) in application memory space. And as in above scenario, if pool makes the string values changing undesirably, then no one will prefer to use it.

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.