Hook: Java String Pool is one of those features that quietly saves memory every day, which is why interviewers love asking about it.
Question: Explain String Pool.
Answer: The String Pool is a special place where Java keeps one shared copy of each unique string literal and each string that you explicitly intern. If the same text appears again, Java can reuse the existing object instead of creating a duplicate. This helps save memory and makes repeated string access cheaper, but it also means you must understand the difference between content equality with equals() and reference equality with ==.
Interview-Ready Answer: In Java, the String Pool is a canonical storage area for unique string values. When I write a string literal like "hello", Java reuses the pooled instance if it already exists, instead of creating a new object. If I create a string with new String("hello"), that makes a separate heap object, but intern() can return the pooled reference. A useful detail is that since Java 7, the pool lives in the heap, not PermGen, so pooled strings can be garbage-collected when no longer referenced.
The String Pool is Java’s cache of unique string values. Think of it as a shared warehouse of labels: if the exact same text is needed again, Java points to the existing label instead of printing a new one. The official idea behind it is canonicalization, which means keeping one standard representative object for a value.
"cat", it checks the pool first."cat" is already there, Java reuses that same object reference.new String("cat"), Java creates a new object on the heap, even if "cat" already exists in the pool.intern(), Java looks up the pool version and returns that canonical reference, adding it if needed.In HotSpot JVMs, the pool is implemented with a hash table called the StringTable. That means lookup is usually fast, about O(1) on average, but collisions can make it slower in bad cases. The pool is not a magic infinite store; it is a managed structure, and its size can be tuned in HotSpot with -XX:StringTableSize.
OutOfMemoryError problems.Use the pool automatically through literals and compile-time constants. Use intern() only when you truly have many repeated values, such as IDs, keys, or protocol tokens coming from external input, and you want to reduce duplicate objects. Do not blindly intern everything: the hash lookup costs something, and overuse can make code harder to reason about.
| Way | Where object comes from | Reference reuse? | Typical use |
|---|---|---|---|
| Literal | Pool | Yes | Most common |
new String() | Heap | No | Rarely needed |
intern() | Pool | Yes | Canonical form |
| Compile-time concat | Pool | Yes | Constants |
| Runtime concat | Heap first | No | Dynamic text |
== compares references, not text. Two equal strings may still be different objects.equals() compares content, which is what you usually want.intern().Memory hook: “One text, one ticket.” If the text already has a ticket in the pool, Java reuses it instead of printing another one.
Performance-wise, the pool trades a small hash lookup for less memory duplication. In real systems, that is usually a good deal for repeated values like language codes, status names, or feature flags.
Imagine a checkout service in an e-commerce system that reads promo codes from HTTP requests and also keeps a few known codes in constants. A developer mistakenly writes if (promoCode == "SAVE10") instead of using equals(). Most of the time the test fails because request strings usually come from the network and are not the same reference as the literal, even when the text matches.
What goes wrong: customers type a valid code, but the service rejects it. Logs show confusing output such as the same text with different identity hash codes, support tickets rise, and the team may see higher load because the app keeps falling back to a slower validation path or hitting the database unnecessarily. The bug is subtle because it may seem to work in a few local tests where literals are compared to literals, then fail in production with real input.
The fix is simple but important: use equals() for content, and only think about the String Pool when you need sharing or canonicalization, not as a substitute for correct comparison.
import java.util.Objects;
public class StringPoolDemo {
private static void printComparison(String label, String a, String b) {
System.out.println(label);
System.out.println(" a = " + a + " | identity=" + System.identityHashCode(a));
System.out.println(" b = " + b + " | identity=" + System.identityHashCode(b));
System.out.println(" a == b : " + (a == b));
System.out.println(" a.equals(b) : " + a.equals(b));
System.out.println();
}
public static void main(String[] args) {
// Literal strings are pooled automatically.
String literal1 = "hello";
String literal2 = "hello";
printComparison("1) Two literals", literal1, literal2);
// new String(...) creates a separate heap object, even if the text already exists in the pool.
String heapString = new String("hello");
printComparison("2) Literal vs new String", literal1, heapString);
// intern() returns the canonical pooled reference.
String interned = heapString.intern();
printComparison("3) Literal vs interned", literal1, interned);
// Compile-time concatenation is folded by the compiler, so it is pooled.
String folded = "he" + "llo";
printComparison("4) Compile-time concatenation", literal1, folded);
// Runtime concatenation is built at runtime, so it is not the same reference until intern() is used.
String part1 = args.length > 0 ? args[0] : "he";
String part2 = args.length > 1 ? args[1] : "llo";
String runtimeConcat = part1 + part2;
printComparison("5) Runtime concatenation", literal1, runtimeConcat);
String runtimeInterned = runtimeConcat.intern();
printComparison("6) Runtime concat after intern()", literal1, runtimeInterned);
// Failure path: intern() cannot be called on null.
try {
String maybeNull = null;
String shouldFail = maybeNull.intern();
System.out.println("This line will never run: " + shouldFail);
} catch (NullPointerException ex) {
System.out.println("7) Edge case caught: calling intern() on null throws " + ex.getClass().getSimpleName());
}
// Small reminder: equals() is the right tool for content checks.
System.out.println();
System.out.println("Use Objects.equals for safe content comparison: " + Objects.equals(literal1, heapString));
}
}Follow-up & Tricky Questions:
intern() actually do? It returns the pooled canonical instance for that text. If the value is not already in the pool, the JVM adds it and returns the pooled reference.== dangerous with strings? Because it checks whether two variables point to the same object, not whether the text is the same. Two different string objects can contain identical characters and still fail ==.intern().StringBuilder related to the pool? Not directly. It helps build strings efficiently, but the resulting string from toString() is a normal heap object unless it is interned later.final String the same as an interned string? No. final only means the variable cannot be reassigned; it says nothing about pooling.new String("x"), are they ==? No, they are separate objects. They may have equal content, but reference equality is false unless they happen to be the exact same object."a" + "b" pooled? Yes, because it is a compile-time constant expression and the compiler folds it into one pooled literal. The runtime version with variables is different.intern() clone the string? No, it returns the canonical pooled reference, so the whole point is to share one object for that value.Common Mistakes:
== for string content. Correct it by using equals() or Objects.equals().new String("...") is normal or useful. It usually creates unnecessary duplicates and should be avoided unless you have a very specific reason.Memory Hook: The pool is a shared name tag drawer: if the name already exists, Java reuses the same tag instead of cutting a second one.
Cheat Sheet:
new String() makes a new heap object.intern() gives the canonical pooled reference.equals() for content, == only for reference checks.Practice Tasks:
new String(), and intern() using both == and equals().intern().Objects.equals() and explain why the behavior becomes correct.