반복하는 동안 컬렉션에서 요소 제거


215

AFAIK에는 두 가지 접근 방식이 있습니다.

  1. 컬렉션의 사본을 반복
  2. 실제 콜렉션의 반복자를 사용하십시오.

예를 들어

List<Foo> fooListCopy = new ArrayList<Foo>(fooList);
for(Foo foo : fooListCopy){
    // modify actual fooList
}

Iterator<Foo> itr = fooList.iterator();
while(itr.hasNext()){
    // modify actual fooList using itr.remove()
}

한 가지 접근 방식을 다른 접근 방식보다 선호해야하는 이유가 있습니까 (예 : 가독성의 간단한 이유로 첫 번째 접근 방식을 선호)?


1
궁금한 점이 있습니다. 첫 번째 예에서 단순히 foolist를 반복하지 않고 foolist의 사본을 작성하는 이유는 무엇입니까?
Haz

@Haz, 그래서 한 번만 반복하면됩니다.
user1329572

15
참고 : 변수 범위를 제한하려면 반복자와 함께 'while'보다 'for'를 선호하십시오. for (Iterator <Foo> itr = fooList.iterator (); itr.hasNext ();) {}
Puce

내가 알고하지 않았다 while보다 다양한 범위 지정 규칙을했다for
알렉산더 밀스

보다 복잡한 상황 fooList에서 인스턴스 변수가있는 경우 루프와 동일한 클래스에서 다른 메소드를 호출하는 루프 동안 메소드를 호출 할 수 fooList.remove(obj)있습니다. 이런 일이 발생했습니다. 이 경우 목록을 복사하는 것이 가장 안전합니다.
Dave Griffiths

답변:


419

을 피하기위한 몇 가지 대안으로 몇 가지 예를 드리겠습니다 ConcurrentModificationException.

우리가 다음과 같은 책을 가지고 있다고 가정 해보십시오.

List<Book> books = new ArrayList<Book>();
books.add(new Book(new ISBN("0-201-63361-2")));
books.add(new Book(new ISBN("0-201-63361-3")));
books.add(new Book(new ISBN("0-201-63361-4")));

수집 및 제거

첫 번째 기술은 삭제하려는 모든 객체를 수집하는 것입니다 (예 : 향상된 for 루프 사용). 반복을 마치면 찾은 모든 객체를 제거합니다.

ISBN isbn = new ISBN("0-201-63361-2");
List<Book> found = new ArrayList<Book>();
for(Book book : books){
    if(book.getIsbn().equals(isbn)){
        found.add(book);
    }
}
books.removeAll(found);

이것은 당신이하고 싶은 조작이 "삭제"라고 가정합니다.

이 방법을 "추가"하려는 경우에도 효과가 있지만 다른 컬렉션을 반복하여 두 번째 컬렉션에 추가 할 요소를 결정한 다음 addAll마지막에 메소드 를 발행한다고 가정합니다 .

ListIterator 사용

리스트로 작업하는 경우 ListIterator, 반복 기법 중에 항목을 제거하고 추가 할 수 있는 기능을 사용하는 다른 기술이 사용 됩니다.

ListIterator<Book> iter = books.listIterator();
while(iter.hasNext()){
    if(iter.next().getIsbn().equals(isbn)){
        iter.remove();
    }
}

다시 한 번, 위 예제에서 "remove"메소드를 사용했는데, 이는 질문에 암시 된 것처럼 보이지만이 add메소드를 사용하여 반복 중에 새 요소를 추가 할 수도 있습니다 .

JDK 사용> = 8

Java 8 이상 버전을 사용하는 사용자에게는이를 활용할 수있는 몇 가지 다른 기술이 있습니다.

기본 클래스 removeIf에서 새 메소드를 사용할 수 있습니다 Collection.

ISBN other = new ISBN("0-201-63361-2");
books.removeIf(b -> b.getIsbn().equals(other));

또는 새로운 스트림 API를 사용하십시오.

ISBN other = new ISBN("0-201-63361-2");
List<Book> filtered = books.stream()
                           .filter(b -> b.getIsbn().equals(other))
                           .collect(Collectors.toList());

이 마지막 경우, 콜렉션에서 요소를 필터링하려면 원래 참조를 필터링 된 콜렉션 ( books = filtered)에 재 지정 하거나 필터링 된 콜렉션을 removeAll원래 콜렉션 (즉 books.removeAll(filtered)) 에서 찾은 요소에 사용하십시오 .

하위 목록 또는 하위 집합 사용

다른 대안도 있습니다. 목록이 정렬되어 있고 연속 요소를 제거하려는 경우 하위 목록을 작성하고 지울 수 있습니다.

books.subList(0,5).clear();

