Evitando a ConcurrentModificationException em Java
- #Java
O problema de receber uma ConcurrentModificationException.
Ao manipular coleções em Java, você muitas vezes você pode se deparar com essa exceção o que causa confusão em muita gente. Como pode uma aplicação single threaded lançar uma exceção sobre concorrência? A resposta para isso é que a concorrência aqui se refere a tentar por exemplo remover um elemento enquanto ao mesmo tempo se itera em uma lista.
Entendendo de forma fácil a ConcurrentModificationException
Esse é um mecanisco chamado de fast fail iterators por perceberem quando uma modificação concorrente é detectada, utilizado propositalmente em aplicações single threaded como uma tentativa de encontrar/ evitar bugs e comportamentos estranhos, como iterar em uma lista que acabou de ser modificada e isso pode evitar vários problemas. É daí que vem a exceção.
Exemplo prático de como lançar essa exceção
public void removeNamedItem(String name){
for(Item item : this.items){
if(item.getName().equalsIgnoreCase(name)){
items.remove(item);
}
}
}
Esse método recebe como paramêtro o nome de um item a ser removido da lista. Então o método faz uma varredura na lista e verifica se o nome do item na lista corresponde ao nome que foi passado por prarâmetro. Se o item indicado estiver na última posição na lista não haverá problema algum, agora se o item estiver no início ou em qualquer lugar no meio da lista a exceção ConcurrentModificationException será lançada.
Soluções para o problema
Usar um Iterator diretamente
Aqui abrimos mão da simplicidade do for each para ter acesso ao método remove() invocado pelo iterator
public void removeByNameWithIterator(String name){
for(Iterator<Item> iterator = items.iterator(); iterator.hasNext();){
Item item = iterator.next();
if(item.getName().equalsIgnoreCase(name)){
iterator.remove();
}
}
}
O segredo aqui é usar o método remove() que o iterator tem acesso. Esse método é seguro para se usar enquanto se itera e não lança ConcurrentModificationException.
Iterar primeiro depois remover
Nesse caso fazemos uso do método removeAll(), que recebe uma coleção de elementos. Aqui fazemos a iteração na lista armazenando os elementos que atingem os critérios de filtragem impostos em uma lista secundária chamada toRemove e posteriormente utilizamos o metodo removelAll() para remover os elementos.
public void removeAllByName(String name){
List<Item> toRemove = new ArrayList<>();
for(Item item : items){
if(item.getName().equalsIgnoreCase(name)){
toRemove.add(item);
}
}
items.removeAll(toRemove);
}
Usando o removeIf()
Com o Java 8 foi introduzido o metodo removeIf() no qual podemos fazer uso de uma forma declarativa para chegar ao mesmo resultado com menos verbosidade.
public void removeNamedItemRevamped(String name){
items.removeIf(item -> item.getName().equalsIgnoreCase(name));
}
Aqui montamos um predicado substituindo o for each por uma variável seguida do operador de seta e o filtro que querremos aplicar.
Considerações finais
Existem uma opção fazendo uso de streams mas vou deixar para talvez um próximo artigo. O Java é uma linguagem muito madura e disponibiliza muitos mecanismos para se chegar a solução desejada. Sigamos aprendendo juntos nessa jornada.