"hello" == "hello" — is that an O(1) pointer comparison or an O(n) character-by-character
scan? The answer is a runtime decision called string interning: giving every distinct
string value exactly one canonical object, so that value equality collapses into pointer
equality. Interning is not an optimization you opt into — it's happening silently inside
your compiler's symbol table, your runtime's literal pool, and your language's
operator semantics. Here's what the machine actually does.
What interning is
A string is interned when the runtime guarantees that a given value exists in only one
place. Compare "abc" and "abc" in a world where every occurrence is a separate buffer:
equality requires scanning both buffers, byte by byte — and the scan is O(n) even when the
strings are unequal in the first byte, because you must check the first byte first. In an
interned world, both references point at the same object, so equality is a single pointer
comparison. Interning trades a one-time cost (hashing the string, storing a canonical copy)
for perpetual O(1) comparisons. The catch: you must know when the runtime interns, because
the same literal can behave both ways in one program.
Symbol tables: where compilers intern
Compilers were the first heavy interners. Every identifier — variable names, function
names, keywords — lands in a symbol table: a hash map from the name to a canonical
node. Every occurrence of counter in your source resolves to the same table entry, so
the compiler compares identifiers by pointer, not by string. This is why switch on
keywords and hash-based lookups are fast in compilers: the expensive strcmp happens once,
at insertion, and every subsequent comparison is an 8-byte pointer check. Your compiler's
entire name-resolution speed rests on this trick.
String pools in runtimes
Runtimes apply the same idea to program strings:
- Java: literals are interned into a pool on the heap (in modern JDKs, a
StringTable— a hash table of weak references).String.intern()manually interns any runtime-created string. - C#: literals are interned in the process-wide intern pool;
string.Intern()does the same by hand, and the CLR guarantees literals with identical values share one instance. - CPython: the interpreter interns small integers, but for strings it caches literals,
short strings, and identifiers in an internal dict, and reuses them for common patterns.
There is no public
intern()— interning happens by policy, invisibly. - Ruby/Elixir/Erlang: symbols and atoms are interned strings that live forever and are never garbage collected — the pool is a permanent memory commit.
The "abc" literal in your Java source is not the same object as the "abc" you read from
a file — the first is interned, the second is a fresh allocation with equal bytes.
Why == works on interned strings
This is where language semantics make interning observable. Equality operators fall into two camps:
// Java: == is REFERENCE equality. Works on interned strings, lies on file input.
String a = "abc", b = "abc"; // interned literals -> same object
a == b; // true, pointer comparison
String c = new String("abc"); // fresh heap copy
a == c; // false! Same bytes, different object# Python: == is VALUE equality, always. `is` is identity.
a = "abc"; b = "abc" # CPython may reuse the literal object
a == b # True (value comparison — always correct)
a is b # True here, but NOT guaranteed for dynamically built stringsC# is the hybrid: == on string is overloaded to value equality (safe), while
ReferenceEquals exposes identity — and Object.ReferenceEquals("abc", "abc") is true
because literals are interned. The danger pattern is Java: == on strings from a file
fails in production even though it worked in your REPL, purely because literals are
interned and file input isn't.
The memory accounting
Interning pays a permanent price for deduplication: an interned string is (almost) never
freed. Java's pool and C#'s intern pool keep their entries alive for the process lifetime;
Ruby symbols and Erlang atoms are immortal by design. That's fine for literals and
identifiers — bounded, repeated data — and catastrophic for user input. Interning every
ORDER_ID your API sees is a memory leak with a hash table's face on it.
Interning is why one == costs one pointer compare and another costs a scan, why your
compiler survives name-heavy code, and why "just use ==" is a correctness trap in Java.
Know your runtime's policy: it decides whether equality is instant or linear.