Iterator

Memahami for-each di balik layar, Iterator secara langsung, dan penyebab ConcurrentModificationException.

Daftar isi

Iterator adalah objek kecil yang menyusuri isi koleksi satu elemen demi satu elemen, dan for-each sebenarnya hanyalah bungusannya. Memahami perantara ini menjelaskan dua hal yang sering membingungkan, yaitu mengapa elemen tidak bisa dihapus lewat cara biasa saat perulangan berjalan, dan dari mana ConcurrentModificationException berasal. Topik ini membongkar keduanya, dari for-each di balik layar sampai pola penghapusan yang aman.

For-each di balik layar#

Setiap koleksi menurunkan Iterable, interface yang menjanjikan satu metode bernama iterator(). Ketika kompilator bertemu for-each, perulangan itu diterjemahkan menjadi pemanggilan iterator() disertai perulangan while yang berpegang pada dua metode Iterator, yaitu hasNext() untuk menanyakan sisa elemen dan next() untuk mengambil elemen berikutnya. Bentuk hasil terjemahannya terlihat seperti berikut.

java Contoh: bentuk yang dihasilkan kompilator dari for-each
// tidak-dijalankan
for (Iterator<String> iter = daftar.iterator(); iter.hasNext(); ) {
    String nama = iter.next();
    System.out.println(nama);
}

Terjemahan itu menjelaskan beberapa perilaku yang selama ini terasa otomatis. For-each bisa dipakai pada array maupun koleksi karena keduanya punya cara disusuri, meski bentuk terjemahannya berbeda. Array tidak punya iterator(), sehingga kompilator menghasilkan perulangan indeks biasa untuknya. Perulangan berhenti sendiri karena hasNext() akhirnya bernilai false. Yang paling penting, iterator memegang posisi bacanya sendiri, terpisah dari koleksinya, dan pemisahan inilah yang kelak menjadi sumber konflik saat koleksi ikut diubah.

Satu batas juga ikut jelas dari sini, yaitu Map tidak bisa langsung disusuri for-each. Interface Map tidak menurunkan Iterable, sehingga tidak punya iterator() untuk dipanggil kompilator. Yang bisa disusuri adalah tampilan koleksinya, seperti keySet() dan entrySet(), sebagaimana dipraktikkan pada topik Map.

Interface Iterator sebenarnya masih menyimpan satu kemampuan lagi yang jarang dipanggil, yaitu forEachRemaining(aksi) yang memproses seluruh sisa elemen dengan satu pemanggilan. Metode forEach milik Iterable bekerja pada konsep serupa, menerima aksi per elemen tanpa menampakkan iteratornya. Dua-duanya tidak menambah kemampuan baru di atas hasNext dan next, tetapi memangkas kerangka perulangan ketika tujuannya memang memproses semua sisa.

Iterator dipakai langsung#

Mengambil iterator secara manual jarang diperlukan, tetapi satu kemampuannya tidak digantikan apa pun, yaitu metode remove(). Metode ini menghapus elemen yang baru saja dikembalikan next(), dan pemanggilannya sah di tengah perulangan karena iterator tahu persis elemen mana yang sedang ditunjuk.

java DemoIterator.java
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class DemoIterator {

    public static void main(String[] args) {
        List<String> namaPeserta = new ArrayList<>();
        namaPeserta.add("Ali");
        namaPeserta.add("Budi");
        namaPeserta.add("Citra");
        namaPeserta.add("Dewi");

        Iterator<String> iter = namaPeserta.iterator();
        System.out.println("dibaca satu per satu:");
        while (iter.hasNext()) {
            System.out.println("  " + iter.next());
        }

        iter = namaPeserta.iterator();
        while (iter.hasNext()) {
            if (iter.next().startsWith("C")) {
                iter.remove();
            }
        }
        System.out.println("sesudah hapus lewat iterator: " + namaPeserta);
    }
}
text Keluaran
dibaca satu per satu:
  Ali
  Budi
  Citra
  Dewi
sesudah hapus lewat iterator: [Ali, Budi, Dewi]

Dua perulangan di atas memakai pola yang sama dengan perbedaan satu baris. Perulangan pertama hanya membaca, sedangkan perulangan kedua memanggil iter.remove() ketika elemen yang baru dibaca diawali huruf C. Catatan pentingnya, remove() harus selalu mengikuti next(), karena metode ini menghapus “elemen yang terakhir dibaca”, bukan elemen yang sedang ditunjuk sembarang cara.

Dua iterator boleh diambil dari koleksi yang sama, dan keduanya membawa posisi baca masing-masing tanpa saling bergantung. Keduanya tetap tenang selama koleksinya tidak berubah struktural. Bila salah satu mengubah list, iterator yang lain akan menemukan pencacahnya tidak cocok saat dipakai berikutnya dan melempar exception yang sama, sebab perbandingan pencacah berlaku pada setiap iterator secara terpisah.

