Bir ArrayList öğesinde yineleme ve öğe kaldırma sırasında java.util.ConcurrentModificationException nasıl önlenir


205

Tekrarlamak istediğim bir ArrayList'im var. Üzerinde yineleme yaparken aynı zamanda elemanları kaldırmak zorundayım. Açıkçası bu a atar java.util.ConcurrentModificationException.

Bu sorunu çözmek için en iyi uygulama hangisidir? Önce listeyi klonlamalı mıyım?

Döngünün kendisi değil kodun başka bir parçasındaki öğeleri kaldırırım.

Kodum şöyle görünüyor:

public class Test() {
    private ArrayList<A> abc = new ArrayList<A>();

    public void doStuff() {
        for (A a : abc) 
        a.doSomething();
    }

    public void removeA(A a) {
        abc.remove(a);
    }
}

a.doSomethingarayabilir Test.removeA();


Yanıtlar:


328

İki seçenek:

  • , Kaldırmak istediğiniz bir değerler listesi oluşturma ekleyerek döngü içinde bu listeye, daha sonra çağrı originalList.removeAll(valuesToRemove)sonunda
  • remove()Yineleyicinin kendisindeki yöntemi kullanın . Bunun, gelişmiş for döngüsünü kullanamayacağınız anlamına geldiğini unutmayın.

İkinci seçeneğe örnek olarak, 5'ten büyük uzunluktaki dizeleri listeden kaldırmak:

List<String> list = new ArrayList<String>();
...
for (Iterator<String> iterator = list.iterator(); iterator.hasNext(); ) {
    String value = iterator.next();
    if (value.length() > 5) {
        iterator.remove();
    }
}

2
Ben döngü kendisi değil kodun başka bir bölümündeki öğeleri kaldırmak bahsetmişti.
RoflcoptrException

@Roflcoptr: İki kod parçasının nasıl etkileştiğini görmeden cevaplamak zor. Temel olarak, bunu yapamazsınız. Listenin klonlanmasının, hepsinin nasıl bir arada durduğunu görmeden yardımcı olup olmayacağı açık değildir. Sorunuzda daha fazla ayrıntı verebilir misiniz?
Jon Skeet

Listeyi klonlamanın işe yarayacağını biliyorum, ama bunun iyi bir yaklaşım olup olmadığını bilmiyorum. Ama biraz daha kod ekleyeceğim.
RoflcoptrException

2
Bu çözüm ayrıca java.util.ConcurrentModificationException özelliğine yol açar, bkz. Stackoverflow.com/a/18448699/2914140 .
CoolMind

1
@CoolMind: Birden fazla iş parçacığı olmadan bu kodun iyi olması gerekir.
Jon Skeet

17

ArrayList'in JavaDoc'larından

Bu sınıfın yineleyicisi ve listIterator yöntemleri tarafından döndürülen yineleyiciler hata hızındadır: liste yineleyici oluşturulduktan sonra herhangi bir zamanda yapısal olarak değiştirilirse , yineleyicinin kendi kaldırma veya ekleme yöntemleri dışında herhangi bir şekilde yineleyici bir ConcurrentModificationException özel durumu atar.


6
ve sorunun cevabı nerede?
Adelin

Dediği gibi, yineleyicinin kendi kaldırma veya ekleme yöntemleri dışında
Varun Achar

14

Herhangi bir hile uygulasanız bile (kodunuzda yaptığınız) gelişmiş "for loop" içindeki listeden değeri kaldırmaya çalışıyorsunuz. Daha iyi bir yol, burada önerildiği gibi tekrarlayıcı seviyesini kodlamaktır.

İnsanların döngü yaklaşımı için nasıl geleneksel önermediklerini merak ediyorum.

for( int i = 0; i < lStringList.size(); i++ )
{
    String lValue = lStringList.get( i );
    if(lValue.equals("_Not_Required"))
    {
         lStringList.remove(lValue);
         i--; 
    }  
}

Bu da işe yarıyor.


2
Bu doğru değil!!! bir elemanı kaldırdığınızda, bir sonraki pozisyonu alır ve i artarken sonraki eleman bir sonraki tekrarda kontrol edilmez. Bu durumda (int i = lStringList.size (); i> -1; i--)
Johntor

1
Katılıyorum! Alternatif olarak i--; durumunda döngü için durumunda.
suhas0sn07

Bu cevabın yukarıdaki yorumlardaki sorunları ele almak için düzenlendiğini düşünüyorum, bu yüzden şimdi olduğu gibi, en azından benim için iyi çalışıyor.
Kira Resari

11

Gerçekten diziyi geleneksel şekilde tekrar etmelisiniz

Bir öğeyi listeden her kaldırışınızda, sonraki öğeler ileriye doğru itilir. Yinelemeden başka öğeleri değiştirmediğiniz sürece, aşağıdaki kodun çalışması gerekir.

public class Test(){
    private ArrayList<A> abc = new ArrayList<A>();

    public void doStuff(){
        for(int i = (abc.size() - 1); i >= 0; i--) 
            abc.get(i).doSomething();
    }

    public void removeA(A a){
        abc.remove(a);
    }
}

11

Java 8'de, Koleksiyon Arayüzünü kullanabilir ve removeIf yöntemini çağırarak bunu yapabilirsiniz:

yourList.removeIf((A a) -> a.value == 2);

Daha fazla bilgiyi burada bulabilirsiniz


6

Döngüyü normal şekilde yapın, java.util.ConcurrentModificationExceptionerişilen öğelerle ilgili bir hatadır.

O zaman dene:

for(int i = 0; i < list.size(); i++){
    lista.get(i).action();
}

Sen kaçınılması java.util.ConcurrentModificationExceptionlisteden şey kaldırarak vermeyerek. Zor. :) Gerçekten bir liste yineleme için bu "normal yol" diyemezsiniz.
Zsolt Sky

6

Listeyi yinelerken, öğeyi kaldırmak istiyorsanız mümkündür. Aşağıdaki örneklerime bakalım,

ArrayList<String>  names = new ArrayList<String>();
        names.add("abc");
        names.add("def");
        names.add("ghi");
        names.add("xyz");

Array listesinin yukarıdaki isimlerine sahibim. Ve yukarıdaki listeden "def" adını kaldırmak istiyorum,

for(String name : names){
    if(name.equals("def")){
        names.remove("def");
    }
}

Yukarıdaki kod , yineleme sırasında listeyi değiştirdiğiniz için ConcurrentModificationException özel durumunu atar .

Böylece, "def" adını Arraylist'ten bu yolla kaldırmak için,

Iterator<String> itr = names.iterator();            
while(itr.hasNext()){
    String name = itr.next();
    if(name.equals("def")){
        itr.remove();
    }
}

Yukarıdaki kod, yineleyici aracılığıyla Arraylist "def" adını kaldırabilir ve dizi yazdırmaya çalışın, aşağıdaki çıktı göreceksiniz.

Çıktı: [abc, ghi, xyz]


Aksi takdirde, eşzamanlı pakette bulunan eşzamanlı listeyi kullanabiliriz, böylece yineleme sırasında kaldırma ve ekleme işlemleri gerçekleştirebilirsiniz. Örneğin, aşağıdaki kod snippet'ine bakın. ArrayList <String> adlar = yeni ArrayList <String> (); CopyOnWriteArrayList <String> copyNames = yeni CopyOnWriteArrayList <String> (adlar); for (String name: copyNames) {if (name.equals ("def")) {copyNames.remove ("def"); }}
Indra K

CopyOnWriteArrayList en pahalı işlemler olacak.
Indra K

5

Bir seçenek, removeAyöntemi bu şekilde değiştirmektir -

public void removeA(A a,Iterator<A> iterator) {
     iterator.remove(a);
     }

Ancak bu , yönteme doSomething()geçebilmeniz gerektiği anlamına gelir . Çok iyi bir fikir değil.iteratorremove

Bunu iki adımlı bir yaklaşımla yapabilir misiniz: Listenin üzerinde tekrarladığınız ilk döngüde, seçilen öğeleri kaldırmak yerine, silinecek olarak işaretleyin . Bunun için bu öğeleri (sığ kopya) diğerine kopyalayabilirsiniz .List

Ardından, yinelemeniz tamamlandığında, removeAllikinci listeden tüm öğeleri ilk listeden bir yapın.


Mükemmel, aynı yaklaşımı kullandım, ancak iki kez döngü yapıyorum. işleri basitleştirir ve onunla eşzamanlı sorunları yoktur :)
Pankaj Nimgade

