Hapus elemen dari koleksi saat iterasi

215

AFAIK, ada dua pendekatan:

  1. Iterate di atas salinan koleksi
  2. Gunakan iterator koleksi aktual

Misalnya,

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

dan

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

Adakah alasan untuk lebih menyukai satu pendekatan daripada yang lain (mis. Lebih suka pendekatan pertama karena alasan mudah dibaca)?

pengguna1329572
sumber
1
Hanya ingin tahu, mengapa Anda membuat salinan orang bodoh alih-alih hanya mengulang-ulang orang bodoh dalam contoh pertama?
Haz
@ Ya ampun, jadi saya hanya perlu mengulang satu kali.
user1329572
15
Catatan: lebih suka 'untuk' lebih 'sementara' juga dengan iterator untuk membatasi ruang lingkup variabel: untuk (Iterator <Foo> itr = fooList.iterator (); itr.hasNext ();) {}
Puce
Saya tidak tahu whilememiliki aturan pelingkupan yang berbeda darifor
Alexander Mills
Dalam situasi yang lebih kompleks, Anda mungkin memiliki kasus di mana fooListvariabel instan dan Anda memanggil metode selama loop yang akhirnya memanggil metode lain di kelas yang sama fooList.remove(obj). Telah melihat ini terjadi. Dalam hal menyalin daftar itu adalah yang paling aman.
Dave Griffiths

Jawaban:

416

Biarkan saya memberi beberapa contoh dengan beberapa alternatif untuk menghindari a ConcurrentModificationException.

Misalkan kita memiliki koleksi buku berikut

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")));

Kumpulkan dan Hapus

Teknik pertama terdiri dari mengumpulkan semua objek yang ingin kita hapus (mis. Menggunakan loop yang disempurnakan) dan setelah kita selesai iterasi, kita menghapus semua objek yang ditemukan.

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);

Seandainya operasi yang ingin Anda lakukan adalah "hapus".

Jika Anda ingin "menambahkan" pendekatan ini juga akan berfungsi, tetapi saya akan menganggap Anda akan mengulangi koleksi yang berbeda untuk menentukan elemen apa yang ingin Anda tambahkan ke koleksi kedua dan kemudian mengeluarkan addAllmetode di akhir.

Menggunakan ListIterator

Jika Anda bekerja dengan daftar, teknik lain terdiri dari penggunaan ListIteratoryang memiliki dukungan untuk penghapusan dan penambahan item selama iterasi itu sendiri.

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

Sekali lagi, saya menggunakan metode "hapus" dalam contoh di atas yang tampaknya menyiratkan pertanyaan Anda, tetapi Anda juga dapat menggunakan addmetode ini untuk menambahkan elemen baru selama iterasi.

Menggunakan JDK> = 8

Bagi mereka yang bekerja dengan Java 8 atau versi superior, ada beberapa teknik lain yang bisa Anda gunakan untuk memanfaatkannya.

Anda bisa menggunakan removeIfmetode baru di Collectionkelas dasar:

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

Atau gunakan API aliran baru:

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

Dalam kasus terakhir ini, untuk memfilter elemen dari koleksi, Anda menetapkan kembali referensi asli ke koleksi yang difilter (yaitu books = filtered) atau menggunakan koleksi yang difilter ke removeAllelemen yang ditemukan dari koleksi asli (yaitu books.removeAll(filtered)).

Gunakan Sublist atau Subset

Ada alternatif lain juga. Jika daftar diurutkan, dan Anda ingin menghapus elemen berturut-turut Anda dapat membuat sublist dan kemudian menghapusnya:

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

Karena sublist didukung oleh daftar asli, ini akan menjadi cara yang efisien untuk menghilangkan subkoleksi elemen ini.

Hal serupa dapat dicapai dengan set yang diurutkan menggunakan NavigableSet.subSetmetode, atau metode pemotongan apa pun yang ditawarkan di sana.

Pertimbangan:

Metode apa yang Anda gunakan mungkin tergantung pada apa yang ingin Anda lakukan

  • Koleksi dan removeAlteknik bekerja dengan Koleksi apa pun (Koleksi, Daftar, Set, dll.).
  • The ListIteratorTeknik jelas hanya bekerja dengan daftar, asalkan diberikan mereka ListIteratormenawarkan implementasi dukungan untuk add dan menghapus operasi.
  • The Iteratorpendekatan akan bekerja dengan semua jenis koleksi, tetapi hanya mendukung operasi menghapus.
  • Dengan pendekatan ListIterator/ Iteratorkeuntungan yang jelas adalah tidak perlu menyalin apa pun karena kita menghapus seperti yang kita lakukan berulang kali. Jadi, ini sangat efisien.
  • Contoh JDK 8 stream sebenarnya tidak menghapus apa pun, tetapi mencari elemen yang diinginkan, dan kemudian kami mengganti referensi koleksi asli dengan yang baru, dan membiarkan yang lama menjadi sampah yang dikumpulkan. Jadi, kami hanya mengulang koleksi sekali saja dan itu akan efisien.
  • Dalam mengumpulkan dan removeAllmendekati kerugiannya adalah kita harus mengulang dua kali. Pertama kita mengulang di loop foor mencari objek yang cocok dengan kriteria penghapusan kami, dan setelah kami menemukannya, kami meminta untuk menghapusnya dari koleksi asli, yang akan menyiratkan pekerjaan iterasi kedua untuk mencari item ini untuk Singkirkan.
  • Saya pikir perlu disebutkan bahwa metode hapus Iteratorantarmuka ditandai sebagai "opsional" di Javadocs, yang berarti bahwa mungkin ada Iteratorimplementasi yang melempar UnsupportedOperationExceptionjika kita memanggil metode hapus. Karena itu, saya akan mengatakan pendekatan ini kurang aman dibandingkan yang lain jika kami tidak dapat menjamin dukungan iterator untuk menghilangkan elemen.
Edwin Dalorzo
sumber
Bravo! ini adalah panduan definitif.
Magno C
Ini jawaban yang sempurna! Terima kasih.
Wilhelm
7
Dalam paragraf Anda tentang JDK8 Streaming yang Anda sebutkan removeAll(filtered). Jalan pintas untuk itu adalahremoveIf(b -> b.getIsbn().equals(other))
ifloop
Apa perbedaan antara Iterator dan ListIterator?
Alexander Mills
Belum dianggap dihapus jika, tapi itu adalah jawaban untuk doaku. Terima kasih!
Akabelle
15

Di Java 8, ada pendekatan lain. Koleksi # removeIf

misalnya:

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

list.removeIf(i -> i > 2);
Santhosh
sumber
13

Adakah alasan untuk memilih satu pendekatan daripada yang lain

Pendekatan pertama akan berhasil, tetapi memiliki overhead yang jelas untuk menyalin daftar.

Pendekatan kedua tidak akan berhasil karena banyak kontainer tidak mengizinkan modifikasi selama iterasi. Ini termasukArrayList .

Jika hanya modifikasi adalah untuk menghapus elemen saat ini, Anda dapat membuat pekerjaan pendekatan kedua dengan menggunakan itr.remove()(yaitu, menggunakan iterator 's remove()metode, bukan wadah ' s). Ini akan menjadi metode pilihan saya untuk iterator yang mendukung remove().

NPE
sumber
Ups, maaf ... tersirat bahwa saya akan menggunakan metode hapus iterator, bukan penampungnya. Dan berapa banyak overhead yang menyalin daftar? Itu tidak bisa banyak dan karena sudah mencakup metode, itu harus dikumpulkan dengan cepat. Lihat edit ..
user1329572
1
@aix Saya pikir perlu menyebutkan metode hapus Iteratorantarmuka ditandai sebagai opsional di Javadocs, yang berarti bahwa mungkin ada implementasi Iterator yang mungkin melempar UnsupportedOperationException. Karena itu, saya akan mengatakan pendekatan ini kurang aman daripada yang pertama. Bergantung pada implementasi yang dimaksudkan untuk digunakan, pendekatan pertama bisa lebih cocok.
Edwin Dalorzo
@EdwinDalorzo remove()pada koleksi aslinya sendiri juga dapat membuang UnsupportedOperationException: docs.oracle.com/javase/7/docs/api/java/util/… . Antarmuka Java container, sayangnya, didefinisikan sangat tidak dapat diandalkan (mengalahkan titik antarmuka, jujur). Jika Anda tidak tahu implementasi pasti yang akan digunakan saat runtime, lebih baik melakukan hal-hal dengan cara yang tidak dapat diubah - misalnya, gunakan Java 8+ Streams API untuk memfilter elemen ke bawah dan mengumpulkannya ke dalam wadah baru, lalu sepenuhnya ganti yang lama dengan itu.
Matius Baca
5

