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):
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:
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
outerVar
kurang 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
outerVar
tidak 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?
Untuk setiap putaran
Dimulai dengan Java 1.5, pengembang Java memberi kami desain
for each loop
yang dijelaskan di situs Oracle dalam Panduan yang disebut "
The For-Each Loop " atau untuk versi
1.5.0 . Secara umum akan terlihat seperti ini:
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" :
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(); i.hasNext(); ) {
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
ArrayList
bagaimana 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.this
iterator 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
forEachRemaining
yang 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
ArrayList
dan
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.asList
mengembalikan 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,
ListIterator
hal ini diperbolehkan karena
List
akses berdasarkan indeks diterapkan.
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();
while (iterator.hasNext()) {
iterator.next();
iterator.remove();
}
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();
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 loop
hanyalah “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
forEachRemaining
mungkin 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
GO TO FULL VERSION