Yineleme sırasında öğeleri koleksiyondan kaldırma


215

AFAIK, iki yaklaşım var:

  1. Koleksiyonun bir kopyası üzerinden yineleme
  2. Gerçek koleksiyonun yineleyicisini kullanma

Örneğin,

List<Foo> fooListCopy = new ArrayList<Foo>(fooList);
for(Foo foo : fooListCopy){
    // modify actual fooList
}

ve

Iterator<Foo> itr = fooList.iterator();
while(itr.hasNext()){
    // modify actual fooList using itr.remove()
}

Bir yaklaşımı diğerine tercih etmek için herhangi bir neden var mı (örneğin basit okunabilirlik nedeni için ilk yaklaşımı tercih etmek)?


1
Sadece merak ediyorum, neden ilk örnekte sadece aptalca dolaşmak yerine aptalcunun bir kopyasını oluşturuyorsunuz?
Haz

@Haz, Yani sadece bir kez döngü yapmam gerekiyor.
user1329572

15
Not: değişkenin kapsamını sınırlamak için yineleyicilerle de 'over' while 'seçeneğini tercih edin: for (Iterator <Foo> itr = fooList.iterator (); itr.hasNext ();) {}
Puce

whileFarklı kapsam belirleme kuralları olduğunu bilmiyordumfor
Alexander Mills

Daha karmaşık bir durumda fooList, bir örnek değişkeni olan bir durumunuz olabilir ve döngü sırasında, aynı sınıfta başka bir yöntemi çağıran bir yöntemi çağırırsınız fooList.remove(obj). Bunun olduğunu gördüm. Bu durumda listeyi kopyalamak en güvenlidir.
Dave Griffiths

Yanıtlar:


419

Bir kaçınmak için bazı alternatiflerle birkaç örnek vereyim ConcurrentModificationException.

Aşağıdaki kitap koleksiyonuna sahip olduğumuzu varsayalım

List<Book> books = new ArrayList<Book>();
books.add(new Book(new ISBN("0-201-63361-2")));
books.add(new Book(new ISBN("0-201-63361-3")));
books.add(new Book(new ISBN("0-201-63361-4")));

Topla ve Kaldır

İlk teknik, silmek istediğimiz tüm nesneleri (örn. Döngü için gelişmiş bir kullanarak) toplamaktır ve yinelemeyi bitirdikten sonra bulunan tüm nesneleri kaldırırız.

ISBN isbn = new ISBN("0-201-63361-2");
List<Book> found = new ArrayList<Book>();
for(Book book : books){
    if(book.getIsbn().equals(isbn)){
        found.add(book);
    }
}
books.removeAll(found);

Bu, yapmak istediğiniz işlemin "sil" olduğunu varsayar.

"Eklemek" istiyorsanız bu yaklaşım da işe yarayacaktır, ancak ikinci bir koleksiyona hangi öğeleri eklemek istediğinizi belirlemek için farklı bir koleksiyon üzerinde yineleme yapacağınızı addAllve sonunda bir yöntem yayınlayacağınızı varsayarım .

ListIterator'ı kullanma

Listelerle çalışıyorsanız, başka bir teknik ListIterator, yinelemenin kendisi sırasında öğelerin kaldırılması ve eklenmesi için destek olan bir yöntemi kullanmaktan oluşur .

ListIterator<Book> iter = books.listIterator();
while(iter.hasNext()){
    if(iter.next().getIsbn().equals(isbn)){
        iter.remove();
    }
}

Yine, yukarıdaki örnekte sorunuzun ima ettiği gibi görünen "kaldır" yöntemini kullandım, ancak addyöntemini yineleme sırasında yeni öğeler eklemek için de kullanabilirsiniz .

JDK kullanma> = 8

Java 8 veya üstün sürümlerle çalışanlar için, bundan yararlanmak için kullanabileceğiniz birkaç teknik daha vardır.

Temel sınıftaki yeni removeIfyöntemi kullanabilirsiniz Collection:

ISBN other = new ISBN("0-201-63361-2");
books.removeIf(b -> b.getIsbn().equals(other));

Veya yeni akış API'sını kullanın:

ISBN other = new ISBN("0-201-63361-2");
List<Book> filtered = books.stream()
                           .filter(b -> b.getIsbn().equals(other))
                           .collect(Collectors.toList());

Bu son durumda, öğeleri bir koleksiyondan filtrelemek için, orijinal referansı filtrelenmiş koleksiyona yeniden atayabilirsiniz (yani books = filtered) veya filtrelenmiş koleksiyonu removeAllorijinal koleksiyondaki (yani books.removeAll(filtered)) bulunan öğelere yeniden kullanırsınız .

Alt Liste veya Altküme Kullan

Başka alternatifler de var. Liste sıralıysa ve art arda gelen öğeleri kaldırmak istiyorsanız bir alt liste oluşturabilir ve listeyi temizleyebilirsiniz:

