Iterando através de uma Coleção, evitando ConcurrentModificationException ao remover objetos em um loop


1194

Todos sabemos que você não pode fazer o seguinte por causa de ConcurrentModificationException:

for (Object i : l) {
    if (condition(i)) {
        l.remove(i);
    }
}

Mas isso aparentemente funciona às vezes, mas nem sempre. Aqui está um código específico:

public static void main(String[] args) {
    Collection<Integer> l = new ArrayList<>();

    for (int i = 0; i < 10; ++i) {
        l.add(4);
        l.add(5);
        l.add(6);
    }

    for (int i : l) {
        if (i == 5) {
            l.remove(i);
        }
    }

    System.out.println(l);
}

Obviamente, isso resulta em:

Exception in thread "main" java.util.ConcurrentModificationException

Mesmo que vários threads não estejam fazendo isso. De qualquer forma.

Qual é a melhor solução para esse problema? Como posso remover um item da coleção em um loop sem lançar essa exceção?

Também estou usando um arbitrário Collectionaqui, não necessariamente um ArrayList, então você não pode confiar get.


Nota para os leitores: leia docs.oracle.com/javase/tutorial/collections/interfaces/… , pode ser uma maneira mais fácil de conseguir o que você deseja fazer.
GKFX

Respostas:


1601

Iterator.remove() é seguro, você pode usá-lo assim:

List<String> list = new ArrayList<>();

// This is a clever way to create the iterator and call iterator.hasNext() like
// you would do in a while-loop. It would be the same as doing:
//     Iterator<String> iterator = list.iterator();
//     while (iterator.hasNext()) {
for (Iterator<String> iterator = list.iterator(); iterator.hasNext();) {
    String string = iterator.next();
    if (string.isEmpty()) {
        // Remove the current element from the iterator and the list.
        iterator.remove();
    }
}

Observe que Iterator.remove()é a única maneira segura de modificar uma coleção durante a iteração; o comportamento não é especificado se a coleção subjacente for modificada de qualquer outra maneira enquanto a iteração estiver em andamento.

Fonte: docs.oracle> A interface da coleção


Da mesma forma, se você tem um ListIteratore deseja adicionar itens, pode usá-lo ListIterator#add, pelo mesmo motivo que pode usá Iterator#remove -lo - ele foi projetado para permitir isso.


No seu caso, você tentou remover de uma lista, mas a mesma restrição se aplica se você tentar putentrar um Mappouco enquanto itera seu conteúdo.


19
E se você deseja remover um elemento que não seja o elemento retornado na iteração atual?
21813 Eugen

2
Você precisa usar o .remove no iterador e isso só pode remover o elemento atual, portanto não :) :) #
K Bill Bill

1
Esteja ciente de que este é mais lento em comparação ao uso ConcurrentLinkedDeque ou CopyOnWriteArrayList (pelo menos no meu caso)
Dan

1
Não é possível fazer a iterator.next()chamada no loop for? Se não, alguém pode explicar o porquê?
Blake

1
@GonenI É implementado para todos os iteradores de coleções que não são imutáveis. List.addtambém é "opcional" no mesmo sentido, mas você não diria que é "inseguro" adicionar a uma lista.
Radiodef 9/08/19

345

Isso funciona:

Iterator<Integer> iter = l.iterator();
while (iter.hasNext()) {
    if (iter.next() == 5) {
        iter.remove();
    }
}

Eu assumi que, como um loop foreach é um açúcar sintático para a iteração, o uso de um iterador não ajudaria ... mas fornece essa .remove()funcionalidade.


43
O loop foreach é um açúcar sintático para a iteração. No entanto, como você apontou, é necessário chamar remove no iterador - ao qual foreach não lhe dá acesso. Daí a razão pela qual você não pode remover em um loop foreach (mesmo que você está realmente usando um iterador sob o capô)
madlep

36
+1 por exemplo, código para usar iter.remove () no contexto, que a resposta de Bill K não possui [diretamente].
Eddified

202

Com o Java 8, você pode usar o novo removeIfmétodo . Aplicado ao seu exemplo:

Collection<Integer> coll = new ArrayList<>();
//populate

coll.removeIf(i -> i == 5);

3
Ooooo! Eu esperava que algo no Java 8 ou 9 pudesse ajudar. Isso ainda parece bastante detalhado para mim, mas eu ainda gosto.
James T Snell

A implementação de equals () também é recomendada neste caso?
Anmol Gupta

pela maneira como removeIfusa Iteratore whileloop. Você pode vê-lo em java 8java.util.Collection.java
omerhakanbilici

3
@omerhakanbilici Algumas implementações, como ArrayListsubstituí-lo por motivos de desempenho. O que você está se referindo é apenas a implementação padrão.
Didier L

@AnmolGupta: Não, equalsnão é usado aqui, por isso não precisa ser implementado. (Mas é claro, se você usar equalsem seu teste, ele deverá ser implementado da maneira que você deseja.) #
0100

42

Como a pergunta já foi respondida, ou seja, a melhor maneira é usar o método remove do objeto iterador, eu entraria nas especificidades do local em que o erro "java.util.ConcurrentModificationException"é gerado.

Toda classe de coleção tem uma classe privada que implementa a interface Iterator e fornece métodos como next(), remove()e hasNext().

O código para o próximo se parece com isso ...

public E next() {
    checkForComodification();
    try {
        E next = get(cursor);
        lastRet = cursor++;
        return next;
    } catch(IndexOutOfBoundsException e) {
        checkForComodification();
        throw new NoSuchElementException();
    }
}

Aqui o método checkForComodificationé implementado como

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

Portanto, como você pode ver, se tentar explicitamente remover um elemento da coleção. Isso resulta em modCountficar diferente de expectedModCount, resultando na exceção ConcurrentModificationException.


Muito interessante. Obrigado! Geralmente, eu não chamo de remove (), prefiro limpar a coleção depois de iterá-la. Para não dizer que é um bom padrão, exatamente o que tenho feito ultimamente.
James T Snell

26

Você pode usar o iterador diretamente como você mencionou, ou manter uma segunda coleção e adicionar cada item que deseja remover à nova coleção e, em seguida, removeAll no final. Isso permite que você continue usando a segurança de tipo do loop for-each ao custo de maior uso de memória e tempo de CPU (não deve ser um grande problema, a menos que você tenha listas realmente grandes ou um computador muito antigo)

public static void main(String[] args)
{
    Collection<Integer> l = new ArrayList<Integer>();
    Collection<Integer> itemsToRemove = new ArrayList<>();
    for (int i=0; i < 10; i++) {
        l.add(Integer.of(4));
        l.add(Integer.of(5));
        l.add(Integer.of(6));
    }
    for (Integer i : l)
    {
        if (i.intValue() == 5) {
            itemsToRemove.add(i);
        }
    }

    l.removeAll(itemsToRemove);
    System.out.println(l);
}

7
isto é o que eu normalmente faço, mas o iterador explícito é uma solução mais elegante que sinto.
Claudiu

1
É justo o suficiente, desde que você não esteja fazendo mais nada com o iterador - tê-lo exposto facilita fazer coisas como chamar .next () duas vezes por loop etc. Não é um grande problema, mas pode causar problemas se você estiver fazendo algo mais complicado do que apenas percorrer uma lista para excluir entradas.
RodeoClown 21/10/08

@RodeoClown: na pergunta original, Claudiu está removendo da coleção, não o iterador.
matt b

1
Remover do iterador remove da coleção subjacente ... mas o que eu estava dizendo no último comentário é que se você estiver fazendo algo mais complicado do que apenas procurar exclusões no loop (como processar dados corretos) usando o iterador, pode erros mais fáceis de fazer.
RodeoClown 22/10/08

Se for um simples valor de exclusão que não é necessário e o loop estiver fazendo apenas uma coisa, usar o iterador diretamente e chamar .remove () está absolutamente correto.
RodeoClown 22/10/08

17

Nesses casos, um truque comum é (foi?) Retroceder:

for(int i = l.size() - 1; i >= 0; i --) {
  if (l.get(i) == 5) {
    l.remove(i);
  }
}

Dito isto, estou mais do que feliz por você ter maneiras melhores no Java 8, por exemplo, removeIfou filterem fluxos.


2
Este é um bom truque. Mas não funcionaria em coleções não indexadas, como conjuntos, e seria muito lento em digitar listas vinculadas.
Claudiu

@ Claudiu Sim, isso é definitivamente apenas para ArrayListcoleções similares.
Landei

Estou usando um ArrayList, isso funcionou perfeitamente, obrigado.
StarSweeper 21/02

2
índices são ótimos. Se é tão comum, por que você não usa for(int i = l.size(); i-->0;) {?
John

16

A mesma resposta que Claudius com um loop for:

for (Iterator<Object> it = objects.iterator(); it.hasNext();) {
    Object object = it.next();
    if (test) {
        it.remove();
    }
}

12

Com o Eclipse Collections , o método removeIfdefinido em MutableCollection funcionará:

MutableList<Integer> list = Lists.mutable.of(1, 2, 3, 4, 5);
list.removeIf(Predicates.lessThan(3));
Assert.assertEquals(Lists.mutable.of(3, 4, 5), list);

Com a sintaxe do Java 8 Lambda, isso pode ser escrito da seguinte maneira:

MutableList<Integer> list = Lists.mutable.of(1, 2, 3, 4, 5);
list.removeIf(Predicates.cast(integer -> integer < 3));
Assert.assertEquals(Lists.mutable.of(3, 4, 5), list);

A chamada para Predicates.cast()é necessária aqui porque um removeIfmétodo padrão foi incluído na java.util.Collectioninterface no Java 8.

Nota: Sou um colaborador das Coleções Eclipse .


10

Faça uma cópia da lista existente e itere sobre a nova cópia.

for (String str : new ArrayList<String>(listOfStr))     
{
    listOfStr.remove(/* object reference or index */);
}

19
Fazer uma cópia parece um desperdício de recursos.
Antzi

3
@ Antzi Isso depende do tamanho da lista e da densidade dos objetos dentro. Ainda é uma solução valiosa e válida.
Mre

Eu tenho usado esse método. É preciso um pouco mais de recursos, mas muito mais flexível e claro.
Tao Zhang

Essa é uma boa solução quando você não pretende remover objetos dentro do próprio loop, mas eles são removidos "aleatoriamente" de outros threads (por exemplo, operações de rede que atualizam dados). Se você está fazendo estas cópias muito há ainda a aplicação java fazendo exatamente isto: docs.oracle.com/javase/8/docs/api/java/util/concurrent/...
A1m

8

As pessoas afirmam que não é possível remover de uma coleção que está sendo iterada por um loop foreach. Eu só queria salientar que isso é tecnicamente incorreto e descrever exatamente (eu sei que a pergunta do OP é tão avançada que evita saber disso) o código por trás dessa suposição:

for (TouchableObj obj : untouchedSet) {  // <--- This is where ConcurrentModificationException strikes
    if (obj.isTouched()) {
        untouchedSet.remove(obj);
        touchedSt.add(obj);
        break;  // this is key to avoiding returning to the foreach
    }
}

Não é que você não possa remover da iteração Colletione não pode continuar a iteração depois de fazer isso. Daí o breakcódigo acima.

Desculpas se esta resposta for um caso de uso um tanto especializado e mais adequado ao encadeamento original de onde cheguei, que seja marcado como duplicado (apesar de esse encadeamento parecer mais matizado) disso e bloqueado.


8

Com um loop for tradicional

ArrayList<String> myArray = new ArrayList<>();

for (int i = 0; i < myArray.size(); ) {
    String text = myArray.get(i);
    if (someCondition(text))
        myArray.remove(i);
    else
        i++;   
}

Ah, então é realmente apenas o loop for aprimorado que lança a exceção.
Cellepo

FWIW - o mesmo código ainda funcionaria depois de modificado para aumentar i++a proteção do loop em vez de dentro do corpo do loop.
Cellepo

Correção ^: Isso é se o i++incremento não eram condicionais - Vejo agora que é por isso que você fazê-lo no corpo :)
cellepo

2

A ListIteratorpermite adicionar ou remover itens da lista. Suponha que você tenha uma lista de Carobjetos:

List<Car> cars = ArrayList<>();
// add cars here...

for (ListIterator<Car> carIterator = cars.listIterator();  carIterator.hasNext(); )
{
   if (<some-condition>)
   { 
      carIterator().remove()
   }
   else if (<some-other-condition>)
   { 
      carIterator().add(aNewCar);
   }
}

Os métodos adicionais na interface ListIterator (extensão do Iterator) são interessantes - particularmente o previousmétodo.
Cellepo

1

Eu tenho uma sugestão para o problema acima. Não há necessidade de lista secundária ou tempo extra. Por favor, encontre um exemplo que faria o mesmo, mas de uma maneira diferente.

//"list" is ArrayList<Object>
//"state" is some boolean variable, which when set to true, Object will be removed from the list
int index = 0;
while(index < list.size()) {
    Object r = list.get(index);
    if( state ) {
        list.remove(index);
        index = 0;
        continue;
    }
    index += 1;
}

Isso evitaria a exceção de simultaneidade.


1
A pergunta afirma explicitamente que o OP não é necessário ArrayListe, portanto, não pode ser utilizado get(). Caso contrário, provavelmente uma boa abordagem, no entanto.
22414 kaskelotti

(Esclarecimento ^) O OP está usando uma interface arbitrária Collection- Collectionnão inclui get. (Embora a Listinterface do FWIW inclua 'get').
Cellepo

Acabei de adicionar uma resposta separada e mais detalhada aqui também para while-looping a List. Mas +1 para esta resposta, porque ela veio primeiro.
Cellepo

1

ConcurrentHashMap ou ConcurrentLinkedQueue ou ConcurrentSkipListMap pode ser outra opção, porque eles nunca lançam nenhuma ConcurrentModificationException, mesmo se você remover ou adicionar um item.


Sim, e observe que todos estão no java.util.concurrentpacote. Algumas outras classes de casos de uso semelhantes / comuns desse pacote são CopyOnWriteArrayList & CopyOnWriteArraySet [mas não limitadas a elas].
Cellepo

Na verdade, eu só aprendi que embora aqueles estrutura de dados Objetos evite ConcurrentModificationException , usá-los em uma reforçada -por-circuito pode ainda causar problemas de indexação (ou seja: ainda pular elementos, ou IndexOutOfBoundsException...)
cellepo

1

Eu sei que esta pergunta é muito antiga para ser sobre o Java 8, mas para aqueles que usam o Java 8 você pode usar facilmente removeIf ():

Collection<Integer> l = new ArrayList<Integer>();

for (int i=0; i < 10; ++i) {
    l.add(new Integer(4));
    l.add(new Integer(5));
    l.add(new Integer(6));
}

l.removeIf(i -> i.intValue() == 5);

1

Outra maneira é criar uma cópia do seu arrayList:

List<Object> l = ...

List<Object> iterationList = ImmutableList.copyOf(l);

for (Object i : iterationList) {
    if (condition(i)) {
        l.remove(i);
    }
}

Nota: inão é um indexobjeto, mas um objeto. Talvez chamá-lo objfosse mais adequado.
luckydonald 29/03/19

1

A melhor maneira (recomendada) é o uso do pacote java.util.Concurrent. Ao usar este pacote, você pode evitar facilmente essa exceção. consulte Código Modificado

public static void main(String[] args) {
    Collection<Integer> l = new CopyOnWriteArrayList<Integer>();

    for (int i=0; i < 10; ++i) {
        l.add(new Integer(4));
        l.add(new Integer(5));
        l.add(new Integer(6));
    }

    for (Integer i : l) {
        if (i.intValue() == 5) {
            l.remove(i);
        }
    }

    System.out.println(l);
}

0

No caso de ArrayList: remove (int index) - se (index for a posição do último elemento), ele evita sem System.arraycopy()e não leva tempo para isso.

o tempo de arraycopy aumenta se (o índice diminui), a propósito, os elementos da lista também diminuem!

a melhor maneira eficaz de remover é- remover seus elementos em ordem decrescente: while(list.size()>0)list.remove(list.size()-1);// leva O (1) while(list.size()>0)list.remove(0);// leva O (fatorial (n))

//region prepare data
ArrayList<Integer> ints = new ArrayList<Integer>();
ArrayList<Integer> toRemove = new ArrayList<Integer>();
Random rdm = new Random();
long millis;
for (int i = 0; i < 100000; i++) {
    Integer integer = rdm.nextInt();
    ints.add(integer);
}
ArrayList<Integer> intsForIndex = new ArrayList<Integer>(ints);
ArrayList<Integer> intsDescIndex = new ArrayList<Integer>(ints);
ArrayList<Integer> intsIterator = new ArrayList<Integer>(ints);
//endregion

// region for index
millis = System.currentTimeMillis();
for (int i = 0; i < intsForIndex.size(); i++) 
   if (intsForIndex.get(i) % 2 == 0) intsForIndex.remove(i--);
System.out.println(System.currentTimeMillis() - millis);
// endregion

// region for index desc
millis = System.currentTimeMillis();
for (int i = intsDescIndex.size() - 1; i >= 0; i--) 
   if (intsDescIndex.get(i) % 2 == 0) intsDescIndex.remove(i);
System.out.println(System.currentTimeMillis() - millis);
//endregion

// region iterator
millis = System.currentTimeMillis();
for (Iterator<Integer> iterator = intsIterator.iterator(); iterator.hasNext(); )
    if (iterator.next() % 2 == 0) iterator.remove();
System.out.println(System.currentTimeMillis() - millis);
//endregion
  • para loop de índice: 1090 ms
  • para índice desc: 519 ms --- o melhor
  • para iterador: 1043 ms

0
for (Integer i : l)
{
    if (i.intValue() == 5){
            itemsToRemove.add(i);
            break;
    }
}

O problema é depois de remover o elemento da lista se você pular a chamada iterator.next () interna. ainda funciona! Embora eu não proponha escrever código como este, ajuda a entender o conceito por trás dele :-)

Felicidades!


0

Exemplo de modificação de coleção segura de encadeamento:

public class Example {
    private final List<String> queue = Collections.synchronizedList(new ArrayList<String>());

    public void removeFromQueue() {
        synchronized (queue) {
            Iterator<String> iterator = queue.iterator();
            String string = iterator.next();
            if (string.isEmpty()) {
                iterator.remove();
            }
        }
    }
}

0

Eu sei que esta pergunta assume apenas um Collection, e não mais especificamente nenhum List. Mas, para aqueles que estão lendo esta pergunta que estão de fato trabalhando com uma Listreferência, é possível evitar ConcurrentModificationExceptioncom um whileloop (enquanto modifica nela), se desejar evitarIterator (ou se deseja evitá-la em geral ou evitá-la especificamente para obter uma ordem de loop diferente da parada do início ao fim em cada elemento [que eu acredito que é a única ordem que Iteratorpode fazer]):

* Atualização: Veja os comentários abaixo que esclarecem que o análogo também é possível com o tradicional for -loop.

final List<Integer> list = new ArrayList<>();
for(int i = 0; i < 10; ++i){
    list.add(i);
}

int i = 1;
while(i < list.size()){
    if(list.get(i) % 2 == 0){
        list.remove(i++);

    } else {
        i += 2;
    }
}

Não ConcurrentModificationException desse código.

Aí vemos que o loop não começa no começo e não pára em todos os elementos (o que eu acredito Iteratorque não pode fazer).

FWIW também vemos getser chamado list, o que não poderia ser feito se sua referência fosse justa Collection(em vez do Listtipo mais específico deCollection ) - Listinterface inclui get, mas Collectioninterface não. Se não fosse essa diferença, a listreferência poderia ser uma Collection[e, portanto, tecnicamente essa resposta seria uma resposta direta, em vez de uma resposta tangencial].

O mesmo código do FWIWW ainda funciona após a modificação para começar do início ao fim de cada elemento (assim como a Iteratorordem):

final List<Integer> list = new ArrayList<>();
for(int i = 0; i < 10; ++i){
    list.add(i);
}

int i = 0;
while(i < list.size()){
    if(list.get(i) % 2 == 0){
        list.remove(i);

    } else {
        ++i;
    }
}

Isso ainda requer um cálculo muito cuidadoso das indicações para remover, no entanto.
OneCricketeer

Além disso, esta é apenas uma explicação mais detalhada dessa resposta stackoverflow.com/a/43441822/2308683
OneCricketeer

É bom saber - obrigado! Essa outra resposta me ajudou a entender que é o loop for aprimorado que lançaria ConcurrentModificationException, mas não o loop for for tradicional (que a outra resposta usa) - sem perceber que antes era por isso que eu estava motivado a escrever essa resposta (eu erroneamente pensava então que eram todos os loops que lançariam a exceção).
Cellepo

0

Uma solução poderia ser girar a lista e remover o primeiro elemento para evitar ConcurrentModificationException ou IndexOutOfBoundsException

int n = list.size();
for(int j=0;j<n;j++){
    //you can also put a condition before remove
    list.remove(0);
    Collections.rotate(list, 1);
}
Collections.rotate(list, -1);

0

Experimente este (remove todos os elementos da lista iguais i):

for (Object i : l) {
    if (condition(i)) {
        l = (l.stream().filter((a) -> a != i)).collect(Collectors.toList());
    }
}

0

você também pode usar recursão

A recursão em java é um processo no qual um método se chama continuamente. Um método em java que se chama é chamado de método recursivo.


-2

essa pode não ser a melhor maneira, mas para a maioria dos casos pequenos isso deve ser aceitável:

"crie uma segunda matriz vazia e adicione apenas as que você deseja manter"

Eu não me lembro de onde li isso ... por razões de justiça, farei este wiki na esperança de que alguém o encontre ou apenas para não ganhar reputação que eu não mereço.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.