JavaRush /Blog Java /Random-MS /Untuk dan Untuk Setiap Gelung: kisah tentang cara saya me...

Untuk dan Untuk Setiap Gelung: kisah tentang cara saya mengulang, telah diulang, tetapi tidak diulang

Diterbitkan dalam kumpulan

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):
Untuk dan Untuk Setiap Gelung: cerita tentang cara saya mengulang, mengulang, tetapi tidak mengulang - 1
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:
Untuk dan Untuk Setiap Gelung: kisah bagaimana saya mengulang, mengulang, tetapi tidak mengulang - 2
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 outerVarkurang 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 outerVartidak 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 dan Untuk-Setiap Gelung: kisah bagaimana saya mengulang, mengulang, tetapi tidak mengulang - 3

Untuk setiap gelung

Bermula dengan Java 1.5, pembangun Java memberi kami reka bentuk for each loopyang 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:
Untuk dan Untuk Setiap Gelung: kisah bagaimana saya mengulang, mengulang, tetapi tidak mengulang - 4
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" :
Untuk dan Untuk Setiap Gelung: cerita tentang cara saya mengulang, mengulang, tetapi tidak mengulang - 5
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(); /* continue if */ i.hasNext(); /* skip increment */) {
	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 ArrayListcara 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.thisiterator 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 forEachRemainingyang 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 ArrayListdan 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.asListmengembalikan 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, ListIteratoria dibenarkan untuk melakukan ini kerana Listakses mengikut indeks dilaksanakan.
Untuk dan Untuk-Setiap Gelung: kisah bagaimana saya mengulang, mengulang, tetapi tidak mengulang - 6

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(); // Курсор на John
while (iterator.hasNext()) {
    iterator.next(); // Следующий элемент
    iterator.remove(); // Удалor его
}
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(); // Курсор на John
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 loophanyalah "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 forEachRemainingmungkin 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
Komen
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION