[JAVA] 반복문 안에서 컬렉션 요소 변경하기
반복문 안에서 컬렉션의 요소를 변경하는 것은 좋지 않다고 한다.
프로젝트 중에 컬렉션의 요소를 삭제하려고 하다가 원하는 결과가 나오지 않아서 코드를 수정했는데,
어떤 일인지 한번 확인을 해봐야 겠다는 생각이 들었다.
ArrayList를 통해서 반복문 안에서 요소를 삭제할 경우 어떤 문제가 있는지 알아보도록 하자.
우선 발생한 문제를 두가지로 구분하자면 아래와 같다.
인덱스를 기반으로 삭제할 경우 발생하는 문제
ArrayList<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
for (int i = 0; i < list.size(); i++) {
list.remove(i); // 현재 인덱스의 요소를 삭제
}
System.out.println(list);
반복문이 돌면서 해당 인덱스를 삭제하는 코드이다.
여기서 의도한 결과로 출력된 것은 [] 이다.
그렇지만 결과는 [2, 4]이다.
그 이유는 반복문 안에서 ArrayList의 인덱스를 기반으로 요소를 삭제할 경우
ArrayList가 요소를 삭제한 후 나머지 요소들을 한 칸씩 앞으로 이동시키기 때문에 원하는 결과가 나오지 않는다.
그럼 이 경우에는 어떻게 삭제하는 것이 좋을까?
ArrayList<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
for (int i = list.size() - 1; i >= 0; i--) {
list.remove(i); // 뒤에서부터 요소를 삭제
}
System.out.println(list);
첫번째 방법은 뒤에서 부터 요소를 삭제하는 것이다.
ArrayList는 맨 앞이나 중간의 요소를 삭제할 경우 이동되지만, 맨 뒤 요소를 삭제하면 이동할 필요가 없다.
따라서 뒤에서 부터 요소를 삭제하면 위와 같은 문제가 발생하지 않는다.
두번째 방법은 Iterator를 사용하는 것이다.
이 것은 ConcurrentModificationException예외와 함께 알아보도록 하자.
ConcurrentModificationException 예외
ArrayList<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
for (Integer value : list) {
if (value % 2 == 0) { // 짝수를 발견하면 삭제
list.remove(value);
}
}
System.out.println(list);
위의 코드는 foreach문을 사용하여 컬렉션을 순회하면서 요소의 값이 짝수이면 해당 요소를 삭제하는 코드이다.
여기서는 ConcurrentModificationException예외가 발생한다.
그 이유는 리스트를 순회하는 도중에 리스트 자체를 수정하면, 리스트의 구조적인 무결성이 깨지기 때문이라고 한다.
근데 구조적인 무결성이 대체 무엇인가..?
쉽게말해 '리스트의 구조(크기와 요소의 위치)를 바꾸지 않는 것'이다.
위에서 보듯이 반복문 내에서 리스트 요소를 삭제하니 의도하는 것과 다른 결과가 나왔다.
그 이유는 삭제(추가도 마찬가지로)로 인해 리스트의 크기와 각 요소의 위치가 바뀌었기 때문이다.
이렇게 되면, 원래 순회하려고 했던 요소들을 놓치거나, 예상하지 못한 요소를 접근하게 되기 때문에
ConcurrentModificationException예외가 발생하고 반복문 내에서는 요소를 추가하거나 삭제하지 말아야 하는 것이다.
그러면 다른 방법은 없을까?
Iterator를 사용하면 된다!
Iterator를 사용하여 ArrayList나 다른 컬렉션의 요소를 안전하게 삭제할 수 있는데,
Iterator의 remove 메서드는 컬렉션을 순회하는 동안 요소를 삭제할 때
ConcurrentModificationException을 피할 수 있게 해준다.
그럼 어떻게 Iterator가 컬렉션의 변경사항을 안전하게 해주는 것일까?
Iterator는 컬렉션의 수정 상태를 추적하고 변경을 감지처리를 하여 안전하게 수정가능하다.
ArrayList내부에는 modCount라는 필드가 존재하는데 이는, ArrayList내부의 구조적인 변경(예: 요소 추가, 삭제)의 횟수를 추적한다.
그리고 Iterator는 modCount를 사용하여 ArrayList의 변경 사항을 감지하고,
expectedModCount 필드에 Iterator 생성 시점의 modCount 값을 저장한다.
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
remove() 메서드를 살펴보면 요소를 삭제 하기 전에 checkForComodification() 를 통해
expectedModCount와 modCount를 비교하여 두 값이 다를 경우 ConcurrentModificationException를 발생시킨다.
따라서 Iterator는 컬렉션을 안전하게 순회할 수 있도록 도와준다.
ArrayList<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
Integer value = iterator.next();
if (value % 2 == 0) { // 조건에 따라 삭제 (예: 짝수 삭제)
iterator.remove();
}
}
System.out.println(list); // 짝수가 삭제된 리스트 출력
}
다시 앞으로 돌아와서 ConcurrentModificationException이 발생하던 코드를 Iterator를 이용하여 수정하였다.
이렇게 하게되면 해당 예외가 발생하지 않고 정상적으로 짝수만 삭제 되는 것을 확인할 수 있다.
정리하자면, 반복문 안에서 컬렉션의 구조는 변경하지 않는 것이 좋다.
구조가 변경 될 경우 발생하는 오류와, 의도하지 않는 결과가 발생할 수있기 때문이다.
만약 변경이 필요할 경우에는 Iterator를 이용하여 안전하게 변경해야한다.