ConcurrentModificationException
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. Warto tutaj wspomnieć, że nie możemy skorzystać z metody List.of():
List<Integer> numbers = List.of(5, 10, 15, 20, 25, 30, 35, 40);
ponieważ lista zwracana przez metodę List.of() jest niemodyfikowalna, co oznacza, że nie można do niej ani dodawać, ani z niej 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.
Problem 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 i iterator jest nieaktualny.
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. Metoda 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 grupie na Facebooku.