List.remove() Overload Trap: The int That Deletes the Wrong Element2026-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.
}
}
java.util.List declares two remove methods:
boolean remove(Object o) — remove the first element equal to o.E remove(int index) — remove the element at position index.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:
[7, 1, 9, 2, 8, 3, 4]remove(1) drops index 1 → [7, 9, 2, 8, 3, 4]remove(2) drops index 2 → [7, 9, 8, 3, 4]remove(3) drops index 3 → [7, 9, 8, 4]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.
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.
List<Integer>, remove(int) deletes by index and remove(Object) deletes by value — pass an Integer (or use removeIf) whenever you mean the value.
