Logo

[자바] 컬렉션에서 원소 삭제하기 (ConcurrentModificationException 피하면서)

리스트를 순회하면서 특정 원소를 삭제하고 싶을 때가 있습니다.

예를 들어, 다음과 같이 알파벳과 숫자가 섞여있는 리스트가 있다고 가정해봅시다.

List<Character> letters = new ArrayList<>();
letters.addAll(Arrays.asList('A', 'B', '1', '2', 'C', 'D', '3', 'E', '4', '5'));

저는 이 리스트에서 숫자인 원소들은 모두 삭제하고 싶습니다.

ConcurrentModificationException 발생

가장 먼저 떠오르는 방법은 boolean remove(Object o) 메소드를 사용하는 것입니다. for 루프를 돌면서 해당 원소가 숫자인지 체크 후에 숫자이면 remove 메소드를 호출합니다.

for (Character letter : letters) {
  if (Character.isDigit(letter)) {
    letters.remove(letter);
  }
}

하지만 위 코드는 다음과 같은 ConcurrentModificationException 예외를 일으킵니다.

Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
	at java.util.ArrayList$Itr.next(ArrayList.java:851)
(...)

인덱스 기반 for 문으로 해결 시도

이상하네요. 향상된 for 문 대신에, 인덱스를 사용하는 for 문으로 바꿔서 루프를 돌려보겠습니다.

for (int i = 0; i < letters.size(); i++) {
  Character letter = letters.get(i);
  if (Character.isDigit(letter)) {
    letters.remove(i); // 또는 letters.remove(letter);
  }
}

이 번에는 예외는 발생하지 않는데… 리스트를 출력해보면 원하는대로 숫자 원소가 모두 삭제되지 않았음을 확인할 수 있습니다.

System.out.println(letters);
[A, B, 2, C, D, E, 5]

왜 이런 일이 발생하는 걸까요? 원인은 리스트에서 i 번째 원소 삭제되면, i + 1 번째 원소가 그 자리에 오게되고, 리스트의 길이가 1만큼 짧아지는데서 찾을 수 있습니다.

예를 들어, 1이 삭제되면 그 자리에 2가 오게 되므로, for 문은 2를 건너뛰고 바로 C를 체크하게 됩니다.

이터레이터로 해결

자바의 Iterator 인터페이스는 remove 메소드를 제공하고 있는데요. 자바 공식 문서를 보면 이 메소드를 사용하는 것이 컬렉션을 순회하면서 원소를 삭제할 수 있는 유일하게 안전한 방법이라고 가이드하고 있습니다.

Note that Iterator.remove is the only safe way to modify a collection during iteration; the behavior is unspecified if the underlying collection is modified in any other way while the iteration is in progress.

for (Iterator<Character> iter = letters.iterator(); iter.hasNext(); ) {
  Character letter = iter.next();
  if (Character.isDigit(letter)) {
    iter.remove();
  }
}

가이드 대로 위와 같이 이터레이터를 이용하여 코드를 작성하면 원하던 대로 리스트에서 숫자 원소들이 삭제되는 것을 확인할 수 있습니다.

자바8 에서는…

Collection 인터페이스에 removeIf 메소드가 추가되어 다음과 같이 한 줄의 코드면 충분합니다. :)

letters.removeIf(Character::isDigit);

추가로 원본 리스트에 변경을 가하지 않고, 숫자 원소가 삭제된 새로운 리스트를 얻고 싶은 경우, 다음과 같이 스트림 API를 사용할 수 있습니다.

List<Character> alphabets = letters.stream()
  .filter(Character::isAlphabetic)
  .collect(Collectors.toList());

이상으로 자바의 리스트로 부터 특정 원소를 삭제하는 몇 가지 방법을 살펴보았습니다.

참고