1
Yineleyicinin remove (a) yöntemine sahip olduğunu görmüyorum. Remove () hiçbir argüman almaz docs.oracle.com/javase/8/docs/api/java/util/Iterator.html Ne eksik?
c0der

5

Burada, kaldırılacak nesneleri eklemek için farklı bir liste kullandığım, daha sonra orijinal listeden öğeleri kaldırmak için stream.foreach kullandığım bir örnek:

private ObservableList<CustomerTableEntry> customersTableViewItems = FXCollections.observableArrayList();
...
private void removeOutdatedRowsElementsFromCustomerView()
{
    ObjectProperty<TimeStamp> currentTimestamp = new SimpleObjectProperty<>(TimeStamp.getCurrentTime());
    long diff;
    long diffSeconds;
    List<Object> objectsToRemove = new ArrayList<>();
    for(CustomerTableEntry item: customersTableViewItems) {
        diff = currentTimestamp.getValue().getTime() - item.timestamp.getValue().getTime();
        diffSeconds = diff / 1000 % 60;
        if(diffSeconds > 10) {
            // Element has been idle for too long, meaning no communication, hence remove it
            System.out.printf("- Idle element [%s] - will be removed\n", item.getUserName());
            objectsToRemove.add(item);
        }
    }
    objectsToRemove.stream().forEach(o -> customersTableViewItems.remove(o));
}

İki döngüyü yürütmek için ekstra iş yaptığınızı düşünüyorum, en kötü durumda döngüler listenin tamamında olacaktır. Sadece bir döngüde yapmak en basit ve daha ucuz olurdu.
Luis Carlos

İlk döngü içindeki nesneyi kaldırabileceğinizi düşünmüyorum, bu nedenle ekstra kaldırma döngüsüne duyulan ihtiyaç, ayrıca kaldırma döngüsü yalnızca kaldırma için nesnelerdir - belki de sadece bir döngü ile bir örnek yazabilirsiniz, görmek istiyorum - teşekkürler @ LuisCarlos
serup

Bu kodla söylediğiniz gibi, java.util.ConcurrentModificationException özel durumuna neden olduğu için for-loop içindeki hiçbir öğeyi kaldıramazsınız. Ancak için bir temel kullanabilirsiniz. Burada kodunuzun bir kısmını kullanarak bir örnek yazıyorum.
Luis Carlos

1
için (int i = 0; i <customerTableViewItems.size (); i ++) {diff = currentTimestamp.getValue (). getTime () - customerTableViewItems.get (i) .timestamp.getValue (). getTime (); diffSeconds = fark /% 1000 60; if (diffSeconds> 10) {customerTableViewItems.remove (i--); }} Önemli i-- çünkü herhangi bir seçimi atlamak istemezsiniz. Ayrıca ArrayList sınıfı tarafından sağlanan removeIf (Predicate <? Super E> filter) yöntemini de kullanabilirsiniz. Umarım bu yardım
Luis Carlos

1
İstisna oluşur, çünkü for-loop'ta listenin yineleyicisine aktif bir başvuru vardır. Normalde, bir referans yoktur ve verileri değiştirmek için daha fazla esnekliğiniz vardır. Umarım bu yardım
Luis Carlos

3

Kullanmak yerine Her döngü için döngü için normal kullanın. örneğin, aşağıdaki kod, java.util.ConcurrentModificationException vermeden dizi listesindeki tüm öğeleri kaldırır. Kullanım durumunuza göre döngüdeki koşulu değiştirebilirsiniz.

   for(int i=0;i<abc.size();i++)  {

          e.remove(i);
        }

2

Böyle basit bir şey yapın:

for (Object object: (ArrayList<String>) list.clone()) {
    list.remove(object);
}

2

Stream kullanan alternatif bir Java 8 çözümü:

        theList = theList.stream()
            .filter(element -> !shouldBeRemoved(element))
            .collect(Collectors.toList());

Java 7'de Guava'yı kullanabilirsiniz:

        theList = FluentIterable.from(theList)
            .filter(new Predicate<String>() {
                @Override
                public boolean apply(String element) {
                    return !shouldBeRemoved(element);
                }
            })
            .toImmutableList();

Guava örneğinin, istediğiniz gibi olabilecek veya olmayabilecek değişmez bir liste ile sonuçlandığını unutmayın.


1

ArrayList yerine CopyOnWriteArrayList'i de kullanabilirsiniz. JDK 1.5'ten itibaren önerilen en son yaklaşım budur.


1

Benim durumumda, kabul edilen cevap çalışmıyor, İstisnayı durdurur, ancak Listemde bazı tutarsızlıklara neden olur. Aşağıdaki çözüm benim için mükemmel çalışıyor.

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

for (String value: list) {
   if (value.length() > 5) { // your condition
       itemsToRemove.add(value);
   }
}
list.removeAll(itemsToRemove);

Bu kodda, başka bir listede kaldırmak için öğeleri ekledim ve sonra list.removeAllgerekli tüm öğeleri kaldırmak için yöntem kullandım .


0

"Önce listeyi klonlamalı mıyım?"

Bu en kolay çözüm olacaktır, klondan çıkarın ve klonu çıkardıktan sonra geri kopyalayın.

Rummikub oyunumdan bir örnek:

SuppressWarnings("unchecked")
public void removeStones() {
  ArrayList<Stone> clone = (ArrayList<Stone>) stones.clone();
  // remove the stones moved to the table
  for (Stone stone : stones) {
      if (stone.isOnTable()) {
         clone.remove(stone);
      }
  }
  stones = (ArrayList<Stone>) clone.clone();
  sortStones();
}

2
Downvoters, downvot yapmadan önce en azından bir yorum bırakmalıdır.
OneWorld

2
Belki stones = (...) clone.clone();de gereksiz olan bu cevap beklemek doğal olarak yanlış bir şey yok . Olmaz stones = clone;aynı şeyi?
vikingsteve

Katılıyorum, ikinci klonlama gereksiz. Bunu, klon üzerinde yineleyerek ve elemanları doğrudan içinden kaldırarak daha da basitleştirebilirsiniz stones. Bu şekilde clonedeğişkene bile ihtiyacınız yoktur : for (Stone stone : (ArrayList<Stone>) stones.clone()) {...
Zsolt Sky

0

Amacınız listeden tüm öğeleri kaldırmaksa, her öğeyi tekrarlayabilir ve ardından şunu arayabilirsiniz:

list.clear()

0

Geç geldim biliyorum ama buna cevap veriyorum çünkü bu çözümün basit ve zarif olduğunu düşünüyorum:

List<String> listFixed = new ArrayList<String>();
List<String> dynamicList = new ArrayList<String>();

public void fillingList() {
    listFixed.add("Andrea");
    listFixed.add("Susana");
    listFixed.add("Oscar");
    listFixed.add("Valeria");
    listFixed.add("Kathy");
    listFixed.add("Laura");
    listFixed.add("Ana");
    listFixed.add("Becker");
    listFixed.add("Abraham");
    dynamicList.addAll(listFixed);
}

public void updatingListFixed() {
    for (String newList : dynamicList) {
        if (!listFixed.contains(newList)) {
            listFixed.add(newList);
        }
    }

    //this is for add elements if you want eraser also 

    String removeRegister="";
    for (String fixedList : listFixed) {
        if (!dynamicList.contains(fixedList)) {
            removeResgister = fixedList;
        }
    }
    fixedList.remove(removeRegister);
}

Tüm bunlar bir listeden diğerine güncelleme içindir ve hepsini tek bir listeden yapabilir ve yöntem güncellemesinde her iki listeyi de kontrol edebilir ve listeden öğeleri silebilir veya ekleyebilirsiniz. Bu, her iki listenin her zaman aynı boyutta olduğu anlamına gelir


0

Dizi Listesi yerine Yineleyici Kullan

Tür eşleşmesi ile bir kümenin yineleyiciye dönüştürülmesini sağlama

Ve bir sonraki öğeye geç ve kaldır

Iterator<Insured> itr = insuredSet.iterator();
while (itr.hasNext()) { 
    itr.next();
    itr.remove();
}

Burada sonrakine geçmek önemlidir, çünkü öğeyi kaldırmak için dizini almalıdır.


0

Ne hakkında

import java.util.Collections;

List<A> abc = Collections.synchronizedList(new ArrayList<>());

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.