web-dev-qa-db-fra.com

Supprimer des éléments de la collection en itérant

Autant que je sache, 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()
}

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

150
user1329572

Laissez-moi vous donner quelques exemples avec quelques alternatives pour éviter une 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 vous souhaitez supprimer (par exemple, en utilisant une boucle for améliorée) et, une fois l'itération terminée, 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);

Ceci est supposé que l'opération que vous voulez faire est "supprimer". 

Si vous voulez "ajouter" cette approche fonctionnerait aussi, mais je suppose que vous feriez une itération sur une collection différente pour déterminer les éléments que vous souhaitez ajouter à une seconde collection, puis utilisez une méthode addAll à la fin.

Utilisation de ListIterator

Si vous travaillez avec des listes, une autre technique consiste à utiliser une variable ListIterator qui prend en charge la suppression et l'ajout d'éléments pendant 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 méthode add pour ajouter de nouveaux éléments lors de l'itération.

Utilisation de JDK 8

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

Vous pouvez utiliser la nouvelle méthode removeIf dans la classe de base Collection:

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 des éléments d'une collection, vous réaffectez la référence d'origine à la collection filtrée (à savoir books = filtered) ou utilisez la collection filtrée à removeAll les éléments trouvés de la collection d'origine (à savoir books.removeAll(filtered)).

Utilisez des sous-listes ou des sous-ensembles  

Il y a aussi 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 l'effacer:

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

Étant donné que la sous-liste est complétée par la liste d'origine, cela constituerait un moyen efficace de supprimer cette sous-collection d'éléments.

Quelque chose de similaire pourrait être obtenu avec des ensembles triés utilisant la méthode NavigableSet.subSet ou l'une des méthodes de découpage proposées ici.

Considérations:

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

  • La technique de collecte et removeAl fonctionne avec n’importe quelle collection (Collection, Liste, Ensemble, etc.). 
  • La technique ListIterator ne fonctionne évidemment qu'avec des listes, à condition que leur implémentation ListIterator offre la prise en charge des opérations d'ajout et de suppression. 
  • L'approche Iterator 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 que vous n'avez rien à copier car nous supprimons en itérant. Donc, c'est très efficace. 
  • L’exemple de flux JDK 8 ne supprime rien, mais recherche les éléments souhaités, puis nous remplaçons la référence de collection originale par la nouvelle, et l’ancien est récupéré. Nous parcourons donc la collection une seule fois, ce qui serait efficace.
  • Dans la méthode de collecte et removeAll, l’inconvénient est qu’il faut effectuer une itération deux fois. Tout d'abord, nous parcourons la boucle du sol à la recherche d'un objet correspondant à nos critères de suppression, puis, une fois que nous l'avons trouvé, nous le demandons de le supprimer de la collection d'origine, ce qui impliquerait un second travail d'itération pour rechercher cet élément afin de: retirez-le. 
  • Je pense qu'il est utile de mentionner que la méthode remove de l'interface Iterator est marquée comme "facultative" dans Javadocs, ce qui signifie qu'il pourrait y avoir des implémentations Iterator qui jetteraient UnsupportedOperationException si nous appelons 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 soutien de l’itérateur pour la suppression d’éléments.
290
Edwin Dalorzo

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

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

La seconde approche ne fonctionnera pas car beaucoup de conteneurs ne permettent pas de modification pendant l’itération. Ceci inclut ArrayList .

Si la seule modification consiste à supprimer l'élément actuel, vous pouvez faire fonctionner la deuxième approche en utilisant itr.remove() (c'est-à-dire, utilisez la méthode iterator 's remove(), pas le conteneur ). Ce serait ma méthode préférée pour les itérateurs prenant en charge remove().

11
NPE

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

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

4
AlexR

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

Personnellement, je préférerais la première pour toutes les instances Collection, malgré la découverte supplémentaire de la création de la nouvelle Collection, je la trouve moins sujette aux erreurs lors de la modification par d'autres développeurs. Sur certaines implémentations de Collection, Iterator remove() est pris en charge, sur d'autres, il ne l'est pas. Vous pouvez en lire plus dans la documentation pour Iterator

La troisième alternative consiste à créer une nouvelle variable Collection, à la parcourir par rapport à l'original et à ajouter tous les membres de la première variable Collection à la seconde Collection qui sont non à supprimer. En fonction de la taille de la variable Collection et du nombre de suppressions, cela pourrait permettre d'économiser beaucoup de mémoire, par rapport à la première approche. 

1
Jon

Je choisirais le second car vous n’auriez pas besoin de faire une copie de la mémoire et l’Iterator fonctionnera plus rapidement. Donc, vous économisez de la mémoire et du temps.

0
Calin Andrei

Il existe également une solution simple pour "itérer" une Collection et supprimer chaque élément.

List<String> list = new ArrayList<>();
//Fill the list

Il conciste simplement sur le bouclage jusqu'à ce que la liste soit vide, et à chaque itération, nous supprimons le premier élément avec remove(0).

while(!list.isEmpty()){
    String s = list.remove(0);
    // do you thing
}

Je ne crois pas que cela apporte une amélioration par rapport à la variable Iterator; il fallait toujours une liste modifiable, mais j'aime la simplicité de cette solution.

0
AxelH