Object Identity vs Equality — Junior¶
What? Java has two distinct questions: "are these the same object?" (identity) and "do these two objects mean the same thing?" (equality). The operator
==answers the first; the method.equals()answers the second. For reference types, mixing them up is the single most common source of "looks right, breaks silently" bugs in the language. How? For every comparison between two reference variables, decide which question you actually want. If you mean "same instance in memory" — keep==. If you mean "same value, regardless of which object holds it" — use.equals(). For primitives,==is value comparison; there is no.equals()to call.
1. Two questions, one syntax mistake¶
Take this:
Today a == b is true, because both literals share the same entry in the JVM's string pool. Tomorrow somebody reads the same value from a database:
String a = "user-42";
String b = new String(loadFromDatabase("user.id")); // fresh object, not pooled
if (a == b) { ... } // false
Same characters, different object, == returns false. The bug is silent: nothing throws, the if simply doesn't execute. The fix is .equals():
This is the canonical trap. == on reference types asks "is this the same object?". .equals() (when properly overridden, as it is on String) asks "is this the same value?". You almost always want the second.
The reason juniors get bitten is that Java allows == between any two reference types, and the compiler will not warn. The code reads like Python or JavaScript, but the semantics are sharper: the operator is asking a different question than the eye expects.
2. The Integer cache trap¶
Java boxes small integers in a cache. The same value comes back as the same object — until it doesn't.
Integer a = 100;
Integer b = 100;
System.out.println(a == b); // true
Integer c = 200;
Integer d = 200;
System.out.println(c == d); // false
Both Integer.valueOf calls return cached objects for 100 because 100 falls inside the -128..127 range that Integer caches eagerly. For 200, the JVM allocates two distinct Integer objects — == compares the references, sees they are different, returns false.
The trap is that the code reads identically. Two Integer variables, two equal numeric values, one ==. The answer flips at the boundary 127 → 128:
Integer.valueOf(127) == Integer.valueOf(127); // true
Integer.valueOf(128) == Integer.valueOf(128); // false
The cure is the same as for strings: stop comparing wrapper types with ==.
if (a.equals(b)) { ... } // compares values
if (Objects.equals(a, b)) { ... } // also null-safe
if (a.intValue() == b.intValue()) { ... } // unbox to primitives and compare
The same cache exists, by spec, for Boolean, Byte, Character (0..127), Short, and Long. It does not exist for Float and Double. So Boolean.TRUE == Boolean.TRUE is true (a single shared instance), but Double.valueOf(1.0) == Double.valueOf(1.0) is false. Don't rely on either; use .equals().
3. == is fine for primitives¶
The == operator changes meaning depending on the operands' type. For primitive types (int, long, double, boolean, char, byte, short, float), == compares values — there are no references to compare.
int x = 127, y = 127;
System.out.println(x == y); // true — value comparison
double a = 0.1 + 0.2, b = 0.3;
System.out.println(a == b); // false — but for a different reason (FP precision)
x == y works because int is a value type. The double example fails not because of identity vs equality, but because 0.1 + 0.2 does not equal 0.3 in IEEE-754 — a different topic (../../../06-numerical-types-precision/).
The rule of thumb is binary: primitive operand → value comparison; reference operand → identity comparison. When in doubt about which world you are in, look at the declared type. int is value; Integer is reference.
4. Auto-unboxing changes the question¶
The rule above gets cloudy because of auto-unboxing. If one operand is primitive and the other is a wrapper, the wrapper is unboxed to a primitive, and == becomes value comparison.
Integer wrapped = 200;
int primitive = 200;
System.out.println(wrapped == primitive); // true — wrapped is unboxed
But if both sides are wrappers, no unboxing happens — and == is identity.
This mix is why senior code reviewers go cold whenever they see == near a wrapper type. The compiler will not warn. The only safe rule for Integer, Long, Boolean, Double and friends is: never use == on them. Always .equals() or Objects.equals(). The same goes for String, BigDecimal, BigInteger, LocalDate, UUID, and every other reference type you might absent-mindedly compare with ==.
5. Objects.equals for null-safe comparison¶
Calling .equals() directly throws NullPointerException if the left side is null:
java.util.Objects.equals(a, b) (since Java 7) handles both nulls correctly:
import java.util.Objects;
if (Objects.equals(a, b)) { ... } // false (one null, one not), no exception
if (Objects.equals(null, null)) { ... } // true
The implementation is two lines:
It first uses == as a fast-path: if both references are the same (including both being null), return true without touching .equals. Then it null-checks a and delegates to a.equals(b). This is the idiomatic Java way to ask "are these two possibly-null values equal?" — every code review will flag a raw .equals() on a value that could be null.
Use Objects.equals everywhere you'd write .equals() unless you have already null-checked the receiver. In modern code, it's the default.
6. Working domain example — order IDs¶
You have an Order class with an orderId field. Two parts of the system both load the same order:
public final class Order {
private final String orderId;
private final BigDecimal total;
/* constructor, getters */
}
Order fromDb = orderRepository.findById("ORD-2026-0019");
Order fromCache = orderCache.peek("ORD-2026-0019");
When you ask "is this the same order?", you almost certainly mean same logical order, not same Java object. The cache and the database returned two separate Order instances, so:
if (fromDb == fromCache) { ... } // false — two objects
if (fromDb.equals(fromCache)) { ... } // depends on equals() override
if (fromDb.orderId().equals(fromCache.orderId())) { ... } // explicit and safe
If you wrote Order.equals to compare by orderId, the middle line works. If you didn't, equals falls back to Object.equals, which is just ==, and you get the wrong answer silently. This is exactly why the next section in this roadmap — ../01-equals-hashcode-tostring-contracts/ — is mandatory reading for every reference type that participates in equality.
The safer working pattern, when you control the type and equality is well-defined, is to override .equals() once and use it everywhere. Until you do, == will sometimes accidentally work (same instance, e.g., from a cache hit) and sometimes accidentally fail (fresh instances from different sources). Tests written one way will succeed; the production path will fail.
7. Strings — the easiest trap to walk into¶
Strings deserve their own paragraph because they look like values but are reference objects.
String a = "abc"; // pooled literal
String b = "abc"; // same pool entry
String c = new String("abc"); // fresh object
String d = a + ""; // concatenation result, fresh
System.out.println(a == b); // true
System.out.println(a == c); // false
System.out.println(a == d); // false
System.out.println(a.equals(c)); // true
System.out.println(a.equals(d)); // true
a == b works only because both operands are compile-time string literals, and the JVM keeps one shared object per literal value. The instant you build a string from anything — a database row, a JSON body, a concatenation, a new String(...), a substring — you get a fresh object and == returns false.
The rule for strings is the same as for every other reference type: use .equals(). If == happened to work for you once, it was a coincidence of the pool.
There is one exception: comparing a string to null with == is fine and idiomatic (if (s == null)). null is not a value, so .equals() cannot be applied to it.
8. Where identity is actually what you want (briefly)¶
There are real cases where == is the right operator, and they're worth naming so you know when not to reach for .equals():
nullchecks.if (x == null)is always correct and idiomatic.enumconstants. Eachenumvalue is a singleton per JVM.Status.OPEN == Status.OPENistrue, faster than.equals, and immune to null surprises. Senior code uses==for enums.- Sentinel objects. A library might define
public static final Object MISSING = new Object();and ask you to checkif (result == MISSING). That's identity comparison and the intent. - Identity-based collections (rare).
IdentityHashMap,Collections.newSetFromMap(new IdentityHashMap<>()). These exist specifically when you want to track distinct objects regardless of equality — for example, when checking for cycles in a graph.
senior.md goes deep on each. For now, the rule for juniors is simple: outside null checks, enums, and explicit sentinels, never use == on reference types.
9. Quick rules¶
- For primitives (
int,long,double, ...):==is value comparison; there is no.equals. - For reference types:
==is identity (same object),.equals()is value equality. - On
String,Integer,Boolean,BigDecimal,LocalDate, etc., always use.equals()orObjects.equals(). - Use
Objects.equals(a, b)when either side could benull. -
Integer.valueOf(127) == Integer.valueOf(127)istrue,Integer.valueOf(128) == Integer.valueOf(128)isfalse— the boxing cache covers-128..127only. - Comparing strings with
==may work today and break tomorrow when one operand stops being a literal. -
enumconstants are the one reference type where==is preferred over.equals— singleton-per-JVM by spec. -
nullchecks (x == null) always use==—.equalswould throw.
10. What's next¶
| Topic | File |
|---|---|
Refactoring == to .equals, identity collections | middle.md |
| When identity is the right contract, intern pools, classloaders | senior.md |
| Code-review vocabulary, Sonar/ArchUnit rules, mentoring | professional.md |
| JLS §15.21, §5.1.7 boxing cache, identity hash specification | specification.md |
| 10 buggy snippets, identity-vs-equality bug taxonomy | find-bug.md |
Cost of == vs .equals, intern pool footprint, JIT fast-paths | optimize.md |
| 8 hands-on refactors and design exercises | tasks.md |
| 20 interview Q&A | interview.md |
Memorize this: == asks "same object?". .equals() asks "same value?". For primitives, those questions collapse into one and == is right. For reference types — String, Integer, BigDecimal, your own domain classes — they are different questions, and the second one is almost always the one you want. The Integer cache and the string pool make == occasionally give the right answer for the wrong reason; that is the trap. Use .equals() (or Objects.equals() when nulls are possible), reserve == for null checks, enum constants, and explicit identity comparisons, and your reference-type bugs vanish.