Mengapa ConcurrentModificationException muncul#

Kesalahan yang paling sering dijumpai adalah mengubah koleksi lewat koleksinya sendiri saat for-each masih berjalan. Iterator ArrayList membawa dua pencacah internal, yaitu modCount milik list yang naik setiap strukturnya berubah, dan expectedModCount yang disalin saat iterator dibuat. Sebelum setiap next(), keduanya dibandingkan, dan ketidakcocokan dianggap tanda bahwa pembacaan sudah tidak bisa dipercaya, sehingga dilemparlah ConcurrentModificationException. Perilaku waspada seperti ini disebut fail-fast, gagal secepatnya agar kerusakan terdeteksi di tempat terjadinya.

java DemoCME.java
import java.util.ArrayList;
import java.util.ConcurrentModificationException;
import java.util.List;

public class DemoCME {

    public static void main(String[] args) {
        List<String> namaPeserta = new ArrayList<>();
        namaPeserta.add("Ali");
        namaPeserta.add("Budi");
        namaPeserta.add("Citra");
        namaPeserta.add("Dewi");

        try {
            for (String nama : namaPeserta) {
                if (nama.equals("Budi")) {
                    namaPeserta.remove(nama);
                }
            }
        } catch (ConcurrentModificationException e) {
            System.out.println("Tertangkap: " + e);
        }
        System.out.println("isi list sesudahnya: " + namaPeserta);

        namaPeserta.removeIf(nama -> nama.equals("Citra"));
        System.out.println("hapus dengan removeIf     : " + namaPeserta);
    }
}
text Keluaran
Tertangkap: java.util.ConcurrentModificationException
isi list sesudahnya: [Ali, Citra, Dewi]
hapus dengan removeIf     : [Ali, Dewi]

Keluarannya memperlihatkan dua jalan keluar sekaligus. Pemanggilan namaPeserta.remove(nama) di dalam for-each menaikkan modCount tanpa menyentuh expectedModCount milik iterator, sehingga next() berikutnya melempar exception yang pada program nyata menghentikan jalannya kode. Alternatif removeIf bekerja karena metode ini menyusuri list lewat iterator internalnya sendiri dan menghapus lewat remove() yang sah. Coba demo berikut untuk melihat kedua pencacah itu bergerak, ubah list lewat tombol kiri saat iterasi aktif, lalu bandingkan dengan penghapusan lewat tombol iterator.

Demo di atas meniru pencacah milik ArrayList.Itr. Penghapusan lewat iter.remove() aman karena metode itu menaikkan kedua pencacah sekaligus, sehingga perbandingan tetap cocok sampai elemen habis. Pemisahan peran ini juga menjadi alasan dua thread yang menyentuh koleksi yang sama tetap butuh koleksi konkuren, karena pencacah fail-fast tidak dirancang untuk kendali antar-thread.

Kesalahan umum#

  • Menghapus elemen lewat list.remove(...) di dalam for-each. Iterator akan mendeteksi perubahan dan melempar ConcurrentModificationException pada next() berikutnya. Gunakan removeIf, atau kumpulkan elemen yang mau dihapus lalu hapus setelah perulangan selesai.
  • Memanggil iter.remove() tanpa next() di depannya, atau memanggilnya dua kali berturut-turut. Keduanya memicu IllegalStateException. Panggil next() dulu, lalu remove() boleh satu kali untuk elemen itu.
  • Menganggap exception ini tanda adanya thread kedua. Namanya memuat kata concurrent, tetapi pencacahnya tidak menyentuh urusan thread, dan satu thread pun bisa memicunya. Bila memang ada dua thread, yang dibutuhkan adalah koleksi konkuren, bukan pemeriksaan tambahan.
  • Mengira for-each selalu aman karena tidak menulis indeks. Aman hanya bila koleksinya tidak berubah struktural selama perulangan, dan itu berlaku untuk semua koleksi, bukan hanya List.
  • Memakai ConcurrentModificationException sebagai alat deteksi bug. Pemeriksaan pencacahnya hanya dijalankan sewaktu-waktu dan tidak menjamin apa pun, sebagaimana dinyatakan dokumentasinya. Perlakukan exception ini sebagai alarm kecurigaan, bukan sebagai mekanisme kendali.

Ringkasan#

  • For-each adalah bungkus pemanggilan iterator(), hasNext(), dan next(), sehingga semua koleksi menyusuri dengan cara yang sama.
  • Iterator menyediakan remove() yang menghapus elemen terakhir dibaca dan tetap sah di tengah perulangan.
  • ConcurrentModificationException muncul karena modCount dan expectedModCount tidak cocok, tanda koleksi berubah struktural saat iterator aktif.
  • Jalur aman untuk menghapus massal adalah removeIf, sedangkan penghapusan tunggal di tengah perulangan lewat iter.remove().

Referensi#