web-dev-qa-db-fra.com

ConcurrentModificationException lors de l'utilisation d'un flux avec le jeu de clés Maps

Je souhaite supprimer tous les éléments de someMap dont les clés ne sont pas présentes dans someList. Regardez dans mon code:

someMap.keySet().stream().filter(v -> !someList.contains(v)).forEach(someMap::remove);

Je reçois Java.util.ConcurrentModificationException. Pourquoi? Stream n'est pas parallèle. Quel est le moyen le plus élégant de faire cela?

18
jaskmar

@Eran déjà a expliqué comment mieux résoudre ce problème. Je vais expliquer pourquoi ConcurrentModificationException se produit.

La ConcurrentModificationException se produit parce que vous modifiez la source du flux. Votre Map est susceptible d'être HashMap ou TreeMap ou une autre carte non concurrente. Supposons que c'est une HashMap. Chaque flux est soutenu par Spliterator . Si le séparateur n'a pas de caractéristiques IMMUTABLE et CONCURRENT, alors, comme le dit la documentation:

Après avoir lié un Spliterator, vous devriez, au mieux, lancer ConcurrentModificationException si une interférence structurelle est détectée. Les spliterators qui font cela s'appellent fail-fast.

Donc, HashMap.keySet().spliterator() n'est pas IMMUTABLE (car cette Set peut être modifiée) et non pas CONCURRENT (les mises à jour simultanées ne sont pas sûres pour HashMap). Donc, il détecte simplement les modifications simultanées et jette une ConcurrentModificationException comme le prescrit la documentation du spliterator.

En outre, il convient de citer la HashMap documentation:

Les itérateurs renvoyés par toutes les "méthodes de vue de collection" de cette classe sont fail-fast: si la carte est structurellement modifiée à tout moment après la création de l'itérateur, de quelque manière que ce soit, sauf par le biais de la méthode de suppression de cet itérateur jettera une ConcurrentModificationException. Ainsi, face à une modification concurrente, l'itérateur échoue rapidement et proprement, au lieu de risquer un comportement arbitraire non déterministe à une date future indéterminée.

Notez que le comportement de défaillance d'un itérateur ne peut pas être garanti car il est généralement impossible de donner des garanties absolues en présence de modifications simultanées non synchronisées. Les itérateurs qui échouent rapidement lancent ConcurrentModificationException au mieux. Par conséquent, il serait erroné d'écrire un programme dont l'exactitude est fonction de cette exception: le comportement d'échec rapide des itérateurs ne devrait être utilisé que pour détecter des bogues

Bien que cela ne concerne que les itérateurs, je crois que c'est la même chose pour les spliterators.

27
Tagir Valeev

Vous n'avez pas besoin de l'API Stream pour cela. Utilisez retainAll sur keySet. Toute modification apportée à la Set renvoyée par keySet() est reflétée dans la Map d'origine.

someMap.keySet().retainAll(someList);
13
Eran

Votre appel de flux fait (logiquement) la même chose que:

for (K k : someMap.keySet()) {
    if (!someList.contains(k)) {
        someMap.remove(k);
    }
}

Si vous exécutez cette opération, vous constaterez qu'elle jette ConcurrentModificationException, car elle modifie la carte en même temps que vous la parcourez. Si vous consultez les docs , vous remarquerez ce qui suit:

Notez que cette exception n'indique pas toujours qu'un objet a été modifié simultanément par un autre thread. Si un seul thread émet une séquence d'appels de méthode qui ne respecte pas le contrat d'un objet, celui-ci peut lever cette exception. Par exemple, si un thread modifie une collection directement alors qu'il parcourt la collection avec un itérateur à réponse rapide, l'itérateur lève cette exception. 

C’est ce que vous faites, l’implémentation de la carte que vous utilisez a évidemment des itérateurs à grande vitesse, donc cette exception est levée.

Une alternative possible consiste à supprimer les éléments à l'aide de l'itérateur directement:

for (Iterator<K> ks = someMap.keySet().iterator(); ks.hasNext(); ) {
    K next = ks.next();
    if (!someList.contains(k)) {
        ks.remove();
    }
}
8
thecoop

Une réponse ultérieure, mais vous pouvez insérer un collecteur dans votre pipeline de sorte que forEach fonctionne sur un ensemble contenant une copie des clés:

someMap.keySet()
    .stream()
    .filter(v -> !someList.contains(v))
    .collect(Collectors.toSet())
    .forEach(someMap::remove);
2
Ron McLeod