Understanding Java String Immutability and the String Constant Pool
This article explains why Java strings are immutable, how the JVM uses the string constant pool and the final keyword to manage memory, and demonstrates the behavior with code examples that show reference handling, object creation, and common string operations.
Many developers mistakenly think that reassigning a String variable changes the original object; however, Java strings are immutable, and each modification creates a new object.
The JVM stores string literals in a string constant pool. When a literal is encountered, the JVM checks the pool; if the literal already exists, it returns the existing reference, otherwise it creates a new String object and adds it to the pool. Because String is immutable, the same literal can safely be shared.
Code example demonstrating a simple reassignment:
public class App {
public static void main(String[] args) {
String a = "111";
a = "222";
System.out.println(a);
}
}The above prints 222 because the variable a now references a different String object; the original "111" remains unchanged in the pool.
The String class is declared as final and stores its characters in a private final char[] array:
public final class String implements java.io.Serializable, Comparable
, CharSequence {
private final char value[];
public String() { this.value = "".value; }
public String(String original) { this.value = original.value; this.hash = original.hash; }
public String(char value[]) { this.value = Arrays.copyOf(value, value.length); }
}Because the class and its internal character array are final, their contents cannot be altered after construction.
The final keyword has several uses: it prevents class inheritance, stops method overriding, and makes variables immutable after initialization. For reference types, the reference cannot change, but the object’s internal state may still be mutable.
Example of a final variable declaration:
public class FinalDemo {
private final String name;
public FinalDemo(String name) { this.name = name; }
}Attempting to assign a new value to name after construction would cause a compilation error.
Common String methods such as concat , replace , substring , and trim never modify the original object; they always create and return a new String instance.
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) return this;
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value;
while (++i < len) {
if (val[i] == oldChar) break;
}
if (i < len) {
char buf[] = new char[len];
for (int j = 0; j < i; j++) buf[j] = val[j];
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
return new String(buf, true);
}
}
return this;
}These operations illustrate that the original string remains unchanged, reinforcing immutability.
Equality and reference comparisons further clarify the behavior:
public class App {
public static void main(String[] args) {
String a = "111";
String a1 = "111";
String b = new String("111");
System.out.println(a == a1); // true, same pool reference
System.out.println(a.equals(a1)); // true, same content
System.out.println(a == b); // false, different objects
System.out.println(a.equals(b)); // true, same content
}
}The output demonstrates that literals share pool references, while explicitly created objects do not, even if their contents are identical.
In summary, a String object is created once and never altered; any operation that appears to modify a string actually produces a new object, and the JVM’s string pool optimizes memory usage by reusing identical literals.
Architect's Tech Stack
Java backend, microservices, distributed systems, containerized programming, and more.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.