Why does == work for Integer 127 but not for 128?
Because == on two Integers compares references, and the compiler turns a boxed literal into Integer.valueOf(), which returns a shared cached object for -128..127 and a fresh one above it. The 127 boundary is not a language rule — it is a default, and -XX:AutoBoxCacheMax moves it. Compare boxed numbers with equals(), always.
The Answer
==on twoIntegervalues compares references, not numbers. It always did.- Writing
Integer x = 127compiles toInteger.valueOf(127). That method keeps a cache of boxed objects for −128 to 127 and hands out the same instance every time. - Above 127 there is no cached instance, so
valueOfallocates a new one and the two references differ. - So
127 == 127istrueand128 == 128isfalse— and both are the same rule producing different answers. - The boundary is a default, not a language guarantee.
-XX:AutoBoxCacheMax=1000makes1000 == 1000true. - Use
equals()for boxed numbers, or unbox tointfirst. Never==.
Understand It
The comparison was never about numbers
Integer is an object. == between two object references asks one question: are these the same
object? That is true for int too — it just happens that with a primitive there is no object, so
== compares the value and everyone learns the wrong general rule.
Integer a = 127, b = 127;
Integer c = 128, d = 128;
System.out.println("127 == 127 : " + (a == b));
System.out.println("128 == 128 : " + (c == d));
System.out.println("128 equals : " + c.equals(d));127 == 127 : true
128 == 128 : false
128 equals : trueNothing about 128 is special to ==. What changed between the two lines is whether the two
references point at one object or two.
Where the object comes from
Integer a = 127 is not a cast. The compiler rewrites it as Integer.valueOf(127), and
valueOf is where the decision is made: inside java.lang.Integer there is a private
IntegerCache holding pre-built boxes for a small range. Ask for a number inside the range and
you get the shared instance; ask for one outside it and you get new Integer(...).
Class<?> cache = Class.forName("java.lang.Integer$IntegerCache");
Field low = cache.getDeclaredField("low");
Field high = cache.getDeclaredField("high");
low.setAccessible(true);
high.setAccessible(true);
System.out.println("cached range : " + low.get(null) + " .. " + high.get(null));
System.out.println("valueOf(127) same : " + (Integer.valueOf(127) == Integer.valueOf(127)));
System.out.println("valueOf(128) same : " + (Integer.valueOf(128) == Integer.valueOf(128)));cached range : -128 .. 127
valueOf(127) same : true
valueOf(128) same : falseThe cache exists because small integers dominate real programs — loop counters, sizes, ids, map keys. Pre-allocating 256 objects once removes an enormous amount of garbage.
The part nearly every article gets wrong
"The cache is −128 to 127" is repeated everywhere as though it were in the language
specification. The lower bound is: the JLS requires values from −128 to 127 to be cached, so
that 127 == 127 is guaranteed. The upper bound is not fixed. The JVM accepts
-XX:AutoBoxCacheMax, and raising it moves the boundary:
java -XX:AutoBoxCacheMax=1000 Demo
Run the same program with that flag and the reflection above reports cached range : -128 .. 1000,
and Integer.valueOf(1000) == Integer.valueOf(1000) becomes true.
This is why "128" is a bad thing to memorise and a worse thing to rely on. Code whose correctness
depends on == between boxed integers can change behaviour because someone tuned a JVM flag for
an unrelated reason. The flag is real, supported, and occasionally set in production to reduce
allocation.
Not every wrapper behaves the same
sinceByte, Short, Integer, Long and Character all cache. Boolean has only two values and
both are cached. Float and Double cache nothing — there is no small useful range to
pre-build.
System.out.println("Long 127 : " + (Long.valueOf(127L) == Long.valueOf(127L)));
System.out.println("Long 128 : " + (Long.valueOf(128L) == Long.valueOf(128L)));
System.out.println("Character 'a' : " + (Character.valueOf('a') == Character.valueOf('a')));
System.out.println("Boolean true : " + (Boolean.valueOf(true) == Boolean.valueOf(true)));
System.out.println("Double 1.0 : " + (Double.valueOf(1.0) == Double.valueOf(1.0)));Long 127 : true
Long 128 : false
Character 'a' : true
Boolean true : true
Double 1.0 : falseDouble is the useful one to remember: == on two boxed doubles is false even for the same
small value, because nothing is shared.
Arithmetic quietly changes the question
If either side of == is a primitive, the other side is unboxed and you are comparing numbers
again. So adding a harmless-looking + 0 flips the result:
Integer big1 = 1000, big2 = 1000;
System.out.println("big1 == big2 : " + (big1 == big2));
System.out.println("big1 == big2 + 0 : " + (big1 == big2 + 0));big1 == big2 : false
big1 == big2 + 0 : trueBoth lines look like they ask the same thing. The first compares two references; the second
compares two int values. This is the single most confusing consequence of the rule, and it is
why "just use equals" is better advice than any amount of understanding.
Reference
The complete rule, and the safe ways to write it.
// WRONG — compares references, works by accident below 128
if (countA == countB) { }
// RIGHT — compares values
if (countA.equals(countB)) { }
// RIGHT — unbox explicitly, fastest, throws NPE on null
if (countA.intValue() == countB.intValue()) { }
// RIGHT — null-safe and readable, works for any boxed number
if (Objects.equals(countA, countB)) { }
// RIGHT — when ordering matters too
if (countA.compareTo(countB) == 0) { }
Unboxing a null throws, and the message names the variable when the class was compiled with
-g:
Integer missing = null;
try {
int value = missing;
System.out.println(value);
} catch (NullPointerException e) {
System.out.println("unboxing null: " + e.getMessage());
}unboxing null: Cannot invoke "java.lang.Integer.intValue()" because "missing" is nullThe overload trap in collections, which is the same boxing rule wearing a different hat:
List<Integer> byObject = new ArrayList<>(List.of(1, 2, 3));
byObject.remove(Integer.valueOf(2)); // remove(Object) — removes the value 2
List<Integer> byIndex = new ArrayList<>(List.of(1, 2, 3));
byIndex.remove(2); // remove(int) — removes index 2
System.out.println("remove(Integer 2) -> " + byObject);
System.out.println("remove(int 2) -> " + byIndex);remove(Integer 2) -> [1, 3]
remove(int 2) -> [1, 2]Scenarios
A cache keyed by Integer that mostly works. You key a HashMap<Integer, Session> by user
id and compare keys with == somewhere in the lookup path. Every test passes, because test users
have ids 1 to 20. In production, ids are six digits and the comparison is always false. The bug
is invisible until the data grows — and no code changed between working and broken.
A team that "fixed" it with a flag. Someone finds -XX:AutoBoxCacheMax=32768 in a
performance article and sets it, and a latent == bug stops reproducing. The code is still
wrong; it now depends on a JVM flag nobody will remember to copy to the next environment. Fix the
comparison, not the cache.
A legitimate use of ==. Comparing a boxed value to a primitive constant, if (count == 0),
is fine: count is unboxed and you get a numeric comparison. It also throws
NullPointerException if count is null, which may be exactly what you want, or may be the
thing that takes the service down at 3am. Know which.
Where it does not matter at all. Two int locals. No boxing, no cache, no trap. Reach for
wrapper types only when you need nullability or a collection, not by habit.
Interviewer's Next Move
1. "Is the −128 to 127 range guaranteed by the language?"
The lower half is. The JLS mandates caching for values between −128 and 127, so 127 == 127 must
be true on any conforming JVM. The upper bound is an implementation default that
-XX:AutoBoxCacheMax can raise, so 1000 == 1000 can legitimately be true on a tuned JVM and
false on the one next to it.
2. "What does Integer x = 127 actually compile to?"
Integer.valueOf(127). Autoboxing is a compiler rewrite, not a runtime conversion. You can see
it in javap -c. That is why the cache is involved at all — valueOf owns the decision, and
new Integer(127) would have bypassed it entirely, which is part of why it is deprecated.
3. "Why is a == b + 0 true when a == b is false?"
Because b + 0 is arithmetic, which unboxes b to an int. Once one operand is a primitive,
the other is unboxed too and == compares numbers. Adding zero changes the kind of comparison,
not the values.
4. "Does Double.valueOf(1.0) == Double.valueOf(1.0) behave the same way?"
No — it is false. Float and Double cache nothing, because there is no small dense range worth
pre-building. Remembering "wrappers cache" without the exception gets this one wrong.
5. "How would you find this bug in an existing codebase?"
Static analysis: SpotBugs reports it as RC_REF_COMPARISON, and both Error Prone and IntelliJ
flag reference comparison of boxed types. It is a good candidate for a build-breaking rule,
because the fix is mechanical and the failure is silent and data-dependent.
Check Yourself
Two Integer variables both hold 1000. What does == return, and what would make it return
the other answer?
False, because 1000 is outside the default cache and each box is a separate object. It returns
true if the JVM was started with -XX:AutoBoxCacheMax=1000 or higher, or if either side is
unboxed by arithmetic or a cast.
Why does list.remove(2) on a List<Integer> not remove the value 2?
Because List has both remove(int index) and remove(Object o), and an int literal matches
the index overload exactly — the compiler prefers it over boxing. remove(Integer.valueOf(2))
selects the other overload.
Is if (count == 0) safe when count is an Integer?
It compares correctly, because count is unboxed against the primitive 0. It is not
null-safe: if count is null it throws NullPointerException rather than evaluating to false.
What changed, and when
Version boundaries are where follow-ups live. Know the change and the behaviour it replaced.
- Java 5 (1.5)
Autoboxing arrives, and with it Integer.valueOf() caching -128..127 by design.
Before Java 5 (1.5): You wrote new Integer(i) by hand, which never shared an instance — so == was reliably false and the trap did not exist.
- Java 9
new Integer(int) is deprecated; valueOf() is the only sanctioned way to box, so the cache is now unavoidable rather than opt-in.
Before Java 9: new Integer(127) == new Integer(127) was false, and plenty of code relied on that without knowing it.
Where this question goes next
Questions that lead here
Why is 0.1 + 0.2 not 0.3, and what should you use for money?
Because a double stores binary fractions, and 0.1 has no exact binary form — it is really 0.1000000000000000055511151231257827…, so the sum lands just past 0.3. Use BigDecimal built from a String, or count in the smallest unit as a long. And know that BigDecimal.equals compares scale, so 1.0 does not equal 1.00 — use compareTo.
Asked constantlyjunior0–8 yrs9 min readLanguage basics
Every runnable example above was compiled and executed against openjdk 21.0.11 on this build, and its output diffed against what this page claims. Last updated 2026-09-11.