Enhanced For 문에서 remove를 하면 안되는 이유

진환·2023년 12월 27일
0

remove를 사용하게 되면

Enhanced For 문에서 ArrayList의 remove 메서드를 사용하게 되면 아래와 같은 예외를 마주칠 수 있다.

exception

Enhanced For 문에서 remove 메서드를 사용하면 왜 예외가 발생하는지 알아보자.


Enhanced For 문을 사용하려면

Enhanced For 문을 사용하기 위해서는 객체가 Iterable 인터페이스를 구현해야 한다.

public interface Iterable<T> {

	Iterator<T> iterator();
    
    default void forEach(Consumer<? super T> action) {
    	Objects.requireNonNull(action);
        for (T t : this) {
        	action.accept(t);
        }
    }
    
    default Spliterator<T> spliterator() {
    	return Spliterators.spliteratorUnknownSize(iterator(), 0);
    }
}

Enhanced For 문을 사용하면 우선 iterator() 메서드를 호출하게 된다.
iterator() 메서드는 그저 Iterator 객체를 반환하면 된다.

public interface Iterator<E> {

	boolean hasNext();
    
    E next();
    
    default void remove() {
    	throw new UnsupportedOperationException("remove");
    }
    
    default void forEachRemaining(Consumer<? super E> action) {
    	Objects.requireNonNull(action);
        while (hasNext())
        	action.accept(next());
    }
}

Enhanced For 문이 진행되는 과정을 보면 아래와 같다.

  1. 해당 객체의 iterator() 메서드를 호출한다.
  2. 위의 메서드에서 반환된 Iterator 객체의 hasNext() 메서드의 반환값이 false면 for문이 종료되고 true면 다음 단계로 진행한다.
  3. next() 메서드를 호출하여 원소를 반환한다.
  4. Enhanced For 문의 몸체의 로직을 수행한다.
  5. 2번부터 다시 진행한다.

ArrayList

remove() 메서드를 호출했을 때 예외가 발생하는 이유를 알기 위해서 ArrayList를 대표로 알아보자.

arraylist

ArrayList 클래스는 위와 같이 구성되어 있다.
ArrayList 클래스는 Iterable 인터페이스를 구현하고, AbstractList를 상속받는다는 것을 기억하자.
우선 ArrayList의 iterator() 메서드를 보자.

public Iterator<E> iterator() {
	return new Itr();
}

Enhanced For 문이 선언되는 시점에 해당 메서드가 호출이 되어 Itr (ArrayList의 내부 클래스) 객체를 반환하는 것이다.

그런 다음 hasNext()와 next() 메서드를 호출한다.

아래는 Itr 클래스의 hasNext()와 next() 메서드 이다.

method

hasNext() 메서드에서 cursor 와 size가 같지 않으면 true를 반환한다.
여기서 cursor 는 다음 원소의 인덱스, size는 현재 ArrayList의 크기(원소의 개수)를 뜻한다.

next() 메서드에서는 cursor를 이용해 다음 원소를 반환한다.
메서드를 보면 cursor가 elementData의 길이보다 크거나 같으면 ConcurrentModificationException 예외를 발생시키는 것을 볼 수 있다.


transient Object[] elementData; // ArrayList의 원소들이 실제로 저장되는 배열

if (i >= elementData.length) {
	throw new ConcurrentModificationException();
}

그렇다면 remove 메서드를 사용하여 원소의 수를 줄여 cursor보다 원소들의 수가 적으면 ConcurrentModificationException 예외가 발생하는 것이라고 생각할 수 있다.
하지만 아니다.
예외가 발생하는 위치를 보면 next() 메서드의 첫 줄인 checkForComodification() 메서드에서 예외가 발생한다.


예외가 발생하는 이유

checkForComodification() 메서드는 아래와 같다.

checkforcomodification

modCount와 expectedModCount가 같지 않으면 예외가 발생하는 것을 볼 수 있다.
그렇다면 modCount와 expectedModCount는 무엇일까

modCount

expectedModCount는 Itr 클래스에 선언되어 있는데 그저 modCount의 값을 복사한다.
modCount는 ArrayList가 상속받는 AbstractList에 선언되어 있고 주석에 이렇게 적혀있다.

The number of times this list has been structurally modified.

해석해 보면 구조적으로 변경된 횟수를 뜻한다.
ArrayList 객체에 구조적인 변경이 있을 경우 modCount의 값이 증가하게 된다.

Enhanced For 문을 사용하여 Itr 객체를 생성하면 현재 ArrayList의 구조적으로 변경된 횟수인 modCount를 expectedModCount라는 이름으로 저장하고 해당 ArrayList 객체가 구조적인 변경이 있을 경우에 modCount가 증가하여 ConcurrentModificationException 예외가 발생하는 것이다.

modCount가 증가하는 메서드는 아래와 같다.

trimToSize()
ensureCapacity()
add()
addAll()
remove()
clear()
removeIf()
replaceAll()
sort()

remove() 메서드를 사용하여 원소를 제거하는 것 뿐만 아니라 원소의 추가, 정렬 등도 구조적인 변경이 있으므로 예외가 발생한다는 것을 알 수 있다.
ArrayList의 원소들이 저장된 elementData 배열에 변경이 일어나면 Enhanced For 문이 제대로 작동할 수 없기 때문에 예외가 발생하는 것이다.

ArrayList 뿐만 아니라 다른 컬렉션들도 예외가 발생한다.(LinkedList, PriorityQueue, HashSet, ...)

profile
끄적끄적

0개의 댓글