OneBug All write-ups

Java ·

Java Integer == works until 128: the boxing cache trap

Comparing boxed Integer with == in Java passes for small ids and fails above 127 because of the Integer cache. Why that happens, and the exact fix.

Test yourself before reading - the write-up below gives it away.

Spot the bug Java
1
// Reject an order id we have already processed.
2
static List<Integer> seen = new ArrayList<>();
3
 
4
static boolean isDuplicate(Integer orderId) {
5
    for (Integer id : seen) {
6
        if (id == orderId) return true;
7
    }
8
    seen.add(orderId);
9
    return false;
10
}
11
 
12
// isDuplicate(70);  isDuplicate(70)  -> true
13
// isDuplicate(700); isDuplicate(700) -> false  (!)
Powered by OneBug

What the code looks like it does

A dedup check for incoming order ids: keep a running list of ids already processed, and reject anything already in it. Simple, and it passes every test the author wrote against it.

// Reject an order id we have already processed.
static List<Integer> seen = new ArrayList<>();

static boolean isDuplicate(Integer orderId) {
    for (Integer id : seen) {
        if (id == orderId) return true;
    }
    seen.add(orderId);
    return false;
}

// isDuplicate(70);  isDuplicate(70)  -> true
// isDuplicate(700); isDuplicate(700) -> false  (!)

seen.add(orderId) is the decoy - it faithfully appends the boxed id to the list, and there is nothing wrong with it. The bug is one line above: the comparison itself.

The bug: == compares references, not values, on boxed types

id and orderId are both Integer, a reference type. == on two references checks whether they point to the same object, not whether the numbers inside them are equal. id.equals(orderId) would compare values; id == orderId does not.

This "works" for small numbers because of a caching rule in the JLS. Section 5.1.7 (boxing conversion) guarantees that autoboxing a value in the range -128 to 127 returns a value from a cache, so Integer.valueOf(70) called twice in the same run returns the exact same object both times. == on two references to the same cached object is true, so isDuplicate(70) looks correct. There is no such guarantee outside that range. Integer.valueOf(700) allocates a fresh object on each call, id == orderId compares two different objects, and the comparison is always false regardless of the actual values - so the "duplicate" at 700 is never caught.

This stays hidden for the same reason it is easy to write in the first place: unit tests reach for small, convenient numbers. Order id 1, 2, 3, or whatever fixture value someone typed without thinking - all inside the cache range, all passing. The bug only shows up once real ids start exceeding 127, which in production is essentially guaranteed and in a test suite is essentially never, unless someone deliberately picks a large value.

Why it bites in real code

This is one of the most common correctness bugs in Java specifically because autoboxing makes Integer and int look interchangeable at the syntax level. A method that takes an Integer parameter, a list that stores Integer, a loop that compares two Integer values with == - none of it throws a compiler warning, none of it looks unusual, and the code behaves correctly in a huge number of executions before it does not. It shows up in places where a primitive int quietly became a boxed Integer - a generic collection, a nullable field, an API boundary that needs to represent "no value" - and the comparison operator was never revisited for the new type. It is also a frequent source of intermittent-looking production bugs, because whether two particular ids happen to both fall in the cache range is a coincidence of the data, not something anyone consciously controls.

The fix

Use .equals() for value comparison on boxed types:

if (id.equals(orderId)) return true;

If nullability is not a concern, unboxing both sides to int before comparing works too, since == on primitive int is a value comparison:

if (id.intValue() == orderId.intValue()) return true;

Either fix removes the dependence on caching behavior entirely - the comparison is correct at every value, not just the ones that happen to be small.

War stories

The Integer cache is not folklore, it is specified: JLS 5.1.7 requires that -128 to 127 be cached, and the upper bound is tunable at JVM startup via -XX:AutoBoxCacheMax, which only matters because the behavior is real and occasionally gets adjusted for memory or performance reasons. It is also one of the most frequently asked interview questions and Stack Overflow topics in the language, precisely because the failure mode - correct below 128, wrong above it - is so easy to reproduce and so easy to miss until it hits production data.

Related bugs

The same "correct for small test values, wrong once real data crosses a threshold" shape shows up in Java binary search integer overflow, where the bug only appears once array indices get large enough to overflow. Python runs the identical trap with different constants - its small-int cache makes is work until 256. Browse the rest of the bug write-ups.