서브리스트는 원래리스트에 의해 지원되므로이 서브 요소 콜렉션을 제거하는 효율적인 방법입니다.

NavigableSet.subSet방법을 사용하여 정렬 된 세트 또는 여기에 제공된 슬라이싱 방법을 사용하여 유사한 것을 얻을 수 있습니다 .

고려 사항 :

사용하려는 방법은 수행하려는 작업에 따라 다를 수 있습니다

  • 수집 및 removeAl기술은 모든 컬렉션 (컬렉션, 목록, 세트 등)과 함께 작동합니다.
  • ListIterator기술은 분명히 그들의 주어진 것을 제공,리스트와 함께 작동 ListIterator구현 이벤트 추가 및 제거 작업에 지원합니다.
  • Iterator접근 방식은 모든 유형의 컬렉션에서 작동하지만 제거 작업 만 지원합니다.
  • ListIterator/ Iterator접근 방식을 사용하면 반복 할 때 제거하기 때문에 아무것도 복사 할 필요가 없습니다. 따라서 이것은 매우 효율적입니다.
  • JDK 8 스트림 예제는 실제로 아무것도 제거하지 않았지만 원하는 요소를 찾은 다음 원래 컬렉션 참조를 새 것으로 교체하고 이전 컬렉션을 가비지 수집합니다. 따라서 컬렉션을 한 번만 반복하면 효율적입니다.
  • 수집 및 removeAll접근 방식의 단점은 두 번 반복해야한다는 것입니다. 먼저 제거 루프에서 제거 기준과 일치하는 객체를 찾기 위해 루프에서 반복하고, 일단 찾은 후에는 원래 컬렉션에서 객체를 제거하도록 요청합니다. 이는 다음 항목을 찾기 위해이 항목을 찾기위한 두 번째 반복 작업을 의미합니다. 제거하십시오.
  • Iterator인터페이스 의 remove 메소드가 Javadocs에서 "선택적"으로 표시 된다는 것을 언급 할 가치가 있다고 생각합니다 . 이는 remove 메소드를 호출하면 Iterator구현 이 발생할 수 있음을 의미합니다 UnsupportedOperationException. 따라서 요소 제거를위한 반복자 지원을 보장 할 수없는 경우이 방법이 다른 방법보다 안전하지 않다고 말하고 싶습니다.

브라보! 이것은 확실한 가이드입니다.
Magno C

이것은 완벽한 답변입니다! 감사합니다.
Wilhelm

7
JDK8 스트림에 대한 단락에서 언급했습니다 removeAll(filtered). 이에 대한 지름길은removeIf(b -> b.getIsbn().equals(other))
ifloop

Iterator와 ListIterator의 차이점은 무엇입니까?
Alexander Mills

removeIf를 고려하지 않았지만 그것은 나의기도에 대한 답이었습니다. 감사!
Akabelle

15

Java 8에는 또 다른 접근법이 있습니다. 컬렉션 #removeIf

예 :

List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);

list.removeIf(i -> i > 2);

13

한 가지 접근법을 다른 접근법보다 선호하는 이유가 있습니까?

첫 번째 방법은 효과가 있지만 목록을 복사하는 명백한 오버 헤드가 있습니다.

많은 컨테이너가 반복 중에 수정을 허용하지 않기 때문에 두 번째 방법은 작동하지 않습니다. 이 포함됩니다ArrayList .

유일한 수정은 현재 요소를 제거하는 경우, 당신은 사용하여 두 번째 방법 작업을 할 수 있습니다 itr.remove()(즉, 사용하는 반복자를remove()방법이 아닌 용기 의). 이것은 지원하는 반복자에 대해 내가 선호하는 방법입니다 remove().


죄송합니다 ... 컨테이너가 아닌 반복자의 remove 메소드를 사용한다는 것을 암시합니다. 그리고 목록을 복사하면 얼마나 많은 오버 헤드가 발생합니까? 그것은 많을 수 없으며 메소드의 범위에 속하기 때문에 가비지 수집이 더 빨리 이루어져야합니다. 편집을 참조하십시오.
user1329572

1
@aix Iterator인터페이스 의 remove 메소드가 Javadocs에서 선택적으로 표시되어 있다고 언급 할 가치가 있다고 생각합니다 . 이는 Iterator 구현이 발생할 수 있음을 의미합니다 UnsupportedOperationException. 따라서이 방법은 첫 번째 방법보다 안전하지 않습니다. 사용하려는 구현에 따라 첫 번째 방법이 더 적합 할 수 있습니다.
Edwin Dalorzo

