AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

16 Memory and the JVM runtime / Lesson 4 of 6

The string pool, interning and the Integer cache

13 min · +10 XP · 2 exercises · 5 questions

Read the text13 min
Exercises0/2
Quiz5 q

Quick answer

What is the string pool in Java?

The string pool in Java is the JVM's table of interned String objects. Every string literal with the same characters refers to one pooled object, so "java" == "java" is true, while new String("java") always creates a separate object. intern() returns the pooled instance for any string's content. Since Java 7 the pool lives on the heap, so unused entries can be collected.

Two interview questions live here, and both are really questions about the heap. "Why is "java" == "java" true but new String("java") == "java" false?" — the string pool. "Why is Integer.valueOf(127) == Integer.valueOf(127) true but Integer.valueOf(128) == Integer.valueOf(128) false?" — the Integer cache. Both are the JVM sharing immutable objects behind your back, both are invisible until you compare with ==, and both reward knowing exactly where the line is drawn. This lesson also covers the growth rule of StringBuilder, because it is the same idea — memory the runtime manages for you, with behaviour you can predict.

The string pool

Every string literal in a class file is interned: when the class is loaded, "java" becomes one String object in the JVM's string table, and every literal with the same characters — in this class or any other — refers to that one object.

String a = "java";
String b = "java";
System.out.println(a == b);              // true — same pooled object
String c = new String("java");
System.out.println(a == c);              // false — `new` always makes a fresh object
System.out.println(a.equals(c));         // true — same characters
System.out.println(a == c.intern());     // true — intern() returns the pooled instance

intern() looks the string up in the pool, adds it if absent, and returns the pooled reference. It is how you get identity equality for strings that arrive at runtime — from a file, a socket, a Scanner — and it is mostly used to save memory when millions of strings have few distinct values (city names in a dataset). It is not a way to make == correct; equals is.

Compile-time constant folding extends the pool to expressions: "ja" + "va" is a constant expression, folded to "java" by javac, so ("ja" + "va") == "java" is true. But add a variable and the concatenation happens at run time, producing a new object:

String ja = "ja";
System.out.println((ja + "va") == "java");         // false — runtime concatenation
final String JA = "ja";
System.out.println((JA + "va") == "java");         // true — a final String with a constant initialiser is itself a constant

The pool lives in the heap (since Java 7; it was in PermGen before), so pooled strings are collectable when nothing refers to them — though literals stay reachable from their classes. Its size is a hash table tunable with -XX:StringTableSize.

Why the rule matters: == on strings is a bug

Because pooling makes == sometimes true, code that compares strings with == passes its tests with literals and fails in production with input from anywhere else. The rule has no exceptions: compare strings with equals (or equalsIgnoreCase, or compareTo). Use == only to test for null, or when you have deliberately interned both sides and can prove it.

The Integer cache

Integer.valueOf(int) — which autoboxing calls — returns a cached instance for values in -128..127 and a new object outside that range:

Integer p = 127, q = 127;   System.out.println(p == q);   // true — same cached object
Integer r = 128, s = 128;   System.out.println(r == s);   // false — two objects
System.out.println(r.equals(s));                          // true

The lower bound is fixed; the upper bound is guaranteed to be at least 127 and can be raised with -XX:AutoBoxCacheMax=…, so 128 giving false is the default HotSpot behaviour, not a language guarantee. Long, Short and Byte cache the same range; Character caches 0..127; Boolean has exactly two instances; Double and Float cache nothing. new Integer(127) (deprecated) bypasses the cache entirely.

The lesson is identical to the string one: compare boxed numbers with equals or unbox first. Integer in a List<Integer> or a Map<String, Integer> is exactly where == sneaks in — if (map.get(a) == map.get(b)) works for small counts and silently breaks past 127.

Autoboxing allocates

Every boxing outside the cache is an allocation. Integer sum = 0; for (…) sum += x; allocates a new Integer per iteration (sum + x is an int, boxed back into a new object). A Map<Integer, Integer> counting frequencies boxes on every merge. It is rarely the bottleneck at interview scale and always the first thing to fix in a real hot loop: int/long locals, int[] arrays, IntStream instead of Stream<Integer>.

StringBuilder growth

