ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Java] subList 사용시 주의점 - ConcurrentModificationException 발생 이슈
    JAVA 2024. 2. 27. 14:21
    반응형

     

     

    LeetCode를 풀다가 ConcurrentModificationException가 발생했다.

    원인을 찾다가 subList를 잘못 사용하고 있다는 것을 깨달아서 정리해본 오늘의 이슈.

     

    subList(int fromIndex, int toIndex)

    subList 메서드는 리스트 컬렉션의 메서드로,
    일정 범위(fromIndex부터 toIndex - 1까지의 요소)까지 리스트 요소로 구성된 리스트를 반환한다.

     

    간단하게 원하는 범위만큼 리스트를 반환한다고 생각하면 된다.

    그렇다면 ArrayList 클래스의 subList를 구현부를 한번 살펴보자.

    public List<E> subList(int fromIndex, int toIndex) {
            subListRangeCheck(fromIndex, toIndex, size);
            return new SubList<>(this, fromIndex, toIndex);
        }

     

    파라미터로 받은 인덱스가 범위에 포함하는 지 확인하는 subListRangeCheck 메서드를 호출하고 SubList를 생성하여 반환한다.

    여기서 눈여겨 볼 점은 새 ArrayList를 생성하거나, 원본을 잘라서 변형해서 반환하는 것이 아니라 SubList 인스턴스로 반환한다는 점이다.

     

    SubList 클래스의 구현부를 살펴보면 원본 리스트를 참조하고 있고

    추가, 삭제 등 모든 동작을 원본 리스트인 root에 접근해서 하는 것을 확인할 수 있다.

    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;
            }
            
            ...
    
    		public void add(int index, E element) {
                rangeCheckForAdd(index);
                checkForComodification();
                root.add(offset + index, element);
                updateSizeAndModCount(1);
            }
    
            public E remove(int index) {
                Objects.checkIndex(index, size);
                checkForComodification();
                E result = root.remove(offset + index);
                updateSizeAndModCount(-1);
                return result;
            }
    
    }

     

     

     

    따라서 subList는 일정 범위의 새 리스트를 반환하는 것이 아니라,

    원본 리스트의 일정 범위 만큼 접근할 수 있는 SubList 인스턴스를 반환한다는 것.
    (이하 subList는 메서드를 호출해서 반환받은 리스트를 지칭)

     

    그렇기 때문에 당연히 subList를 변경한다면 원본 리스트도 변경된다.

    반대로 범위에 해당하는 원본 리스트를 한다면 subList는 더 이상 접근할 수 없고,
    접근한다면 ConcurrentModificationException가 발생한다.

    List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));
    List<Integer> subList = list.subList(0, 2);
    
    System.out.println(subList); //[1, 2]
    subList.remove(0); //subList 변경 
    System.out.println(list); //[2, 3] : 원본 리스트 변경 확인
    list.add(4); //원본 리스트 변경
    System.out.println(subList);//subList 접근시 ConcurrentModificationException 발생
    
    [참고] 
    The semantics of the list returned by this method become undefined if the backing list (i.e., this list) is structurally modified in any way other than via the returned list. (Structural modifications are those that change the size of this list, or otherwise perturb it in such a fashion that iterations in progress may yield incorrect results.)

     

     

     

     

    나는 당연히 원본 리스트와 다른 리스트 객체를 반환 받을 것이라고 생각했고,

    원본리스트를 수정하고 subList를 접근했기 때문에 ConcurrentModificationException가 발생한 것.

     

    나처럼 일정 범위의 리스트를 새 객체로 얻고자 한다면 아래와 같이 복사하여 사용해야 한다.

    List<Integer> subList = new ArrayList<>(list.subList(fromIndex, toIndex));
    

     

     

     

    정리

    • List 컬렉션의 subList 메서드는 원본 리스트를 참조하는 SubList 인스턴스를 반환한다.
      (이하 subList는 메서드를 호출해서 반환받은 리스트를 지칭)
    • subList를 변경하면 참조 중인 원본 리스트를 변경한다.
    • 원본 리스트를 요소 추가, 삭제 등 구조적인 변경을 할 경우 subList에 접근할 수 없다.
      - subList에 접근시 ConcurrentModificationException 발생
    • 일정 범위의 리스트를 얻고자 한다면 subList를 복사하여 새로 생성하여 사용해야 한다.

    'JAVA' 카테고리의 다른 글

    [JAVA] 객체 지향 프로그래밍(OOP)의 정의 및 특징  (0) 2020.11.10

    댓글