Why does == work for Integer 127 but not for 128?

Asked constantlyjunior0–6 yrs8 min readJava 5 (1.5)Java 9

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 two Integer values compares references, not numbers. It always did.
  • Writing Integer x = 127 compiles to Integer.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 valueOf allocates a new one and the two references differ.
  • So 127 == 127 is true and 128 == 128 is false — and both are the same rule producing different answers.
  • The boundary is a default, not a language guarantee. -XX:AutoBoxCacheMax=1000 makes 1000 == 1000 true.
  • Use equals() for boxed numbers, or unbox to int first. 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.

Compiled and run on this buildEdit and run
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));
Output
127 == 127  : true
128 == 128  : false
128 equals  : true

Nothing 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(...).

Compiled and run on this buildEdit and run
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)));
Output
cached range      : -128 .. 127
valueOf(127) same : true
valueOf(128) same : false

The 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

since

Byte, 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.

Compiled and run on this buildEdit and run
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)));
Output
Long 127      : true
Long 128      : false
Character 'a' : true
Boolean true  : true
Double 1.0    : false

Double 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:

Compiled and run on this buildEdit and run
Integer big1 = 1000, big2 = 1000;

System.out.println("big1 == big2     : " + (big1 == big2));
System.out.println("big1 == big2 + 0 : " + (big1 == big2 + 0));
Output
big1 == big2     : false
big1 == big2 + 0 : true

Both 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:

Compiled and run on this buildEdit and run
Integer missing = null;
try {
    int value = missing;
    System.out.println(value);
} catch (NullPointerException e) {
    System.out.println("unboxing null: " + e.getMessage());
}
Output
unboxing null: Cannot invoke "java.lang.Integer.intValue()" because "missing" is null

The overload trap in collections, which is the same boxing rule wearing a different hat:

Compiled and run on this buildEdit and run
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);
Output
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.

  1. 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.

  2. 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.