JavaRush /Java Blogu /Random-AZ /For və For-Each Loop: necə təkrarladığım, təkrarlandığım,...
Viacheslav
Səviyyə

For və For-Each Loop: necə təkrarladığım, təkrarlandığım, lakin təkrarlanmadığım barədə hekayə

Qrupda dərc edilmişdir

Giriş

Döngülər proqramlaşdırma dillərinin əsas strukturlarından biridir. Məsələn, Oracle veb saytında " Dərs: Dil əsasları " bölməsi var, burada döngələrdə ayrıca " The for Statement " dərsi var . Əsasları təzələyək: Döngə üç ifadədən (ifadələrdən) ibarətdir: başlatma (başlanğıclaşdırma), şərt (xitam) və artım (artırma):
For və For-Each Loop: necə təkrarladığım, təkrarladığım, lakin təkrarlamadığım haqqında hekayə - 1
Maraqlıdır ki, onların hamısı isteğe bağlıdır, yəni istəsək yaza bilərik:
for (;;){
}
Doğrudur, bu halda biz sonsuz bir döngə alacağıq, çünki Döngədən çıxmaq (xitam vermə) üçün şərt göstərmirik. Başlanğıc ifadəsi bütün dövrə yerinə yetirilməzdən əvvəl yalnız bir dəfə yerinə yetirilir. Həmişə bir dövrün öz əhatə dairəsi olduğunu xatırlamağa dəyər. Bu o deməkdir ki, başlatma , sonlandırma , artım və döngə gövdəsi eyni dəyişənləri görür. Buruq mötərizələrdən istifadə edərək əhatə dairəsini müəyyən etmək həmişə asandır. Mötərizədə olan hər şey mötərizənin xaricində görünmür, lakin mötərizənin içərisindən kənarda olan hər şey görünür. Initializasiya sadəcə bir ifadədir. Məsələn, dəyişəni işə salmaq əvəzinə, ümumiyyətlə heç nə qaytarmayacaq metodu çağıra bilərsiniz. Və ya sadəcə birinci nöqtəli vergüldən əvvəl boş yer qoyaraq onu keçin. Aşağıdakı ifadə xitam şərtini təyin edir . Nə qədər ki, doğrudur , dövrə icra olunur. Və əgər false , yeni iterasiya başlamayacaq. Aşağıdakı şəklə baxsanız, kompilyasiya zamanı xəta alırıq və IDE şikayət edəcək: dövrədəki ifadəmiz əlçatmazdır. Döngədə tək bir iterasiyamız olmayacağından, dərhal çıxacağıq, çünki yalan:
For və For-Each Loop: necə təkrarladığım, təkrarladığım, lakin təkrar etmədiyim bir nağıl - 2
Sonlandırma ifadəsindəki ifadəyə diqqət yetirməyə dəyər : bu, tətbiqinizdə sonsuz döngələrin olub olmadığını birbaşa müəyyənləşdirir. Artırma ən sadə ifadədir. Döngənin hər uğurlu iterasiyasından sonra yerinə yetirilir. Və bu ifadə də atlana bilər. Misal üçün:
int outerVar = 0;
for (;outerVar < 10;) {
	outerVar += 2;
	System.out.println("Value = " + outerVar);
}
Nümunədən göründüyü kimi, döngənin hər iterasiyasını 2 artımla artıracağıq, ancaq dəyər outerVar10-dan az olduğu müddətcə. Bundan əlavə, artım ifadəsindəki ifadə əslində sadəcə bir ifadə olduğundan, o, hər şeyi ehtiva edə bilər. Buna görə də, heç kim artım əvəzinə azalma istifadə etməyi qadağan etmir, yəni. dəyərini azaltmaq. Siz həmişə artımın yazılmasına nəzarət etməlisiniz. +=əvvəlcə artımı, sonra isə tapşırığı yerinə yetirir, lakin yuxarıdakı misalda bunun əksini yazsaq, sonsuz döngə əldə edəcəyik, çünki dəyişən outerVarheç vaxt dəyişdirilmiş qiyməti almayacaq: bu halda o, =+tapşırıqdan sonra hesablanacaq. Yeri gəlmişkən, görünüş artımları ilə də eynidir ++. Məsələn, bir döngəmiz var idi:
String[] names = {"John","Sara","Jack"};
for (int i = 0; i < names.length; ++i) {
	System.out.println(names[i]);
}
Döngü işlədi və heç bir problem yox idi. Amma sonra refaktor adam gəldi. O, artımı başa düşmədi və sadəcə bunu etdi:
String[] names = {"John","Sara","Jack"};
for (int i = 0; i < names.length;) {
	System.out.println(names[++i]);
}
Əgər artım işarəsi dəyərin qarşısında görünürsə, bu o deməkdir ki, əvvəlcə artacaq, sonra isə göstərilən yerə qayıdacaq. Bu misalda biz dərhal birincini atlayaraq 1-ci indeksdəki elementi massivdən çıxarmağa başlayacağıq. Və sonra 3-cü indeksdə " java.lang.ArrayIndexOutOfBoundsException " xətası ilə qəzaya uğrayacağıq . Təxmin etdiyiniz kimi, bu, sadəcə olaraq, artımın iterasiya tamamlandıqdan sonra çağırıldığı üçün əvvəllər işləyirdi. Bu ifadəni iterasiyaya köçürərkən hər şey pozuldu. Göründüyü kimi, hətta sadə bir döngədə də qarışıqlıq yarada bilərsiniz) Əgər massiviniz varsa, bəlkə bütün elementləri göstərməyin daha asan yolu var?
For və For-Each Loop: necə təkrarladığım, təkrarladığım, lakin təkrar etmədiyim bir nağıl - 3