A StringBuilder is a resizable byte[] plus a count. Its default capacity is 16 (or 16 + s.length() when built from a string). When an append needs more room than the current capacity, the array is replaced by one of capacity max(needed, 2 × old + 2) — the +2 is a historical quirk from the days when the minimum growth had to accommodate a two-character append. So 16 → 34 → 70 → 142 → 286 → …: a doubling that makes repeated appends O(1) amortised, the same argument as ArrayList.

StringBuilder sb = new StringBuilder();      // capacity 16
sb.append("0123456789ABCDEFG");              // 17 chars: needs 17, 2×16+2 = 34 → capacity 34
sb.append("x".repeat(40));                   // 57 chars: needs 57, 2×34+2 = 70 → capacity 70

sb.capacity() reports it; new StringBuilder(n) pre-sizes when you know the final length, avoiding every copy. s += x in a loop, by contrast, allocates a new String of the full length each time — O(n²) characters copied for n appends — which is why "use StringBuilder in loops" is on every Java checklist. The compiler does turn a single expression a + b + c into one efficient concatenation; it is the loop it cannot fix.

Hash codes are cached too

String.hashCode() is computed once and stored in the object (strings are immutable, so it never changes) — which is part of why strings make excellent map keys, and why a mutable object as a key is a disaster: its hash code is cached in the map, in the bucket index, and a mutation leaves it in the wrong bucket forever.

Interview angle

  • ""a" == "a"?" True: literals are interned into the pool. new String("a") == "a" is false; .intern() restores identity.
  • "What does intern() do?" Returns the pooled instance for that content, adding it if absent.
  • "Integer a = 127, b = 127; a == b? And 128?" True for 127 (cached −128..127); false for 128 by default — always equals.
  • "Why StringBuilder in a loop?" Concatenation copies the whole string each time (O(n²)); the builder grows geometrically and copies once per doubling.
  • "Is "ja" + "va" == "java"?" True — constant folding at compile time. With a non-final variable it is false.

Key takeaways

  • Literals and compile-time constants are pooled; new String and runtime concatenation are not; intern() fetches the pooled one.
  • == on strings is a bug by construction; equals always.
  • Integer.valueOf caches −128..127 (upper bound tunable); Boolean two instances; Double none. Compare boxed values with equals.
  • Boxing allocates; go primitive in hot code.
  • StringBuilder grows to max(needed, 2·old + 2); pre-size it when you know the length; never += a string in a loop.

Common questions

Why is new String("java") == "java" false?

new always creates a fresh object, so == compares two different references and returns false. equals compares the characters and returns true, and new String("java").intern() == "java" is true because intern() returns the pooled literal.

Why does Integer == Integer return false for 128?

Integer.valueOf, which autoboxing calls, returns cached instances for -128 to 127 and a new object outside that range. Two boxed 127s are therefore the same object and two boxed 128s are not, by default. Compare boxed numbers with equals, or unbox them first.

What does String intern() do in Java?

intern() looks the string's content up in the string pool, adds it if absent and returns the pooled reference. It gives identity for strings read at run time and saves memory when millions of strings share a few values; it is never a replacement for equals.

Is "ja" + "va" == "java" true in Java?

Yes. Two literals form a compile-time constant expression, which javac folds into the pooled "java". With a non-final variable in the expression the concatenation happens at run time and creates a new object, so == is false.

Why use StringBuilder instead of string concatenation in a loop?

s += x in a loop builds a new String of the full length on every pass, copying O(n²) characters for n appends. A StringBuilder grows its buffer geometrically, to max(needed, 2 × old + 2), so each append is amortised O(1); pre-size it when the final length is known.

Mark as readSign in to keep your place.

Java String Pool, intern() and the Integer Cache Explained

The Java string pool shares one object per string literal and the Integer cache covers -128 to 127, which is why == sometimes works. How intern() works.

  • Course: Java study plan
  • Module: Memory and the JVM runtime
  • Kind: Lesson
  • Reading time: 13 min
  • Runtime: OpenJDK 18

What is the string pool in Java?

The string pool in Java is the JVM's table of interned String objects. Every string literal with the same characters refers to one pooled object, so "java" == "java" is true, while new String("java") always creates a separate object. intern() returns the pooled instance for any string's content. Since Java 7 the pool lives on the heap, so unused entries can be collected.

