反復中にコレクションから要素を削除する


215

私の知る限り、2つのアプローチがあります。

  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

@ハズ、だから私は一度だけループする必要があります。
user1329572

15
注:変数のスコープを制限するには、イテレータでも 'while'より 'for'を優先します:for(Iterator <Foo> itr = fooList.iterator(); itr.hasNext();){}
Puce

whileスコープ規則が異なることを知りませんでしたfor
Alexander Mills

より複雑な状況でfooListは、がインスタンス変数であり、ループ中にメソッドを呼び出す場合があります。ループ中に、同じクラスの別のメソッドが呼び出されfooList.remove(obj)ます。これが起こるのを見てきました。その場合、リストをコピーするのが最も安全です。
デイブグリフィス

回答:


416

を回避するためのいくつかの代替例をいくつか紹介し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);

これは、実行する操作が「削除」であると想定しています。

このアプローチも「追加」したい場合も機能しますが、別のコレクションを繰り返し処理して、2番目のコレクションに追加する要素を決定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以上のバージョンを使用している人のために、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ストリームの例では実際には何も削除されていませんが、目的の要素を探してから、元のコレクション参照を新しいものに置き換え、古いものをガベージコレクションしました。したがって、コレクションに対して繰り返し処理を行うのは1回だけであり、効率的です。
  • 収集とremoveAllアプローチの欠点は、2回繰り返す必要があることです。最初にfoor-loopで反復して、削除基準に一致するオブジェクトを探します。オブジェクトが見つかったら、元のコレクションから削除するように要求します。これは、このアイテムを探すための2回目の反復作業を意味します。それを除く。
  • IteratorインターフェースのremoveメソッドがJavadocsで「オプション」としてマークされていることは言及する価値があると思います。つまり、removeメソッドを呼び出すIteratorとスローされる実装が存在する可能性があるということUnsupportedOperationExceptionです。したがって、要素の削除に対するイテレータのサポートを保証できない場合、このアプローチは他のアプローチよりも安全性が低いと思います。

ブラボー!これは決定的なガイドです。
マグノC

これは完璧な答えです!ありがとうございました。
Wilhelm

6
JDK8ストリームに関する段落で、あなたが言及したremoveAll(filtered)。そのための近道はremoveIf(b -> b.getIsbn().equals(other))
ifloop 2018年

IteratorとListIteratorの違いは何ですか?
Alexander Mills

removeIfは考慮していませんが、私の祈りに対する答えでした。ありがとう!
Akabelle


13

どちらか一方のアプローチを優先する理由はありますか

最初のアプローチは機能しますが、リストをコピーする明らかなオーバーヘッドがあります。

多くのコンテナは反復中に変更を許可しないため、2番目のアプローチは機能しません。これにはが含まれArrayListます。

唯一の変更が現在の要素の削除である場合itr.remove()は、を使用して2番目のアプローチを機能させることができます(つまり、コンテナーではなくイテレーターremove()メソッドを使用します)。これは、をサポートするイテレータに推奨される方法ですremove()


申し訳ありません。申し訳ありません...コンテナではなくイテレータのremoveメソッドを使用することを暗示しています。また、リストをコピーするとどのくらいのオーバーヘッドが生じますか?それは多くすることはできません、それはメソッドにスコープされているので、かなり速くガベージコレクションされるべきです。編集を参照してください。..
user1329572

1
@aix私は、IteratorインターフェイスのremoveメソッドがJavadocsでオプションとしてマークされていることを言及する価値があると思いますUnsupportedOperationException。つまり、スローする可能性のあるIterator実装がある可能性があります。したがって、このアプローチは最初のアプローチよりも安全性が低いと思います。使用する予定の実装によっては、最初のアプローチの方が適している場合があります。
エドウィンダロルゾ

@EdwinDalorzo remove()元のコレクション自体にも投げることUnsupportedOperationExceptiondocs.oracle.com/javase/7/docs/api/java/util/...を。残念ながら、Javaコンテナインターフェースは非常に信頼性が低いと定義されています(正直に言って、インターフェースのポイントを無効にします)。実行時に使用される正確な実装がわからない場合は、不変の方法で実行することをお勧めします。たとえば、Java 8+ Streams APIを使用して要素をフィルタリングし、新しいコンテナに収集してから、古いものを完全に置き換えます。
マシュー

5

2番目のアプローチのみが機能します。繰り返し使用中にiterator.remove()のみコレクションを変更できます。他のすべての試みは原因となりConcurrentModificationExceptionます。


2
最初の試行はコピーで繰り返されます。つまり、オリジナルを変更できます。
コリンD

2

古いタイマーのお気に入り(それでも動作します):

List<String> list;

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

1

Iteratorremove()メソッドを使用しても例外がスローされるため、2番目の処理は実行できません。

個人的には、私はすべてのCollectionインスタンスで最初のものを好みますが、新しいを作成するという耳にしがみがあっても、Collection他の開発者による編集中にエラーが発生する可能性は低くなります。コレクションの実装によっては、イテレーターremove()がサポートされている場合とサポートされていない場合があります。詳細については、Iteratorのドキュメントをご覧ください。

3番目の方法は、新しいを作成Collectionし、元のを反復処理し、削除の対象になっていない最初のメンバーCollectionから2番目のメンバーをすべて追加することです。のサイズと削除の数によっては、最初のアプローチと比較すると、メモリを大幅に節約できます。CollectionCollection


0

メモリのコピーを行う必要がなく、Iteratorがより高速に動作するため、2番目を選択します。したがって、メモリと時間を節約できます。


イテレーターはより速く動作します」。この主張を裏付けるものはありますか?また、リストのコピーを作成する場合のメモリフットプリントは非常に簡単です。特に、メソッド内にスコープが設定され、ほとんどすぐにガベージコレクションが行われるためです。
user1329572

1
最初のアプローチの欠点は、2回繰り返す必要があることです。要素を探すfoor-loopで反復し、それを見つけたら、元のリストから削除するように要求します。これは、この特定のアイテムを探すための2回目の反復作業を意味します。これは、少なくともこの場合、イテレータアプローチの方が高速である必要があるという主張を支持します。コレクションの構造空間のみが作成されるものであり、コレクション内のオブジェクトはコピーされないことを考慮する必要があります。どちらのコレクションも同じオブジェクトへの参照を保持します。GCが発生したとき、私たちはわかりません!!!
エドウィンダロルゾ

-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
エドウィンダロルゾ

通常、これを使用して、探していた単一のアイテムを削除したため、これを考慮したことはありません。知っておくと良い。
ドレイククラリス

4
私はパーティーに遅れましたが、確かにFoo.remove(i);あなたがやった後のifブロックコードではi--;
バーティWheen

バグがあるため
Jack
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.