Java's Autoboxing Equality Trap

2026-04-28

You're building a simple caching layer that deduplicates work items by their priority ID. Two items with the same priority should be considered duplicates. Your colleague wrote this and it passed every test in the suite:

import java.util.ArrayList;
import java.util.List;

public class WorkQueue {
    private final List<Integer> seen = new ArrayList<>();

    /** Returns true if this is a new priority we haven't seen. */
    public boolean addIfNew(int priority) {
        for (Integer s : seen) {
            if (s == priority) {   // already seen
                return false;
            }
        }
        seen.add(priority);
        return true;
    }

    public static void main(String[] args) {
        WorkQueue q = new WorkQueue();
        System.out.println(q.addIfNew(42));   // true
        System.out.println(q.addIfNew(42));   // false — correct!
        System.out.println(q.addIfNew(200));  // true
        System.out.println(q.addIfNew(200));  // ???
    }
}

The first duplicate check (42) works perfectly. The second (200) does not — addIfNew(200) returns true both times, silently letting duplicate work through. The output is:

true
false
true
true    ← bug: should be false

The Bug

The comparison s == priority is doing two different things depending on the value of the integer, even though the types look identical every time.

Here's what happens step by step:

This is what makes the bug so insidious: your tests pass as long as the test data stays small. The moment production traffic introduces priority IDs above 127 (or below -128), duplicates slip through silently. No exception, no warning — just wrong behavior.

The Fix

Use .equals() for object comparison, or force an unboxing comparison by keeping both sides primitive:

    public boolean addIfNew(int priority) {
        for (Integer s : seen) {
            if (s.intValue() == priority) {  // unbox explicitly
                return false;
            }
        }
        seen.add(priority);
        return true;
    }

Alternatively, s.equals(priority) also works, since Integer.equals() compares by value. But intValue() makes the intent crystal clear and avoids another autoboxing round-trip.

Static analysis tools like SpotBugs flag == between boxed types as RC_REF_COMPARISON_BAD_PRACTICE — but many teams don't run them, and the mixed Integer == int case is especially easy to miss because the autoboxing is invisible in the source code.

Key Takeaway: Never use == to compare Integer objects in Java — it checks reference identity, and the internal cache only guarantees shared instances for -128 to 127, making the bug pass small-value tests and fail silently in production.

All newsletters