Lesson

Two interview questions live here, and both are really questions about the heap. "Why is "java" == "java" true but new String("java") == "java" false?" — the string pool. "Why is Integer.valueOf(127) == Integer.valueOf(127) true but Integer.valueOf(128) == Integer.valueOf(128) false?" — the Integer cache. Both are the JVM sharing immutable objects behind your back, both are invisible until you compare with ==, and both reward knowing exactly where the line is drawn. This lesson also covers the growth rule of StringBuilder, because it is the same idea — memory the runtime manages for you, with behaviour you can predict.

The string pool

Every string literal in a class file is interned: when the class is loaded, "java" becomes one String object in the JVM's string table, and every literal with the same characters — in this class or any other — refers to that one object.

String a = "java";
String b = "java";
System.out.println(a == b);              // true — same pooled object
String c = new String("java");
System.out.println(a == c);              // false — `new` always makes a fresh object
System.out.println(a.equals(c));         // true — same characters
System.out.println(a == c.intern());     // true — intern() returns the pooled instance

intern() looks the string up in the pool, adds it if absent, and returns the pooled reference. It is how you get identity equality for strings that arrive at runtime — from a file, a socket, a Scanner — and it is mostly used to save memory when millions of strings have few distinct values (city names in a dataset). It is not a way to make == correct; equals is.

Compile-time constant folding extends the pool to expressions: "ja" + "va" is a constant expression, folded to "java" by javac, so ("ja" + "va") == "java" is true. But add a variable and the concatenation happens at run time, producing a new object:

String ja = "ja";
System.out.println((ja + "va") == "java");         // false — runtime concatenation
final String JA = "ja";
System.out.println((JA + "va") == "java");         // true — a final String with a constant initialiser is itself a constant

The pool lives in the heap (since Java 7; it was in PermGen before), so pooled strings are collectable when nothing refers to them — though literals stay reachable from their classes. Its size is a hash table tunable with -XX:StringTableSize.

Why the rule matters: == on strings is a bug

Because pooling makes == sometimes true, code that compares strings with == passes its tests with literals and fails in production with input from anywhere else. The rule has no exceptions: compare strings with equals (or equalsIgnoreCase, or compareTo). Use == only to test for null, or when you have deliberately interned both sides and can prove it.

The Integer cache

Integer.valueOf(int) — which autoboxing calls — returns a cached instance for values in -128..127 and a new object outside that range:

Integer p = 127, q = 127;   System.out.println(p == q);   // true — same cached object
Integer r = 128, s = 128;   System.out.println(r == s);   // false — two objects
System.out.println(r.equals(s));                          // true

The lower bound is fixed; the upper bound is guaranteed to be at least 127 and can be raised with -XX:AutoBoxCacheMax=…, so 128 giving false is the default HotSpot behaviour, not a language guarantee. Long, Short and Byte cache the same range; Character caches 0..127; Boolean has exactly two instances; Double and Float cache nothing. new Integer(127) (deprecated) bypasses the cache entirely.

The lesson is identical to the string one: compare boxed numbers with equals or unbox first. Integer in a List<Integer> or a Map<String, Integer> is exactly where == sneaks in — if (map.get(a) == map.get(b)) works for small counts and silently breaks past 127.

Autoboxing allocates

Every boxing outside the cache is an allocation. Integer sum = 0; for (…) sum += x; allocates a new Integer per iteration (sum + x is an int, boxed back into a new object). A Map<Integer, Integer> counting frequencies boxes on every merge. It is rarely the bottleneck at interview scale and always the first thing to fix in a real hot loop: int/long locals, int[] arrays, IntStream instead of Stream<Integer>.

StringBuilder growth

A StringBuilder is a resizable byte[] plus a count. Its default capacity is 16 (or 16 + s.length() when built from a string). When an append needs more room than the current capacity, the array is replaced by one of capacity max(needed, 2 × old + 2) — the +2 is a historical quirk from the days when the minimum growth had to accommodate a two-character append. So 16 → 34 → 70 → 142 → 286 → …: a doubling that makes repeated appends O(1) amortised, the same argument as ArrayList.

