JavaRush /Java Blog /Random-ID /For dan For-Each Loop: kisah tentang bagaimana saya mengu...
Viacheslav
Level 3

For dan For-Each Loop: kisah tentang bagaimana saya mengulangi, mengulangi, tetapi tidak mengulangi

Dipublikasikan di grup Random-ID

Perkenalan

Loop adalah salah satu struktur dasar bahasa pemrograman. Misalnya, di situs web Oracle terdapat bagian “ Pelajaran: Dasar-Dasar Bahasa ”, di mana loop memiliki pelajaran terpisah “ Pernyataan for ”. Mari kita segarkan kembali dasar-dasarnya: Perulangan terdiri dari tiga ekspresi (pernyataan): inisialisasi (inisialisasi), kondisi (penghentian) dan kenaikan (kenaikan):
For dan For-Each Loop: sebuah cerita tentang bagaimana saya mengulangi, mengulangi, tetapi tidak mengulangi - 1
Menariknya, semuanya bersifat opsional, artinya kita dapat, jika mau, menulis:
for (;;){
}
Benar, dalam hal ini kita akan mendapatkan perulangan tanpa akhir, karena Kami tidak menentukan kondisi untuk keluar dari loop (terminasi). Ekspresi inisialisasi dijalankan hanya sekali, sebelum seluruh loop dijalankan. Perlu diingat bahwa suatu siklus mempunyai cakupannya sendiri. Ini berarti inisialisasi , penghentian , kenaikan , dan badan perulangan melihat variabel yang sama. Cakupannya selalu mudah ditentukan dengan menggunakan kurung kurawal. Semua yang ada di dalam tanda kurung tidak terlihat di luar tanda kurung, tetapi semua yang di luar tanda kurung terlihat di dalam tanda kurung. Inisialisasi hanyalah sebuah ekspresi. Misalnya, alih-alih menginisialisasi variabel, Anda biasanya dapat memanggil metode yang tidak akan mengembalikan apa pun. Atau lewati saja, sisakan ruang kosong sebelum titik koma pertama. Ekspresi berikut menentukan kondisi terminasi . Selama bernilai true , perulangan akan dijalankan. Dan jika false , iterasi baru tidak akan dimulai. Jika Anda melihat gambar di bawah, kami mendapatkan kesalahan saat kompilasi dan IDE akan mengeluh: ekspresi kami di loop tidak dapat dijangkau. Karena kita tidak akan memiliki satu iterasi pun dalam loop, kita akan segera keluar, karena PALSU:
For dan For-Each Loop: kisah tentang bagaimana saya mengulangi, mengulangi, tetapi tidak mengulangi - 2
Sebaiknya perhatikan ekspresi dalam pernyataan penghentian : pernyataan ini secara langsung menentukan apakah aplikasi Anda akan memiliki perulangan tanpa akhir. Kenaikan adalah ekspresi paling sederhana. Ini dijalankan setelah setiap iterasi loop berhasil. Dan ungkapan ini juga bisa dilewati. Misalnya:
int outerVar = 0;
for (;outerVar < 10;) {
	outerVar += 2;
	System.out.println("Value = " + outerVar);
}
Seperti yang dapat Anda lihat dari contoh, setiap iterasi perulangan akan kita tambah dengan kelipatan 2, tetapi hanya selama nilainya outerVarkurang dari 10. Selain itu, karena ekspresi dalam pernyataan kenaikan sebenarnya hanyalah sebuah ekspresi, maka ekspresi dalam pernyataan kenaikan sebenarnya hanyalah sebuah ekspresi. bisa berisi apa saja. Oleh karena itu, tidak ada yang melarang penggunaan pengurangan, bukan kenaikan, yaitu. mengurangi nilai. Anda harus selalu memantau penulisan kenaikannya. +=melakukan peningkatan terlebih dahulu dan kemudian penugasan, tetapi jika pada contoh di atas kita menulis sebaliknya, kita akan mendapatkan loop tak terbatas, karena variabel outerVartidak akan pernah menerima nilai yang diubah: dalam hal ini akan =+dihitung setelah penugasan. Ngomong-ngomong, hal yang sama juga terjadi pada peningkatan tampilan ++. Misalnya, kami memiliki satu lingkaran:
String[] names = {"John","Sara","Jack"};
for (int i = 0; i < names.length; ++i) {
	System.out.println(names[i]);
}
Siklusnya berhasil dan tidak ada masalah. Tapi kemudian orang yang melakukan refactoring datang. Dia tidak memahami kenaikan tersebut dan hanya melakukan ini:
String[] names = {"John","Sara","Jack"};
for (int i = 0; i < names.length;) {
	System.out.println(names[++i]);
}
Jika tanda kenaikan muncul di depan suatu nilai, artinya nilai tersebut akan bertambah terlebih dahulu dan kemudian kembali ke tempat yang ditunjukkan. Dalam contoh ini, kita akan segera mulai mengekstraksi elemen pada indeks 1 dari array, melewatkan yang pertama. Dan kemudian pada indeks 3 kita akan crash dengan error " java.lang.ArrayIndexOutOfBoundsException ". Seperti yang sudah Anda duga, ini berhasil sebelumnya hanya karena kenaikan dipanggil setelah iterasi selesai. Ketika ekspresi ini ditransfer ke iterasi, semuanya rusak. Ternyata, bahkan dalam loop sederhana pun Anda dapat membuat kekacauan) Jika Anda memiliki array, mungkin ada cara yang lebih mudah untuk menampilkan semua elemen?
For dan For-Each Loop: kisah tentang bagaimana saya mengulangi, mengulangi, tetapi tidak mengulangi - 3

Untuk setiap putaran

Dimulai dengan Java 1.5, pengembang Java memberi kami desain for each loopyang dijelaskan di situs Oracle dalam Panduan yang disebut " The For-Each Loop " atau untuk versi 1.5.0 . Secara umum akan terlihat seperti ini:
For dan For-Each Loop: kisah tentang bagaimana saya mengulangi, mengulangi, tetapi tidak mengulangi - 4
Anda dapat membaca deskripsi konstruksi ini di Spesifikasi Bahasa Java (JLS) untuk memastikan bahwa ini bukan sihir. Konstruksi ini dijelaskan dalam bab " 14.14.2. Pernyataan yang disempurnakan ". Seperti yang Anda lihat, perulangan for every dapat digunakan dengan array dan yang mengimplementasikan antarmuka java.lang.Iterable . Artinya, jika Anda benar-benar ingin, Anda bisa mengimplementasikan antarmuka java.lang.Iterable dan untuk setiap loop dapat digunakan dengan kelas Anda. Anda akan segera berkata, "Oke, ini adalah objek yang dapat diubah, tetapi array bukanlah objek. Semacam itu." Dan Anda akan salah, karena... Di Java, array adalah objek yang dibuat secara dinamis. Spesifikasi bahasa memberi tahu kita hal ini: “ Dalam bahasa pemrograman Java, array adalah objek .” Secara umum, array adalah sedikit keajaiban JVM, karena... bagaimana array disusun secara internal tidak diketahui dan terletak di suatu tempat di dalam Java Virtual Machine. Siapa pun yang tertarik dapat membaca jawaban di stackoverflow: " Bagaimana cara kerja kelas array di Java? " Ternyata jika kita tidak menggunakan array, maka kita harus menggunakan sesuatu yang mengimplementasikan Iterable . Misalnya:
List<String> names = Arrays.asList("John", "Sara", "Jack");
for (String name : names) {
	System.out.println("Name = " + name);
}
Di sini Anda hanya perlu mengingat bahwa jika kita menggunakan koleksi ( java.util.Collection ), berkat ini kita mendapatkan Iterable . Jika suatu objek memiliki kelas yang mengimplementasikan Iterable, maka objek tersebut wajib menyediakan, ketika metode iterator dipanggil, sebuah Iterator yang akan melakukan iterasi pada konten objek tersebut. Kode di atas, misalnya, akan memiliki bytecode seperti ini (di IntelliJ Idea Anda dapat melakukan "View" -> "Show bytecode" :
For dan For-Each Loop: sebuah cerita tentang bagaimana saya mengulangi, mengulangi, tetapi tidak mengulangi - 5
Seperti yang Anda lihat, iterator sebenarnya digunakan. Jika bukan karena for every loop , kita harus menulis sesuatu seperti:
List<String> names = Arrays.asList("John", "Sara", "Jack");
for (Iterator i = names.iterator(); /* continue if */ i.hasNext(); /* skip increment */) {
	String name = (String) i.next();
	System.out.println("Name = " + name);
}

Pengulangan

Seperti yang kita lihat di atas, antarmuka Iterable mengatakan bahwa untuk contoh beberapa objek, Anda bisa mendapatkan iterator yang dapat digunakan untuk mengulangi konten. Sekali lagi, ini bisa dikatakan sebagai Prinsip Tanggung Jawab Tunggal dari SOLID . Struktur data itu sendiri tidak seharusnya mendorong traversal, namun dapat menyediakan traversal yang seharusnya. Implementasi dasar dari Iterator adalah biasanya dideklarasikan sebagai kelas dalam yang memiliki akses ke konten kelas luar dan menyediakan elemen yang diinginkan yang terdapat di kelas luar. Berikut ini contoh dari kelas ArrayListbagaimana iterator mengembalikan elemen:
public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
}
Seperti yang bisa kita lihat, dengan bantuan ArrayList.thisiterator mengakses kelas luar dan variabelnya elementData, lalu mengembalikan elemen dari sana. Jadi, mendapatkan iterator sangat sederhana:
List<String> names = Arrays.asList("John", "Sara", "Jack");
Iterator<String> iterator = names.iterator();
Tugasnya adalah kita dapat memeriksa apakah ada elemen lebih lanjut ( metode hasNext ), mendapatkan elemen berikutnya ( metode selanjutnya ) dan metode hapus , yang menghapus elemen terakhir yang diterima melalui next . Metode penghapusan bersifat opsional dan tidak dijamin dapat diterapkan. Faktanya, seiring berkembangnya Java, antarmuka juga berkembang. Oleh karena itu, di Java 8 juga terdapat metode forEachRemainingyang memungkinkan Anda melakukan beberapa tindakan pada elemen lain yang tidak dikunjungi oleh iterator. Apa yang menarik tentang iterator dan koleksi? Misalnya ada kelas AbstractList. Ini adalah kelas abstrak yang merupakan induk dari ArrayListdan LinkedList. Dan ini menarik bagi kami karena bidang seperti modCount . Setiap perubahan isi daftar berubah. Jadi, apa pentingnya hal itu bagi kita? Dan fakta bahwa iterator memastikan bahwa selama operasi, koleksi yang diiterasi tidak berubah. Seperti yang Anda pahami, implementasi iterator untuk daftar terletak di tempat yang sama dengan modcount , yaitu di kelas AbstractList. Mari kita lihat contoh sederhana:
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Iterator<String> iterator = names.iterator();
names.add("modcount++");
System.out.println(iterator.next());
Inilah hal pertama yang menarik, meski bukan topik. Sebenarnya Arrays.asListmengembalikan yang spesial ArrayList( java.util.Arrays.ArrayList ). Itu tidak menerapkan metode penambahan, sehingga tidak dapat dimodifikasi. Ada tertulis di JavaDoc: fixed-size . Namun kenyataannya, ini lebih dari sekadar ukuran tetap . Itu juga tidak dapat diubah , yaitu tidak dapat diubah; hapus juga tidak akan berhasil. Kami juga akan mendapatkan kesalahan, karena... Setelah membuat iterator, kami mengingat modcount di dalamnya . Kemudian kami mengubah status koleksi “secara eksternal” (yaitu, bukan melalui iterator) dan menjalankan metode iterator. Oleh karena itu, kami mendapatkan kesalahan: java.util.ConcurrentModificationException . Untuk menghindari hal ini, perubahan selama iterasi harus dilakukan melalui iterator itu sendiri, dan bukan melalui akses ke koleksi:
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Iterator<String> iterator = names.iterator();
iterator.next();
iterator.remove();
System.out.println(iterator.next());
Seperti yang Anda pahami, jika iterator.remove()Anda tidak melakukannya sebelumnya iterator.next(), maka karena. iterator tidak menunjuk ke elemen apa pun, maka kita akan mendapatkan error. Dalam contoh, iterator akan menuju ke elemen John , menghapusnya, dan kemudian mendapatkan elemen Sara . Dan di sini semuanya akan baik-baik saja, tapi sial, sekali lagi ada "nuansa") java.util.ConcurrentModificationException hanya akan terjadi ketika hasNext()mengembalikan true . Artinya, jika Anda menghapus elemen terakhir melalui koleksi itu sendiri, iteratornya tidak akan hilang. Untuk lebih jelasnya lebih baik simak laporan tentang teka-teki Java dari “ #ITsubbotnik Bagian JAVA: Teka-teki Java ”. Kami memulai percakapan mendetail karena alasan sederhana bahwa nuansa yang sama berlaku ketika for each loop... Iterator favorit kami digunakan di bawah tenda. Dan semua nuansa ini juga berlaku di sana. Satu-satunya hal adalah, kami tidak akan memiliki akses ke iterator, dan kami tidak akan dapat menghapus elemen tersebut dengan aman. Omong-omong, seperti yang Anda pahami, keadaan diingat pada saat iterator dibuat. Dan penghapusan aman hanya berfungsi jika dipanggil. Artinya, opsi ini tidak akan berfungsi:
Iterator<String> iterator1 = names.iterator();
Iterator<String> iterator2 = names.iterator();
iterator1.next();
iterator1.remove();
System.out.println(iterator2.next());
Karena untuk iterator2 penghapusan melalui iterator1 bersifat “eksternal”, yaitu dilakukan di suatu tempat di luar dan dia tidak tahu apa-apa tentangnya. Mengenai topik iterator, saya juga ingin mencatat hal ini. Iterator khusus yang diperluas dibuat khusus untuk implementasi antarmuka List. Dan mereka menamai dia ListIterator. Ini memungkinkan Anda untuk bergerak tidak hanya maju, tetapi juga mundur, dan juga memungkinkan Anda mengetahui indeks elemen sebelumnya dan berikutnya. Selain itu, ini memungkinkan Anda mengganti elemen saat ini atau menyisipkan elemen baru pada posisi antara posisi iterator saat ini dan posisi berikutnya. Seperti yang Anda duga, ListIteratorhal ini diperbolehkan karena Listakses berdasarkan indeks diterapkan.
For dan For-Each Loop: kisah tentang bagaimana saya mengulangi, mengulangi, tetapi tidak mengulangi - 6

Java 8 dan Iterasi

Peluncuran Java 8 telah membuat hidup lebih mudah bagi banyak orang. Kami juga tidak mengabaikan iterasi pada isi objek. Untuk memahami cara kerjanya, Anda perlu menyampaikan beberapa patah kata tentang ini. Java 8 memperkenalkan kelas java.util.function.Consumer . Berikut ini contohnya:
Consumer consumer = new Consumer() {
	@Override
	public void accept(Object o) {
		System.out.println(o);
	}
};
Konsumen adalah antarmuka fungsional, artinya di dalam antarmuka hanya ada 1 metode abstrak yang belum diimplementasikan yang memerlukan implementasi wajib di kelas-kelas yang menentukan implementasi antarmuka ini. Ini memungkinkan Anda untuk menggunakan hal ajaib seperti lambda. Artikel ini bukan tentang itu, tapi kita perlu memahami mengapa kita bisa menggunakannya. Jadi, dengan menggunakan lambda, Konsumen di atas dapat ditulis ulang seperti ini: Consumer consumer = (obj) -> System.out.println(obj); Ini berarti bahwa Java melihat bahwa sesuatu yang disebut obj akan diteruskan ke input, dan kemudian ekspresi setelah -> akan dieksekusi untuk objek ini. Adapun iterasi, sekarang kita dapat melakukan ini:
List<String> names = Arrays.asList("John", "Sara", "Jack");
Consumer consumer = (obj) -> System.out.println(obj);
names.forEach(consumer);
Jika Anda pergi ke metode ini forEach, Anda akan melihat bahwa semuanya sangat sederhana. Ada yang favorit kami for-each loop:
default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
}
Dimungkinkan juga untuk menghapus elemen dengan indah menggunakan iterator, misalnya:
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Predicate predicate = (obj) -> obj.equals("John");
names.removeIf(predicate);
Dalam hal ini, metode deleteIf tidak mengambil input Consumer , melainkan Predicate . Ini mengembalikan boolean . Dalam hal ini, jika predikatnya berbunyi " benar ", maka elemen tersebut akan dihilangkan. Sangat menarik bahwa tidak semuanya jelas di sini)) Apa yang Anda inginkan? Masyarakat perlu diberi ruang untuk menciptakan teka-teki di konferensi. Sebagai contoh, mari kita ambil kode berikut untuk menghapus semua yang dapat dijangkau oleh iterator setelah beberapa iterasi:
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Iterator<String> iterator = names.iterator();
iterator.next(); // Курсор на John
while (iterator.hasNext()) {
    iterator.next(); // Следующий элемент
    iterator.remove(); // Удалor его
}
System.out.println(names);
Oke, semuanya berfungsi di sini. Tapi kita ingat Java 8 itu. Oleh karena itu, mari kita coba menyederhanakan kodenya:
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Iterator<String> iterator = names.iterator();
iterator.next(); // Курсор на John
iterator.forEachRemaining(obj -> iterator.remove());
System.out.println(names);
Apakah ini benar-benar menjadi lebih indah? Namun, akan ada java.lang.IllegalStateException . Dan alasannya adalah... bug di Java. Ternyata sudah diperbaiki, tapi di JDK 9. Berikut link tugas di OpenJDK: Iterator.forEachRemaining vs. Iterator.hapus . Tentu saja, ini sudah dibahas: Mengapa iterator.forEachRemaining tidak menghapus elemen di lambda Konsumen? Nah, cara lainnya adalah langsung melalui Stream API:
List<String> names = new ArrayList(Arrays.asList("John", "Sara", "Jack"));
Stream<String> stream = names.stream();
stream.forEach(obj -> System.out.println(obj));

kesimpulan

Seperti yang kita lihat dari semua materi di atas, loop for-each loophanyalah “gula sintaksis” di atas sebuah iterator. Namun kini digunakan di banyak tempat. Selain itu, produk apa pun harus digunakan dengan hati-hati. Misalnya, orang yang tidak berbahaya forEachRemainingmungkin menyembunyikan kejutan yang tidak menyenangkan. Dan ini sekali lagi membuktikan bahwa pengujian unit diperlukan. Tes yang baik dapat mengidentifikasi kasus penggunaan seperti itu dalam kode Anda. Apa yang dapat Anda tonton/baca tentang topik ini: #Viacheslav
Komentar
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION