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 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:
seen stores Integer objects (boxed). When you call seen.add(priority), the int is autoboxed into an Integer.s is an Integer (object) and priority is an int (primitive). Java autoboxes the int to an Integer for the == comparison — but == on objects compares reference identity, not value.Integer class maintains an internal cache of Integer objects for values -128 through 127. For values in this range, Integer.valueOf(42) always returns the same object, so == works by coincidence.200 — each autoboxing call creates a new Integer object. Two distinct objects representing 200 are never == to each other.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.
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.
== 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.
