가장 먼저 떠오르는 방법은 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를 사용할 수 있습니다.