Hər döngə üçün

for each loopJava 1.5-dən başlayaraq, Java tərtibatçıları bizə Oracle saytında təsvir olunan " The For-Each Loop " və ya 1.5.0 versiyası üçün Bələdçidə təsvir edilmiş dizayn təqdim etdilər . Ümumiyyətlə, bu belə görünəcək:
For və For-Each Loop: necə təkrarladığım, təkrarladığım, lakin təkrar etmədiyim bir nağıl - 4
Sehrli olmadığına əmin olmaq üçün bu konstruksiyanın təsvirini Java Dil Spesifikasiyasında (JLS) oxuya bilərsiniz. Bu konstruksiya " 14.14.2. Bəyanat üçün təkmilləşdirilmiş " fəslində təsvir edilmişdir . Gördüyünüz kimi, for hər bir dövrə massivlərlə və java.lang.Iterable interfeysini həyata keçirənlərlə istifadə oluna bilər . Yəni, həqiqətən istəyirsinizsə, java.lang.Iterable interfeysini həyata keçirə bilərsiniz və hər bir loop üçün sinifinizlə istifadə edilə bilər. Siz dərhal deyəcəksiniz: "Yaxşı, bu təkrarlana bilən obyektdir, lakin massiv obyekt deyil. Bir növ." Və yanılacaqsan, çünki... Java-da massivlər dinamik şəkildə yaradılmış obyektlərdir. Dil spesifikasiyası bizə bunu deyir: “ Java proqramlaşdırma dilində massivlər obyektlərdir .” Ümumiyyətlə, massivlər bir az JVM sehridir, çünki... massivin daxili olaraq necə qurulduğu bilinmir və Java Virtual Maşının içərisində bir yerdə yerləşir. Maraqlanan hər kəs stackoverflow-da cavabları oxuya bilər: " Massiv sinfi Java-da necə işləyir? " Belə çıxır ki, əgər biz massivdən istifadə etmiriksə, onda Iterable tətbiq edən bir şeydən istifadə etməliyik . Misal üçün:
List<String> names = Arrays.asList("John", "Sara", "Jack");
for (String name : names) {
	System.out.println("Name = " + name);
}
Burada sadəcə xatırlaya bilərsiniz ki, əgər biz kolleksiyalardan ( java.util.Collection ) istifadə etsək, bunun sayəsində tam olaraq İterable əldə edirik . Əgər obyektin İterativi həyata keçirən bir sinfi varsa, o, iterator metodu çağırıldıqda həmin obyektin məzmunu üzərində təkrarlanan iteratoru təmin etməyə borcludur. Yuxarıdakı kodun, məsələn, belə bir bayt kodu olacaq (IntelliJ Idea-da siz "Görünüş" -> "Bayt kodunu göstər" edə bilərsiniz:
For və For-Each Loop: necə təkrarladığım, təkrarladığım, lakin təkrar etmədiyim bir nağıl - 5
Gördüyünüz kimi, iterator əslində istifadə olunur. Hər bir döngə üçün olmasaydı , belə bir şey yazmalı olardıq:
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);
}

İterator

