이 문서에서는 Java의 collection에서 원소를 삭제하는 것 관련내용을 정리합니다.
Java개발을 하다보면, 리스트를 순회하면서 특정 원소를 삭제하고 싶을 때가 있습니다. 예를 들어, 다음과 같이 알파벳과 숫자가 섞여있는 리스트가 있다고 가정해봅시다.
List<Character> letters = new ArrayList<>();
letters.addAll(Arrays.asList('A', 'B', '1', '2', 'C', 'D', '3', 'E', '4', '5'));
저는 이 리스트에서 숫자만 찾아서 모두 삭제하려고 한다는걸 가정합니다.
ConcurrentModificationException 발생
Collection에서 원소를 지우는 것은 boolean remove(Object o)
메소드를 사용하면 됩니다.
Collection 의 각 원소에 대해서 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)
...
이해를 위해서 개념적으로만 접근하자면, 해당 element 기준으로 for 문을 돌고 있을 때, 해당 엘리먼트를 Collection에서 삭제해 버리면, 아직 for문을 빠져나가기 전이고, 그래서 해당 element를 기준으로 돌고 있는데, 삭제되었다고 생각하여 문제를 일으키게 되는 것입니다.
인덱스 기반 for 문으로 해결 시도
그럼, 위 처럼 element기준으로 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);
}
}
이번에는 Exception은 발생하지 않는데, 리스트를 출력해보면 원하는대로 숫자 원소가 모두 삭제되지 않았음을 확인할 수 있습니다.
System.out.println(letters);
[A, B, 2, C, D, E, 5]
왜 이런 일이 발생하는 걸까요? 원인은 리스트에서 i 번째 원소 삭제되면, i + 1 번째 원소가 그 자리에 오게되고, 리스트의 길이가 1만큼 짧아지는데서 찾을 수 있습니다. 예를 들어, 1이 삭제되면 그 자리에 2가 오게 되므로, for 문은 2를 건너뛰고 바로 C를 체크하게 됩니다.
이런 문제를 해결하기 위해서, 삭제하고 나면 내 자리에 다음 element가 오니까, 아래와 같이 해결하기도 합니다. 하지만 온전히 동작하기엔 조금 이상한 부분은 있습니다. 개념적으로만 이해하세요.
for (int i = 0; i < letters.size(); i++) {
Character letter = letters.get(i);
if (Character.isDigit(letter)) {
letters.remove(i);
i--;
}
}
이터레이터로 해결
자바의 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를 사용할 수 있습니다. 하지만 이렇게 하면 하나의 Collection을 재 생성하기 때문에 크게 권장하는 방법은 아닐 수 있습니다. (필요에 따라서 선택!)
List<Character> alphabets = letters.stream()
.filter(Character::isAlphabetic)
.collect(Collectors.toList());
참고자료 및 출처
- https://www.daleseo.com/how-to-remove-from-list-in-java/
- https://docs.oracle.com/javase/8/docs/api/java/util/Iterator.html#remove--
- https://docs.oracle.com/javase/8/docs/api/java/util/Collection.html#removeIf-java.util.function.Predicate-
Leave a comment