docs.oracle.com/javase/7/docs/api/java/util/… : remove()원본 컬렉션 자체의 @EdwinDalorzo 도 발생할 수 있습니다 . 안타깝게도 Java 컨테이너 인터페이스는 매우 신뢰할 수없는 것으로 정의되었습니다 (정말로 인터페이스의 요점을 무시 함). 런타임에 사용될 정확한 구현을 모르는 경우 변경 불가능한 방식으로 작업하는 것이 좋습니다. 예를 들어, Java 8+ Streams API를 사용하여 요소를 필터링하고 새 컨테이너에 수집 한 다음 오래된 것을 완전히 대체하십시오. UnsupportedOperationException
Matthew 읽기

5

두 번째 접근 방식 만 작동합니다. 반복하는 동안 iterator.remove()만 콜렉션을 수정할 수 있습니다 . 다른 모든 시도는 원인이 ConcurrentModificationException됩니다.


2
첫 번째 시도는 복사본을 반복하므로 원본을 수정할 수 있습니다.
Colin D

3

이전 타이머 즐겨 찾기 (아직 작동) :

List<String> list;

for(int i = list.size() - 1; i >= 0; --i) 
{
        if(list.get(i).contains("bad"))
        {
                list.remove(i);
        }
}

1

당신이 사용하는 경우에도 때문에, 두 번째 할 수 없습니다 remove()에 대한 방법 Iterator를 , 당신이 던져 예외를 얻을 수 있습니다 .

개인적으로, 나는 Collection새로운 인스턴스 생성에 대한 추가 소식에도 불구하고 모든 인스턴스에 대해 첫 번째를 선호하지만 Collection다른 개발자가 편집하는 동안 오류가 발생하기 쉽습니다. 일부 Collection 구현에서는 Iterator remove()가 지원되지만 그렇지 않은 경우에는 Iterator 가 지원됩니다. Iterator 관련 문서에서 더 많은 내용을 읽을 수 있습니다 .

세 번째 대안은 새를 만드는 것입니다 Collection원래 이상 반복, 그리고 첫 번째의 모든 구성원을 추가 Collection두 번째에 Collection있습니다 없습니다 까지 삭제. Collection삭제 크기 및 삭제 수에 따라 첫 번째 방법과 비교할 때 메모리를 크게 절약 할 수 있습니다.


0

메모리 사본을 할 필요가 없으므로 Iterator가 더 빨리 작동하므로 두 번째를 선택합니다. 따라서 메모리와 시간을 절약 할 수 있습니다.


" 반복기가 더 빨리 작동합니다 ". 이 주장을 뒷받침 할만한 것이 있습니까? 또한 목록의 복사본을 만드는 메모리 공간은 매우 사소한 데, 특히 메서드 내에서 범위가 지정되고 거의 즉시 가비지 수집되기 때문에 매우 중요합니다.
user1329572

1
첫 번째 접근 방식의 단점은 두 번 반복해야한다는 것입니다. 우리는 요소를 찾는 루프에서 반복하고, 일단 그것을 찾으면, 원래 목록에서 제거하도록 요청합니다. 이것은 주어진 항목을 찾기 위해 두 번째 반복 작업을 의미합니다. 이것은 적어도이 경우 반복자 접근이 더 빨라야한다는 주장을지지 할 것입니다. 컬렉션의 구조 공간 만 만들어지고 컬렉션 내부의 개체는 복사되지 않습니다. 두 컬렉션 모두 동일한 객체에 대한 참조를 유지합니다. GC가 발생하면 말할 수 없습니다 !!!
Edwin Dalorzo

-2

왜 안돼?

for( int i = 0; i < Foo.size(); i++ )
{
   if( Foo.get(i).equals( some test ) )
   {
      Foo.remove(i);
   }
}

그리고 목록이 아닌 맵 인 경우 keyset ()


4
이 방법에는 많은 단점이 있습니다. 먼저 요소를 제거 할 때마다 인덱스가 재구성됩니다. 따라서 요소 0을 제거하면 요소 1이 새 요소 0이됩니다.이 작업을 수행하려면 최소한이 문제를 피하기 위해 거꾸로하십시오. 둘째, 모든 List 구현이 요소에 대한 직접 액세스를 제공하지는 않습니다 (ArrayList처럼). LinkedList에서는 이것을 발행 할 때마다에 get(i)도달 할 때까지 모든 노드를 방문해야하기 때문에 이는 매우 비효율적 i입니다.
Edwin Dalorzo

내가 일반적으로 찾고 있던 단일 항목을 제거하는 데 사용했기 때문에 이것을 고려하지 않았습니다. 알아 둘만 한.
Drake Clarris

4
나는 파티에 늦었지만 반드시 Foo.remove(i);해야 한다면 if 블록 코드에 있어야합니까 i--;?
Bertie Wheen

그것이 버그이기 때문에
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.