Yuxarıda gördüyümüz kimi, Təkrarlanan interfeys deyir ki, bəzi obyektlərin nümunələri üçün məzmunu təkrarlaya biləcəyiniz bir iterator əldə edə bilərsiniz. Yenə də bunun SOLID- dən Vahid Məsuliyyət Prinsipi olduğunu söyləmək olar . Məlumat strukturunun özü keçidi idarə etməməlidir, lakin lazım olanı təmin edə bilər. İteratorun əsas tətbiqi ondan ibarətdir ki, o, adətən xarici sinifin məzmununa çıxışı olan və xarici sinifdə olan istənilən elementi təmin edən daxili sinif kimi elan edilir. ArrayListBudur , iteratorun elementi necə qaytardığına dair sinifdən bir nümunə :
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];
}
Gördüyümüz kimi, ArrayList.thisiteratorun köməyi ilə xarici sinifə və onun dəyişəninə daxil olur elementDatavə oradan elementi qaytarır. Beləliklə, iterator əldə etmək çox sadədir:
List<String> names = Arrays.asList("John", "Sara", "Jack");
Iterator<String> iterator = names.iterator();
Onun işi ondan ibarətdir ki, biz əlavə elementlərin olub-olmadığını yoxlaya bilərik ( hasNext metodu ), növbəti elementi ( növbəti metod ) və növbəti vasitəsilə alınan sonuncu elementi silən çıxarma metodunu əldə edə bilərik . Silinmə üsulu isteğe bağlıdır və həyata keçirilməsinə zəmanət verilmir. Əslində Java inkişaf etdikcə interfeyslər də inkişaf edir. Buna görə də, Java 8-də iteratorun ziyarət etmədiyi qalan elementlər üzərində bəzi hərəkətlər etməyə imkan verən bir üsul da var idi . İterator və kolleksiyalar haqqında nə maraqlıdır? Məsələn, bir sinif var . Bu, və -nin valideyni olan mücərrəd sinifdir . Və modCount kimi bir sahəyə görə bizim üçün maraqlıdır . Hər dəyişiklik siyahının məzmunu dəyişir. Bəs bunun bizə nə əhəmiyyəti var? İteratorun əməliyyat zamanı təkrarlandığı kolleksiyanın dəyişmədiyinə əmin olması faktı. Anladığınız kimi, siyahılar üçün iteratorun tətbiqi modcount ilə eyni yerdə , yəni sinifdə yerləşir . Sadə bir misala baxaq: 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());
Mövzu ilə bağlı olmasa da, ilk maraqlı şey budur. Əslində Arrays.asListöz xüsusi birini qaytarır ArrayList( java.util.Arrays.ArrayList ). Əlavə metodlarını tətbiq etmir, ona görə də dəyişdirilə bilməz. Bu barədə JavaDoc-da yazılmışdır: sabit ölçülü . Ancaq əslində, sabit ölçüdən daha çoxdur . O, həm də dəyişməzdir , yəni dəyişməzdir; silmək də onun üzərində işləməyəcək. Biz də xəta alacağıq, çünki... İteratoru yaratdıqdan sonra onun içindəki modcount'u xatırladıq . Sonra kolleksiyanın vəziyyətini "xarici olaraq" dəyişdirdik (yəni iterator vasitəsilə deyil) və iterator metodunu icra etdik. Beləliklə, biz xətanı alırıq: java.util.ConcurrentModificationException . Bunun qarşısını almaq üçün iterasiya zamanı dəyişiklik kolleksiyaya giriş yolu ilə deyil, iteratorun özü vasitəsilə həyata keçirilməlidir:
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());
Anladığınız kimi, əgər əvvəllər iterator.remove()bunu etməmisinizsə iterator.next(), deməli, çünki. iterator heç bir elementə işarə etmir, onda xəta alacağıq. Nümunədə, iterator Con elementinə gedəcək , onu siləcək və sonra Sara elementini alacaq . Və burada hər şey yaxşı olardı, amma uğursuzluq, yenə də “nüanslar” var) java.util.ConcurrentModificationException yalnız doğruhasNext() olanda baş verəcək . Yəni, sonuncu elementi kolleksiyanın özü vasitəsilə silsəniz, iterator düşməyəcək. Daha ətraflı məlumat üçün “ #ITsubbotnik JAVA Bölməsindən Java tapmacaları haqqında hesabata baxmaq daha yaxşıdır : Java bulmacaları ”. Sadə bir səbəbdən belə ətraflı söhbətə başladıq ki, eyni nüanslar ... Bizim sevimli iterator başlıq altında istifadə olunur. Və bütün bu nüanslar oraya da aiddir. Yeganə odur ki, biz iteratora daxil ola bilməyəcəyik və elementi təhlükəsiz şəkildə silə bilməyəcəyik. Yeri gəlmişkən, başa düşdüyünüz kimi, dövlət iteratorun yaradıldığı anda xatırlanır. Və təhlükəsiz silmə yalnız çağırıldığı yerdə işləyir. Yəni bu seçim işləməyəcək: for each loop
Iterator<String> iterator1 = names.iterator();
Iterator<String> iterator2 = names.iterator();
iterator1.next();
iterator1.remove();
System.out.println(iterator2.next());
Çünki iterator2 üçün iterator1 vasitəsilə silinmə “xarici” idi, yəni hardasa xaricdə həyata keçirilib və onun bu haqda heç nə bilmir. İteratorlar mövzusunda bunu da qeyd etmək istərdim. Xüsusi, genişləndirilmiş iterator xüsusi olaraq interfeys tətbiqləri üçün hazırlanmışdır List. Və onun adını qoydular ListIterator. Bu, yalnız irəli deyil, həm də geriyə doğru hərəkət etməyə imkan verir, həm də əvvəlki və sonrakı elementin indeksini öyrənməyə imkan verir. Bundan əlavə, o, cari elementi əvəz etməyə və ya cari iterator mövqeyi ilə növbəti mövqe arasında yenisini daxil etməyə imkan verir. Təxmin etdiyiniz kimi, indekslə giriş həyata keçirildiyi ListIteratorüçün bunu etməyə icazə verilir .List
For və For-Each Loop: necə təkrarladığım, təkrarladığım, lakin təkrar etmədiyim bir nağıl - 6

