JavaRush /Java blogi /Random-UZ /For va For-Each Loop: men qanday takrorlaganim haqidagi e...
Viacheslav
Daraja

For va For-Each Loop: men qanday takrorlaganim haqidagi ertak, takrorlangan, ammo takrorlanmagan

Guruhda nashr etilgan

Kirish

Looplar dasturlash tillarining asosiy tuzilmalaridan biridir. Masalan, Oracle veb-saytida " Dars: Til asoslari " bo'limi mavjud bo'lib, unda tsikllarda alohida dars " The for Statement " mavjud. Keling, asosiy ma'lumotlarni yangilaymiz: Loop uchta ifodadan (bayonotlardan) iborat: ishga tushirish (boshlash), shart (tugatish) va o'sish (o'sish):
For va For-Each Loop: men qanday takrorlaganim, takrorlaganim, lekin takrorlamaganim haqidagi hikoya - 1
Qizig'i shundaki, ularning barchasi ixtiyoriy, ya'ni agar xohlasak, yozishimiz mumkin:
for (;;){
}
To'g'ri, bu holda biz cheksiz pastadir olamiz, chunki Biz tsikldan chiqish (tugatish) uchun shartni belgilamaymiz. Initsializatsiya ifodasi butun tsikl bajarilgunga qadar faqat bir marta bajariladi. Tsiklning o'z ko'lami borligini doimo yodda tutish kerak. Bu shuni anglatadiki, ishga tushirish , tugatish , o'sish va tsikl tanasi bir xil o'zgaruvchilarni ko'radi. Qo'llanish doirasini jingalak qavslar yordamida aniqlash har doim oson. Qavslar ichidagi hamma narsa qavslar tashqarisida ko'rinmaydi, lekin qavslar ichidan tashqarida hamma narsa ko'rinadi. Initializatsiya shunchaki ifodadir. Misol uchun, o'zgaruvchini ishga tushirish o'rniga, odatda hech narsa qaytarmaydigan usulni chaqirishingiz mumkin. Yoki birinchi nuqta-vergul oldidan bo'sh joy qoldirib, uni o'tkazib yuboring. Quyidagi ifoda tugatish shartini bildiradi . Modomiki bu true , tsikl bajariladi. Va agar false bo'lsa , yangi iteratsiya boshlanmaydi. Agar siz quyidagi rasmga qarasangiz, kompilyatsiya paytida xatoga yo'l qo'yamiz va IDE shikoyat qiladi: tsikldagi bizning ifodamizga etib bo'lmaydi. Bizda tsiklda bitta iteratsiya bo'lmasligi sababli, biz darhol chiqamiz, chunki yolg'on:
For va For-Each Loop: men qanday takrorlaganim, takrorlaganim, lekin takrorlamaganim haqidagi ertak - 2
Tugatish bayonotidagi iboraga e'tibor qaratish kerak : bu sizning ilovangizda cheksiz tsikllarga ega bo'lishini bevosita aniqlaydi. O'sish - bu eng oddiy ifoda. U tsiklning har bir muvaffaqiyatli iteratsiyasidan keyin bajariladi. Va bu ifodani o'tkazib yuborish ham mumkin. Masalan:
int outerVar = 0;
for (;outerVar < 10;) {
	outerVar += 2;
	System.out.println("Value = " + outerVar);
}
Misoldan ko'rinib turibdiki, tsiklning har bir iteratsiyasi biz 2 ga oshib boramiz, lekin qiymat outerVar10 dan kichik bo'lgandagina. Bundan tashqari, o'sish bayonotidagi ifoda aslida shunchaki ifoda bo'lgani uchun, u har qanday narsani o'z ichiga olishi mumkin. Shuning uchun, hech kim o'sish o'rniga kamayishdan foydalanishni taqiqlamaydi, ya'ni. qiymatini kamaytirish. Siz har doim o'sishning yozilishini kuzatishingiz kerak. +=avval ortishni, keyin esa topshiriqni bajaradi, lekin agar yuqoridagi misolda biz buning aksini yozsak, biz cheksiz tsiklga ega bo'lamiz, chunki o'zgaruvchi outerVarhech qachon o'zgartirilgan qiymatni olmaydi: bu holda u =+topshiriqdan keyin hisoblab chiqiladi. Aytgancha, ko'rish o'sishi bilan ham xuddi shunday ++. Masalan, bizda pastadir bor edi:
String[] names = {"John","Sara","Jack"};
for (int i = 0; i < names.length; ++i) {
	System.out.println(names[i]);
}
Tsikl ishladi va hech qanday muammo yo'q edi. Ammo keyin refaktorchi keldi. U o'sishni tushunmadi va shunchaki shunday qildi:
String[] names = {"John","Sara","Jack"};
for (int i = 0; i < names.length;) {
	System.out.println(names[++i]);
}
Agar o'sish belgisi qiymat oldida paydo bo'lsa, demak, u avval ko'payadi va keyin ko'rsatilgan joyga qaytadi. Ushbu misolda biz darhol birinchisini o'tkazib yuborgan holda 1-indeksdagi elementni massivdan ajratib olishni boshlaymiz. Va keyin 3-indeksda biz " java.lang.ArrayIndexOutOfBoundsException " xatosi bilan qulab tushamiz . Siz taxmin qilganingizdek, bu faqat oldin ishlagan, chunki o'sish iteratsiya tugagandan so'ng chaqiriladi. Ushbu ifodani iteratsiyaga o'tkazishda hamma narsa buzildi. Ma'lum bo'lishicha, hatto oddiy tsiklda ham tartibsizlik qilishingiz mumkin) Agar sizda massiv bo'lsa, ehtimol barcha elementlarni ko'rsatishning osonroq yo'li bormi?
For va For-Each Loop: men qanday takrorlaganim, takrorlaganim, lekin takrorlamaganim haqidagi ertak - 3

Har bir tsikl uchun

Java 1.5 dan boshlab, Java ishlab chiquvchilari bizga Oracle saytida tasvirlangan " Har bir Loop " yoki 1.5.0for each loop versiyasi uchun qo'llanmada tasvirlangan dizaynni taqdim etdilar . Umuman olganda, u quyidagicha ko'rinadi:
For va For-Each Loop: men qanday takrorlaganim, takrorlaganim, lekin takrorlamaganim haqidagi ertak - 4
Sehrli emasligiga ishonch hosil qilish uchun ushbu konstruksiya tavsifini Java tili spetsifikatsiyasida (JLS) o‘qishingiz mumkin. Ushbu qurilish " 14.14.2. Bayonot uchun kengaytirilgan " bobida tasvirlangan . Ko'rib turganingizdek, for har bir sikl massivlar va java.lang.Iterable interfeysini amalga oshiradiganlar bilan ishlatilishi mumkin . Ya'ni, agar chindan ham xohlasangiz, java.lang.Iterable interfeysini amalga oshirishingiz mumkin va har bir tsikl uchun sinfingiz bilan foydalanish mumkin. Siz darhol shunday deysiz: "Yaxshi, bu takrorlanadigan ob'ekt, lekin massiv ob'ekt emas. Bir xil." Va siz xato qilasiz, chunki ... Java-da massivlar dinamik ravishda yaratilgan ob'ektlardir. Til spetsifikatsiyasi bizga buni aytadi: " Java dasturlash tilida massivlar ob'ektlardir ." Umuman olganda, massivlar biroz JVM sehridir, chunki... massivning ichki tuzilishi noma'lum va Java virtual mashinasi ichida bir joyda joylashgan. Har bir qiziquvchi stackoverflow bo'yicha javoblarni o'qishi mumkin: " Javada massiv sinfi qanday ishlaydi? " Ma'lum bo'lishicha, agar biz massivdan foydalanmasak, unda Iterable ni amalga oshiradigan narsadan foydalanishimiz kerak . Masalan:
List<String> names = Arrays.asList("John", "Sara", "Jack");
for (String name : names) {
	System.out.println("Name = " + name);
}
Bu erda shuni esda tutishingiz mumkinki, agar biz to'plamlardan foydalansak ( java.util.Collection ), buning natijasida biz aynan Iterable ga erishamiz . Agar ob'ektda Iterable-ni amalga oshiradigan sinf mavjud bo'lsa, u iterator usuli chaqirilganda ushbu ob'ektning mazmunini takrorlaydigan Iteratorni ta'minlashi shart. Yuqoridagi kod, masalan, bayt-kodga ega bo'ladi (IntelliJ Idea-da siz "Ko'rish" -> "Bayt kodini ko'rsatish" ni bajarishingiz mumkin:
For va For-Each Loop: men qanday takrorlaganim, takrorlaganim, lekin takrorlamaganim haqidagi hikoya - 5
Ko'rib turganingizdek, iterator aslida ishlatiladi. Agar bu har bir tsikl uchun bo'lmasa , biz shunday yozishimiz kerak edi:
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

Yuqorida ko'rganimizdek, Iterable interfeysi ba'zi bir ob'ekt misollari uchun siz tarkibni takrorlashingiz mumkin bo'lgan iteratorni olishingiz mumkinligini aytadi. Shunga qaramay, buni SOLID dan Yagona javobgarlik printsipi deb aytish mumkin . Ma'lumotlar strukturasining o'zi o'tishni boshqarmasligi kerak, lekin u kerak bo'lgan narsani ta'minlay oladi. Iteratorning asosiy amalga oshirilishi shundaki, u odatda tashqi sinf tarkibiga kirish huquqiga ega bo'lgan va tashqi sinf tarkibidagi kerakli elementni ta'minlaydigan ichki sinf sifatida e'lon qilinadi. ArrayListBu erda iterator elementni qanday qaytarishi haqidagi sinfdan misol :
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];
}
Ko'rib turganimizdek, iterator yordamida ArrayList.thistashqi sinfga va uning o'zgaruvchisiga kiradi elementDatava u erdan elementni qaytaradi. Shunday qilib, iteratorni olish juda oddiy:
List<String> names = Arrays.asList("John", "Sara", "Jack");
Iterator<String> iterator = names.iterator();
Uning ishi shundan kelib chiqadiki, biz elementlarning mavjudligini tekshirishimiz mumkin ( hasNext usuli ), keyingi elementni olish ( keyingi usul ) va keyingi orqali olingan oxirgi elementni olib tashlaydigan olib tashlash usuli . O'chirish usuli ixtiyoriy va amalga oshirilishi kafolatlanmaydi. Aslida, Java rivojlanishi bilan interfeyslar ham rivojlanadi. Shu sababli, Java 8 da iterator tashrif buyurmagan qolgan elementlarda ba'zi harakatlarni bajarishga imkon beruvchi usul ham mavjud edi . Iterator va to'plamlar haqida nima qiziq? Masalan, sinf mavjud . Bu va ning ota-onasi bo'lgan mavhum sinfdir . Va modCount kabi soha tufayli biz uchun qiziq . Har bir o'zgarish ro'yxat mazmunini o'zgartiradi. Xo'sh, buning bizga nima ahamiyati bor? Va iterator ish paytida u takrorlangan to'plam o'zgarmasligiga ishonch hosil qiladi. Siz tushunganingizdek, ro'yxatlar uchun iteratorni amalga oshirish modcount bilan bir joyda , ya'ni sinfda joylashgan . Keling, oddiy misolni ko'rib chiqaylik: forEachRemainingAbstractListArrayListLinkedListAbstractList
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());
Mavzu bo'yicha bo'lmasa-da, birinchi qiziqarli narsa. Aslida Arrays.asListo'zining maxsus qismini qaytaradi ArrayList( java.util.Arrays.ArrayList ). U qo'shish usullarini qo'llamaydi, shuning uchun uni o'zgartirib bo'lmaydi. Bu haqida JavaDoc da yozilgan: fixed-size . Lekin, aslida, bu belgilangan o'lchamdan ko'proq . U ham o'zgarmas , ya'ni o'zgarmasdir; olib tashlash ham unda ishlamaydi. Biz ham xatoga duch kelamiz, chunki... Iteratorni yaratganimizdan so'ng, biz undagi modcountni esladik . Keyin biz to'plamning holatini "tashqi" (ya'ni, iterator orqali emas) o'zgartirdik va iterator usulini bajardik. Shuning uchun biz xatoni olamiz: java.util.ConcurrentModificationException . Bunga yo'l qo'ymaslik uchun iteratsiya paytidagi o'zgartirish to'plamga kirish orqali emas, balki iteratorning o'zi orqali amalga oshirilishi kerak:
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());
Siz tushunganingizdek, agar iterator.remove()siz buni oldin qilmagan bo'lsangiz iterator.next(), demak, chunki. iterator hech qanday elementga ishora qilmaydi, keyin biz xatoga yo'l qo'yamiz. Misolda, iterator Jon elementiga o'tadi , uni olib tashlaydi va keyin Sara elementini oladi . Va bu erda hamma narsa yaxshi bo'lar edi, lekin omadsizlik, yana "nyuanslar" bor) java.util.ConcurrentModificationException faqat true qiymatinihasNext() qaytarganda paydo bo'ladi . Ya'ni, agar siz oxirgi elementni to'plamning o'zi orqali o'chirsangiz, iterator tushmaydi. Batafsil ma’lumot uchun “ #ITsubbotnik JAVA: Java boshqotirmalari” bo‘limidagi Java boshqotirmalari haqidagi hisobotni tomosha qilgan ma’qul . Biz shunday batafsil suhbatni oddiy sababga ko'ra boshladikki, xuddi shu nuanslar qachon qo'llaniladi ... Bizning sevimli iteratorimiz qopqoq ostida ishlatiladi. Va bu nuancelarning barchasi u erda ham qo'llaniladi. Bitta narsa shundaki, biz iteratorga kira olmaymiz va elementni xavfsiz olib tashlay olmaymiz. Aytgancha, siz tushunganingizdek, davlat iterator yaratilgan paytda esga olinadi. Va xavfsiz o'chirish faqat chaqirilgan joyda ishlaydi. Ya'ni, bu variant ishlamaydi: for each loop
Iterator<String> iterator1 = names.iterator();
Iterator<String> iterator2 = names.iterator();
iterator1.next();
iterator1.remove();
System.out.println(iterator2.next());
Chunki iterator2 uchun iterator1 orqali o'chirish "tashqi" edi, ya'ni u tashqarida bir joyda amalga oshirildi va u bu haqda hech narsa bilmaydi. Iteratorlar mavzusida men buni ham ta'kidlamoqchiman. Maxsus kengaytirilgan iterator interfeyslarni amalga oshirish uchun maxsus yaratilgan List. Va unga ism qo'yishdi ListIterator. Bu nafaqat oldinga, balki orqaga ham harakat qilish imkonini beradi, shuningdek, oldingi va keyingi elementning indeksini aniqlashga imkon beradi. Bunga qo'shimcha ravishda, u joriy elementni almashtirish yoki joriy iterator pozitsiyasi va keyingisi o'rtasidagi joyga yangisini qo'shish imkonini beradi. Siz taxmin qilganingizdek, ListIteratorbuni qilishga ruxsat berilgan, chunki Listindeks bo'yicha kirish amalga oshirilgan.
For va For-Each Loop: men qanday takrorlaganim, takrorlaganim, lekin takrorlamaganim haqidagi ertak - 6

Java 8 va iteratsiya

Java 8-ning chiqarilishi ko'pchilikning hayotini osonlashtirdi. Shuningdek, biz ob'ektlarning mazmuni bo'yicha iteratsiyani e'tiborsiz qoldirmadik. Bu qanday ishlashini tushunish uchun siz bu haqda bir necha so'z aytishingiz kerak. Java 8 java.util.function.Consumer sinfini taqdim etdi . Mana bir misol:
Consumer consumer = new Consumer() {
	@Override
	public void accept(Object o) {
		System.out.println(o);
	}
};
Iste'molchi - bu funktsional interfeys, ya'ni interfeys ichida faqat 1 ta bajarilmagan mavhum usul mavjud bo'lib, u ushbu interfeysning vositalarini belgilaydigan sinflarda majburiy amalga oshirilishini talab qiladi. Bu lambda kabi sehrli narsadan foydalanishga imkon beradi. Ushbu maqola bu haqda emas, lekin biz uni nima uchun ishlatishimiz mumkinligini tushunishimiz kerak. Shunday qilib, lambdalardan foydalangan holda, yuqoridagi Consumer quyidagicha qayta yozilishi mumkin: Consumer consumer = (obj) -> System.out.println(obj); Bu Java obj deb nomlangan narsa kirishga uzatilishini ko'radi va keyin -> dan keyingi ifoda ushbu obj uchun bajariladi. Iteratsiyaga kelsak, endi buni qilishimiz mumkin:
List<String> names = Arrays.asList("John", "Sara", "Jack");
Consumer consumer = (obj) -> System.out.println(obj);
names.forEach(consumer);
Agar siz usulga o'tsangiz forEach, hamma narsa aqldan ozganini ko'rasiz. Mana bizning eng sevimlimiz for-each loop:
default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
}
Iterator yordamida elementni chiroyli tarzda olib tashlash ham mumkin, masalan:
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Predicate predicate = (obj) -> obj.equals("John");
names.removeIf(predicate);
Bunday holda, removeIf usuli kirish sifatida Consumer ni emas , balki Predikatni oladi . Bu booleanni qaytaradi . Bunday holda, agar predikat " to'g'ri " deb aytsa, u holda element o'chiriladi. Qizig'i shundaki, bu erda ham hamma narsa aniq emas)) Xo'sh, nima xohlaysiz? Konferentsiyada boshqotirmalarni yaratish uchun odamlarga joy berish kerak. Misol uchun, iterator ba'zi iteratsiyadan keyin erisha oladigan barcha narsalarni o'chirish uchun quyidagi kodni olaylik:
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);
OK, bu erda hamma narsa ishlaydi. Ammo biz Java 8-ni eslaymiz. Shuning uchun, kodni soddalashtirishga harakat qilaylik:
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);
Haqiqatan ham u yanada chiroyli bo'lib qoldimi? Biroq, java.lang.IllegalStateException bo'ladi . Buning sababi esa... Java’dagi xato. Ma'lum bo'lishicha, u tuzatilgan, ammo JDK 9 da. Mana OpenJDK-dagi vazifaga havola: Iterator.forEachRemaining va boshqalar. Iterator.remove . Tabiiyki, bu allaqachon muhokama qilingan: Nima uchun iterator.forEachRemaining Consumer lambda elementini olib tashlamaydi? Boshqa yo'l to'g'ridan-to'g'ri Stream API orqali:
List<String> names = new ArrayList(Arrays.asList("John", "Sara", "Jack"));
Stream<String> stream = names.stream();
stream.forEach(obj -> System.out.println(obj));

xulosalar

Yuqoridagi barcha materiallardan ko'rganimizdek, halqa for-each loopfaqat iterator tepasida joylashgan "sintaktik shakar" dir. Biroq, u hozir ko'p joylarda qo'llaniladi. Bundan tashqari, har qanday mahsulotni ehtiyotkorlik bilan ishlatish kerak. Misol uchun, zararsiz kishi forEachRemainingyoqimsiz kutilmagan hodisalarni yashirishi mumkin. Va bu birlik sinovlari zarurligini yana bir bor isbotlaydi. Yaxshi test sizning kodingizda bunday foydalanish holatini aniqlashi mumkin. Mavzu bo'yicha nimani ko'rishingiz/o'qishingiz mumkin: #Viacheslav
Izohlar
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION