'자바 프로그래머가 자주 실수하는 실수 10가지' 에도 나와 있는 내용입니다.
콜렉션 클래스들은 저장된 객체들에 대한 순차적 접근을 제공합니다.
주의할 점이 있습니다.
반복자(Iterator)가 콜렉션(Collection)을 순회하는 도중에 콜렉션에 대한 변경이 일어날 경우 순차적인 접근은 실패하고 ConcurrentModificationException 이 발생하는데, 이를 Fail-Fast 방식이라고 합니다.
자세한 내용은 Fail Fast vs Fail Safe 이 글을 참고하여 주시기 바랍니다.
안전하지 않은 행위를 함으로써 생길 수 있는 위험으로부터 데이터의 무결성을 지키기위해, 반복자의 탐색을 실패하여 예외를 발생시킵니다. Iterator의 경우 Fail-Fast 이므로 콜렉션의 순차적인 접근은 실패하고 ConcurrentModificationException 를 발생하고 반면에 Enumeration 은 Fail-Safe 로서 실패하더라도 끝까지 순차적인 접근을 합니다. 이는 주로 멀티쓰레드나 이벤트 방식의 모델에서 나타날 수 있는 상황입니다.
예시 ) 반복문 안에서 특정 요소를 제거하는 경우
ArrayList<String> fruitList = new ArrayList<String> (Arrays.asList("apple", "lime", "plum", "orange"));
for (String fruit : fruitList) {
if (fruit.equals("apple"))
fruitList.remove(fruit);
}
// ConcurrentModificationException 발생
여기서 ConcurrentModificationException 의 원인은 뭘까요?
foreach 도 내부적으로 Iterator 를 사용합니다.
위에서 fruitList 에서 직접적으로 remove 를 하였습니다. Iterator 는 원본 콜렉션을 사용하는데, 데이터의 변경이 생겼기때문에 ConcurrentModificationException 이 발생합니다.
올바르게 작동하기 위해서는 아래와 같이 Iterator 를 사용해야합니다.
ArrayList<String> fruitList
= new ArrayList<String>(Arrays.asList("apple", "lime", "plum", "orange"));
Iterator<String> iterator = fruitList.iterator();
while (iterator.hasNext()) {
String fruit = iterator.next();
if ("apple".equals(fruit)) {
iterator.remove();
}
}
// output : [lime, plum, orange]
반드시 .remove() 전에 .next() 가 호출되어야 합니다.
ArrayList.iterator() 의 내부적으로보면 remove()와 next() 에서 checkForComodification() 을 통해 원본 데이터의 변화 여부 체크하는데, remove()가 먼저 호출될 경우 이 과정에서 ConcurrentModificationException 이 발생할 것입니다.