Evitare l'eccezione ConcurrentModificationException in Java

1. Introduzione

In questo articolo daremo uno sguardo alla classe ConcurrentModificationException .

Per prima cosa, daremo una spiegazione su come funziona, quindi lo dimostreremo utilizzando un test per attivarlo.

Infine, proveremo alcune soluzioni alternative utilizzando esempi pratici.

2. Attivazione di un'eccezione ConcurrentModificationException

In sostanza, ConcurrentModificationException viene utilizzato per fallire rapidamente quando viene modificato qualcosa su cui stiamo iterando. Dimostriamolo con un semplice test:

@Test(expected = ConcurrentModificationException.class) public void whilstRemovingDuringIteration_shouldThrowException() throws InterruptedException { List integers = newArrayList(1, 2, 3); for (Integer integer : integers) { integers.remove(1); } }

Come possiamo vedere, prima di terminare la nostra iterazione stiamo rimuovendo un elemento. Questo è ciò che fa scattare l'eccezione.

3. Soluzioni

A volte, potremmo effettivamente voler rimuovere elementi da una raccolta durante l'iterazione. Se questo è il caso, allora ci sono alcune soluzioni.

3.1. Utilizzo diretto di un iteratore

Un ciclo for-each utilizza un iteratore dietro le quinte ma è meno dettagliato. Tuttavia, se abbiamo modificato il nostro test precedente per utilizzare un Iterator, avremo accesso a metodi aggiuntivi, come remove (). Proviamo invece a utilizzare questo metodo per modificare il nostro elenco:

for (Iterator iterator = integers.iterator(); iterator.hasNext();) { Integer integer = iterator.next(); if(integer == 2) { iterator.remove(); } }

Ora noteremo che non ci sono eccezioni. La ragione di ciò è che il metodo remove () non causa un'eccezione ConcurrentModificationException. È sicuro chiamare durante l'iterazione.

3.2. Non rimuovere durante l'iterazione

Se vogliamo mantenere il nostro ciclo for-each , allora possiamo. È solo che dobbiamo aspettare fino a dopo l'iterazione prima di rimuovere gli elementi. Proviamo aggiungendo ciò che vogliamo rimuovere a un elenco toRemove mentre iteriamo :

List integers = newArrayList(1, 2, 3); List toRemove = newArrayList(); for (Integer integer : integers) { if(integer == 2) { toRemove.add(integer); } } integers.removeAll(toRemove); assertThat(integers).containsExactly(1, 3); 

Questo è un altro modo efficace per aggirare il problema.

3.3. Usare removeIf ()

Java 8 ha introdotto il metodo removeIf () nell'interfaccia Collection . Ciò significa che se ci stiamo lavorando, possiamo usare idee di programmazione funzionale per ottenere di nuovo gli stessi risultati:

List integers = newArrayList(1, 2, 3); integers.removeIf(i -> i == 2); assertThat(integers).containsExactly(1, 3);

Questo stile dichiarativo ci offre la minima quantità di verbosità. Tuttavia, a seconda del caso d'uso, potremmo trovare altri metodi più convenienti.

3.4. Filtraggio tramite flussi

Quando ci immergiamo nel mondo della programmazione funzionale / dichiarativa, possiamo dimenticarci di mutare le collezioni, invece, possiamo concentrarci sugli elementi che dovrebbero essere effettivamente elaborati:

Collection integers = newArrayList(1, 2, 3); List collected = integers .stream() .filter(i -> i != 2) .map(Object::toString) .collect(toList()); assertThat(collected).containsExactly("1", "3");

Abbiamo fatto il contrario al nostro esempio precedente, fornendo un predicato per determinare gli elementi da includere, non da escludere. Il vantaggio è che possiamo concatenare altre funzioni insieme alla rimozione. Nell'esempio, usiamo una map () funzionale, ma potremmo usare anche più operazioni se vogliamo.

4. Conclusione

In questo articolo abbiamo mostrato i problemi che potresti incontrare se rimuovi elementi da una raccolta durante l'iterazione e abbiamo anche fornito alcune soluzioni per annullare il problema.

L'implementazione di questi esempi può essere trovata su GitHub. Questo è un progetto Maven, quindi dovrebbe essere facile da eseguire così com'è.