StringBuilder sb = new StringBuilder();      // capacity 16
sb.append("0123456789ABCDEFG");              // 17 chars: needs 17, 2×16+2 = 34 → capacity 34
sb.append("x".repeat(40));                   // 57 chars: needs 57, 2×34+2 = 70 → capacity 70

sb.capacity() reports it; new StringBuilder(n) pre-sizes when you know the final length, avoiding every copy. s += x in a loop, by contrast, allocates a new String of the full length each time — O(n²) characters copied for n appends — which is why "use StringBuilder in loops" is on every Java checklist. The compiler does turn a single expression a + b + c into one efficient concatenation; it is the loop it cannot fix.

Hash codes are cached too

String.hashCode() is computed once and stored in the object (strings are immutable, so it never changes) — which is part of why strings make excellent map keys, and why a mutable object as a key is a disaster: its hash code is cached in the map, in the bucket index, and a mutation leaves it in the wrong bucket forever.

Interview angle

  • ""a" == "a"?" True: literals are interned into the pool. new String("a") == "a" is false; .intern() restores identity.
  • "What does intern() do?" Returns the pooled instance for that content, adding it if absent.
  • "Integer a = 127, b = 127; a == b? And 128?" True for 127 (cached −128..127); false for 128 by default — always equals.
  • "Why StringBuilder in a loop?" Concatenation copies the whole string each time (O(n²)); the builder grows geometrically and copies once per doubling.
  • "Is "ja" + "va" == "java"?" True — constant folding at compile time. With a non-final variable it is false.

Key takeaways

  • Literals and compile-time constants are pooled; new String and runtime concatenation are not; intern() fetches the pooled one.
  • == on strings is a bug by construction; equals always.
  • Integer.valueOf caches −128..127 (upper bound tunable); Boolean two instances; Double none. Compare boxed values with equals.
  • Boxing allocates; go primitive in hot code.
  • StringBuilder grows to max(needed, 2·old + 2); pre-size it when you know the length; never += a string in a loop.

Common questions

Why is new String("java") == "java" false?

new always creates a fresh object, so == compares two different references and returns false. equals compares the characters and returns true, and new String("java").intern() == "java" is true because intern() returns the pooled literal.

Why does Integer == Integer return false for 128?

Integer.valueOf, which autoboxing calls, returns cached instances for -128 to 127 and a new object outside that range. Two boxed 127s are therefore the same object and two boxed 128s are not, by default. Compare boxed numbers with equals, or unbox them first.

What does String intern() do in Java?

intern() looks the string's content up in the string pool, adds it if absent and returns the pooled reference. It gives identity for strings read at run time and saves memory when millions of strings share a few values; it is never a replacement for equals.

Is "ja" + "va" == "java" true in Java?

Yes. Two literals form a compile-time constant expression, which javac folds into the pooled "java". With a non-final variable in the expression the concatenation happens at run time and creates a new object, so == is false.

Why use StringBuilder instead of string concatenation in a loop?

s += x in a loop builds a new String of the full length on every pass, copying O(n²) characters for n appends. A StringBuilder grows its buffer geometrically, to max(needed, 2 × old + 2), so each append is amortised O(1); pre-size it when the final length is known.

Exercises

Same or equal?

Read an integer n and n words. For each word w print <w>: equals=<w.equals("java")> same=<w == "java"> interned=<w.intern() == "java">. Then read an integer m and m integers; for each x print <x>: boxedSame=<Integer.valueOf(x) == Integer.valueOf(x)>. Predict every line before you run it: a token read from input is never the pooled literal, and the Integer cache ends at 127.

Example: 2 then java Java, 2 then 127 128 →

java: equals=true same=false interned=true
Java: equals=false same=false interned=false
127: boxedSame=true
128: boxedSame=false

Watch the builder grow

Read an integer n and n words. Append each to one StringBuilder created with the default constructor, and after every append print len=<length()> cap=<capacity()>. Finally print reallocations=<how many times the capacity changed>. Predict the capacities first with the rule max(needed, 2 × old + 2) starting from 16, then let the JDK confirm it.

Example: 3 then abcdefghij klmnopqrst u →

len=10 cap=16
len=20 cap=34
len=21 cap=34
reallocations=1

In this module: Memory and the JVM runtime

← Garbage collection — generations, pauses and the collectors · Class loading, initialisation order and the JIT →