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?
@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.
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);
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();
}
}
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);