Hanya pendekatan kedua yang akan bekerja. Anda dapat mengubah koleksi selama iterasi iterator.remove()hanya menggunakan . Semua upaya lain akan menyebabkan ConcurrentModificationException.

AlexR
sumber
2
Upaya pertama iterates pada salinan, artinya dia dapat memodifikasi aslinya.
Colin D
3

Timer Lama Favorit (masih berfungsi):

List<String> list;

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

Anda tidak dapat melakukan yang kedua, karena meskipun Anda menggunakan remove()metode di Iterator , Anda akan mendapatkan Exception .

Secara pribadi, saya lebih suka yang pertama untuk semua Collectioncontoh, meskipun tidak sengaja membuat yang baru Collection, saya merasa kurang rentan terhadap kesalahan selama mengedit oleh pengembang lain. Pada beberapa implementasi Koleksi, Iterator remove()didukung, di lain itu tidak. Anda dapat membaca lebih lanjut di dokumen untuk Iterator .

Alternatif ketiga, adalah membuat yang baru Collection, beralih dari yang asli, dan tambahkan semua anggota yang pertama Collectionke yang kedua Collectionyang tidak bisa dihapus. Tergantung pada ukuran Collectiondan jumlah penghapusan, ini dapat secara signifikan menghemat memori, bila dibandingkan dengan pendekatan pertama.

Jon
sumber
0

Saya akan memilih yang kedua karena Anda tidak perlu menyalin memori dan Iterator bekerja lebih cepat. Jadi Anda menghemat memori dan waktu.

Calin Andrei
sumber
" Iterator bekerja lebih cepat ". Ada yang mendukung klaim ini? Juga, jejak memori membuat salinan daftar sangat sepele, terutama karena akan dicakup dalam suatu metode dan sampah akan dikumpulkan segera.
user1329572
1
Dalam pendekatan pertama, kerugiannya adalah kita harus mengulang dua kali. Kami beralih di loop foor mencari elemen, dan setelah kami menemukannya, kami meminta untuk menghapusnya dari daftar asli, yang akan menyiratkan pekerjaan iterasi kedua untuk mencari item yang diberikan ini. Ini akan mendukung klaim bahwa, setidaknya dalam kasus ini, pendekatan iterator harus lebih cepat. Kita harus mempertimbangkan bahwa hanya ruang struktural koleksi yang dibuat, objek di dalam koleksi tidak disalin. Kedua koleksi akan menyimpan referensi ke objek yang sama. Ketika GC terjadi, kami tidak dapat memberi tahu !!!
Edwin Dalorzo
-2

kenapa tidak ini?

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

Dan jika itu peta, bukan daftar, Anda dapat menggunakan keyset ()

Drake Clarris
sumber
4
Pendekatan ini memiliki banyak kelemahan utama. Pertama, setiap kali Anda menghapus elemen, indeks ditata ulang. Oleh karena itu, jika Anda menghapus elemen 0, maka elemen 1 menjadi elemen baru 0. Jika Anda akan melakukannya, setidaknya lakukan mundur untuk menghindari masalah ini. Kedua, tidak semua implementasi Daftar menawarkan akses langsung ke elemen (seperti yang dilakukan ArrayList). Dalam LinkedList ini akan sangat tidak efisien karena setiap kali Anda mengeluarkan get(i)Anda harus mengunjungi semua node sampai Anda mencapai i.
Edwin Dalorzo
Tidak pernah menganggap ini karena saya biasanya hanya menggunakannya untuk menghapus satu item yang saya cari. Senang mendengarnya.
Drake Clarris
4
Saya terlambat ke pesta, tapi pasti di blok jika Foo.remove(i);Anda harus lakukan i--;?
Bertie Wheen
karena disadap
Jack