Question 9 · Section 12

Can You Use == to Compare Strings?

Technically — yes, but in 99% of cases it will be a mistake.

Language versions: English Russian Ukrainian

🟢 Junior Level

Technically — yes, but in 99% of cases it will be a mistake.

The == operator compares references (memory addresses), not string content.

Example of the problem:

String s1 = "Hello";
String s2 = new String("Hello");

System.out.println(s1 == s2);      // false — different objects!
System.out.println(s1.equals(s2)); // true — content is the same

Rule: For string content comparison always use equals(). The == operator — only for null checks.

When == will work:

String s1 = "Hello";
String s2 = "Hello";
System.out.println(s1 == s2); // true — both from String Pool, same object

But you can’t rely on this — data from DB or files will be in regular heap, not in the pool.


🟡 Middle Level

When == returns true

Scenario Code == Result
Two literals "A" == "A" true (String Pool)
Literal + new "A" == new String("A") false
new + new new String("A") == new String("A") false
Assignment s1 = s2; s1 == s2 true (same reference)
After intern() s1.intern() == s2.intern() true

Typical mistakes

  1. Mistake: if (str == "admin") — works only if "admin" is from pool and str is too Solution: if ("admin".equals(str)) — safe and with constant on the left (null-safe)

  2. Mistake: Comparing strings from DB via == Solution: Strings from ResultSet — always new objects. Only equals()

Practical rule

// BAD
if (status == "ACTIVE") { ... }

// GOOD
if ("ACTIVE".equals(status)) { ... }

// BETTER (null-safe)
if (Objects.equals(status, "ACTIVE")) { ... }

🔴 Senior Level

Internal Implementation

Operator ==:

  • JVM instruction: if_acmpeq (if address compare equal)
  • Compares reference values (32/64-bit addresses)
  • Time: O(1) — single pointer comparison

String.equals():

public boolean equals(Object anObject) {
    if (this == anObject) return true;       // Optimization: identity check
    if (anObject instanceof String another) {
        if (coder == another.coder) {         // Same encoding?
            return isLatin1()
                ? StringLatin1.equals(value, another.value)
                : StringUTF16.equals(value, another.value);
        }
    }
    return false;
}
  • Identity check (==) as fast path
  • Then: coder check, length check, character-by-character comparison
  • Time: O(n) — character-by-character iteration

Architectural Trade-offs

When == may be justified:

In ultra-low-latency systems (HFT, trading), where every nanosecond matters:

// All strings pre-interned
String eventType = parseEvent().intern();
if (eventType == ORDER_EVENT) { // O(1) instead of O(n)
    processOrder();
}
  • Profit: 1 CPU cycle vs n cycles (string length)
  • Risk: one non-interned string — and logic is broken

Why this is almost never worth the risk:

  • equals() has a fast path == — for identical objects returns instantly
  • JVM inlines equals() for short strings
  • CPU branch prediction works well with character-by-character comparison

Edge Cases

  1. Constant folding: "A" + "B" == "AB"true (compiler folds both into "AB")

  2. Runtime concatenation:
    String a = "A";
    String ab = a + "B";
    System.out.println(ab == "AB"); // false — runtime concatenation
    
  3. Interned strings:
    String s1 = new String("test").intern();
    String s2 = "test";
    System.out.println(s1 == s2); // true — both from pool
    
  4. String deduplication (G1 GC): Does NOT make == true! Deduplication combines byte[], but String objects remain different.

Performance

| Operation | Time | When to use | | ————————— | ————————– | ————————————– | | == | ~0.3ns (1 CPU cycle) | Only after guaranteed intern() | | equals() (identical) | ~0.3ns (fast path) | Always — fast path catches identity | | equals() (same content) | ~2-10ns (depends on length)| Always — correct choice |

equals() for short strings is optimized by JVM via SIMD instructions (comparing 8-16 bytes per clock cycle), so the difference from == is minimal.

Production Experience

Scenario: Protocol parser (10M messages/sec):

  • Developer used == to compare event types after intern()
  • After a month: a new library version returned non-interned strings
  • Bug: 0.01% of messages processed incorrectly → financial loss
  • Fix: equals() — overhead 5ns on 10M = 50ms/sec (acceptable)

Monitoring

// Assert that strings are interned (in tests)
assert s1 == s2 : "Strings should be interned";

// JMH for benchmarking
@Benchmark
public boolean testEquals() { return s1.equals(s2); }

@Benchmark
public boolean testIdentity() { return s1 == s2; }

Best Practices for Highload

  • Default: always equals() or Objects.equals()
  • null-check: "CONSTANT".equals(variable) or Objects.equals(a, b)
  • Low-latency edge case: == is acceptable in two cases: (1) null check, (2) guaranteed interned strings.
    • There are unit tests verifying this guarantee
    • Benchmark shows the difference is critical
  • For comparison with null: == is the correct choice

🎯 Interview Cheat Sheet

Must know:

  • == compares references (addresses), equals() — content
  • == returns true for literals ("A" == "A"), but false for new String("A")
  • equals() has a fast path: if == is true — returns immediately without character comparison
  • After intern() both strings are from pool — == returns true
  • String deduplication (G1 GC) does NOT make == true — objects are different, only byte[] is shared
  • In ultra-low-latency systems == is acceptable for guaranteed interned strings

Frequent follow-up questions:

  • When does == return true? — For literals, assignments (s1 = s2), after intern(), constant folding ("A"+"B" == "AB").
  • Why does equals() have a fast path ==? — If it’s the same object — no need to compare content. Saves time for identical references.
  • Can you use == after intern()? — Yes, but it’s risky: one non-interned string — and logic is broken. equals() is safer.
  • What happens with runtime concatenation == literal?false. String a = "A"; (a + "B") == "AB" — runtime concatenation creates a new object.

Red flags (DON’T say):

  • ❌ “== compares content” — compares only references
  • ❌ “equals() is always slower than ==” — for identical objects equals() = == via fast path
  • ❌ “String deduplication makes == true” — no, objects remain different
  • ❌ “== works after concatenation” — runtime concatenation creates a new object

Related topics:

  • [[10. Difference Between == and equals() for String]]
  • [[1. How String Pool Works]]
  • [[3. When to Use intern()]]