빌드노트
List<Integer>에서 숫자 1 대신 두 번째 값이 지워지는 이유
List<Integer>의 remove(1)은 인덱스 1의 원소를 지운다. 수정 가능한 ArrayList에서 값 삭제와 반환값·목록을 비교하고, 없는 값의 false와 잘못된 인덱스의 예외를 구별해 호출 의도를 명시한다.
List<Integer>에 [1, 20, 1]이 들어 있는데 remove(1)을 호출하면 20이 지워진다. 인자 1이 int이므로 값을 찾는 메서드가 아니라 인덱스의 원소를 지우는 remove(int)가 선택되기 때문이다. 인덱스는 0부터 시작하므로 1은 두 번째 칸이다. 값 1을 지우려면 remove(Integer.valueOf(1))로 호출한다.
먼저 문제가 난 줄의 인자 타입을 확인한다. remove에는 이름은 같고 매개변수가 다른 두 메서드, 즉 오버로드가 있다. Java 21의 인덱스 삭제 계약 (새 탭에서 열림)은 E remove(int index)이며 지워진 원소를 반환한다. 값 삭제 계약 (새 탭에서 열림)은 boolean remove(Object o)이며 같은 값이 있으면 앞에서부터 하나를 지우고 true를 반환한다. 없으면 목록을 유지하고 false를 반환한다.
List<Integer>에서 E는 Integer가 된다. 하지만 값 삭제의 매개변수 선언은 여전히 Object다. 목록의 원소 타입이 Integer라고 해서 숫자 리터럴 1이 자동으로 값 삭제를 뜻하지는 않는다.
같은 목록 두 개에서 결과를 비교한다
다음 비교는 삭제할 위치와 값이 겹치지 않도록 두 번째 원소를 20으로 두었다. 각 호출을 새 목록에서 시작해야 앞선 삭제가 다음 결과에 영향을 주지 않는다. 준비물은 JDK 17 이상과 그 안의 jshell 명령이다. 터미널에서 jshell --version으로 버전을 확인한 뒤 jshell --feedback silent를 실행한다. 명령을 찾지 못하면 JDK의 bin 경로부터 확인한다. JShell 명령 문서 (새 탭에서 열림)에 실행 옵션이 설명돼 있다.
아래 입력은 두 삭제의 반환값과 남은 목록을 함께 확인한다. 열린 JShell에 순서대로 붙여 넣는다.
import java.util.ArrayList;
import java.util.List;
List<Integer> byIndex = new ArrayList<>(List.of(1, 20, 1));
Integer removed = byIndex.remove(1);
System.out.println("index: returned=" + removed + ", list=" + byIndex);
List<Integer> byValue = new ArrayList<>(List.of(1, 20, 1));
boolean changed = byValue.remove(Integer.valueOf(1));
System.out.println("value: returned=" + changed + ", list=" + byValue);
Windows 11, OpenJDK 17.0.2에서 직접 얻은 출력이다. JShell의 입력 표시를 빼고 System.out.println 출력만 옮겼다.
index: returned=20, list=[1, 1]
value: returned=true, list=[20, 1]
첫 호출의 반환값 20은 삭제 성공 여부가 아니라 지워진 원소다. 두 번째 호출은 값 1이 두 개 있어도 맨 앞의 하나만 지웠다. true가 전체 중복 제거를 뜻하지는 않는다.
여기서 삭제하는 객체는 new ArrayList<>(...)로 만든 수정 가능한 목록이다. List.of는 처음 넣을 원소를 전달하는 데만 썼다. 삭제를 지원하는 구현이라는 조건은 Java 17 ArrayList 문서 (새 탭에서 열림)에서도 확인할 수 있다.
없는 값과 잘못된 인덱스는 다르게 끝난다
위 세션에서 이어서 다음을 입력한다. 같은 숫자 99를 값과 인덱스로 각각 넘겨, 삭제가 일어나지 않았을 때 무엇을 확인할지 비교한다.
boolean missing = byValue.remove(Integer.valueOf(99));
System.out.println("missing: returned=" + missing + ", list=" + byValue);
try {
byValue.remove(99);
} catch (IndexOutOfBoundsException e) {
System.out.println("index 99: " + e.getClass().getSimpleName() + ", list=" + byValue);
}
실제 출력은 다음과 같다. 예외는 코드에서 잡아서 이름과 목록을 출력했다.
missing: returned=false, list=[20, 1]
index 99: IndexOutOfBoundsException, list=[20, 1]
false면 값 삭제를 요청했으나 일치하는 원소가 없었던 것이다. 이 예제의 IndexOutOfBoundsException은 크기 2인 목록에서 99번 위치를 지우려 해서 발생했다. 인덱스 삭제의 유효 범위는 0 <= index < size()다. 별도 실행에서도 인덱스 -1과 size()는 같은 예외를 냈고 목록은 유지됐다. 값을 찾지 못한 상황을 인덱스 예외로 처리하거나, 잘못된 인덱스를 단순한 삭제 실패로 넘기면 원인을 놓친다.
변수에 담긴 1도 타입을 확인한다
리터럴만 바꾸고 끝내기 쉽지만 기존 코드에서는 remove(target)처럼 변수를 넘기는 경우가 많다. List<Integer>를 통해 호출할 때 int target = 1이면 인덱스 삭제, Integer target = 1이면 값 삭제가 선택된다. 별도 목록으로 실행한 결과도 전자는 20과 [1, 1], 후자는 true와 [20, 1]이었다.
Java 언어 명세의 오버로드 선택 규칙 (새 탭에서 열림)은 먼저 박싱·언박싱 없이 호출 가능한 메서드를 찾는다. int 인자는 그대로 remove(int)에 맞는다. Integer 인자는 Object로 전달할 수 있어 remove(Object)가 선택된다. Integer를 int로 언박싱해 인덱스로 해석할 단계까지 가지 않는다.
수정할 때는 지우려는 대상을 코드에 드러낸다. 값 삭제가 목적이고 target이 int라면 list.remove(Integer.valueOf(target))로 호출한다. 위치 삭제가 목적이라면 유효 범위의 int index를 넘긴다. Integer로 받은 위치는 null이 아닌지 확인한 뒤 index.intValue()로 넘기면 인덱스 삭제가 된다. 별도 실행한 remove(boxed.intValue())도 두 번째 원소 20을 지웠다.
반환값을 받는 변수의 타입만 바꿔서 다른 오버로드를 고르려 하지는 않는다. 이 호출에서는 인자 타입으로 메서드가 결정되므로, 반환값은 선택된 메서드와 의도가 맞는지 확인하는 단서로 쓴다. 위치 삭제는 지워진 Integer, 값 삭제는 boolean을 받는다.
직접 검증한 범위는 OpenJDK 17.0.2의 ArrayList<Integer>와 단일 스레드다. Java 21은 문서 계약만 확인했고 해당 버전으로 실행하지 않았다. UnsupportedOperationException이 난다면 목록 생성 경로와 삭제 지원 여부를 별도로 확인한다. Arrays.asList의 고정 크기 제한이나 변경 불가 목록은 이번 두 오버로드의 선택과 다른 문제다. JShell 실습을 마치면 /exit로 종료한다.