pengenalan
Gelung adalah salah satu struktur asas bahasa pengaturcaraan. Sebagai contoh, di laman web Oracle terdapat bahagian "
Pelajaran: Asas Bahasa ", di mana gelung mempunyai pelajaran berasingan "
The for Statement ". Mari segarkan semula asas: Gelung terdiri daripada tiga ungkapan (pernyataan):
pemula (initialization),
syarat (penamatan) dan
kenaikan (increment):
Menariknya, semuanya adalah pilihan, bermakna kita boleh, jika kita mahu, menulis:
for (;;){
}
Benar, dalam kes ini kita akan mendapat gelung yang tidak berkesudahan, kerana Kami tidak menyatakan syarat untuk keluar dari gelung (penamatan). Ungkapan permulaan dilaksanakan sekali sahaja, sebelum keseluruhan gelung dilaksanakan. Perlu diingat bahawa kitaran mempunyai skopnya sendiri. Ini bermakna
permulaan ,
penamatan ,
kenaikan dan badan gelung melihat pembolehubah yang sama. Skop sentiasa mudah ditentukan menggunakan pendakap kerinting. Segala-galanya di dalam kurungan tidak kelihatan di luar kurungan, tetapi segala-galanya di luar kurungan kelihatan di dalam kurungan.
Inisialisasi hanyalah ungkapan. Sebagai contoh, bukannya memulakan pembolehubah, anda secara amnya boleh memanggil kaedah yang tidak akan mengembalikan apa-apa. Atau langkau sahaja, tinggalkan ruang kosong sebelum koma bertitik pertama. Ungkapan berikut menyatakan
syarat penamatan . Selagi ia
benar , gelung dilaksanakan. Dan jika
false , lelaran baharu tidak akan bermula. Jika anda melihat gambar di bawah, kami mendapat ralat semasa penyusunan dan IDE akan mengadu: ekspresi kami dalam gelung tidak dapat dicapai. Oleh kerana kita tidak akan mempunyai satu lelaran dalam gelung, kita akan keluar serta-merta, kerana salah:
Perlu mengawasi ungkapan dalam
pernyataan penamatan : ia secara langsung menentukan sama ada aplikasi anda akan mempunyai gelung yang tidak berkesudahan.
Kenaikan ialah ungkapan yang paling mudah. Ia dilaksanakan selepas setiap lelaran gelung yang berjaya. Dan ungkapan ini juga boleh dilangkau. Sebagai contoh:
int outerVar = 0;
for (;outerVar < 10;) {
outerVar += 2;
System.out.println("Value = " + outerVar);
}
Seperti yang anda boleh lihat daripada contoh, setiap lelaran gelung kami akan menambah dalam kenaikan 2, tetapi hanya selagi nilainya
outerVar
kurang daripada 10. Di samping itu, kerana ungkapan dalam
pernyataan kenaikan sebenarnya hanyalah ungkapan, ia boleh mengandungi apa sahaja. Oleh itu, tiada siapa yang melarang menggunakan pengurangan dan bukannya kenaikan, i.e. mengurangkan nilai. Anda harus sentiasa memantau penulisan kenaikan.
+=
melakukan peningkatan dahulu dan kemudian tugasan, tetapi jika dalam contoh di atas kita menulis sebaliknya, kita akan mendapat gelung tak terhingga, kerana pembolehubah
outerVar
tidak akan menerima nilai yang diubah: dalam kes ini ia
=+
akan dikira selepas tugasan. Ngomong-ngomong, ia adalah sama dengan kenaikan paparan
++
. Sebagai contoh, kami mempunyai gelung:
String[] names = {"John","Sara","Jack"};
for (int i = 0; i < names.length; ++i) {
System.out.println(names[i]);
}
Kitaran berfungsi dan tidak ada masalah. Tetapi kemudian lelaki pemfaktoran semula datang. Dia tidak memahami kenaikan itu 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 hadapan nilai, ini bermakna ia akan meningkat dahulu dan kemudian kembali ke tempat di mana ia ditunjukkan. Dalam contoh ini, kami akan segera mula mengekstrak elemen pada indeks 1 daripada tatasusunan, melangkau yang pertama. Dan kemudian pada indeks 3 kita akan ranap dengan ralat "
java.lang.ArrayIndexOutOfBoundsException ". Seperti yang anda duga, ini berfungsi sebelum ini hanya kerana kenaikan dipanggil selepas lelaran selesai. Apabila memindahkan ungkapan ini kepada lelaran, semuanya rosak. Ternyata, walaupun dalam gelung mudah anda boleh membuat kekacauan) Jika anda mempunyai tatasusunan, mungkin ada cara yang lebih mudah untuk memaparkan semua elemen?
Untuk setiap gelung
Bermula dengan Java 1.5, pembangun Java memberi kami reka bentuk
for each loop
yang diterangkan pada tapak Oracle dalam Panduan yang dipanggil "
The For-Each Loop " atau untuk versi
1.5.0 . Secara umum, ia akan kelihatan seperti ini:
Anda boleh membaca huraian binaan ini dalam Spesifikasi Bahasa Java (JLS) untuk memastikan bahawa ia bukan sihir. Pembinaan ini diterangkan dalam bab "
14.14.2. Pernyataan yang dipertingkatkan ". Seperti yang anda lihat,
untuk setiap gelung boleh digunakan dengan tatasusunan dan mereka yang melaksanakan antara muka
java.lang.Iterable . Iaitu, jika anda benar-benar mahu, anda boleh melaksanakan antara muka
java.lang.Iterable dan
untuk setiap gelung boleh digunakan dengan kelas anda. Anda akan segera berkata, "Baiklah, ia adalah objek boleh lelar, tetapi tatasusunan bukan objek. Macam-macam." Dan anda akan salah, kerana... Di Java, tatasusunan ialah objek yang dicipta secara dinamik. Spesifikasi bahasa memberitahu kita ini: "
Dalam bahasa pengaturcaraan Java, tatasusunan adalah objek ." Secara umum, tatasusunan adalah sedikit sihir JVM, kerana... bagaimana tatasusunan berstruktur secara dalaman tidak diketahui dan terletak di suatu tempat di dalam Mesin Maya Java. Sesiapa yang berminat boleh membaca jawapan pada stackoverflow: "
Bagaimanakah kelas tatasusunan berfungsi di Jawa? " Ternyata jika kita tidak menggunakan tatasusunan, maka kita mesti menggunakan sesuatu yang melaksanakan
Iterable . Sebagai contoh:
List<String> names = Arrays.asList("John", "Sara", "Jack");
for (String name : names) {
System.out.println("Name = " + name);
}
Di sini anda hanya boleh ingat bahawa jika kami menggunakan koleksi (
java.util.Collection ), terima kasih kepada ini kami mendapat
Iterable tepat . Jika objek mempunyai kelas yang melaksanakan Iterable, ia wajib menyediakan, apabila kaedah iterator dipanggil, Iterator yang akan berulang ke atas kandungan objek itu. Kod di atas, sebagai contoh, akan mempunyai bytecode seperti ini (dalam IntelliJ Idea anda boleh melakukan "View" -> "Show bytecode" :
Seperti yang anda lihat, iterator sebenarnya digunakan. Jika bukan
untuk setiap loop , kita perlu 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);
}
Iterator
Seperti yang kita lihat di atas, antara muka
Iterable mengatakan bahawa untuk contoh beberapa objek, anda boleh mendapatkan iterator yang anda boleh lelaran ke atas kandungan. Sekali lagi, ini boleh dikatakan sebagai Prinsip Tanggungjawab Tunggal daripada
SOLID . Struktur data itu sendiri tidak seharusnya memacu traversal, tetapi ia boleh memberikan yang sepatutnya. Pelaksanaan asas
Iterator ialah ia biasanya diisytiharkan sebagai kelas dalam yang mempunyai akses kepada kandungan kelas luar dan menyediakan elemen yang dikehendaki terkandung dalam kelas luar. Berikut ialah contoh dari kelas
ArrayList
cara peulang 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 dapat kita lihat, dengan bantuan
ArrayList.this
iterator mengakses kelas luar dan pembolehubahnya
elementData
, dan kemudian mengembalikan elemen dari sana. Jadi, mendapatkan iterator adalah sangat mudah:
List<String> names = Arrays.asList("John", "Sara", "Jack");
Iterator<String> iterator = names.iterator();
Kerjanya datang kepada fakta bahawa kita boleh menyemak sama ada terdapat elemen lagi ( kaedah
hasNext ), dapatkan elemen seterusnya ( kaedah
seterusnya ) dan kaedah
buang , yang mengalih keluar elemen terakhir yang diterima melalui
next . Kaedah
alih keluar adalah pilihan dan tidak dijamin untuk dilaksanakan. Malah, apabila Java berkembang, antara muka juga berkembang. Oleh itu, dalam Java 8, kaedah juga muncul
forEachRemaining
yang membolehkan anda melakukan beberapa tindakan pada elemen yang tinggal yang tidak dilawati oleh iterator. Apa yang menarik tentang iterator dan koleksi? Sebagai contoh, terdapat kelas
AbstractList
. Ini ialah kelas abstrak yang merupakan induk kepada
ArrayList
dan
LinkedList
. Dan ia menarik kepada kami kerana medan seperti
modCount . Setiap perubahan kandungan senarai berubah. Jadi apa yang penting kepada kita? Dan hakikat bahawa iterator memastikan bahawa semasa operasi pengumpulan di mana ia diulang tidak berubah. Seperti yang anda fahami, pelaksanaan iterator untuk senarai terletak di tempat yang sama dengan
modcount , iaitu dalam kelas
AbstractList
. Mari lihat contoh mudah:
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());
Berikut adalah perkara pertama yang menarik, walaupun tidak mengenai topik. Sebenarnya
Arrays.asList
mengembalikan yang istimewanya sendiri
ArrayList
(
java.util.Arrays.ArrayList ). Ia tidak melaksanakan kaedah penambahan, jadi ia tidak boleh diubah suai. Ia ditulis tentang dalam JavaDoc:
fixed-size . Tetapi sebenarnya, ia lebih daripada
saiz tetap . Ia juga
tidak boleh diubah , iaitu, tidak boleh diubah; alih keluar tidak akan berfungsi padanya sama ada. Kami juga akan mendapat ralat, kerana...
Setelah mencipta iterator, kami teringat modcount di dalamnya . Kemudian kami menukar keadaan koleksi "secara luaran" (iaitu, bukan melalui iterator) dan melaksanakan kaedah iterator. Oleh itu, kami mendapat ralat:
java.util.ConcurrentModificationException . Untuk mengelakkan ini, perubahan semasa lelaran mesti dilakukan melalui iterator itu sendiri, dan bukan melalui akses kepada 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 faham, jika
iterator.remove()
anda tidak melakukannya sebelum ini
iterator.next()
, maka kerana. iterator tidak menunjuk kepada mana-mana elemen, maka kita akan mendapat ralat. Dalam contoh, iterator akan pergi ke elemen
John , alih keluarnya dan kemudian dapatkan elemen
Sara . Dan di sini semuanya akan baik-baik saja, tetapi nasib malang, sekali lagi terdapat "nuansa")
java.util.ConcurrentModificationException hanya akan berlaku apabila
hasNext()
ia mengembalikan
true . Iaitu, jika anda memadamkan elemen terakhir melalui koleksi itu sendiri, iterator tidak akan jatuh. Untuk butiran lanjut, adalah lebih baik untuk menonton laporan tentang teka-teki Java daripada "
#ITsubbotnik Section JAVA: Java puzzles ". Kami memulakan perbualan terperinci sedemikian untuk alasan mudah bahawa nuansa yang sama berlaku apabila
for each loop
... Iterator kegemaran kami digunakan di bawah hud. Dan semua nuansa ini juga berlaku di sana. Satu-satunya perkara ialah, kami tidak akan mempunyai akses kepada iterator, dan kami tidak akan dapat mengalih keluar elemen dengan selamat. Ngomong-ngomong, seperti yang anda fahami, keadaan diingati pada masa iterator dibuat. Dan pemadaman selamat hanya berfungsi apabila ia dipanggil. Iaitu, pilihan ini tidak akan berfungsi:
Iterator<String> iterator1 = names.iterator();
Iterator<String> iterator2 = names.iterator();
iterator1.next();
iterator1.remove();
System.out.println(iterator2.next());
Kerana untuk iterator2 pemadaman melalui iterator1 adalah "luar", iaitu, ia dilakukan di suatu tempat di luar dan dia tidak tahu apa-apa mengenainya. Mengenai topik iterator, saya juga ingin ambil perhatian ini. Iterator yang dilanjutkan khas dibuat khusus untuk pelaksanaan antara muka
List
. Dan mereka menamakan dia
ListIterator
. Ia membolehkan anda bergerak bukan sahaja ke hadapan, tetapi juga ke belakang, dan juga membolehkan anda mengetahui indeks elemen sebelumnya dan yang seterusnya. Selain itu, ia membolehkan anda menggantikan elemen semasa atau memasukkan elemen baharu pada kedudukan antara kedudukan lelaran semasa dan yang seterusnya. Seperti yang anda rasa,
ListIterator
ia dibenarkan untuk melakukan ini kerana
List
akses mengikut indeks dilaksanakan.
Java 8 dan Lelaran
Pengeluaran Java 8 telah menjadikan hidup lebih mudah bagi ramai orang. Kami juga tidak mengabaikan lelaran ke atas kandungan objek. Untuk memahami cara ini berfungsi, anda perlu mengatakan beberapa perkataan tentang perkara ini. Java 8 memperkenalkan kelas
java.util.function.Consumer . Berikut ialah contoh:
Consumer consumer = new Consumer() {
@Override
public void accept(Object o) {
System.out.println(o);
}
};
Pengguna ialah antara muka berfungsi, yang bermaksud bahawa di dalam antara muka terdapat hanya 1 kaedah abstrak yang tidak dilaksanakan yang memerlukan pelaksanaan mandatori dalam kelas tersebut yang menentukan pelaksanaan antara muka ini. Ini membolehkan anda menggunakan perkara ajaib seperti lambda. Artikel ini bukan tentang itu, tetapi kita perlu memahami mengapa kita boleh menggunakannya. Jadi, menggunakan lambdas,
Pengguna di atas boleh ditulis semula seperti ini:
Consumer consumer = (obj) -> System.out.println(obj);
Ini bermakna Java melihat bahawa sesuatu yang dipanggil obj akan dihantar ke input, dan kemudian ungkapan selepas -> akan dilaksanakan untuk obj ini. Bagi lelaran, kini kita boleh melakukan ini:
List<String> names = Arrays.asList("John", "Sara", "Jack");
Consumer consumer = (obj) -> System.out.println(obj);
names.forEach(consumer);
Jika anda pergi ke kaedah
forEach
, anda akan melihat bahawa semuanya sangat mudah. Ada yang kegemaran kami
for-each loop
:
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
Ia juga mungkin untuk mengalih keluar elemen dengan cantik menggunakan iterator, sebagai contoh:
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Predicate predicate = (obj) -> obj.equals("John");
names.removeIf(predicate);
Dalam kes ini, kaedah
removeIf mengambil sebagai input bukan
Consumer , tetapi
Predicate . Ia mengembalikan
boolean . Dalam kes ini, jika predikat mengatakan "
benar ", maka elemen akan dialih keluar. Adalah menarik bahawa tidak semuanya jelas di sini sama ada)) Nah, apa yang anda mahu? Orang ramai perlu diberi ruang untuk mencipta teka-teki di persidangan itu. Sebagai contoh, mari ambil kod berikut untuk memadamkan semua yang boleh dicapai oleh lelaran selepas beberapa lelaran:
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);
Okay, semuanya berfungsi di sini. Tetapi kami ingat bahawa Java 8 selepas semua. Oleh itu, mari cuba permudahkan kod:
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);
Adakah ia benar-benar menjadi lebih cantik? Walau bagaimanapun, akan ada
java.lang.IllegalStateException . Dan sebabnya ialah... pepijat di Jawa. Ternyata ia telah ditetapkan, tetapi dalam JDK 9. Berikut ialah pautan kepada tugas dalam OpenJDK:
Iterator.forEachRemaining vs. Iterator.buang . Sememangnya, ini telah pun dibincangkan:
Mengapa iterator.forEachRemaining tidak mengalih keluar elemen dalam lambda Pengguna? Nah, cara lain adalah terus melalui API Strim:
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 daripada semua bahan di atas, gelung
for-each loop
hanyalah "gula sintaksis" di atas lelaran. Walau bagaimanapun, ia kini digunakan di banyak tempat. Di samping itu, sebarang produk mesti digunakan dengan berhati-hati. Sebagai contoh, yang tidak berbahaya
forEachRemaining
mungkin menyembunyikan kejutan yang tidak menyenangkan. Dan ini sekali lagi membuktikan bahawa ujian unit diperlukan. Ujian yang baik boleh mengenal pasti kes penggunaan sedemikian dalam kod anda. Perkara yang anda boleh tonton/baca mengenai topik:
#Viacheslav
GO TO FULL VERSION