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

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

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

1
2
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 메소드를 호출합니다.

1
2
3
4
5
for (Character letter : letters) {
if (Character.isDigit(letter)) {
letters.remove(letter);
}
}

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

1
2
3
4
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 문으로 바꿔서 루프를 돌려보겠습니다.

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

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

1
System.out.println(letters);
1
[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.

1
2
3
4
5
6
for (Iterator<Character> iter = letters.iterator(); iter.hasNext(); ) {
Character letter = iter.next();
if (Character.isDigit(letter)) {
iter.remove();
}
}

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

자바8 에서는…

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

1
letters.removeIf(Character::isDigit);

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

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

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

참고

공유하기