[Java] List - subList() 주의점
List의 subList() 메서드를 사용시 주의할 점
얼마전 List의 subList() 메서드를 사용하던 중 예상과 다르게 다음과 같은 예외가 발생하였습니다.
Exception in thread "main" java.util.ConcurrentModificationException
at java.base/java.util.ArrayList$SubList$1.checkForComodification(ArrayList.java:1394)
at java.base/java.util.ArrayList$SubList$1.next(ArrayList.java:1298)
에러가 난 코드를 간단하게 작성해보면 다음과 같습니다.
List<Integer> arr = new ArrayList<>();
for (int i = 0; i < 10; i++) {
arr.add(i);
}
int index = arr.indexOf(5);
List<Integer> sub = arr.subList(index, arr.size());
System.out.println("arr : " + arr);
System.out.println("sub : " + sub);
arr.removeAll(sub);
System.out.println("=================");
System.out.println("arr : " + arr);
System.out.println("sub : " + sub); // << 예외 발생
Collections.reverse(sub);
System.out.println(sub);
원래 예상대로라면 subList() 메서드로 arr에서 인덱스 5부터 끝까지로 별도의 리스트(sub)를 만들고 그 리스트의 내용을 원래 arr 에서 삭제하여 다음과 같은 상태가 될것이라고 생각했지만 에러가 발생했습니다.
arr : [0, 1, 2, 3, 4] sub : [5, 6, 7, 8, 9]
원인
우선 ArrayList의 subList() 메서드를 열어보면 다음과 같습니다.
public List<E> subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);
return new SubList<>(this, fromIndex, toIndex);
}
위 코드에서 알 수 있듯이 return new SubList 로 새롭게 메모리를 할당하여 list를 반환하고 있고 매개변수에 현재 리스트 객체(this)를 넣어주고 있습니다.
SubList 클래스의 생성자를 보면 다음과 같습니다.
private static class SubList<E> extends AbstractList<E> implements RandomAccess {
private final ArrayList<E> root;
private final SubList<E> parent;
private final int offset;
private int size;
public SubList(ArrayList<E> root, int fromIndex, int toIndex) {
this.root = root;
this.parent = null;
this.offset = fromIndex;
this.size = toIndex - fromIndex;
this.modCount = root.modCount;
}
....
}
this로 넘겨줬던 원본 리스트 객체는 root 로 저장되어 SubList 자체에서 관리되는 것을 알 수 있습니다. subList() 메서드에서 return new SubList로 SubList 객체를 반환해줄때 내부적으로 root에 원본 리스트 객체가 있어서 root 값이 함께 반환되기 때문에 인덱싱 한 값이라도 영향을 받습니다. 원본 리스트와 여전히 연결되어 있습니다. 그래서 원본 리스트를 변경하게 되면 기존 subLIst는 더 이상 사용할 수 없습니다.
현재 문제와 별개의 추가적인 문제로 subList는 불필요한 메모리를 점유할 수 있다. 예를 들면 1만개의 리스트에서 단순 10개만 subList로 만들고 이를 캐시에 저장하는 경우, 10 개의 데이터만 캐시되는 것이 아닌, 1만개의 리스트 값들이 모두 캐시가 됩니다.
subList() 메서드 결과의 클래스를 실제로 확인해보면 다음과 같습니다.
System.out.println(arr.subList(index, arr.size()).getClass());
>> class java.util.ArrayList$SubList
Solution
해결책은 subList() 로 반환된 SubList를 이용하여 ArrayList를 만드는 방법을 이용해야 합니다. 아래처럼 코드를 수정하면 처음 예상한대로 결과가 나오는것을 알 수 있습니다.
List<Integer> arr = new ArrayList<>();
for (int i = 0; i < 10; i++) {
arr.add(i);
}
int index = arr.indexOf(5);
// List<Integer> sub = arr.subList(index, arr.size());
// solution
List<Integer> sub = new ArrayList<>(arr.subList(index, arr.size()));
arr.removeAll(sub);
참고사항
java.util.ConcurrentModificationException 문제는 보통 List 등 Iterable 객체를 순회하면서 요소를 삭제할 때 발생합니다. 예를 들면 아래 코드처럼 List를 순회하면서 요소를 삭제하는 경우 요소의 인덱스가 기존 인덱스 값과 달라지고 remove로 size 값도 맞지 않기 때문입니다.
List<Integer> arr = new ArrayList<>();
for (int i = 0; i < 5; i++) {
arr.add(i);
}
for (int n : arr) {
arr.remove(n);
}
이런 문제는 보통 다음과 같이 삭제를 위한 리스트를 별도로 만들고 삭제요소들을 보관했다가 제거하는 형태로 해결 가능합니다.
List<Integer> removed = new ArrayList<>();
List<Integer> arr = new ArrayList<>();
for (int i = 0; i < 5; i++) {
arr.add(i);
}
for (int n : arr) {
removed.add(n);
}
arr.removeAll(removed);
reference
https://mattmk.tistory.com/71
댓글남기기