String identity, interning and concatenation
Why == sometimes works on strings, which is worse than never working.
Open this lesson in the learning hubKey points
- String literals are interned: identical literals in the same JVM refer to one object, so
==appears to work. That is exactly what makes it dangerous. - A string built at runtime - from concatenation of variables, a substring, or input - is a different object, so the same comparison suddenly fails with no visible change in the code.
- A concatenation of two compile-time constants is folded by the compiler into a literal, so it is interned. Involve one non-final variable and it is not.
intern()returns the canonical instance from the string pool. It was a memory optimisation in the permanent-generation era; today its cost usually outweighs the benefit outside specific deduplication cases.- Since Java 9, strings holding only Latin-1 characters are stored one byte per character rather than two. A single non-Latin-1 character switches the whole string to two bytes, which is a real memory consideration.
- Since Java 9,
+on strings compiles to aninvokedynamiccall rather than a StringBuilder chain, which lets the JVM optimise it. Explicit StringBuilder is still what you want inside a loop.
Example
public class StringIdentity {
static final String PREFIX = "hello"; // compile-time constant
public static void main(String[] args) {
String a = "hello";
String b = "hello";
System.out.println("two literals a == b : " + (a == b));
String c = new String("hello");
System.out.println("new String a == c : " + (a == c));
System.out.println(" a.equals(c): " + a.equals(c));
System.out.println("interned a == c.intern(): " + (a == c.intern()));
// Constant folding: this becomes the literal "hello world".
String folded = PREFIX + " world";
String literal = "hello world";
System.out.println();
System.out.println("constant-folded folded == literal : " + (folded == literal));
// One non-final variable and the folding stops.
String prefixVar = "hello";
String runtime = prefixVar + " world";
System.out.println("built at runtime runtime == literal: " + (runtime == literal));
System.out.println(" .equals : " + runtime.equals(literal));
// Compact strings: Latin-1 costs 1 byte/char, anything else costs 2.
String latin = "abcdefghij";
String unicode = "abcdefghi\u00e9"; // one accented char
System.out.println();
System.out.println("latin length : " + latin.length());
System.out.println("unicode length : " + unicode.length()
+ " (same length, twice the bytes internally)");
// In a loop, build explicitly - do not concatenate in place.
long start = System.nanoTime();
StringBuilder sb = new StringBuilder(1024);
for (int i = 0; i < 1000; i++) {
sb.append(i).append(',');
}
long ns = System.nanoTime() - start;
System.out.println();
System.out.println("built " + sb.length() + " chars in " + ns / 1000 + " us");
System.out.println();
System.out.println("RULE: compare strings with equals(), always.");
}
}
Literals are interned so == appears to work until a string is built at runtime - which is why it is a bug that hides in testing.
This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the Core Java course, and every lesson in it is listed on the Core Java contents page.