Itération à travers une collection, en évitant ConcurrentModificationException lors de la suppression d'objets dans une boucle


1194

Nous savons tous que vous ne pouvez pas faire ce qui suit à cause de ConcurrentModificationException:

for (Object i : l) {
    if (condition(i)) {
        l.remove(i);
    }
}

Mais cela fonctionne apparemment parfois, mais pas toujours. Voici un code spécifique:

public static void main(String[] args) {
    Collection<Integer> l = new ArrayList<>();

    for (int i = 0; i < 10; ++i) {
        l.add(4);
        l.add(5);
        l.add(6);
    }

    for (int i : l) {
        if (i == 5) {
            l.remove(i);
        }
    }

    System.out.println(l);
}

Cela se traduit bien sûr par:

Exception in thread "main" java.util.ConcurrentModificationException

Même si plusieurs threads ne le font pas. En tous cas.

Quelle est la meilleure solution à ce problème? Comment puis-je supprimer un élément de la collection dans une boucle sans lever cette exception?

J'utilise également un arbitraire Collectionici, pas nécessairement un ArrayList, donc vous ne pouvez pas vous fier get.


Note aux lecteurs: lisez bien docs.oracle.com/javase/tutorial/collections/interfaces/… , cela peut être un moyen plus simple de réaliser ce que vous voulez faire.
GKFX

Réponses:


1601

Iterator.remove() est sûr, vous pouvez l'utiliser comme ceci:

List<String> list = new ArrayList<>();

// This is a clever way to create the iterator and call iterator.hasNext() like
// you would do in a while-loop. It would be the same as doing:
//     Iterator<String> iterator = list.iterator();
//     while (iterator.hasNext()) {
for (Iterator<String> iterator = list.iterator(); iterator.hasNext();) {
    String string = iterator.next();
    if (string.isEmpty()) {
        // Remove the current element from the iterator and the list.
        iterator.remove();
    }
}

Notez que Iterator.remove()c'est le seul moyen sûr de modifier une collection pendant l'itération; le comportement n'est pas spécifié si la collection sous-jacente est modifiée d'une autre manière pendant l'itération.

Source: docs.oracle> L'interface de collection


Et de même, si vous en avez un ListIteratoret que vous souhaitez ajouter des éléments, vous pouvez utiliser ListIterator#add, pour la même raison que vous pouvez utiliser Iterator#remove - il est conçu pour le permettre.


Dans votre cas, vous avez tenté de supprimer une liste, mais la même restriction s'applique si vous essayez de le faire puten Mapitérant son contenu.


19
Que faire si vous souhaitez supprimer un élément autre que l'élément renvoyé dans l'itération actuelle?
Eugen

2
Vous devez utiliser le .remove dans l'itérateur et cela ne peut que supprimer l'élément actuel, donc non :)
Bill K

1
Sachez que c'est plus lent que d'utiliser ConcurrentLinkedDeque ou CopyOnWriteArrayList (au moins dans mon cas)
Dan

1
N'est-il pas possible de mettre l' iterator.next()appel dans la boucle for? Sinon, quelqu'un peut-il expliquer pourquoi?
Blake

1
@GonenI Il est implémenté pour tous les itérateurs des collections qui ne sont pas immuables. List.addest "facultatif" dans le même sens aussi, mais vous ne diriez pas qu'il est "dangereux" d'ajouter à une liste.
Radiodef

345

Cela marche:

Iterator<Integer> iter = l.iterator();
while (iter.hasNext()) {
    if (iter.next() == 5) {
        iter.remove();
    }
}

J'ai supposé que, comme une boucle foreach est un sucre syntaxique pour l'itération, l'utilisation d'un itérateur n'aiderait pas ... mais cela vous donne cette .remove()fonctionnalité.


43
foreach loop est un sucre syntaxique pour l'itération. Cependant, comme vous l'avez souligné, vous devez appeler remove sur l'itérateur - auquel foreach ne vous donne pas accès. D' où la raison pour laquelle vous ne pouvez pas supprimer dans une boucle foreach (même si vous êtes réellement en utilisant un itérateur sous le capot)
madlep

36
+1, par exemple, du code pour utiliser iter.remove () dans le contexte, ce que la réponse de Bill K n'a pas [directement].
Modifié

202

Avec Java 8, vous pouvez utiliser la nouvelle removeIfméthode . Appliqué à votre exemple:

Collection<Integer> coll = new ArrayList<>();
//populate

coll.removeIf(i -> i == 5);

3
Ooooo! J'espérais que quelque chose en Java 8 ou 9 pourrait aider. Cela me semble encore assez verbeux, mais je l'aime toujours.
James T Snell

L'implémentation de equals () est-elle également recommandée dans ce cas?
Anmol Gupta

par la façon dont removeIfutilise Iteratoret whileboucle. Vous pouvez le voir au java 8java.util.Collection.java
omerhakanbilici

3
@omerhakanbilici Certaines implémentations telles que le ArrayListremplacer pour des raisons de performances. Celui dont vous parlez n'est que l'implémentation par défaut.
Didier L

@AnmolGupta: Non, equalsn'est pas utilisé du tout ici, donc il n'a pas besoin d'être implémenté. (Mais bien sûr, si vous utilisez equalsdans votre test, il doit être implémenté comme vous le souhaitez.)
Lii

42

Puisque la question a déjà été répondue, c'est-à-dire que la meilleure façon est d'utiliser la méthode remove de l'objet itérateur, j'entrerais dans les détails de l'endroit où l'erreur "java.util.ConcurrentModificationException"est levée.

Chaque classe de collection a une classe privée qui implémente l'interface Iterator et fournit des méthodes comme next(), remove()et hasNext().

Le code pour next ressemble à ceci ...

public E next() {
    checkForComodification();
    try {
        E next = get(cursor);
        lastRet = cursor++;
        return next;
    } catch(IndexOutOfBoundsException e) {
        checkForComodification();
        throw new NoSuchElementException();
    }
}

Ici, la méthode checkForComodificationest implémentée comme

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

Donc, comme vous pouvez le voir, si vous essayez explicitement de supprimer un élément de la collection. Il en résulte modCountune différence avec expectedModCount, ce qui entraîne l'exception ConcurrentModificationException.


Très intéressant. Je vous remercie! Souvent, je n'appelle pas remove () moi-même, je préfère plutôt effacer la collection après l'avoir parcourue. Cela ne veut pas dire que c'est un bon modèle, juste ce que je fais récemment.
James T Snell

26

Vous pouvez soit utiliser l'itérateur directement comme vous l'avez mentionné, soit conserver une deuxième collection et ajouter chaque élément que vous souhaitez supprimer à la nouvelle collection, puis supprimer tout à la fin. Cela vous permet de continuer à utiliser la sécurité de type de la boucle for-each au prix d'une utilisation accrue de la mémoire et du temps processeur (cela ne devrait pas être un gros problème sauf si vous avez de très, très grandes listes ou un très vieil ordinateur)

public static void main(String[] args)
{
    Collection<Integer> l = new ArrayList<Integer>();
    Collection<Integer> itemsToRemove = new ArrayList<>();
    for (int i=0; i < 10; i++) {
        l.add(Integer.of(4));
        l.add(Integer.of(5));
        l.add(Integer.of(6));
    }
    for (Integer i : l)
    {
        if (i.intValue() == 5) {
            itemsToRemove.add(i);
        }
    }

    l.removeAll(itemsToRemove);
    System.out.println(l);
}

7
c'est ce que je fais normalement, mais l'itérateur explicite est une solution plus élégante que je ressens.
Claudiu

1
Assez juste, tant que vous ne faites rien d'autre avec l'itérateur - le faire exposer rend plus facile de faire des choses comme appeler .next () deux fois par boucle, etc. Pas un gros problème, mais peut causer des problèmes si vous le faites rien de plus compliqué que de simplement parcourir une liste pour supprimer des entrées.
RodeoClown

@RodeoClown: dans la question d'origine, Claudiu supprime de la collection, pas l'itérateur.
mat

1
La suppression de l'itérateur supprime de la collection sous-jacente ... mais ce que je disais dans le dernier commentaire est que si vous faites quelque chose de plus compliqué que de simplement rechercher des suppressions dans la boucle (comme traiter des données correctes) en utilisant l'itérateur, erreurs plus faciles à commettre.
RodeoClown

S'il s'agit d'une simple suppression de valeurs qui ne sont pas nécessaires et que la boucle ne fait qu'une chose, utiliser l'itérateur directement et appeler .remove () est tout à fait correct.
RodeoClown

17

Dans de tels cas, une astuce courante est (était?) De revenir en arrière:

for(int i = l.size() - 1; i >= 0; i --) {
  if (l.get(i) == 5) {
    l.remove(i);
  }
}

Cela dit, je suis plus qu'heureux que vous ayez de meilleures méthodes en Java 8, par exemple removeIfou filtersur les flux.


2
C'est une bonne astuce. Mais cela ne fonctionnerait pas sur les collections non indexées comme les ensembles, et ce serait vraiment lent sur les listes liées, par exemple.
Claudiu

@Claudiu Oui, c'est certainement juste pour ArrayListles collections s ou similaires.
Landei

J'utilise une ArrayList, cela a parfaitement fonctionné, merci.
StarSweeper

2
les index sont super. Si c'est si courant, pourquoi n'utilisez-vous pas for(int i = l.size(); i-->0;) {?
John

16

Même réponse que Claudius avec une boucle for:

for (Iterator<Object> it = objects.iterator(); it.hasNext();) {
    Object object = it.next();
    if (test) {
        it.remove();
    }
}

12

Avec les collections Eclipse , la méthode removeIfdéfinie sur MutableCollection fonctionnera:

MutableList<Integer> list = Lists.mutable.of(1, 2, 3, 4, 5);
list.removeIf(Predicates.lessThan(3));
Assert.assertEquals(Lists.mutable.of(3, 4, 5), list);

Avec la syntaxe Java 8 Lambda, cela peut être écrit comme suit:

MutableList<Integer> list = Lists.mutable.of(1, 2, 3, 4, 5);
list.removeIf(Predicates.cast(integer -> integer < 3));
Assert.assertEquals(Lists.mutable.of(3, 4, 5), list);

L'appel à Predicates.cast()est nécessaire ici car une removeIfméthode par défaut a été ajoutée sur l' java.util.Collectioninterface dans Java 8.

Remarque: je suis un committer pour les collections Eclipse .


10

Faites une copie de la liste existante et parcourez la nouvelle copie.

for (String str : new ArrayList<String>(listOfStr))     
{
    listOfStr.remove(/* object reference or index */);
}

19
Faire une copie sonne comme un gaspillage de ressources.
Antzi

3
@Antzi Cela dépend de la taille de la liste et de la densité des objets à l'intérieur. Encore une solution valable et valable.
mre

J'utilise cette méthode. Cela prend un peu plus de ressources, mais beaucoup plus flexible et clair.
Tao Zhang

C'est une bonne solution lorsque vous n'avez pas l'intention de supprimer des objets à l'intérieur de la boucle elle-même, mais ils sont plutôt "aléatoirement" supprimés des autres threads (par exemple, les opérations réseau mettant à jour les données). Si vous vous retrouvez à faire beaucoup de ces copies, il y a même une implémentation Java qui fait exactement cela: docs.oracle.com/javase/8/docs/api/java/util/concurrent/…
A1m

8

Les gens affirment que l' on ne peut pas supprimer d'une collection en cours d'itération par une boucle foreach. Je voulais juste souligner que c'est techniquement incorrect et décrire exactement (je sais que la question de l'OP est si avancée qu'elle évite de le savoir) le code derrière cette hypothèse:

for (TouchableObj obj : untouchedSet) {  // <--- This is where ConcurrentModificationException strikes
    if (obj.isTouched()) {
        untouchedSet.remove(obj);
        touchedSt.add(obj);
        break;  // this is key to avoiding returning to the foreach
    }
}

Ce n'est pas que vous ne pouvez pas supprimer de l'itéré Colletionplutôt que vous ne pouvez pas continuer l'itération une fois que vous l'avez fait. D'où le breakdans le code ci-dessus.

Toutes mes excuses si cette réponse est un cas d'utilisation quelque peu spécialisé et plus adapté au fil d' origine d'où je suis arrivé ici, celui-ci est marqué comme doublon (malgré ce fil apparaissant plus nuancé) de celui-ci et verrouillé.


8

Avec une boucle for traditionnelle

ArrayList<String> myArray = new ArrayList<>();

for (int i = 0; i < myArray.size(); ) {
    String text = myArray.get(i);
    if (someCondition(text))
        myArray.remove(i);
    else
        i++;   
}

Ah, donc c'est vraiment juste la boucle améliorée qui lève l'exception.
cellepo

FWIW - le même code fonctionnerait toujours après avoir été modifié pour incrémenter i++dans le bouclier plutôt que dans le corps de la boucle.
cellepo

Correction ^: C'est si l' i++incrémentation n'était pas conditionnelle - je vois maintenant c'est pourquoi vous le faites dans le corps :)
cellepo

2

A ListIteratorvous permet d'ajouter ou de supprimer des éléments dans la liste. Supposons que vous ayez une liste d' Carobjets:

List<Car> cars = ArrayList<>();
// add cars here...

for (ListIterator<Car> carIterator = cars.listIterator();  carIterator.hasNext(); )
{
   if (<some-condition>)
   { 
      carIterator().remove()
   }
   else if (<some-other-condition>)
   { 
      carIterator().add(aNewCar);
   }
}

Les méthodes supplémentaires de l' interface ListIterator (extension d'Iterator) sont intéressantes - en particulier sa previousméthode.
cellepo

1

J'ai une suggestion pour le problème ci-dessus. Pas besoin de liste secondaire ni de temps supplémentaire. Veuillez trouver un exemple qui ferait la même chose mais d'une manière différente.

//"list" is ArrayList<Object>
//"state" is some boolean variable, which when set to true, Object will be removed from the list
int index = 0;
while(index < list.size()) {
    Object r = list.get(index);
    if( state ) {
        list.remove(index);
        index = 0;
        continue;
    }
    index += 1;
}

Cela éviterait l'exception de concurrence.


1
La question indique explicitement que le PO n'est pas nécessaire à utiliser ArrayListet ne peut donc pas compter get(). Sinon, c'est probablement une bonne approche.
kaskelotti

(Clarification ^) OP utilise un arbitraire Collection- l' Collectioninterface ne comprend pas get. (Bien que l' Listinterface FWIW inclue 'get').
cellepo

Je viens d'ajouter une réponse séparée et plus détaillée ici également pour le whilebouclage a List. Mais +1 pour cette réponse car elle est venue en premier.
cellepo

1

ConcurrentHashMap ou ConcurrentLinkedQueue ou ConcurrentSkipListMap peuvent être une autre option, car ils ne lèveront jamais d'exception ConcurrentModificationException, même si vous supprimez ou ajoutez un élément.


Oui, et notez que ce sont tous dans le java.util.concurrentpaquet. Certaines autres classes de cas d'utilisation similaires / communes de ce package sont CopyOnWriteArrayList & CopyOnWriteArraySet [mais sans s'y limiter].
cellepo

En fait, je viens d'apprendre que même si ces objets de structure de données évitent ConcurrentModificationException , leur utilisation dans une boucle améliorée peut toujours causer des problèmes d'indexation (c.-à-d. Toujours sauter des éléments, ou IndexOutOfBoundsException...)
cellepo

1

Je sais que cette question est trop ancienne pour être sur Java 8, mais pour ceux qui utilisent Java 8, vous pouvez facilement utiliser removeIf ():

Collection<Integer> l = new ArrayList<Integer>();

for (int i=0; i < 10; ++i) {
    l.add(new Integer(4));
    l.add(new Integer(5));
    l.add(new Integer(6));
}

l.removeIf(i -> i.intValue() == 5);

1

Une autre façon consiste à créer une copie de votre arrayList:

List<Object> l = ...

List<Object> iterationList = ImmutableList.copyOf(l);

for (Object i : iterationList) {
    if (condition(i)) {
        l.remove(i);
    }
}

Remarque: in'est pas un indexmais plutôt l'objet. Peut-être que l'appeler objserait plus approprié.
luckydonald

1

La meilleure façon (recommandée) est d'utiliser le package java.util.Concurrent. En utilisant ce package, vous pouvez facilement éviter cette exception. référez-vous au code modifié

public static void main(String[] args) {
    Collection<Integer> l = new CopyOnWriteArrayList<Integer>();

    for (int i=0; i < 10; ++i) {
        l.add(new Integer(4));
        l.add(new Integer(5));
        l.add(new Integer(6));
    }

    for (Integer i : l) {
        if (i.intValue() == 5) {
            l.remove(i);
        }
    }

    System.out.println(l);
}

0

Dans le cas où ArrayList: remove (int index) - si (index est la position du dernier élément), il évite sans System.arraycopy()et ne prend pas de temps pour cela.

le temps de tableau augmente si (l'index diminue), d'ailleurs les éléments de la liste diminuent aussi!

le meilleur moyen de suppression efficace est de supprimer ses éléments dans l'ordre décroissant: while(list.size()>0)list.remove(list.size()-1);// prend O (1) while(list.size()>0)list.remove(0);// prend O (factoriel (n))

//region prepare data
ArrayList<Integer> ints = new ArrayList<Integer>();
ArrayList<Integer> toRemove = new ArrayList<Integer>();
Random rdm = new Random();
long millis;
for (int i = 0; i < 100000; i++) {
    Integer integer = rdm.nextInt();
    ints.add(integer);
}
ArrayList<Integer> intsForIndex = new ArrayList<Integer>(ints);
ArrayList<Integer> intsDescIndex = new ArrayList<Integer>(ints);
ArrayList<Integer> intsIterator = new ArrayList<Integer>(ints);
//endregion

// region for index
millis = System.currentTimeMillis();
for (int i = 0; i < intsForIndex.size(); i++) 
   if (intsForIndex.get(i) % 2 == 0) intsForIndex.remove(i--);
System.out.println(System.currentTimeMillis() - millis);
// endregion

// region for index desc
millis = System.currentTimeMillis();
for (int i = intsDescIndex.size() - 1; i >= 0; i--) 
   if (intsDescIndex.get(i) % 2 == 0) intsDescIndex.remove(i);
System.out.println(System.currentTimeMillis() - millis);
//endregion

// region iterator
millis = System.currentTimeMillis();
for (Iterator<Integer> iterator = intsIterator.iterator(); iterator.hasNext(); )
    if (iterator.next() % 2 == 0) iterator.remove();
System.out.println(System.currentTimeMillis() - millis);
//endregion
  • pour la boucle d'index: 1090 msec
  • pour l'index desc: 519 msec --- le meilleur
  • pour itérateur: 1043 msec

0
for (Integer i : l)
{
    if (i.intValue() == 5){
            itemsToRemove.add(i);
            break;
    }
}

La capture est après avoir supprimé l'élément de la liste si vous ignorez l'appel interne iterator.next (). ça fonctionne encore! Bien que je ne propose pas d'écrire du code comme celui-ci, cela aide à comprendre le concept derrière :-)

À votre santé!


0

Exemple de modification de collection thread-safe:

public class Example {
    private final List<String> queue = Collections.synchronizedList(new ArrayList<String>());

    public void removeFromQueue() {
        synchronized (queue) {
            Iterator<String> iterator = queue.iterator();
            String string = iterator.next();
            if (string.isEmpty()) {
                iterator.remove();
            }
        }
    }
}

0

Je sais que cette question suppose juste un Collection, et pas plus spécifiquement aucun List. Mais pour ceux qui lisent cette question qui travaillent en effet avec une Listréférence, vous pouvez éviter ConcurrentModificationExceptionavec un while-loop (tout en le modifiant) à la place si vous voulez éviterIterator (soit si vous voulez l'éviter en général, soit l'éviter spécifiquement pour atteindre un ordre de bouclage différent de l'arrêt de bout en bout à chaque élément [ce qui, à mon avis, est le seul ordre en Iteratorsoi]]:

* Mise à jour: Voir les commentaires ci-dessous qui précisent que l'analogue est également réalisable avec la boucle traditionnelle .

final List<Integer> list = new ArrayList<>();
for(int i = 0; i < 10; ++i){
    list.add(i);
}

int i = 1;
while(i < list.size()){
    if(list.get(i) % 2 == 0){
        list.remove(i++);

    } else {
        i += 2;
    }
}

Aucune exception ConcurrentModificationException de ce code.

Là, nous voyons que le bouclage ne démarre pas au début et ne s'arrête pas à chaque élément (ce que je crois Iteratorlui-même ne peut pas faire).

FWIW, nous voyons également getêtre appelé list, ce qui ne pourrait pas être fait si sa référence était juste Collection(au lieu du Listtype plus spécifique de Collection) - l' Listinterface inclut get, mais pas l' Collectioninterface. Si ce n'est pas pour cette différence, alors la listréférence pourrait plutôt être un Collection[et donc techniquement cette réponse serait alors une réponse directe, au lieu d'une réponse tangentielle].

FWIWW même code fonctionne toujours après modification pour commencer au début à l'arrêt à chaque élément (comme l' Iteratorordre):

final List<Integer> list = new ArrayList<>();
for(int i = 0; i < 10; ++i){
    list.add(i);
}

int i = 0;
while(i < list.size()){
    if(list.get(i) % 2 == 0){
        list.remove(i);

    } else {
        ++i;
    }
}

Cela nécessite cependant un calcul très minutieux des indices à supprimer.
OneCricketeer

En outre, ceci n'est qu'une explication plus détaillée de cette réponse stackoverflow.com/a/43441822/2308683
OneCricketeer

Bon à savoir - merci! Cette autre réponse m'a aidé à comprendre que c'est la boucle améliorée pour qui lancerait ConcurrentModificationException, mais pas la boucle traditionnelle (que l'autre réponse utilise) - ne réalisant pas qu'avant, c'est pourquoi j'étais motivé à écrire cette réponse (j'ai erronément pensait alors que toutes les boucles for allaient lever l'exception).
cellepo

0

Une solution pourrait être de faire pivoter la liste et de supprimer le premier élément pour éviter la ConcurrentModificationException ou IndexOutOfBoundsException

int n = list.size();
for(int j=0;j<n;j++){
    //you can also put a condition before remove
    list.remove(0);
    Collections.rotate(list, 1);
}
Collections.rotate(list, -1);

0

Essayez celui-ci (supprime tous les éléments de la liste qui sont égaux i):

for (Object i : l) {
    if (condition(i)) {
        l = (l.stream().filter((a) -> a != i)).collect(Collectors.toList());
    }
}

0

vous pouvez également utiliser la récursivité

La récursivité en java est un processus dans lequel une méthode s'appelle elle-même en continu. Une méthode en Java qui s'appelle elle-même est appelée méthode récursive.


-2

ce n'est peut-être pas le meilleur moyen, mais pour la plupart des petits cas, cela devrait être acceptable:

"créer un deuxième tableau vide et n'ajouter que ceux que vous souhaitez conserver"

Je ne me souviens pas d'où je lis ceci ... par justesse, je ferai ce wiki dans l'espoir que quelqu'un le trouve ou simplement pour ne pas gagner de réputation que je ne mérite pas.

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.