books.subList(0,5).clear();

Alt liste orijinal liste ile desteklendiğinden, bu, öğelerin bu alt toplanmasını kaldırmanın etkili bir yolu olacaktır.

Benzer bir şey NavigableSet.subSetyöntemi kullanarak sıralı kümelerle veya orada sunulan dilimleme yöntemlerinden herhangi biri ile elde edilebilir .

hususlar:

Hangi yöntemi kullandığınız, ne yapmak istediğinize bağlı olabilir

  • removeAlKoleksiyon ve teknik herhangi bir Koleksiyon (Koleksiyon, Liste, Set, vb.) İle çalışır.
  • ListIteratorTeknik, açıkça, kendilerine verilen şartıyla listeleri ile çalışır ListIteratoruygulama teklifler eklemek ve kaldırmak işlemleri için destekler.
  • IteratorYaklaşım koleksiyonun her türlü ile çalışacak, ama bu kaldırma işlemlerini destekleyen tek.
  • İle ListIterator/ Iteratorbariz avantajı biz yineleme olarak kaldırmak beri hiçbir şey kopyalamak zorunda değildir yaklaşır. Yani, bu çok verimli.
  • JDK 8 akış örneği aslında hiçbir şeyi kaldırmadı, ancak istenen öğeleri aradık ve orijinal koleksiyon referansını yenisiyle değiştirdik ve eskisinin çöp toplanmasına izin verdik. Yani, koleksiyon üzerinde sadece bir kez tekrarlıyoruz ve bu verimli olacaktır.
  • Toplama ve removeAllyaklaşımda dezavantaj iki kez yinelememiz gerektiğidir. İlk olarak, foop-döngüsünde kaldırma ölçütlerimizle eşleşen bir nesneyi ararız ve onu bulduktan sonra, orijinal koleksiyondan kaldırmayı isteriz, bu da bu öğeyi aramak için ikinci bir yineleme çalışması anlamına gelir. onu kaldır.
  • IteratorArayüz kaldırma yönteminin Javadocs'ta "isteğe bağlı" olarak işaretlendiğinden bahsetmeye değer olduğunu düşünüyorum, bu da kaldırma yöntemini çağırdığımızda Iteratoratılan uygulamalar olabileceği anlamına gelir UnsupportedOperationException. Bu nedenle, elemanların kaldırılması için yineleyici desteğini garanti edemezsek, bu yaklaşımın diğerlerinden daha az güvenli olduğunu söyleyebilirim.

Bravo! bu kesin kılavuzdur.
Magno C

Bu mükemmel bir cevap! Teşekkür ederim.
Wilhelm

7
Bahsettiğiniz JDK8 Akışları hakkındaki paragrafınızda removeAll(filtered). Bunun için bir kısayol olurduremoveIf(b -> b.getIsbn().equals(other))
ifloop

Iterator ve ListIterator arasındaki fark nedir?
Alexander Mills

Kaldırmayı düşünmedim ama dualarımın cevabı buydu. Teşekkürler!
Akabelle

15

Java 8'de başka bir yaklaşım var. Koleksiyon # removeIf

Örneğin:

List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);

list.removeIf(i -> i > 2);

13

Bir yaklaşımı diğerine tercih etmek için herhangi bir neden var mı

İlk yaklaşım işe yarayacaktır, ancak listeyi kopyalamanın bariz yükü vardır.

İkinci yaklaşım işe yaramayacaktır, çünkü birçok kap yineleme sırasında değişikliğe izin vermez. Buna dahildirArrayList .

Tek modifikasyon mevcut elemanını kaldırmak için ise, kullanarak ikinci bir yaklaşım iş yapabilir itr.remove()(olduğundan, kullanımı yineleyici 'in remove()yöntemi değil, kap ' ler). Bu, destekleyen yineleyiciler için tercih ettiğim yöntem olacaktır remove().


Hata! Üzgünüm ... kabın değil, yineleyicinin kaldırma yöntemini kullanacağım ima ediliyor. Ve listeyi kopyalamak ne kadar ek yük oluşturur? Çok fazla olamaz ve bir yönteme dahil edildiğinden, çok hızlı bir şekilde çöp toplanmalıdır. Düzenlemeye bakın ..
user1329572

1
@aix IteratorArayüz kaldırma yönteminin Javadocs'ta isteğe bağlı olarak işaretlendiğinden bahsetmeye değer olduğunu düşünüyorum , bu da fırlatabilecek yineleyici uygulamaları olabileceği anlamına gelir UnsupportedOperationException. Bu nedenle, bu yaklaşımın birincisinden daha az güvenli olduğunu söyleyebilirim. Kullanılması amaçlanan uygulamalara bağlı olarak, ilk yaklaşım daha uygun olabilir.
Edwin Dalorzo

@EdwinDalorzo remove()orijinal koleksiyonun kendisinde de atabilir UnsupportedOperationException: docs.oracle.com/javase/7/docs/api/java/util/… . Java kapsayıcı arabirimleri, ne yazık ki, son derece güvenilmez olarak tanımlanmıştır (arabirimin noktasını dürüstçe yenmek). Çalışma zamanında kullanılacak tam uygulamayı bilmiyorsanız, şeyleri değişmez bir şekilde yapmak daha iyidir - örneğin, öğeleri filtrelemek ve yeni bir kapta toplamak için Java 8+ Streams API'sını kullanın, ardından tamamen eskisini onunla değiştir.
Matthew

5

Sadece ikinci yaklaşım işe yarayacaktır. Koleksiyonu yineleme sırasında iterator.remove()yalnızca aşağıdakileri kullanarak değiştirebilirsiniz . Diğer tüm girişimler buna neden olacaktır ConcurrentModificationException.


2
İlk deneme bir kopya üzerinde tekrarlanır, yani orijinali değiştirebilir.
Colin D

3

Old Timer Favorite (hala çalışıyor):

List<String> list;

for(int i = list.size() - 1; i >= 0; --i) 
{
        if(list.get(i).contains("bad"))
        {
                list.remove(i);
        }
}

1

İkincisi yapamazsınız, çünkü remove()yöntemi Iterator'da kullansanız bile , bir İstisna atılır .

Kişisel olarak, Collectionyeniyi yaratmanın ek aşırı duygusuna rağmen, tüm örnekler için ilkini tercih ederim, Collectiondiğer geliştiriciler tarafından düzenleme sırasında hataya daha az eğilimli buluyorum. Bazı Koleksiyon uygulamalarında Yineleyici remove()desteklenir, diğer yandan desteklenmez. Iterator belgelerinde daha fazla bilgi edinebilirsiniz .

Üçüncü alternatif, yeni bir yaratmaktır Collectionorijinale kıyasla, yinelerler ve ilk tüm üyelerini eklemek Collectionikinciye Collectionolan değil yukarı silinmek üzere. Silme Collectionsayısı ve sayısına bağlı olarak, bu ilk yaklaşıma kıyasla bellekte önemli ölçüde tasarruf sağlayabilir.


0

Hafızanın bir kopyasını yapmak zorunda olmadığınız ve İteratör daha hızlı çalıştığı için ikincisini seçerdim. Böylece hafızadan ve zamandan tasarruf edersiniz.


" Yineleyici daha hızlı çalışır ". Bu iddiayı destekleyecek bir şey var mı? Ayrıca, bir listenin kopyasını oluşturmanın bellek ayak izi çok önemsizdir, çünkü özellikle bir yöntem içinde kapsamlandırılacak ve neredeyse hemen toplanacak.
user1329572

1
İlk yaklaşımda dezavantaj iki kez yinelememiz gerektiğidir. Bir döngüyü bir element aramak için yineliyoruz ve onu bulduktan sonra, orijinal listeden kaldırmayı istiyoruz, bu da bu verilen öğeyi aramak için ikinci bir yineleme çalışması anlamına gelecektir. Bu, en azından bu durumda yineleyici yaklaşımının daha hızlı olması gerektiği iddiasını destekleyecektir. Koleksiyonun yalnızca yapısal alanının yaratıldığını, koleksiyonların içindeki nesnelerin kopyalanmadığını düşünmeliyiz. Her iki koleksiyon da aynı nesnelere referansta bulunacaktır. GC olduğunda söyleyemeyiz !!!
Edwin Dalorzo

-2

neden olmasın

for( int i = 0; i < Foo.size(); i++ )
{
   if( Foo.get(i).equals( some test ) )
   {
      Foo.remove(i);
   }
}

Ve bu bir liste değilse, bir harita ise, keyset () kullanabilirsiniz


4
Bu yaklaşımın birçok büyük dezavantajı vardır. İlk olarak, bir öğeyi her kaldırışınızda, dizinler yeniden düzenlenir. Bu nedenle, 0 öğesini kaldırırsanız, 1 öğesi yeni öğe 0 olur. Buna gidecekseniz, bu sorunu önlemek için en azından geriye doğru yapın. İkinci olarak, tüm Liste uygulamaları öğelere doğrudan erişim sunmaz (ArrayList'in yaptığı gibi). LinkedList'te bu son derece verimsiz olacaktır, çünkü her yayınladığınızda, get(i)ulaşana kadar tüm düğümleri ziyaret etmeniz gerekir i.
Edwin Dalorzo

Bunu hiç düşünmedim, genellikle aradığım tek bir öğeyi kaldırmak için kullandım. Bunu bildiğim iyi oldu.
Drake Clarris

4
Partiye geç kaldım, ancak mutlaka if blok kodunda Foo.remove(i);mı yapmalısınız i--;?
Bertie Wheen

uyandırdı çünkü
Jack
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.