Java 8 və İterasiya

Java 8-in buraxılması çoxlarının həyatını asanlaşdırdı. Obyektlərin məzmunu üzərində iterasiyaya da məhəl qoymadıq. Bunun necə işlədiyini başa düşmək üçün bu barədə bir neçə söz söyləmək lazımdır. Java 8 java.util.function.Consumer sinfini təqdim etdi . Budur bir nümunə:
Consumer consumer = new Consumer() {
	@Override
	public void accept(Object o) {
		System.out.println(o);
	}
};
İstehlakçı funksional interfeysdir, yəni interfeysin içərisində bu interfeysin alətlərini təyin edən siniflərdə məcburi tətbiqi tələb edən yalnız 1 həyata keçirilməmiş mücərrəd metod var. Bu, lambda kimi sehrli bir şeydən istifadə etməyə imkan verir. Bu məqalə bununla bağlı deyil, amma niyə istifadə edə biləcəyimizi başa düşməliyik. Beləliklə, lambdalardan istifadə edərək, yuxarıdakı İstehlakçı bu şəkildə yenidən yazıla bilər: Consumer consumer = (obj) -> System.out.println(obj); Bu o deməkdir ki, Java girişə obj adlı bir şeyin ötürüləcəyini görür və sonra bu obyekt üçün ->-dən sonra ifadə yerinə yetiriləcəkdir. İterasiyaya gəldikdə, indi bunu edə bilərik:
List<String> names = Arrays.asList("John", "Sara", "Jack");
Consumer consumer = (obj) -> System.out.println(obj);
names.forEach(consumer);
Metoduna keçsəniz forEach, hər şeyin çox sadə olduğunu görəcəksiniz. Ən sevdiyimiz budur for-each loop:
default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
}
Bir iteratordan istifadə edərək elementi gözəl şəkildə silmək də mümkündür, məsələn:
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Predicate predicate = (obj) -> obj.equals("John");
names.removeIf(predicate);
Bu halda, removeIf metodu giriş kimi İstehlakçı deyil , Predikat qəbul edir . Boolean qaytarır . Bu halda, əgər predikat " doğru " deyirsə, o zaman element silinəcəkdir. Maraqlıdır ki, burada da hər şey aydın deyil)) Yaxşı, nə istəyirsən? Konfransda tapmacalar yaratmaq üçün insanlara yer vermək lazımdır. Məsələn, iteratorun bəzi iterasiyadan sonra əldə edə biləcəyi hər şeyi silmək üçün aşağıdakı kodu götürək:
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);
Yaxşı, burada hər şey işləyir. Ancaq Java 8-i xatırlayırıq. Buna görə kodu sadələşdirməyə çalışaq:
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);
Həqiqətənmi daha da gözəlləşib? Bununla belə, java.lang.IllegalStateException olacaq . Səbəb isə... Java-da bir səhvdir. Məlum oldu ki, o, düzəldilib, lakin JDK 9-da. OpenJDK-da tapşırığa keçid: Iterator.forEachRemaining vs. Iterator.remove . Təbii ki, bu artıq müzakirə olunub: Nə üçün iterator.forEachRemaining İstehlakçı lambdasındakı elementi silmir? Yaxşı, başqa bir yol birbaşa Stream API vasitəsilədir:
List<String> names = new ArrayList(Arrays.asList("John", "Sara", "Jack"));
Stream<String> stream = names.stream();
stream.forEach(obj -> System.out.println(obj));

nəticələr

Yuxarıdakı bütün materiallardan gördüyümüz kimi, bir döngə for-each loopyalnız bir iteratorun üstündəki "sintaktik şəkərdir". Ancaq indi bir çox yerlərdə istifadə olunur. Bundan əlavə, hər hansı bir məhsul ehtiyatla istifadə edilməlidir. Məsələn, zərərsiz olan forEachRemainingxoşagəlməz sürprizləri gizlədə bilər. Və bu, vahid testlərə ehtiyac olduğunu bir daha sübut edir. Yaxşı bir test kodunuzda belə bir istifadə halını müəyyən edə bilər. Mövzu ilə bağlı nələrə baxa/oxuya bilərsiniz: #Viaçeslav
Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION