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.

Claudiu
fonte
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.

Bill K
fonte
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.

Claudiu
fonte
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);
assilias
fonte
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.

Ashish
fonte
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);
}
RodeoClown
fonte
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.

Landei
fonte
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();
    }
}
Antzi
fonte
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 .

Donald Raab
fonte
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 */);
}
Priyank Doshi
fonte
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.

John
fonte
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++;   
}
Lluis Felisart
fonte
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);
   }
}
james.garriss
fonte
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.

Nandhan Thiravia
fonte
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.

Yessy
fonte
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);
pedram bashiri
fonte
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);
    }
}
Nestor Milyaev
fonte
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);
}
gueto jagdish
fonte
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
Nurlan
fonte
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!

Srinivasan Thoyyeti
fonte
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();
            }
        }
    }
}
Yazon2006
fonte
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;
    }
}
cellepo
fonte
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);
Rahul Vala
fonte
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());
    }
}
Oleg Tatarchuk
fonte
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.

Firas Chebbah
fonte
-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.

ajax333221
fonte