ConcurrentModificationException

Spis treści

Opis

ConcurrentModificationException jest wyjątkiem z grupy wyjątków niekontrolowanych (unchedked exceptions), co oznacza, że dziedziczy po klasie RuntimeException i nie mamy obowiązku jego obsługi. Wyjątek znajduje się w pakiecie java.util, więc pełna ścieżka do niego wygląda następująco java.util.ConcurrentModificationException.

Wyjątek jest rzucany w sytuacji, gdy zmiana stanu obiektu wpływa na stan innego obiektu, który także na nim operował. Zmiany niekoniecznie muszą zachodzić w dwóch niezależnych od siebie wątkach. Najpopularniejszym przypadkiem występowania tego błędu jest usuwanie obiektów z kolekcji, podczas gdy iterujemy po niej korzystając z iteratora.

 

Przykład

ConcurrentExample.java

import java.util.ArrayList;
import java.util.List;

class ConcurrentExample {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>(List.of(5, 10, 15, 20, 25, 30, 35, 40));

        for (Integer number : numbers) {
            if(number % 10 == 0)
                numbers.remove(number);
        }
    }
}

W powyższym przykładzie tworzymy listę typu ArrayList. Dla wygody wykorzystuję metodę List.of(), dostępną od Javy 10, aby od razu wstawić do niej elementy. Nie możemy zapisać po prostu:

List<Integer> numbers = List.of(5, 10, 15, 20, 25, 30, 35, 40);

ponieważ lista zwracana zwracana przez metodę List.of() jest niemodyfikowalna, co oznacza, że nie można do niej ani dodawać ani usuwać elementów w dalszej części kodu.

W pętli for each chcemy usunąć wszystkie elementy, podzielne przez 10, więc w naszym przykładzie liczby 10, 20, 30 i 40. Po uruchomieniu programu zobaczymy jednak wyjątek ConcurrentModificationException.

concurrentmodificationexceptionProblem polega na tym, że korzystając z pętli for each, pod spodem wykorzystywany jest iterator. Powyższy przykład można więc zapisać także w taki sposób:

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

class ConcurrentExample {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>(List.of(5, 10, 15, 20, 25, 30, 35, 40));

//        for (Integer number : numbers) {
//            if(number % 10 == 0)
//                numbers.remove(number);
//        }

        for (Iterator<Integer> it = numbers.iterator(); it.hasNext();) {
            Integer next = it.next();
            if(next % 10 == 0)
                numbers.remove(next);
        }
    }
}

Teraz lepiej widać, że po kolekcji iterujemy korzystając z iteratora, ale elementy usuwamy wywołując metodę remove() na kolekcji. Wyjątek jest rzucany przez metodę next() iteratora, gdy ten orientuje się, że kolekcja w międzyczasie się zmieniła.

Kurs Java

Rozwiązaniem problemu jest używanie metody remove() iteratora, zamiast metody remove() kolekcji.

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

class ConcurrentExample {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>(List.of(5, 10, 15, 20, 25, 30, 35, 40));

        for (Iterator<Integer> it = numbers.iterator(); it.hasNext();) {
            Integer next = it.next();
            if(next % 10 == 0)
                it.remove(); //metoda remove() iteratora
        }
    }
}

Z powyższego można wyciągnąć wniosek, że nie ma możliwości usuwania elementów z kolekcji podczas iterowania po niej korzystając z pętli for each. W Javie 8 pojawiła się także wygodna metoda removeIf(), która pozwala zapisać powyższy kod znacznie prościej. Mowa ta przyjmuje jako argument predykat. Jeśli element kolekcji go spełnia, to zostaje usunięty.

numbers.removeIf(next -> next % 10 == 0);

 

Dyskusja i komentarze

Masz pytania do tego wpisu? Może chcesz się podzieliś spostrzeżeniami? Zapraszamy dyskusji na naszej grupe na Facebooku.