Java's List.remove() Overload Trap: The int That Deletes the Wrong Element

2026-09-14

You have a list of numeric event codes and a handful of "sentinel" codes you want to strip out. You write what looks like the obvious loop, your unit test on a small fixture passes, and you ship. In production, the wrong events keep slipping through — and once in a while, the whole call blows up with IndexOutOfBoundsException.

import java.util.*;

public class LogFilter {
    public static List<Integer> removeErrorCodes(List<Integer> log) {
        int[] sentinels = {1, 2, 3};
        for (int code : sentinels) {
            log.remove(code);
        }
        return log;
    }

    public static void main(String[] args) {
        List<Integer> events = new ArrayList<>(
            Arrays.asList(7, 1, 9, 2, 8, 3, 4));
        System.out.println(removeErrorCodes(events));
        // Expected: [7, 9, 8, 4]
        // Actual:   [7, 9, 8, 4]  ...on this input. Change the order and it lies.
    }
}

The Bug

java.util.List declares two remove methods:

Overload resolution picks the most specific applicable method. When code is a primitive int, remove(int) matches directly, while remove(Object) would require autoboxing. Java picks the primitive overload every time — so log.remove(1) means "delete the element at index 1," not "delete the value 1."

The trap is worse than "off by one," because indices shift as you delete. Trace the fixture:

You appear to have stripped 1, 2, and 3 — but only because the fixture happened to place those values at those very indices. Shuffle the input and different events die; feed a short list and you get IndexOutOfBoundsException instead.

The Fix

Force the Object overload by handing the compiler a reference type:

for (int code : sentinels) {
    log.remove(Integer.valueOf(code));   // clear and explicit
    // or: log.remove((Integer) code);
}

Note that remove(Object) only strips the first match. If duplicates matter — or you want to sidestep the overload entirely — use removeIf, which takes a predicate and can never be misread:

Set<Integer> bad = Set.of(1, 2, 3);
log.removeIf(bad::contains);

The reason this bug is so durable: the signatures compile, the code reads naturally in English, and toy fixtures accidentally arrange values at the "right" indices, so the tests pass. Any time you see List<Integer>.remove(someInt), treat it as a code smell — the static type of the argument, not your intent, decides which method runs.

Key Takeaway: On a List<Integer>, remove(int) deletes by index and remove(Object) deletes by value — pass an Integer (or use removeIf) whenever you mean the value.

All newsletters