Okay, the title might or might not be clickbait. The original article is here, you can check it for yourself.
Or if you don’t want to, you can view this short by Jose Paumard:
TL;DR
Here is your poison:
String is technically mutable, but effectively immutable
Long Answer (Sorry Jose, Let Me Steal Your Catchphrase)
Examining the String class, we will immediately see these fields:
private final byte[] value; // Actual holder of String value
private final byte coder; // either LATIN (0) or UTF16 (1)
private int hash;
private boolean hashIsZero;
If you still use JDK 8 and below (why would you?), you will see this instead:
private final char[] value;
private int hash;
Do you see somet…
Okay, the title might or might not be clickbait. The original article is here, you can check it for yourself.
Or if you don’t want to, you can view this short by Jose Paumard:
TL;DR
Here is your poison:
String is technically mutable, but effectively immutable
Long Answer (Sorry Jose, Let Me Steal Your Catchphrase)
Examining the String class, we will immediately see these fields:
private final byte[] value; // Actual holder of String value
private final byte coder; // either LATIN (0) or UTF16 (1)
private int hash;
private boolean hashIsZero;
If you still use JDK 8 and below (why would you?), you will see this instead:
private final char[] value;
private int hash;
Do you see something?
The Mutability Gotcha
Yes, you saw that right, there are rebellious non-immutable fields in the String class.
And by most definitions of an immutable class (being a final class and all of its fields must be final), this makes String not qualified to be one. The most technical representations of an immutable class in Java include Optional, LocalDate, UUID, and all Java Records.
Note: The finality here is shallow, not deep, but we don’t talk about that here.
Another problem here is the backing array. Sure, it is final, but you know that even though the reference to said array is final, nothing really prevents the elements in said array from being changed. You know, the simple assignment?
char[] chars = new char[5];
chars[0] = 'H';
chars[1] = 'e';
chars[2] = 'l';
chars[3] = 'l';
chars[4] = 'o';
// Nothing prevents you from doing this
chars[0] = 'h';
That’s why the method .toCharArray() needs a defensive copy instead of returning the array that backs the string directly.
There is a JEP Proposal to make truly frozen arrays, but it is still in draft stage, and there are no words about its prospect of ever making it into production JDK.
No Valhalla for You!
For this reason, as explained by Jose Paumard, String cannot be made into a value class (a class that lacks identity), because a value class requires all of its fields to be final, shallowly or not.
Why?
Short Answer
Performance, by using lazy evaluation on the hash value of the String and storing it for future uses.
Long Answer
String uses the hash (and hashIsZero if you use JDK 9+) field to store the computed hash value.
The calculation of a String object’s hash can be very expensive if the string is long (like a blog post, an HTML page, or your feelings for your crush, j/k), and the hash code is only used in some hash-based use cases (like putting one into a HashMap).
It is due to this reason that Java makers decided to lazily evaluate the hash code of a String and store this value for future uses.
You can check the String class source code and see the implementation of its hashCode() method:
public int hashCode() {
int h = this.hash;
if (h == 0 && !this.hashIsZero) {
h = isLatin1()
? StringLatin1.hashCode(value)
: StringUTF16.hashCode(value);
if (h == 0) {
this.hashIsZero = true;
} else {
this.hash = h;
}
}
return h;
}
However, the String is still effectively immutable due to its design:
The internal array does not get exposed to the outside due to defensive copying.
The hash and hashIsZero value initialization only happens once, and they will never change after that because there are no public ways to mutate those values. This mutation is completely invisible to outside observers.
It is also worth mentioning that String is subjected to a lot of internal optimization, for example, the @Stable annotation on its value field, and more recently, on its hash field in JDK 25, allowing some optimizations by the JVM, like constant folding (detailed article is here).
Final Notes
Yes, String is not technically immutable in the truest sense.
No, String is still effectively immutable. You can only read its final fields, and you cannot do anything to its non-final ones (Reflection API can do something about it, but don’t, just... don’t).
String embodies the exact definition of a value class, but unless its internal design is changed (like frozen arrays and stable value JEPs), the journey to Valhalla will not permit an entry for our beloved String class.