Supprimer des éléments de la collection lors de l'itération


215

AFAIK, il existe deux approches:

  1. Itérer sur une copie de la collection
  2. Utilisez l'itérateur de la collection actuelle

Par exemple,

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

et

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

Y a-t-il des raisons de préférer une approche à l'autre (par exemple, préférer la première approche pour la simple raison de la lisibilité)?


1
Juste curieux, pourquoi créez-vous une copie de foolist plutôt que de simplement parcourir foolist dans le premier exemple?
Haz

@Haz, donc je n'ai qu'une boucle.
user1329572

15
Remarque: préférez «for» sur «while» également avec les itérateurs pour limiter la portée de la variable: for (Iterator <Foo> itr = fooList.iterator (); itr.hasNext ();) {}
Puce

Je ne savais pas whileeu différentes règles de portée quefor
Alexander Mills

Dans une situation plus complexe, vous pourriez avoir un cas où fooListest une variable d'instance et vous appelez une méthode pendant la boucle qui finit par appeler une autre méthode de la même classe fooList.remove(obj). J'ai vu cela se produire. Dans ce cas, la copie de la liste est la plus sûre.
Dave Griffiths

Réponses:


418

Permettez-moi de donner quelques exemples avec quelques alternatives pour éviter a ConcurrentModificationException.

Supposons que nous ayons la collection de livres suivante

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")));

Collecter et supprimer

La première technique consiste à collecter tous les objets que nous voulons supprimer (par exemple en utilisant une boucle for améliorée) et après avoir terminé l'itération, nous supprimons tous les objets trouvés.

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);

Cela suppose que l'opération que vous souhaitez effectuer est "supprimer".

Si vous souhaitez "ajouter" cette approche fonctionnerait également, mais je suppose que vous parcourrez une autre collection pour déterminer les éléments que vous souhaitez ajouter à une deuxième collection, puis émettez une addAllméthode à la fin.

Utilisation de ListIterator

Si vous travaillez avec des listes, une autre technique consiste à utiliser une ListIteratorqui prend en charge la suppression et l'ajout d'éléments lors de l'itération elle-même.

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

Encore une fois, j'ai utilisé la méthode "remove" dans l'exemple ci-dessus, ce que votre question semblait impliquer, mais vous pouvez également utiliser sa addméthode pour ajouter de nouveaux éléments pendant l'itération.

Utilisation de JDK> = 8

Pour ceux qui travaillent avec Java 8 ou des versions supérieures, il existe quelques autres techniques que vous pouvez utiliser pour en tirer parti.

Vous pouvez utiliser la nouvelle removeIfméthode dans la Collectionclasse de base:

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

Ou utilisez la nouvelle API de flux:

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

Dans ce dernier cas, pour filtrer les éléments d'une collection, vous réaffectez la référence d'origine à la collection filtrée (c'est-à-dire books = filtered) ou vous avez utilisé la collection filtrée pour removeAllles éléments trouvés de la collection d'origine (c'est-à-dire books.removeAll(filtered)).

Utiliser une sous-liste ou un sous-ensemble

Il existe également d'autres alternatives. Si la liste est triée et que vous souhaitez supprimer des éléments consécutifs, vous pouvez créer une sous-liste, puis la supprimer:

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

Étant donné que la sous-liste est appuyée par la liste d'origine, ce serait un moyen efficace de supprimer cette sous-collection d'éléments.

Quelque chose de similaire pourrait être réalisé avec des ensembles triés à l'aide de la NavigableSet.subSetméthode ou de l'une des méthodes de découpage proposées.

Considérations:

La méthode que vous utilisez peut dépendre de ce que vous avez l'intention de faire

  • La collecte et la removeAltechnique fonctionnent avec n'importe quelle collection (collection, liste, ensemble, etc.).
  • La ListIteratortechnique ne fonctionne évidemment qu'avec des listes, à condition que leur ListIteratorimplémentation donnée prenne en charge les opérations d'ajout et de suppression.
  • L' Iteratorapproche fonctionnerait avec n'importe quel type de collection, mais elle ne prend en charge que les opérations de suppression.
  • Avec l' approche ListIterator/ Iterator, l'avantage évident est de ne pas avoir à copier quoi que ce soit puisque nous supprimons au fur et à mesure de l'itération. Donc, c'est très efficace.
  • L'exemple de flux JDK 8 ne supprime en fait rien, mais a recherché les éléments souhaités, puis nous avons remplacé la référence de collection d'origine par la nouvelle et laissé l'ancienne être récupérée. Donc, nous itérons une seule fois sur la collection et ce serait efficace.
  • Dans la collecte et l' removeAllapproche, l'inconvénient est que nous devons répéter deux fois. D'abord, nous parcourons dans la boucle du plancher à la recherche d'un objet qui correspond à nos critères de suppression, et une fois que nous l'avons trouvé, nous demandons de le supprimer de la collection d'origine, ce qui impliquerait un deuxième travail d'itération pour rechercher cet article afin de le retirer.
  • Je pense qu'il convient de mentionner que la méthode remove de l' Iteratorinterface est marquée comme "facultative" dans Javadocs, ce qui signifie qu'il pourrait y avoir des Iteratorimplémentations qui jettent UnsupportedOperationExceptionsi nous invoquons la méthode remove. En tant que tel, je dirais que cette approche est moins sûre que d'autres si nous ne pouvons pas garantir le support de l'itérateur pour la suppression d'éléments.

Bravo! c'est le guide définitif.
Magno C

Ceci est une réponse parfaite! Je vous remercie.
Wilhelm

7
Dans votre paragraphe sur les flux JDK8 que vous mentionnez removeAll(filtered). Un raccourci pour cela seraitremoveIf(b -> b.getIsbn().equals(other))
ifloop

Quelle est la différence entre Iterator et ListIterator?
Alexander Mills

N'ont pas été considérés comme removeIf, mais c'était la réponse à mes prières. Merci!
Akabelle

15

En Java 8, il existe une autre approche. Collection # removeIf

par exemple:

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

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

13

Y a-t-il des raisons de préférer une approche à l'autre

La première approche fonctionnera, mais a la surcharge évidente de copier la liste.

La deuxième approche ne fonctionnera pas car de nombreux conteneurs ne permettent pas de modification pendant l'itération. Cela comprendArrayList .

Si la seule modification est de supprimer l'élément courant, vous pouvez faire le deuxième travail d'approche à l'aide itr.remove()(qui est, utilisez la iterator « s remove()méthode, pas le contenant » s). Ce serait ma méthode préférée pour les itérateurs qui prennent en charge remove().


Oups, désolé ... il est sous-entendu que j'utiliserais la méthode de suppression de l'itérateur, pas celle du conteneur. Et combien de frais généraux crée la copie de la liste? Cela ne peut pas être grand-chose et comme il est limité à une méthode, il doit être récupéré assez rapidement. Voir edit ..
user1329572

1
@aix Je pense qu'il convient de mentionner que la méthode remove de l' Iteratorinterface est marquée comme facultative dans Javadocs, ce qui signifie qu'il pourrait y avoir des implémentations d'itérateur UnsupportedOperationException. En tant que tel, je dirais que cette approche est moins sûre que la première. En fonction des implémentations destinées à être utilisées, la première approche pourrait être plus adaptée.
Edwin Dalorzo

@EdwinDalorzo remove()sur la collection originale elle-même peut également renvoyerUnsupportedOperationException : docs.oracle.com/javase/7/docs/api/java/util/… . Les interfaces de conteneurs Java sont, malheureusement, définies comme extrêmement peu fiables (ce qui va à l'encontre du point de l'interface, honnêtement). Si vous ne connaissez pas l'implémentation exacte qui sera utilisée lors de l'exécution, il est préférable de faire les choses de manière immuable - par exemple, utilisez l'API Java 8+ Streams pour filtrer les éléments et les collecter dans un nouveau conteneur, puis remplacer entièrement l'ancien par lui.
Matthew Read

5

Seule la deuxième approche fonctionnera. Vous pouvez modifier la collection pendant l'itération en utilisant iterator.remove()uniquement. Toutes les autres tentatives entraîneront ConcurrentModificationException.


2
La première tentative se répète sur une copie, ce qui signifie qu'il peut modifier l'original.
Colin D

3

Old Timer Favorite (ça marche toujours):

List<String> list;

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

1

Vous ne pouvez pas faire la seconde, car même si vous utilisez la remove()méthode sur Iterator , vous obtiendrez une exception levée .

Personnellement, je préférerais le premier pour toutes les Collectioninstances, malgré le surprenant supplémentaire de créer le nouveau Collection, je le trouve moins sujet aux erreurs lors de l'édition par d'autres développeurs. Sur certaines implémentations de Collection, l'itérateur remove()est pris en charge, sur d'autres il ne l'est pas. Vous pouvez en savoir plus dans les documents pour Iterator .

La troisième alternative consiste à créer un nouveau Collection, itérer sur l'original et ajouter tous les membres du premier Collectionau second Collectionqui ne sont pas prêts à être supprimés. En fonction de la taille Collectionet du nombre de suppressions, cela pourrait considérablement économiser de la mémoire par rapport à la première approche.


0

Je choisirais la seconde car vous n'avez pas à faire de copie de la mémoire et l'itérateur fonctionne plus rapidement. Vous économisez ainsi de la mémoire et du temps.


" Iterator travaille plus vite ". Quelque chose pour soutenir cette affirmation? En outre, l'empreinte mémoire de la copie d'une liste est très triviale, d'autant plus qu'elle sera portée dans une méthode et récupérée presque immédiatement.
user1329572

1
Dans la première approche, l'inconvénient est que nous devons répéter deux fois. Nous itérons dans la boucle de foor à la recherche d'un élément, et une fois que nous le trouvons, nous demandons de le supprimer de la liste d'origine, ce qui impliquerait un deuxième travail d'itération pour rechercher cet élément donné. Cela corroborerait l'affirmation selon laquelle, au moins dans ce cas, l'approche itérative devrait être plus rapide. Nous devons considérer que seul l'espace structurel de la collection est celui qui est créé, les objets à l'intérieur des collections ne sont pas copiés. Les deux collections conserveraient des références aux mêmes objets. Lorsque GC arrive, nous ne pouvons pas le dire !!!
Edwin Dalorzo

-2

pourquoi pas ça?

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

Et si c'est une carte, pas une liste, vous pouvez utiliser keyset ()


4
Cette approche présente de nombreux inconvénients majeurs. Tout d'abord, chaque fois que vous supprimez un élément, les index sont réorganisés. Par conséquent, si vous supprimez l'élément 0, l'élément 1 devient le nouvel élément 0. Si vous allez à cela, faites-le au moins à l'envers pour éviter ce problème. Deuxièmement, toutes les implémentations de List n'offrent pas un accès direct aux éléments (comme le fait ArrayList). Dans une LinkedList, cela serait extrêmement inefficace car chaque fois que vous émettez un, get(i)vous devez visiter tous les nœuds jusqu'à ce que vous atteigniez i.
Edwin Dalorzo

Je n'ai jamais considéré cela car je l'ai généralement utilisé pour supprimer un seul élément que je cherchais. Bon à savoir.
Drake Clarris

4
Je suis en retard à la fête, mais sûrement dans le code bloc if après que Foo.remove(i);vous devriez le faire i--;?
Bertie Wheen

parce que c'est buggé
Jack
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.