반응형

바쁜 분은 이것만

  1. 조건에 맞는 것 지우기: list.removeIf(s -> s.equals("b"));
  2. 반복하면서 지워야 하면 Iterator의 it.remove()
  3. List.of(), Arrays.asList()로 만든 리스트는 new ArrayList<>(...)로 감싼 뒤 지우기

ArrayList에서 특정 값을 지우려고 for-each 안에서 remove()를 부르면 이런 에러가 납니다.

Exception in thread "main" java.util.ConcurrentModificationException
	at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1104)
	at java.base/java.util.ArrayList$Itr.next(ArrayList.java:1058)
	at Cme.main(Cme.java:5)

이 에러가 왜 나는지, 그리고 지우는 방법 다섯 가지를 실제로 실행해서 비교했습니다(OpenJDK 27). 예제는 모두 [a, b, b, c, b]에서 "b"를 전부 지우는 코드예요.

1. 에러가 나는 코드

List<String> list = new ArrayList<>(List.of("a", "b", "b", "c", "b"));
for (String s : list) {
    if (s.equals("b")) list.remove(s);
}

for-each는 내부적으로 Iterator를 써서 하나씩 꺼냅니다. Iterator는 꺼낼 때마다 "내가 모르는 사이에 리스트가 바뀌었나?"를 확인하는데, list.remove()로 직접 지우면 그 검사에 걸려서 예외를 던져요. 에러 메시지의 checkForComodification이 바로 그 검사입니다.

예외를 잡고 리스트를 찍어 보면 첫 번째 b만 지워진 상태에서 멈춰 있어요.

[a, b, c, b]

2. 더 위험한 경우: 에러 없이 끝나 버린다

List<String> list = new ArrayList<>(List.of("a", "b", "c"));
for (String s : list) {
    if (s.equals("b")) list.remove(s);
}
System.out.println(list);
[a, c]

이번엔 에러가 안 납니다. 뒤에서 두 번째 원소를 지우면 리스트 크기가 줄면서 반복이 "끝났다"고 판단해 다음 검사를 하지 않기 때문이에요. 결과가 맞아 보여서 넘어갔다가, 데이터가 바뀌면 그때 에러가 납니다. 이 방식은 통과하더라도 쓰지 마세요.

3. 인덱스 for문으로 앞에서부터 지우면 하나를 건너뛴다

for (int i = 0; i < list.size(); i++) {
    if (list.get(i).equals("b")) list.remove(i);
}
[a, b, c]

에러는 안 나지만 b가 하나 남았습니다. 1번 자리의 b를 지우면 뒤의 b가 1번으로 당겨지는데, i는 이미 2로 넘어가서 그 b를 못 보는 거예요. 연속된 값이 있을 때 생기는 문제라 테스트에서 잘 안 잡힙니다.

4. 제대로 지우는 방법 세 가지

① removeIf (자바 8 이상, 가장 간단)

list.removeIf(s -> s.equals("b"));
[a, c]

② Iterator로 직접 돌면서 it.remove()

Iterator<String> it = list.iterator();
while (it.hasNext()) {
    if (it.next().equals("b")) it.remove();
}
[a, c]

Iterator 자신을 통해 지우면 "내가 지웠다"는 걸 알기 때문에 검사에 걸리지 않습니다. 지우면서 다른 처리(로그 출력, 다른 리스트에 옮기기)도 해야 할 때 씁니다.

③ 인덱스 for문을 뒤에서부터

for (int i = list.size() - 1; i >= 0; i--) {
    if (list.get(i).equals("b")) list.remove(i);
}
[a, c]

뒤에서부터 지우면 당겨지는 원소가 이미 지나간 쪽이라 건너뛰는 일이 없어요.

5. UnsupportedOperationException이 나면

List<String> fixed = List.of("a", "b");
fixed.removeIf(s -> s.equals("b"));

List<String> asList = Arrays.asList("a", "b", "c");
asList.remove("b");
java.lang.UnsupportedOperationException
java.lang.UnsupportedOperationException: remove

List.of()는 아예 바꿀 수 없는 리스트, Arrays.asList()는 크기를 바꿀 수 없는 리스트를 만듭니다. 지우거나 추가하려면 ArrayList로 복사하세요.

List<String> list = new ArrayList<>(List.of("a", "b"));

덤: List<Integer>에서 remove(1)의 함정

List<Integer> nums = new ArrayList<>(List.of(10, 20, 30));
nums.remove(1);
System.out.println(nums);   // [10, 30]

nums = new ArrayList<>(List.of(10, 20, 30));
nums.remove(Integer.valueOf(10));
System.out.println(nums);   // [20, 30]

숫자 리스트에서 remove(1)은 "값 1"이 아니라 1번 인덱스를 지웁니다. 값으로 지우려면 Integer.valueOf()로 감싸야 해요.

정리

방법 [a, b, b, c, b]에서 b 삭제 결과
for-each + list.remove() ConcurrentModificationException
인덱스 for 앞에서부터 [a, b, c] ← b가 남음
인덱스 for 뒤에서부터 [a, c]
Iterator.remove() [a, c]
removeIf [a, c]

참고: Oracle Java SE API 문서 — ArrayList, ConcurrentModificationException, Collection.removeIf, List.of, Arrays.asList

반응형

+ Recent posts