실행 시점의 값이 아니라 컴파일 시점의 타입이 메서드를 고른다.
고전적인 사고
List<Integer> list = new ArrayList<>(List.of(10, 20, 30));
list.remove(1); // 정적 타입이 int → remove(int index) → 인덱스 삭제
list.remove(Integer.valueOf(1)); // 정적 타입이 Integer → remove(Object) → 값 삭제
둘 다 정상 컴파일된다. 값을 지우려던 코드가 인덱스를 지우고, 인덱스 범위를 넘으면
엉뚱하게 IndexOutOfBoundsException 이 난다.
규칙은 세 단계다
1단계 박싱·가변인자 없이 맞는 것을 찾는다 ← remove(int) 가 여기서 잡힌다
2단계 박싱을 허용해 다시 찾는다
3단계 가변인자까지 허용해 찾는다
1단계에서 후보가 나오면 거기서 끝난다. int → Integer 박싱은 2단계라서
remove(int) 가 항상 이긴다. 자바가 하위 호환을 지키려고 만든 순서인데,
이 경우엔 직관과 반대로 작동한다.
제네릭이 도와주지 못한다
List<Integer> 라고 선언해도 소용없다. 타입 소거 이전에 시그니처가 두 개이기 때문이다.
remove(Object) 는 제네릭과 무관하게 원래 Collection 인터페이스에 있던 메서드다.
피하는 법
- 값 삭제는
removeIf(x -> x == 30)또는Integer.valueOf(...)로 명시 - 오버로드를 설계할 때 인자 개수가 같은 오버로드는 되도록 피한다(Effective Java Item 52)