개발 학습 실습실

이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.

공부 › Java › 컬렉션

순회 중 삭제

반복문 안에서 원소를 지우면

리스트 [1, 2, 2, 3, 4]를 돌면서 값이 2인 원소를 지우려 합니다. 지우는 방법에 따라 예외가 나기도 하고, 예외 없이 원소 하나를 건너뛰어 버리기도 합니다.

실행으로 대조 · JDK 21에서 열 가지 조합을 실행해 대조

이 실습에 나오는 개념

fail-fast 반복자
컬렉션을 순회하는 도중에 그 컬렉션이 구조적으로 변경되면 ConcurrentModificationException을 던지는 반복자다(감지는 최선의 노력 수준이라 항상 던져진다는 보장은 없다). 변경된 줄 모르고 잘못된 결과로 계속 진행하는 것보다 즉시 실패시켜 문제를 드러내기 위한 설계다. 순회 중 삭제는 반복자의 remove()나 removeIf()를 사용한다.
ArrayList
내부에 배열을 두고 가득 차면 더 큰 배열로 옮겨 담는 리스트다. 인덱스로 원소에 접근하는 속도가 O(1)로 빠르고, 끝에 추가하는 것도 분할 상환으로 O(1)이다. 중간에 삽입하거나 삭제하면 뒤의 원소들을 한 칸씩 옮겨야 해서 O(n)이 걸린다.
ConcurrentHashMap
여러 스레드가 동시에 사용해도 안전한 해시 맵이다. 맵 전체에 락을 거는 대신 버킷 단위로 제어해 동시성이 높고, 조회는 락 없이 수행된다. null 키와 null 값을 허용하지 않는다. "없으면 넣기" 같은 복합 동작은 putIfAbsent, compute처럼 원자성이 보장된 메서드를 써야 한다.
스레드 안전(Thread-safe)
여러 스레드가 동시에 사용해도 올바르게 동작하는 성질이다. 공유하는 상태가 없거나, 상태가 불변이거나, 접근이 적절히 동기화되어 있으면 스레드 안전하다. 스프링의 싱글톤 빈은 여러 요청 스레드가 함께 사용하므로 스레드 안전하게(보통 상태를 갖지 않게) 만들어야 한다.

이렇게 바꿔 보세요

처음 설정으로 실행하면

  1. 리스트는 [1, 2, 2, 3, 4]입니다. 값이 2인 원소 두 개를 지우려 합니다.
  2. 반복자를 만듭니다(향상된 for문도 내부적으로 반복자를 씁니다). 반복자는 이 시점의 리스트 수정 횟수(0)를 기억해 둡니다.
  3. 원소 1 확인: 2가 아니므로 넘어갑니다.
  4. 원소 2를 읽었습니다. list.remove()로 리스트에서 직접 지웁니다. 리스트는 [1, 2, 3, 4]가 됐습니다. 리스트의 수정 횟수가 1로 올라갔지만 반복자는 이를 모릅니다.
  5. 다음 원소를 읽으려는데, 리스트의 수정 횟수(1)가 반복자가 기억한 값(0)과 다릅니다. 반복자가 모르는 사이에 리스트가 바뀌었다는 뜻이므로, 잘못된 결과를 내느니 즉시 ConcurrentModificationException을 던집니다(fail-fast).
  6. 최종 리스트: [1, 2, 3, 4] (예외로 중단됨).

실행 결과

코드

List<Integer> list = new ArrayList<>(List.of(1, 2, 2, 3, 4));

for (Integer x : list) {
    if (x == 2) list.remove(x);          // 순회 중에 리스트를 직접 수정
}

이 모형의 전제

열 가지 조합의 최종 리스트와 예외를 이 PC의 JDK 21에서 같은 코드를 실행한 결과와 대조했습니다. 향상된 for문에서의 삭제는 지우는 위치에 따라 예외 없이 끝나는 경우도 있는데(끝에서 두 번째 원소를 지울 때), 운에 기대는 코드이므로 쓰면 안 됩니다.

앞 실습: 스택·큐·우선순위 큐 · 다음 실습: 정렬과 Comparator

이 실습에 나오는 개념

이 실습에서 보는 것

설정

코드


      
    

그림

실행 비교

이렇게 바꿔 보세요