Course contents

== vs equals() for strings

Reference equality vs content equality, the string pool, intern() and compile-time constant folding: why == sometimes works and why you still shouldn't use it.

FreeLesson10 min readWarm-upReviewed Oct 2026 · Java 21
On this page
  1. Picture it first: two kinds of "same"
  2. How it works under the hood
  3. Real world: comparing strings safely
  4. Advanced: intern(), ordering and the edge cases
  5. How to say it in the interview

⚖️ What's the difference between == and equals() for strings?

== asks "are these two references pointing at the same object?" equals() asks "do these two Strings contain the same characters?" Two Strings can both hold "java" and still be different objects, so == can say false while equals() says true.

This is probably the most-asked Java fundamentals question of all time, and that's precisely why it's dangerous: everyone knows the one-line answer. What separates candidates is the follow-up. Why does "a" + "b" == "ab" print true? How many objects does new String("x") create? When is == on strings actually correct?

What you’ll learn

  • Why == compares references and equals() compares characters
  • How string literals are interned in the string pool, and what intern() returns
  • Compile-time constant folding: why "a" + "b" == "ab" is true but not with a non-final variable
  • How many objects new String("x") really creates
  • Null-safe idioms: "literal".equals(var), Objects.equals, and how switch on String works

For this lesson, we'll focus on reference vs content equality, the string pool, intern(), compile-time constant folding, and the null-safe idioms you should use in real code.

Below the line (out of scope)

  • The String API itself (split, strip, StringBuilder): see Essential String methods
  • The general equals/hashCode contract for your own classes: covered in Core Java & OOP
  • Locale-aware comparison with Collator beyond a brief mention

The 30-second answer

== compares references and equals() compares content, so always use equals() for strings. String literals are interned in the string pool, so two identical literals are the same object and == happens to work, which hides the bug. Concatenations of compile-time constants ("a" + "b", or final variables) are folded by javac into one pooled literal; anything involving a non-final variable is built at runtime and is a new object. new String("x") always creates a new object on top of the pooled literal. In real code I write "literal".equals(var) or Objects.equals(a, b) to stay null-safe.

01Picture it first: two kinds of "same"

Think of two library copies of the same book. They have identical text (equals is true) but they're different physical objects with different barcodes (== is false). Now imagine the library keeps one reference copy of every title on a special shelf, and whenever a literal appears in your code, Java hands you that shelf copy. That shelf is the string pool.

Fig. 1

Variables a, b and d point at the same interned java object in the string pool, while c points at a separate heap object that shares the same byte array

Variables a, b and d point at the same interned java object in the string pool, while c points at a separate heap object that shares the same byte array

Notice three things.

  • Literals are shared. a and b were both written as "java", so they point at the same pooled object. a == b is true, but only by coincidence of how they were created.
  • new always means new. c = new String(a) creates a separate String object, so a == c is false even though the content matches.
  • intern() returns the canonical copy. c.intern() looks up "java" in the pool and returns the pooled object, so d == a is true.
Note

Since Java 7 the pool (HotSpot calls it the StringTable) lives on the ordinary heap, not in PermGen. It's a resizable hash table that starts at 65,536 buckets, and interned Strings that are no longer referenced can be garbage collected. You can inspect it with jcmd <pid> VM.stringtable.

02How it works under the hood

Try it yourself first

Before reading on, write down true or false for each of the six lines.

String a = "java";
String b = "java";
String c = new String("java");
String d = c.intern();
String e = "ja";
String f = e + "va";

System.out.println(a == b);        // true   both are the pooled literal
System.out.println(a == c);        // false  c is a new heap object
System.out.println(a.equals(c));   // true   same characters
System.out.println(a == d);        // true   intern() returns the pooled copy
System.out.println(a == f);        // false  built at runtime: new object
System.out.println(a == f.intern());  // true

Where literals come from

Every string literal in a class goes into that class file's constant pool. When the bytecode instruction ldc "java" runs for the first time, the JVM resolves it: it looks up "java" in the StringTable, adds it if it's missing, and from then on that instruction always yields the same object. The JLS guarantees that identical literals, even in different classes and packages, refer to the same String instance.

How many objects does new String("x") create?

The classic trick question. The honest answer is one or two String objects:

  1. The literal "x" is a pooled String. It's created when the literal is first resolved, unless "x" is already in the pool (from this class or any other), in which case nothing new is created.
  2. new String(...) always allocates one new String object on the heap.

Here's the detail that makes you sound like you've read the source: since compact strings, the String(String) constructor copies the value reference, not the bytes, so both objects share one byte[]. That's why the diagram shows a single array. And there's almost never a reason to write new String("x") in the first place.

Compile-time constant folding

Try it yourself first

Before reading on, predict all three results. The only difference is one keyword.

String ab = "ab";

System.out.println(("a" + "b") == ab);   // true

final String x = "a";
System.out.println((x + "b") == ab);     // true

String y = "a";
System.out.println((y + "b") == ab);     // false

The JLS defines a constant expression: literals, and final variables of primitive or String type initialised with a constant expression (a constant variable). javac evaluates constant expressions at compile time, and a String-valued constant expression is always interned. So both "a" + "b" and x + "b" turn into the literal "ab" in the class file.

y is not final, so y + "b" is not a constant expression. It compiles to an invokedynamic call that builds a new String at runtime.

Fig. 2

When both operands are compile-time constants javac folds them into the pooled literal and == is true; with a non-final variable the concatenation happens at runtime and creates a new object

When both operands are compile-time constants javac folds them into the pooled literal and == is true; with a non-final variable the concatenation happens at runtime and creates a new object

You can see it yourself with javap -c: the first two compile to ldc "ab", the third to invokedynamic makeConcatWithConstants.

Where candidates slip

Many candidates say "final makes == work". It doesn't, in general. final String x = readName(); is final but not a constant variable, because its initializer isn't a constant expression. x + "b" is then built at runtime and == is false. The rule is about compile-time constants, not the keyword.

03Real world: comparing strings safely

In production code the question isn't "does == work here", it's "what happens when one side is null".

String status = request.getParameter("status");   // may be null

status.equals("ACTIVE");               // NullPointerException if status is null
"ACTIVE".equals(status);               // false if null: literal-first idiom
Objects.equals(status, expected);      // null-safe for two variables (Java 7)
status.equalsIgnoreCase("active");     // NPE again if status is null
"active".equalsIgnoreCase(status);     // safe

// Comparing against a StringBuilder or other CharSequence:
"abc".equals(new StringBuilder("abc"));          // false: not a String
"abc".contentEquals(new StringBuilder("abc"));   // true

Objects.equals(a, b) is (a == b) || (a != null && a.equals(b)). Notice the == fast path: String.equals does the same thing internally, then checks the length and coder before comparing bytes. So equals() on two references to the same object is just as fast as ==.

In the real world

The == bug survives code review because it passes tests. Unit tests compare against literals, which are pooled, so status == "ACTIVE" returns true. In production the value comes from JSON, a database driver or request.getParameter, each of which builds a new String, and the comparison silently returns false. The symptom is a branch that "never runs" in production. Static analysis (StringEquality in Error Prone, ES_COMPARING_STRINGS_WITH_EQ in SpotBugs) catches this, and it's worth enabling.

switch on String (Java 7) is safe to use: javac compiles it to a switch on hashCode() followed by an equals() check, so it compares content. A null selector throws NullPointerException in a classic switch. Java 21's pattern-matching switch lets you add an explicit case null.

switch (command) {          // NPE if command is null (unless you add case null, Java 21)
    case "start" -> start();
    case "stop"  -> stop();
    default      -> unknown(command);
}

04Advanced: intern(), ordering and the edge cases

Should I call intern() to save memory? Rarely. It can help when you load millions of Strings with few distinct values, like country codes parsed from a large file, and you keep them all. The cost is a hash-table lookup on every call, and the pool is a global, native-backed structure. In most cases a plain HashMap<String, String> used as your own canonicalising cache, or G1's -XX:+UseStringDeduplication, is the better tool. Note that string deduplication shares the byte[], not the String objects, so it never changes the result of ==.

Performance angle

"x".equals(y) is O(1) when the references are identical or the lengths differ, and O(n) only when it has to compare bytes. Comparing two 1 KB strings that differ only in the last byte compares all 1,024 bytes (vectorised by a JIT intrinsic). Don't reach for == for speed: you'd be trading a few nanoseconds for a correctness bug.

compareTo for ordering. equals answers "same or not"; compareTo answers "which comes first". It compares UTF-16 code units lexicographically and returns a negative number, zero, or a positive number. a.compareTo(b) == 0 exactly when a.equals(b). For case-insensitive ordering use String.CASE_INSENSITIVE_ORDER or compareToIgnoreCase.

equalsIgnoreCase limits. It compares character by character after case mapping, so it can't handle mappings that change length. "Straße".equalsIgnoreCase("STRASSE") is false, even though "Straße".toUpperCase() is "STRASSE". For human text across languages, normalise and use a Collator.

Expression Result Why
"ab" == "ab" true same pooled literal
"a" + "b" == "ab" true folded at compile time
final constant + "b" == "ab" true constant variable, still folded
non-final var + "b" == "ab" false built at runtime
new String("ab") == "ab" false new always allocates
new String("ab").intern() == "ab" true intern returns the pooled copy

05How to say it in the interview

When I ask this, I'm listening for whether you can explain why == sometimes works. If you can connect it to the string pool, then to compile-time constant folding, and finish with how you'd write it safely in production, you've covered everything I want to hear. At this point, a good candidate often asks whether the strings come from literals or from I/O, because that's what decides whether the bug shows up.

In the interview

"== compares references and equals compares characters, so for strings I always use equals. The reason == sometimes looks right is the string pool: identical literals are interned, so they're the same object. Concatenating compile-time constants is folded by javac into one pooled literal, but anything involving a non-final variable is built at runtime and is a new object. new String always allocates, and intern returns the pooled copy. In real code I write the literal first, or use Objects.equals, so a null doesn't throw."

On the whiteboard

==       same object?        equals()  same chars?
literals → string pool (StringTable, on heap since 7)
"a"+"b" == "ab"         true   (constant folding → ldc)
final x="a"; x+"b"      true   (constant variable)
y="a";      y+"b"       false  (indy concat → new String)
new String("x") → 1 new object (+ pooled literal if absent)
intern() → canonical pooled instance
null-safe: "LIT".equals(v)   Objects.equals(a, b)
switch(String) = hashCode + equals, null → NPE
Practise this topicStrings basics: interview questions & scenarios
Further reading (6)