Collection lancia o non lancia ConcurrentModificationException in base ai contenuti della Raccolta

Il seguente codice Java lancia una ConcurrentModificationException , come previsto:

 public class Evil { public static void main(String[] args) { Collection c = new ArrayList(); c.add("lalala"); c.add("sososo"); c.add("ahaaha"); removeLalala(c); System.err.println(c); } private static void removeLalala(Collection c) { for (Iterator i = c.iterator(); i.hasNext();) { String s = i.next(); if(s.equals("lalala")) { c.remove(s); } } } } 

Ma il seguente esempio, che differisce solo nel contenuto della Collection , viene eseguito senza eccezioni:

 public class Evil { public static void main(String[] args) { Collection c = new ArrayList(); c.add("lalala"); c.add("lalala"); removeLalala(c); System.err.println(c); } private static void removeLalala(Collection c) { for (Iterator i = c.iterator(); i.hasNext();) { String s = i.next(); if(s.equals("lalala")) { c.remove(s); } } } } 

Questo stampa l’output “[lalala]”. Perché il secondo esempio non lancia una ConcurrentModificationException quando viene ConcurrentModificationException il primo esempio?

Risposta breve

Perché il comportamento fail-fast di un iteratore non è garantito.

Risposta lunga

Stai ottenendo questa eccezione perché non puoi manipolare una raccolta mentre la iterazione su di essa, tranne attraverso l’iteratore.

Cattivo:

 // we're using iterator for (Iterator i = c.iterator(); i.hasNext();) { // here, the collection will check it hasn't been modified (in effort to fail fast) String s = i.next(); if(s.equals("lalala")) { // s is removed from the collection and the collection will take note it was modified c.remove(s); } } 

Buona:

 // we're using iterator for (Iterator i = c.iterator(); i.hasNext();) { // here, the collection will check it hasn't been modified (in effort to fail fast) String s = i.next(); if(s.equals("lalala")) { // s is removed from the collection through iterator, so the iterator knows the collection changed and can resume the iteration i.remove(); } } 

Ora al “perché”: nel codice qui sopra, si noti come viene eseguito il controllo di modifica: la rimozione contrassegna la raccolta come modificata e la prossima iterazione verifica eventuali modifiche e fallisce se rileva la raccolta modificata. Un’altra cosa importante è che ArrayList (non sono sicuro delle altre raccolte) non controlla la modifica in hasNext() .

Pertanto, possono accadere due cose strane:

  • Se rimuovi l’ultimo elemento durante l’iterazione, non verrà generato nulla
    • Questo perché non esiste un elemento “next”, quindi l’iterazione termina prima di raggiungere il codice di controllo delle modifiche
  • Se rimuovi il penultimo elemento, ArrayList.hasNext() in realtà restituirà anche false , perché l’ current index sta puntando all’ultimo elemento (precedente dal penultimo).
    • Quindi, anche in questo caso, non c’è alcun elemento “successivo” dopo la rimozione

Nota che tutto questo è in linea con la documentazione di ArrayList :

Si noti che il comportamento fail-fast di un iteratore non può essere garantito in quanto è, in generale, imansible fornire garanzie rigide in presenza di modifiche simultanee non sincronizzate. Gli iteratori fail-fast lanciano ConcurrentModificationException su una base best-effort. Pertanto, sarebbe sbagliato scrivere un programma che dipendesse da questa eccezione per la sua correttezza: il comportamento fail-fast degli iteratori dovrebbe essere usato solo per rilevare i bug.

Modificato per aggiungere:

Questa domanda fornisce alcune informazioni sul motivo per cui il controllo della modifica simultanea non viene eseguito in hasNext() e viene eseguito solo in next() .

Se si guarda il codice sorgente per l’iteratore ArrayList (class Itr nidificata privata), vedrete il difetto nel codice.

Il codice dovrebbe essere fail-fast, che viene eseguito internamente checkForComodification() chiamando checkForComodification() , tuttavia hasNext() non effettua quella chiamata, probabilmente per motivi di prestazioni.

hasNext() invece è solo:

 public boolean hasNext() { return cursor != size; } 

Ciò significa che quando sei al penultimo elemento dell’elenco e quindi rimuovi un elemento (qualsiasi elemento), la dimensione viene ridotta e hasNext() pensa che tu sia sull’ultimo elemento (che non eri), e restituisce false , saltando l’iterazione dell’ultimo elemento senza errori.

OOPS !!!!

Da altre risposte sai qual è il modo giusto di rimuovere un elemento nella raccolta mentre stai iterando la raccolta. Dò qui la spiegazione alla domanda di base. E la risposta alla tua domanda si trova nella traccia dello stack sottostante

 Exception in thread "main" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(Unknown Source) at java.util.ArrayList$Itr.next(Unknown Source) at com.ii4sm.controller.Evil.removeLalala(Evil.java:23) at com.ii4sm.controller.Evil.main(Evil.java:17) 

Nello stacktrace è ovvio che i.next(); la linea lancia l’errore. Ma quando hai solo due elementi nella collezione.

 Collection c = new ArrayList(); c.add("lalala"); c.add("lalala"); removeLalala(c); System.err.println(c); 

Quando il primo viene rimosso, i.hasNext() restituisce false e i.next() non viene mai eseguito per generare l’eccezione

dovresti rimuovere direttamente iterator (i) non dalla collection (c);

prova questo:

 for (Iterator i = c.iterator(); i.hasNext();) { String s = i.next(); if(s.equals("lalala")) { i.remove(); //note here, changing c to i with no parameter. } } 

MODIFICARE:

Il motivo per cui la prima prova genera un’eccezione mentre la seconda non è semplicemente a causa del numero di elementi nella tua raccolta.

poiché il primo percorrerà il loop più di una volta e il secondo verrà ripetuto solo una volta. quindi, non ha la possibilità di lanciare un’eccezione

Non è ansible rimuovere dalla lista se si sta navigando con “per ogni” ciclo.

Non puoi rimuovere un elemento da una raccolta su cui stai iterando. Puoi aggirare questo problema usando esplicitamente un Iterator e rimuovendo l’object lì. Puoi usare Iterator.

Se usi il codice qui sotto non otterrai alcuna eccezione:

 private static void removeLalala(Collection c) { /*for (Iterator i = c.iterator(); i.hasNext();) { String s = i.next(); if(s.equals("lalala")) { c.remove(s); } }*/ Iterator it = c.iterator(); while (it.hasNext()) { String st = it.next(); if (st.equals("lalala")) { it.remove(); } } }