Can You Use == to Compare Strings
Technically, the == operator can be applied to String objects (the code compiles and executes without compilation errors), but for comparing the textual contents of strings, it...
🟢 Junior Level
Technically, the == operator can be applied to String objects (the code compiles and executes without compilation errors), but for comparing the textual contents of strings, it must never be used. In 99.9% of application scenarios, doing so is a severe logical bug.
The Essence in 30 Seconds
- The
==Operator Compares Memory References (Addresses): It evaluates totrueonly if both variables point to the exact same physical object on the Java Heap. - The
.equals()Method Compares Character Sequences (Values): It verifies character-by-character equivalence regardless of where, how, or when the objects were allocated.
The only idiomatic and safe use case for == with strings is testing for null references: if (str == null) or if (str != null).
// 1. Two separate objects containing identical characters:
String s1 = new String("Java");
String s2 = new String("Java");
System.out.println(s1 == s2); // false (different memory addresses on Heap)
System.out.println(s1.equals(s2)); // true (identical character content)
// 2. The String Pool illusion:
String s3 = "Java";
String s4 = "Java";
System.out.println(s3 == s4); // true (both references point to the same pooled literal)
// WARNING: External data (from databases, files, or network calls) does not enter the pool automatically!
🟡 Middle Level
The String Pool Trap & The Illusion of Correctness
The danger of == lies in the fact that simple unit tests with hardcoded string literals return true, creating a false sense of security:
// Passes in local unit tests:
String status = "ACTIVE";
if (status == "ACTIVE") { // true! Both point to the canonical String Pool literal
System.out.println("Status OK");
}
However, as soon as the string originates from an external source (e.g., Jackson JSON deserialization, HTTP request headers, JDBC ResultSet, or file I/O), a new object is allocated on the Heap. The comparison status == "ACTIVE" evaluates to false, causing critical production bugs.
String Comparison Truth Matrix
| Scenario | Expression | Result == |
Result .equals() |
Technical Explanation |
|---|---|---|---|---|
| Literals | "test" == "test" |
true |
true |
Both references resolve to the same canonical String Pool entry. |
Literal vs. new |
"test" == new String("test") |
false |
true |
The new keyword allocates an unpooled object on the Regular Heap. |
Two new Instances |
new String("A") == new String("A") |
false |
true |
Two independent Heap objects with distinct memory addresses. |
| Constant Folding | "AB" == ("A" + "B") |
true |
true |
The javac compiler merges constant literals at build time (JLS §15.28). |
| Dynamic Concatenation | String a = "A"; "AB" == (a + "B") |
false |
true |
Concatenation with non-final variables occurs at runtime via invokedynamic. |
After .intern() |
s1.intern() == s2.intern() |
true |
true |
The intern() method returns the canonical reference from the String Pool. |
Safe String Comparison Patterns
- Yoda Condition (Literal on the Left):
// Null-safe: returns false without throwing NullPointerException if userInput is null if ("ADMIN".equals(userInput)) { grantAccess(); } - Standard Utility
Objects.equals():// Handles all null combinations safely (both null, either null, or neither null) if (Objects.equals(str1, str2)) { process(); }
🔴 Senior Level
HotSpot String.equals() Implementation & Fast-Path
In HotSpot JVM, String.equals() is designed with multiple short-circuiting exit paths:
public boolean equals(Object anObject) {
// 1. Fast Path: Identity check
if (this == anObject) {
return true;
}
// 2. Type validation & coder check
return (anObject instanceof String aString)
&& (!COMPACT_STRINGS || this.coder == aString.coder)
&& StringLatin1.equals(value, aString.value); // or StringUTF16
}
this == anObjectIdentity Fast-Path:equals()uses==as its very first step. If two references point to the same physical object (or identical pooled literals),equals()resolves in a single CPU cycle ($O(1)$).- Encoding Check (
coder): If one string isLATIN1(0) and the other isUTF16(1), they cannot be equal; array traversal is skipped entirely. - SIMD Vectorized Intrinsics: The HotSpot C2 compiler replaces
StringLatin1.equalswith a hardware intrinsic. On modern x86/ARM processors, byte arrays are compared in 32-byte (AVX-2) or 64-byte (AVX-512) chunks per CPU cycle. The execution latency difference between==and.equals()for typical strings is negligible (sub-nanosecond).
Bytecode Analysis: if_acmpeq vs. invokevirtual
- The
==operator on references compiles to theif_acmpeq(branch if reference compare equal) orif_acmpneopcode, comparing 32-bit (under-XX:+UseCompressedOops) or 64-bit pointers directly in CPU registers. - The
str == nullcheck compiles toifnull/ifnonnull, comparing against the zero pointer (0x0). - The
equals()method compiles toinvokevirtual, which the C2 compiler aggressively inlines into the calling method body.
Why intern() + == is an Anti-Pattern (Even in Low-Latency Systems)
An outdated high-frequency trading (HFT) pattern recommended calling str.intern() on all incoming strings to enable $O(1)$ reference comparisons via ==.
Why This Pattern Has Been Abandoned:
- Extreme Architectural Fragility: If a single developer forgets to call
.intern()on an incoming string, the==check returnsfalsewithout throwing an exception, introducing silent business-logic bugs. StringTableContention: Global JVMStringTablelookups introduce native locking/synchronization overhead and inflate GC safepoint pause times.- Modern Alternatives: Constrained sets of values should be represented as Java
Enumtypes, numeric identifiers, or off-heap byte buffers (e.g., AgronaDirectBuffer).
The G1 GC String Deduplication Trap
When running with G1 GC and -XX:+UseStringDeduplication, the garbage collector unifies the internal byte[] value arrays of identical strings:
flowchart LR
subgraph JavaHeap["Java Heap Memory"]
StrA["String A Object (0x1000)\nvalue pointer ───────┐"]
StrB["String B Object (0x2000)\nvalue pointer ───────┤"]
SharedArr["Shared byte[] value {'H','e','l','l','o'}\n(0x5000)"]
end
StrA --> SharedArr
StrB --> SharedArr
Senior Trap: Deduplication unifies only the backing byte array, while the String object headers remain separate instances at distinct heap addresses:
\(\text{s1} == \text{s2} \implies \text{false}\)
\(\text{s1.equals(s2)} \implies \text{true}\)
4 Tricky Interview Questions
1. Why does a unit test containing status == "SUCCESS" pass in JUnit, but fail catastrophically in production?
Answer:
In the unit test, status was initialized with a hardcoded compile-time string literal ("SUCCESS"). Both operands referenced the identical canonical instance from the String Pool, causing == to evaluate to true.
In production, status is populated dynamically from external I/O (e.g., deserialized from an incoming HTTP JSON payload via Jackson or read from a database via JDBC). In these scenarios, the JVM allocates a new, unpooled String instance on the Heap. Because its heap address differs from the pooled literal, == evaluates to false.
2. What is the output of ("Java " + 21) == ("Java " + 21), and what happens if 21 is extracted into int ver = 21?
Answer:
- The expression
("Java " + 21) == ("Java " + 21)evaluates totrue. Both operands consist exclusively of compile-time constants (the literal"Java "and the literal21). Under JLS §15.28, thejavaccompiler applies Constant Folding, replacing both sides with a single pooled literal"Java 21"(ldc #X). - If written as
int ver = 21; ("Java " + ver) == ("Java " + ver);, the result isfalse. Becauseveris a non-final variable, concatenation occurs dynamically at runtime viainvokedynamic, allocating two separateStringobjects on the heap.
3. Does the JVM flag -XX:+UseStringDeduplication guarantee that duplicate strings will evaluate to true when compared with ==?
Answer:
No, it never makes them equal under ==.
G1 String Deduplication operates exclusively on the internal byte[] value array. The garbage collector modifies the internal value pointers of distinct String objects so they share a single array in memory. The String object wrappers themselves (their Mark Word, Klass Word, and heap addresses) remain distinct objects. Comparing them via == compares the object references, which remain different (false).
4. Why is str == null preferred over Objects.isNull(str) in performance-critical code?
Answer:
At the bytecode level, str == null translates directly into a single processor instruction (ifnull / ifnonnull) that performs an immediate zero-pointer comparison in a CPU register.
Objects.isNull(str) generates an invokestatic bytecode instruction. While HotSpot C2 will eventually inline this method once warm, during cold execution (interpreter and C1 tiers) it incurs method dispatch overhead. Therefore, str == null remains the idiomatic and most performant standard in core Java libraries.
🎯 Interview Cheat Sheet
30-Second Summary
”
==must never be used to compare string text because it compares memory references (heap addresses), not character contents. Strings with identical text can exist at different heap addresses. For value comparisons, always use.equals()orObjects.equals(). The==operator returnstrueonly when comparing identical references (such as String Pool literals). The sole valid use case for==with strings is testing fornull(str == null).”
Key Concepts Checklist
- Reference vs. Value:
==compares memory pointers (if_acmpeq);.equals()compares character contents. - Fast-Path Optimization:
.equals()begins withif (this == anObject) return true;($O(1)$ on identical references). - SIMD Vectorization: Modern C2 compiles
.equals()into hardware vector intrinsics (AVX-512) comparing up to 64 bytes per clock cycle. - String Deduplication Trap: G1 deduplication unifies backing
byte[]arrays, but leavesStringobject references distinct (==is stillfalse). - Null Safety: Use
"CONSTANT".equals(var)(Yoda condition) orObjects.equals(a, b)to preventNullPointerException.
Common Pitfalls & Red Flags
- ❌ “Short strings are compared by value with
==, while long strings use.equals().” (False:==always compares memory addresses). - ❌ “String Deduplication makes duplicate strings equal under
==.” (False: it only shares internal byte arrays). - ❌ “Using
.intern()and==is recommended for high performance.” (Dangerous anti-pattern: causes severe fragility andStringTablepollution).
Related Topics
- Difference Between == and equals() for String — Detailed comparison
- How String Pool Works — String pool mechanics
- When to Use intern() — Manual pool registration
- What is String Deduplication in G1 GC — G1 GC optimization