web-dev-qa-db-fra.com

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

Nous savons tous que vous ne pouvez pas faire cela:

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

ConcurrentModificationException etc ... cela semble fonctionner 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);
}

Bien entendu, cela se traduit par:

Exception in thread "main" Java.util.ConcurrentModificationException

... même si plusieurs threads ne le font pas ... Quoi qu'il en soit.

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

J'utilise également une variable Collection ici, pas nécessairement une ArrayList, de sorte que vous ne pouvez pas compter sur get.

1140
Claudiu

Iterator.remove() est sans danger, 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() 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 le déroulement de l'itération.

Source: docs.Oracle> L'interface de collection


Et de même, si vous avez un ListIterator et que vous voulez ajouter éléments, vous pouvez utiliser ListIterator#add , pour la même raison vous pouvez utiliser _Iterator#remove_ - il est conçu pour le permettre.


Dans votre cas, vous avez essayé de supprimer d'une liste, mais la même restriction s'applique si vous tentez de put dans un Map tout en itérant son contenu.

1553
Bill K

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 les itérations, utiliser un itérateur ne servirait à rien ... mais cela vous donne cette fonctionnalité .remove().

334
Claudiu

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

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

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

Puisque la question a déjà été répondue, c’est-à-dire que le meilleur moyen est d’utiliser la méthode remove de l’objet itérateur, j’entrerai dans les détails de l’endroit où l’erreur "Java.util.ConcurrentModificationException" est renvoyée.

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

Le code pour next ressemble à quelque chose comme ça ...

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

Ici la méthode checkForComodification est implémentée comme

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

Ainsi, comme vous pouvez le constater, si vous essayez explicitement de supprimer un élément de la collection. Il en résulte que modCount devient différent de expectedModCount, ce qui entraîne l'exception ConcurrentModificationException.

40
Ashish

Vous pouvez soit utiliser l'itérateur directement, comme vous l'avez mentionné, ou bien 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 à moins que vous n'ayez de 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<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)
            itemsToRemove.add(i);
    }

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

Dans de tels cas, une astuce courante consiste (était?) À 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 disposiez de meilleures méthodes dans Java 8, par exemple. removeIf ou filter sur les flux.

17
Landei

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();
    }
}
16
Antzi

Avec Eclipse Collections (anciennement GS Collections ), la méthode removeIf dé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 Java 8 syntaxe Lambda, ceci 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 méthode removeIf par défaut a été ajoutée à l'interface Java.util.Collection dans Java 8.

Remarque: Je suis un committer pour Eclipse Collections .

11
Donald Raab

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

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

Avec une boucle 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++;
      }
7
Lluis Felisart

Les gens affirment qu'un ne peut pas supprimer d'une collection itérée par une boucle foreach. Je voulais juste souligner que est techniquement incorrect et décrire exactement (je sais que la question du PO est suffisamment avancée pour éviter 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 la version itérée Colletion mais que vous ne pouvez pas continuer l'itération une fois que vous le faites. D'où le break dans le code ci-dessus.

Toutes mes excuses si cette réponse est un cas d’utilisation un peu spécialisé et plus adapté à l’original fil Je suis arrivé ici de celui-ci, celui-ci est marqué comme un doublon (bien que ce fil paraisse plus nuancé) et verrouillé.

7
John

Un ListIterator vous permet d'ajouter ou de supprimer des éléments de la liste. Supposons que vous ayez une liste d'objets Car:

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);
   }
}
2
james.garriss

Une autre méthode consiste à créer une copie de votre tableau arrayList:

List<Object> l = ...

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

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

}

1
Nestor Milyaev

La meilleure façon (recommandée) est d'utiliser le package Java.util.Concurrent. En utilisant ce package, vous pouvez facilement éviter cette exception. se référer 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);
    }
1
jagdish khetre

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
Nandhan Thiravia

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

1
Yessy

Je sais que cette question suppose juste un Collection, et pas plus précisément un quelconque List. Mais pour ceux qui lisent cette question et qui travaillent effectivement avec une référence List, vous pouvez éviter ConcurrentModificationException avec une boucle while- (en la modifiant) si vous voulez pour éviter Iterator (soit si vous voulez l'éviter en général, ou bien évitez-le spécifiquement pour obtenir un ordre de bouclage différent de l'arrêt du début à la fin de chaque élément [que je crois être le seul ordre Iterator lui-même peut faire]):

* Mise à jour: Voir les commentaires ci-dessous qui expliquent ce qui est analogue est également réalisable avec le traditionnel - for-loop.

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.

Nous voyons là que la boucle ne commence pas au début, et ne s’arrête pas à tous les éléments (ce que je crois que Iterator ne peut pas faire).

FWIW, nous voyons aussi que get est appelée sur list, ce qui ne pourrait pas être fait si sa référence était simplement Collection (au lieu du type plus spécifique List- de Collection) - List interface comprend get, mais pas Collection interface. Sinon pour cette différence, la référence list pourrait plutôt être un Collection [et, techniquement, cette réponse serait alors une réponse directe, au lieu d'une réponse tangentielle].

Le même code FWIW fonctionne toujours après modification pour commencer au début à chaque arrêt (de la même manière que Iterator ordre):

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;
    }
}
0
cellepo

Une solution pourrait consister à faire pivoter la liste et à supprimer le premier élément pour éviter les exceptions 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
Rahul Vala
for (Integer i : l)
{
    if (i.intValue() == 5){
            itemsToRemove.add(i);
            break;
    }
}

Si vous ignorez l'appel interne iterator.next (), vous devez supprimer l'élément de la liste. ça fonctionne encore! Bien que je ne propose pas d'écrire un code comme celui-ci, il est utile de comprendre le concept sous-jacent :-)

À 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
Yazon2006

Je sais que cette question est trop ancienne pour traiter de 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);
0
pedram bashiri

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

le temps de arraycopy augmente si (l'index diminue), en même temps que les éléments de la liste diminuent!

le meilleur moyen efficace de supprimer 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 (factorial (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'indexation: 1090 ms
  • pour desc index: 519 msec --- le meilleur
  • pour itérateur: 1043 ms
0
Nurlan