Why does == sometimes work and sometimes fail when comparing two equal Integer values?
answer
- == compares references, equals compares values
- boundary is 127 (cached) vs 128 (new object)
- small-number tests hide the bug
- mixed int vs Integer unboxes -> compares value
- use equals/Objects.equals, or unbox carefully
basics
~20 sSmall boxed Integers (-128 to 127) come from a shared cache, so == compares the same object and returns true. Larger values are separate objects, so == returns false even when the numbers are equal. Always use equals().
solid answer
~50 s== on objects compares references, not values — it asks 'is this literally the same object?'. Autoboxing routes through Integer.valueOf, which returns a cached, shared instance for values -128 to 127. So Integer a = 127, b = 127 give the same cached object and a == b is true. But Integer c = 128, d = 128 are above the cache range, so each is freshly allocated; c == d is false despite c.equals(d) being true. This makes == appear to work in tests that only use small numbers and then fail in production with larger ones. The reliable fix is .equals() for wrapper comparison, or unbox to int and compare primitives with ==. The cache's high bound can be raised via -XX:AutoBoxCacheMax, but relying on that to make == work is an anti-pattern.
code
java · 13 linesInteger a = 127, b = 127;
System.out.println(a == b); // true (shared cached flyweight)
Integer c = 128, d = 128;
System.out.println(c == d); // false (two new objects)
System.out.println(c.equals(d)); // true (same value)
Integer x = 1000;
int y = 1000;
System.out.println(x == y); // true (x is unboxed, compared by value)
// Safe value comparison, null-tolerant:
System.out.println(java.util.Objects.equals(c, d)); // truego deeper
Knows to use equals() and that == can give wrong answers for big numbers.
Explains reference vs value equality, the 127/128 boundary from the cache, and why small-number tests mask the bug.
Adds the mixed int/Integer unboxing rule, the NPE risk of unboxing null, and recommends Objects.equals plus static analysis to catch identity comparisons.
Frames it as a class of latent, data-dependent defects; mandates lint/SpotBugs rules and codebase conventions, and explains why tuning AutoBoxCacheMax is the wrong remedy.
## The two meanings of equality In Java there are two different equality questions: 1. **Reference identity** — `a == b` asks whether `a` and `b` are *the very same object* in memory (the same heap address). For primitives `==` compares raw values, but for object references it compares identities. 2. **Value equality** — `a.equals(b)` asks whether the two objects *represent the same value*. `Integer.equals` compares the wrapped `int`s. These coincide for two references to one object, but diverge when you have two distinct objects holding equal values. ## Why the result flips at 127/128 **Autoboxing** turns `Integer x = 127;` into `Integer x = Integer.valueOf(127);`. `Integer.valueOf` consults `IntegerCache`, which pre-builds and stores `Integer` objects for the range **-128 to 127** (the Flyweight pool). For any value in that window it returns the **same shared instance**. So: ```java Integer a = 127, b = 127; // both = the cached Integer(127) a == b; // true — same object ``` For a value **outside** the cache, `valueOf` allocates a brand-new object each time: ```java Integer c = 128, d = 128; // two separate new Integer objects c == d; // false — different objects c.equals(d); // true — same value ``` That is why `==` 'works' for 127 and 'fails' for 128 — the boundary is exactly the top of the cache. ## Why this is a dangerous bug The failure is **data-dependent and silent**: - Unit tests written with small literals (`0`, `1`, `42`) pass, because those are cached and `==` happens to give the right answer. - The same code fails in production the moment a real value exceeds 127 (an id, a count, a price in cents), with no compile error and no exception — just a wrong branch. - It is invisible in code review unless the reviewer knows the cache exists. ## The correct rules - **Compare wrapper values with `.equals()`**: `a.equals(b)` (guard against `a` being null) — or `Objects.equals(a, b)`. - **Or unbox to primitives**: `int x = a; int y = b; x == y;` — but beware that unboxing a `null` `Integer` throws `NullPointerException`. - **Never** rely on the cache to make `==` correct, and **never** widen `AutoBoxCacheMax` to 'fix' identity comparisons — that just hides the bug and makes behavior depend on a JVM flag. ## Subtlety: mixed primitive/wrapper comparison If one operand is a primitive `int` and the other is an `Integer`, Java **unboxes the wrapper** and compares values — so `Integer x = 1000; int y = 1000; x == y;` is `true`. The reference-identity trap only applies when **both** operands are wrapper objects.
- What does Integer x = 1000; int y = 1000; x == y evaluate to, and why?true. When one operand is a primitive int, the Integer is unboxed and the comparison is by value, so the cache is irrelevant here.
- What risk does unboxing introduce when you 'fix' a comparison by converting to int?If the Integer is null, unboxing throws a NullPointerException. Objects.equals(a, b) is null-safe; manual unboxing is not.
saying these in an interview costs you the question
- Concluding == is fine for Integer because it worked for small values
- Saying the boundary is 0..127 (it's -128..127)
- Forgetting that unboxing a null Integer throws NPE
- Not knowing that int == Integer unboxes and compares values