Difference Between == and equals() for String
In Java, the == operator and the .equals() method serve two fundamentally different purposes when working with String objects:
🟢 Junior Level
In Java, the == operator and the .equals() method serve two fundamentally different purposes when working with String objects:
- The
==Operator (Reference Identity): Compares memory addresses (heap pointers). It evaluates totrueonly when both variables reference the exact same physical object instance on the Java Heap. - The
.equals()Method (Logical Equivalence): Compares the textual content (character sequences) of two strings. It evaluates totrueif both strings contain an identical sequence of characters, regardless of where or when the objects were allocated.
The Essence in 30 Seconds
To check whether two strings contain identical text, always use .equals() (or Objects.equals()). Using == with strings in application logic is an anti-pattern and is reserved almost exclusively for testing against null (str == null).
String s1 = "Hello"; // Stored in String Pool
String s2 = "Hello"; // References the same String Pool instance
String s3 = new String("Hello"); // Brand new object allocated on Heap outside pool
System.out.println(s1 == s2); // true (identical memory address in pool)
System.out.println(s1 == s3); // false (different heap addresses!)
System.out.println(s1.equals(s3)); // true (identical character sequence)
🟡 Middle Level
The 5-Step Evaluation Pipeline of String.equals()
In modern Java (Java 9+, HotSpot JVM), String.equals() executes a carefully staged 5-step verification pipeline:
public boolean equals(Object anObject) {
// Step 1: Fast-Path Identity Check
if (this == anObject) {
return true;
}
// Step 2: Class Type Check
return (anObject instanceof String aString)
// Step 3: Encoding Coder Verification (Latin-1 vs UTF-16)
&& (!COMPACT_STRINGS || this.coder == aString.coder)
// Steps 4 & 5: Length Check & SIMD Vectorized Byte Array Comparison
&& StringLatin1.equals(value, aString.value);
}
this == anObject(Fast Path): If both references share the same memory pointer (e.g., identical pooled literals), the method returnstrueimmediately in $O(1)$ time, skipping character array traversal.instanceof String: Safely checks object type. If the argument isnullor a different class, it returnsfalsewithout throwing exceptions.this.coder == aString.coder: Under Compact Strings (JEP 254), if one string is Latin-1 (coder = 0) and the other is UTF-16 (coder = 1), they cannot be equal. Array comparisons are bypassed entirely.- Length Comparison: If internal
valuearray lengths differ, it aborts withfalse. - SIMD Vectorized Array Comparison: Compares array bytes using hardware vector instructions.
Detailed Comparison Table
| Criterion | Operator == |
Method equals() |
|---|---|---|
| Comparison Target | Reference Identity (Memory Pointer) | Logical Equivalence (Character State) |
| Time Complexity | Strictly $O(1)$ (single CPU instruction) | $O(1)$ on identical references; $O(N)$ for character array traversal |
Behavior with null on Left |
Safe (null == str evaluates to false) |
Throws NullPointerException (null.equals(str)) |
Behavior with null on Right |
Safe (str == null) |
Safely evaluates to false (str.equals(null)) |
| String Literals | Evaluates to true via String Pool |
Evaluates to true |
| Dynamic Strings (I/O, JSON, DB) | Evaluates to false |
Evaluates to true |
Common Traps & Antipatterns
- Unsafe Invocation on Potential
null:// BAD: Throws NullPointerException if status is null! if (status.equals("ACTIVE")) { ... } // GOOD: Yoda condition guarantees null-safety: if ("ACTIVE".equals(status)) { ... } // BEST: Standard library utility handles all null combinations: if (Objects.equals(status, "ACTIVE")) { ... } - Comparing
StringandStringBuilderwithequals():
"test".equals(new StringBuilder("test"))always returnsfalsebecauseStringBuilderfails theinstanceof Stringcheck! To compare differentCharSequenceimplementations, usecontentEquals().
🔴 Senior Level
Hardware Optimization: SIMD Vectorization in C2 JIT
The underlying methods StringLatin1.equals and StringUTF16.equals are marked with @IntrinsicCandidate.
During C2 JIT compilation, HotSpot replaces byte comparison loops with the native assembly intrinsic vectorizedMismatch:
- Instead of inspecting bytes one by one in a scalar loop, the CPU loads vector registers of 128, 256, or 512 bits (SSE4.2, AVX-2, AVX-512 on x86, or NEON on ARM64).
- The
VPXORinstruction compares 32 or 64 bytes simultaneously in a single CPU clock cycle. - If the vector XOR yields zero, all 32/64 bytes match, and execution proceeds to the next chunk.
- If a mismatch occurs,
VPMOVMSKBgenerates a bitmask that identifies the exact mismatch index in one instruction.
Consequently, comparing even large strings takes just a few nanoseconds on modern hardware.
Side-Channel Security: Timing Attacks on String.equals()
Standard String.equals() is designed for speed: it short-circuits immediately (early return) upon encountering the first mismatched character.
The Security Vulnerability:
When validating cryptographic tokens, API keys, passwords, or HMAC signatures via String.equals(), an attacker can measure response latency over the network with nanosecond precision:
- Guessing an API key that fails on byte 0 returns
falsein ~1 ns. - Guessing an API key whose first 15 bytes match and only byte 16 fails takes ~4 ns.
This timing discrepancy allows attackers to brute-force secrets byte by byte (Side-Channel Timing Attack).
The Architectural Defense:
Cryptographic secrets must always be compared in Constant Time, ensuring execution latency remains invariant regardless of mismatch position:
// Constant-Time Comparison via MessageDigest.isEqual:
byte[] expected = expectedSecret.getBytes(StandardCharsets.UTF_8);
byte[] provided = userSuppliedSecret.getBytes(StandardCharsets.UTF_8);
boolean isValid = MessageDigest.isEqual(expected, provided);
MessageDigest.isEqual processes all bytes using bitwise | operations without conditional branching, ensuring uniform execution time.
The equals() and hashCode() Invariant
The String class adheres strictly to the fundamental contract defined in java.lang.Object:
- If
s1.equals(s2) == true, thens1.hashCode() == s2.hashCode()must strictly evaluate to true. - The hash code is computed deterministically: \(s[0] \cdot 31^{n-1} + s[1] \cdot 31^{n-2} + \dots + s[n-1]\)
- The computed hash code is permanently cached in
private int hash. - Distinct strings (
equals() == false) can produce identical hash codes (hash collision, e.g.,"FB"and"Ea").
4 Tricky Interview Questions
1. Why does "abc".equals(new StringBuilder("abc")) compile without errors but always return false? What method should be used instead?
Answer:
The signature of String.equals(Object anObject) accepts any Object, making the expression syntactically valid and compilation warning-free.
However, the second line of String.equals() performs if (anObject instanceof String). Because StringBuilder is an independent implementation of CharSequence and not a subclass of String, the instanceof check evaluates to false.
To logically compare a String with any other CharSequence implementation (StringBuilder, StringBuffer, CharBuffer), invoke contentEquals():
String str = "abc";
StringBuilder sb = new StringBuilder("abc");
System.out.println(str.equals(sb)); // false (always!)
System.out.println(str.contentEquals(sb)); // true (character sequence comparison)
2. Does equality of hash codes (s1.hashCode() == s2.hashCode()) guarantee that s1.equals(s2) evaluates to true?
Answer:
No, it does not.
A hash code is a 32-bit signed integer (int) with $2^{32} \approx 4.29 \times 10^9$ possible values, whereas the potential space of string permutations is infinite. By the Pigeonhole Principle, different strings inevitably collide on the same hash value.
A classic Java collision example:
String s1 = "FB";
String s2 = "Ea";
System.out.println(s1.hashCode() == s2.hashCode()); // true (both hash to 2236)
System.out.println(s1.equals(s2)); // false ("FB" != "Ea")
Conversely, the reverse implication is absolute: if s1.equals(s2) is true, their hash codes are guaranteed to be identical.
3. What is the difference between s1.hashCode() and System.identityHashCode(s1), and how does each relate to == and equals()?
Answer:
s1.hashCode(): Computes a value based strictly on the string’s character contents. Two distinctStringobjects with identical text will produce identicalhashCode()results. This powers bucket lookups inHashMapandHashSet.System.identityHashCode(s1): Returns the object’s identity hash code generated by the HotSpot JVM based on the object’s memory address and Mark Word header (as ifhashCode()were never overridden).- If
s1 == s2(same object reference), thenSystem.identityHashCode(s1) == System.identityHashCode(s2). - If
s1ands2are created vianew String("test"),s1.hashCode() == s2.hashCode()evaluates totrue, butSystem.identityHashCode(s1) == System.identityHashCode(s2)evaluates tofalse.
- If
4. Under what condition can String.equals() return false before checking length or character array values in Java 9+?
Answer:
When there is a mismatch in the coder field (Compact Strings).
Starting with Java 9 (JEP 254), String stores character bytes in byte[] value alongside a 1-byte encoding identifier coder:
coder = 0(LATIN1): every character fits within ISO-8859-1 (1 byte per char).coder = 1(UTF16): at least one character requires 2 bytes (Cyrillic, Asian scripts, emoji).
When comparing two strings where one hascoder == 0and the other hascoder == 1, HotSpot immediately returnsfalsewithout inspecting array lengths or comparing bytes, because strings in different encodings cannot represent identical character sequences.
🎯 Interview Cheat Sheet
30-Second Summary
”
==tests reference identity (whether two pointers point to the exact same heap address), whereas.equals()verifies logical equality (character content equivalence). For business logic string comparisons, always use.equals()orObjects.equals(). The==operator returnstrueonly for shared references (like String Pool literals). The sole standard use case for==with strings is testing fornull(str == null).”
Key Concepts Checklist
- 5-Step
String.equals():==identity fast-path $\to$instanceof String$\to$codermatch $\to$lengthmatch $\to$ SIMD byte comparison. - SIMD Intrinsics: HotSpot C2 utilizes vector instructions (AVX-512 / NEON) to compare up to 64 bytes per clock cycle.
- Timing Attack Vulnerability: Standard
equals()early return leaks timing side-channels; useMessageDigest.isEqual()for secret tokens. contentEquals(): The dedicated method to compareStringwithStringBuilderorStringBuffer.- Hash Invariant:
equals()equality implieshashCode()equality, but matching hash codes do not guaranteeequals() == true.
Common Pitfalls & Red Flags
- ❌ “Using
==is fine as long as strings are shorter than 16 characters.” (False:==always compares memory addresses). - ❌ “Comparing
StringandStringBuilderviaequals()works properly.” (False: it always returnsfalse; usecontentEquals()). - ❌ “Invoking
var.equals("CONST")is safe.” (CausesNullPointerExceptionifvaris null; use"CONST".equals(var)). - ❌ “Equal hash codes guarantee that strings are equal.” (False: hash collisions are common and expected).
Related Topics
- Can You Use == to Compare Strings — Reference comparison caveats
- How String Pool Works — String pool mechanics
- When to Use intern() — Manual interning
- What are Compact Strings in Java 9+ — Compact string encoding