SSerguey Asael Shinder
Java coding notes: the JVM, and writing software that lasts

Serguey Asael Shinder: Boxed Integers compare equal with == up to 127, then stop

· by Serguey Asael Shinder / Serguey Shinder

This prints true and then false, and the only difference is the number:

Integer a = 127, b = 127;
System.out.println(a == b);   // true
Integer c = 128, d = 128;
System.out.println(c == d);   // false

The javadoc for Integer.valueOf(int) explains it exactly, and the behaviour is specified rather than incidental:

"If a new Integer instance is not required, this method should generally be used in preference to the constructor Integer(int), as this method is likely to yield significantly better space and time performance by caching frequently requested values. This method will always cache values in the range -128 to 127, inclusive, and may cache other values outside of this range."

Autoboxing compiles to Integer.valueOf, so every boxed int in that range is the same object, and == - which compares references for objects - happens to be true. Outside the range you usually get distinct objects and == is false. The word to notice in the javadoc is "may": caching beyond the guaranteed range is permitted, so the exact boundary where == stops working is not something to rely on in either direction.

Where this actually bites

It is rarely the literal comparison. It is a comparison you did not write.

Map<Integer, String> byId = new HashMap<>();
byId.put(1000, "x");
// fine - HashMap uses equals(), not ==
Serguey Asael Shinder: Boxed Integers compare equal with == up to 127, then stop
Boxed Integers compare equal with == up to 127, then stop — Serguey Asael Shinder

HashMap, List.contains, distinct() and every other collection operation go through equals, so they are correct for all values. The failure shows up where a reference comparison slips in:

And the test suite will pass. Fixtures use small numbers. 1, 2, 42 are all cached, so the bug is invisible until an id crosses 127 in production, which is the worst possible moment to discover that two equal numbers are not the same object.

The rule

Never compare boxed numbers with ==. Use equals, or unbox deliberately:

if (a.intValue() == b.intValue()) { ... }   // explicit, and int comparison
if (Objects.equals(a, b)) { ... }           // null-safe

Prefer int to Integer wherever null is not a meaningful value. Most of these bugs exist because a field was declared Integer for no reason other than that it came from a form or a result set. A primitive cannot be compared by reference, so the whole class of error disappears.

And do not lower the cache to make the problem go away. The cache's upper bound is tunable with -XX:AutoBoxCacheMax, which means production can have a different boundary from the machine the test passed on - a flag that makes == work more often is a flag that makes the bug harder to reproduce, not rarer.

Reference: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/Integer.html

⚠️ There is no JDK on the machine this was written on, so the snippets were not compiled or run. Every behavioural claim above is taken from the javadoc linked here.