JavaRush /Java блогы /Random-KK /For және For-Each циклі: мен қалай қайталағаным, қайталан...
Viacheslav
Деңгей

For және For-Each циклі: мен қалай қайталағаным, қайталанғаным, бірақ қайталанбағаным туралы әңгіме

Топта жарияланған

Кіріспе

Циклдер программалау тілдерінің негізгі құрылымдарының бірі болып табылады. Мысалы, Oracle веб-сайтында « Сабақ: Тіл негіздері » бөлімі бар , онда циклдарда « The for Statement » жеке сабағы бар. Негіздерді жаңартайық: Цикл үш өрнектен (мәлімдемеден) тұрады: инициализация (инициализация), шарт (тоқтату) және өсу (өсу):
For және For-Each циклі: қалай қайталағаным, қайталағаным, бірақ қайталамағаным туралы әңгіме - 1
Бір қызығы, олардың барлығы міндетті емес, яғни біз қаласақ, жаза аламыз:
for (;;){
}
Рас, бұл жағдайда біз шексіз циклды аламыз, өйткені Біз циклден (тоқтату) шығу шартын көрсетпейміз. Баптандыру өрнегі бүкіл цикл орындалғанға дейін бір рет орындалады. Циклдың өз ауқымы бар екенін әрқашан есте ұстаған жөн. Бұл инициализация , аяқтау , өсу және цикл денесі бірдей айнымалыларды көретінін білдіреді . Бұйра жақшаларды қолдану арқылы ауқымды анықтау әрқашан оңай. Кронштейндердің ішіндегінің барлығы жақшаның сыртында көрінбейді, бірақ жақшаның сыртындағының бәрі жақшаның ішінде көрінеді. Инициализация жай ғана өрнек. Мысалы, айнымалыны инициализациялаудың орнына, әдетте ештеңе қайтармайтын әдісті шақыруға болады. Немесе бірінші нүктелі үтірдің алдында бос орын қалдырып, оны өткізіп жіберіңіз. Келесі өрнек тоқтату шартын көрсетеді . Бұл true болғанша , цикл орындалады. Ал егер false болса , жаңа итерация басталмайды. Төмендегі суретті қарасаңыз, біз компиляция кезінде қатені аламыз және IDE шағымданады: циклдегі біздің өрнекке қол жеткізу мүмкін емес. Бізде циклде бір итерация болмайтындықтан, біз бірден шығамыз, өйткені жалған:
For және For-Each циклі: қалай қайталағаным, қайталағаным, бірақ қайталамағаным туралы әңгіме - 2
Аяқтау мәлімдемесіндегі өрнекке назар аударған жөн : ол сіздің қолданбаңызда шексіз циклдар болатынын тікелей анықтайды. Өсу - ең қарапайым өрнек. Ол циклдің әрбір сәтті итерациясынан кейін орындалады. Және бұл өрнекті өткізіп жіберуге де болады. Мысалы:
int outerVar = 0;
for (;outerVar < 10;) {
	outerVar += 2;
	System.out.println("Value = " + outerVar);
}
Мысалдан көріп отырғаныңыздай, циклдің әрбір итерациясын 2 қадаммен көбейтеміз, бірақ мән outerVar10-нан аз болғанша ғана. Сонымен қатар, өсу операторындағы өрнек шын мәнінде жай өрнек болғандықтан, ол кез келген нәрсені қамтуы мүмкін. Сондықтан өсудің орнына азайтуды қолдануға ешкім тыйым салмайды, т.б. құнды азайту. Сіз әрқашан өсудің жазылуын қадағалауыңыз керек. +=алдымен ұлғайтуды, содан кейін тапсырманы орындайды, бірақ жоғарыдағы мысалда керісінше жазсақ, біз шексіз цикл аламыз, өйткені айнымалы outerVarешқашан өзгертілген мәнді алмайды: бұл жағдайда ол =+тапсырмадан кейін есептеледі. Айтпақшы, бұл көру қадамдарымен бірдей ++. Мысалы, бізде цикл болды:
String[] names = {"John","Sara","Jack"};
for (int i = 0; i < names.length; ++i) {
	System.out.println(names[i]);
}
Цикл жұмыс істеді және ешқандай проблемалар болмады. Бірақ содан кейін рефакторинг келді. Ол өсуді түсінбеді және жай ғана осылай жасады:
String[] names = {"John","Sara","Jack"};
for (int i = 0; i < names.length;) {
	System.out.println(names[++i]);
}
Егер өсу белгісі мәннің алдында пайда болса, бұл оның алдымен өсетінін, содан кейін ол көрсетілген жерге оралатынын білдіреді. Бұл мысалда біз біріншісін өткізіп жіберіп, массивтен 1-индекстегі элементті шығаруды бірден бастаймыз. Содан кейін 3 индексінде біз " java.lang.ArrayIndexOutOfBoundsException " қатесіне ұшыраймыз . Сіз болжағандай, бұл өсім итерация аяқталғаннан кейін шақырылғандықтан бұрын жұмыс істеді. Бұл өрнекті итерацияға көшіру кезінде бәрі бұзылды. Белгілі болғандай, тіпті қарапайым циклде сіз тәртіпсіздік жасай аласыз) Егер сізде массив болса, барлық элементтерді көрсетудің оңай жолы бар шығар?
For және For-Each циклі: қалай қайталағаным, қайталағаным, бірақ қайталамағаным туралы әңгіме - 3

Әрбір цикл үшін

Java 1.5 нұсқасынан бастап, Java әзірлеушілері бізге Oracle сайтында « Әрбір цикл үшін » немесе 1.5.0for each loop нұсқасы үшін нұсқаулықта сипатталған дизайнды берді . Жалпы, ол келесідей болады:
For және For-Each циклі: қалай қайталағаным, қайталағаным, бірақ қайталамағаным туралы әңгіме - 4
Бұл құрылымның сиқырлы емес екеніне көз жеткізу үшін Java тілінің сипаттамасынан (JLS) оның сипаттамасын оқуға болады. Бұл құрылыс « 14.14.2. Жетілдірілген мәлімдеме » тарауында сипатталған . Көріп отырғаныңыздай, for әрбір циклды массивтермен және java.lang.Iterable интерфейсін жүзеге асыратындармен пайдалануға болады . Яғни, егер сіз шынымен қаласаңыз, java.lang.Iterable интерфейсін жүзеге асыруға болады және әрбір цикл үшін сыныппен бірге пайдалануға болады. Сіз бірден: "Жарайды, бұл қайталанатын нысан, бірақ массив an object емес. Сұрыптау" деп айтасыз. Ал сіз қателесесіз, өйткені... Java тілінде массивтер динамикалық түрде жасалған нысандар болып табылады. Тіл спецификациясы бізге мынаны айтады: « Java бағдарламалау тілінде массивтер an objectілер болып табылады .» Жалпы алғанда, массивтер JVM сиқырының біразы, өйткені... массивтің ішкі құрылымы белгісіз және Java виртуалды машинасының ішінде бір жерде орналасқан. Кез келген қызығушылық танытқан адам stackoverflow бойынша жауаптарды оқи алады: " Java-да массив класы қалай жұмыс істейді? " Егер біз массивті пайдаланbyteын болсақ, онда біз Iterable іске асыратын нәрсені пайдалануымыз керек екен . Мысалы:
List<String> names = Arrays.asList("John", "Sara", "Jack");
for (String name : names) {
	System.out.println("Name = " + name);
}
Бұл жерде сіз жинақтарды ( java.util.Collection ) қолданатын болсақ , соның арқасында біз дәл Итеративті аламыз . Егер нысанда Iterable орындайтын сынып болса, ол итератор әдісі шақырылғанда, осы нысанның мазмұны бойынша қайталанатын Итераторды қамтамасыз етуге міндетті. Жоғарыдағы codeта, мысалы, осындай byte-code болады (IntelliJ Idea бағдарламасында сіз «Көру» -> «Байт-codeты көрсету» әрекетін орындай аласыз:
For және For-Each циклі: қалай қайталағаным, қайталағаным, бірақ қайталамағаным туралы әңгіме - 5
Көріп отырғаныңыздай, итератор шын мәнінде пайдаланылады. Егер ол әрбір цикл үшін болмаса , біз келесідей нәрсені жазуымыз керек еді:
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);
}

Итератор

Жоғарыда көргеніміздей, қайталанатын интерфейс кейбір нысанның даналары үшін мазмұнды қайталауға болатын итераторды алуға болатынын айтады. Тағы да, бұл SOLID- тің Бірыңғай жауапкершілік қағидасы деп айтуға болады . Деректер құрылымының өзі қозғалысты қозғамауы керек, бірақ ол қажет болатынын қамтамасыз ете алады. Итератордың негізгі іске асуы , ол әдетте сыртқы сыныптың мазмұнына қол жеткізетін және сыртқы сыныпта қамтылған қажетті элементті қамтамасыз ететін ішкі сынып ретінде жарияланады. ArrayListМіне, итератор элементті қалай қайтаратынының сыныбынан мысал :
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];
}
Көріп отырғанымыздай, итератордың көмегімен ArrayList.thisсыртқы сыныпқа және оның айнымалысына қол жеткізеді elementData, содан кейін сол жерден элементті қайтарады. Сонымен, итераторды алу өте қарапайым:
List<String> names = Arrays.asList("John", "Sara", "Jack");
Iterator<String> iterator = names.iterator();
Оның жұмысы одан әрі элементтердің бар-жоғын тексеруге ( hasNext әдісі ), келесі элементті алуға (келесі әдіс ) және келесі арқылы алынған соңғы элементті алып тастайтын жою әдісіне байланысты . Жою әдісі міндетті емес және оның орындалуына кепілдік берілмейді. Шындығында, Java дамыған сайын интерфейстер де дамиды. Сондықтан, Java 8-де итератор кірмеген қалған элементтерде кейбір әрекеттерді орындауға мүмкіндік беретін әдіс болды . Итератор мен жинақтар туралы не қызық? Мысалы, сынып бар . Бұл абстрактілі класс, ол және ата-анасы болып табылады . Бұл бізге modCount сияқты өрістің арқасында қызықты . Әрбір өзгерту тізімнің мазмұнын өзгертеді. Сонда оның бізге не қатысы бар? Итератор жұмыс кезінде қайталанатын коллекцияның өзгермейтініне көз жеткізуі. Түсінгеніңіздей, тізімдер үшін итераторды іске асыру modcount сияқты бір жерде , яғни сыныпта орналасқан . Қарапайым мысалды қарастырайық: 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());
Міне, тақырып бойынша болмаса да, бірінші қызықты нәрсе. Іс жүзінде Arrays.asListөзінің арнайы нұсқасын қайтарады ArrayList( java.util.Arrays.ArrayList ). Ол қосу әдістерін жүзеге асырмайды, сондықтан оны өзгерту мүмкін емес. Бұл туралы JavaDoc-та жазылған: fixed-size . Бірақ шын мәнінде, бұл бекітілген өлшемнен көп . Ол сондай-ақ өзгермейтін , яғни өзгермейтін; жою оған да жұмыс істемейді. Біз де қате аламыз, себебі... Итераторды жасағаннан кейін біз ондағы модконтты еске түсірдік . Содан кейін біз жинақтың күйін «сырттай» өзгерттік (яғни, итератор арқылы емес) және итератор әдісін орындадық. Сондықтан, біз қатені аламыз: java.util.ConcurrentModificationException . Бұған жол бермеу үшін итерация кезінде өзгерту жинаққа кіру арқылы емес, итератордың өзі арқылы орындалуы керек:
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());
Түсінгеніңіздей, егер iterator.remove()сіз мұны бұрын жасамасаңыз iterator.next(), онда себебі. итератор ешбір элементті көрсетпейді, онда біз қатені аламыз. Мысалда итератор Джон элементіне өтіп , оны алып тастайды, содан кейін Sara элементін алады . Бұл жерде бәрі жақсы болар еді, бірақ сәттілік жоқ, тағы да «нюанстар» бар) java.util.ConcurrentModificationException тек true мәнінhasNext() қайтарған кезде пайда болады . Яғни, коллекцияның өзі арқылы соңғы элементті жойсаңыз, итератор құламайды. Толығырақ мәлімет алу үшін « #ITsubbotnik JAVA бөлімінен Java басқатырғыштары туралы репортажды қараңыз : Java басқатырғыштары ». Біз осындай егжей-тегжейлі әңгімені қарапайым себеппен бастадық, себебі дәл сол нюанстар ... Біздің сүйікті итератор сорғыштың астында қолданылады. Және бұл нюанстар сонда да қолданылады. Жалғыз нәрсе, біз итераторға қол жеткізе алмаймыз және элементті қауіпсіз алып тастай алмаймыз. Айтпақшы, сіз түсінгендей, күй итератор жасалған сәтте есте қалады. Ал қауіпсіз жою тек шақырылған жерде жұмыс істейді. Яғни, бұл опция жұмыс істемейді: for each loop
Iterator<String> iterator1 = names.iterator();
Iterator<String> iterator2 = names.iterator();
iterator1.next();
iterator1.remove();
System.out.println(iterator2.next());
Өйткені iterator2 үшін iterator1 арқылы жою «сыртқы» болды, яғни ол сыртта орындалды және ол бұл туралы ештеңе білмейді. Итераторлар тақырыбында мен мұны да атап өткім келеді. Арнайы, кеңейтілген итератор интерфейсті іске асыру үшін арнайы жасалған List. Және олар оны атады ListIterator. Ол алға ғана емес, артқа да жылжуға мүмкіндік береді, сонымен қатар алдыңғы және келесі элементтің индексін білуге ​​мүмкіндік береді. Бұған қоса, ол ағымдағы итератор орны мен келесісі арасындағы орынға ағымдағы элементті ауыстыруға немесе жаңасын енгізуге мүмкіндік береді. Сіз ойлағандай, индекс бойынша қол жеткізу жүзеге асырылғандықтан ListIterator, мұны істеуге рұқсат етіледі .List
For және For-Each циклі: қалай қайталағаным, қайталағаным, бірақ қайталамағаным туралы ертегі - 6

Java 8 және итерация

Java 8 нұсқасының шығарылуы көптеген адамдардың өмірін жеңілдетті. Біз сондай-ақ нысандардың мазмұны бойынша итерацияны елемеппіз. Бұл қалай жұмыс істейтінін түсіну үшін бұл туралы бірнеше сөз айту керек. Java 8 java.util.function.Consumer сыныбын ұсынды . Міне, мысал:
Consumer consumer = new Consumer() {
	@Override
	public void accept(Object o) {
		System.out.println(o);
	}
};
Тұтынушы – бұл функционалды интерфейс, яғни интерфейс ішінде осы интерфейстің инструменттерін көрсететін сыныптарда міндетті түрде енгізуді талап ететін тек 1 ғана іске асырылмаған абстрактілі әдіс бар. Бұл ламбда сияқты сиқырлы нәрсені пайдалануға мүмкіндік береді. Бұл мақала бұл туралы емес, бірақ біз оны не үшін пайдалана алатынымызды түсінуіміз керек. Сонымен, ламбдаларды пайдалана отырып, жоғарыдағы Тұтынушыны келесідей қайта жазуға болады: Consumer consumer = (obj) -> System.out.println(obj); Бұл Java кіріске obj деп аталатын нәрсе жіберілетінін көреді, содан кейін -> кейін өрнек осы an object үшін орындалады. Итерацияға келетін болсақ, енді біз мұны істей аламыз:
List<String> names = Arrays.asList("John", "Sara", "Jack");
Consumer consumer = (obj) -> System.out.println(obj);
names.forEach(consumer);
Егер сіз әдіске барсаңыз forEach, бәрі өте қарапайым екенін көресіз. Міне, біздің сүйіктіміз for-each loop:
default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
}
Сондай-ақ, итератордың көмегімен элементті әдемі түрде жоюға болады, мысалы:
List<String> names = Arrays.asList("John", "Sara", "Jack");
names = new ArrayList(names);
Predicate predicate = (obj) -> obj.equals("John");
names.removeIf(predicate);
Бұл жағдайда removeIf әдісі кіріс ретінде Consumer емес , Предикатты қабылдайды . Ол логикалық қайтарады . Бұл жағдайда, егер предикат « шын » десе, онда элемент жойылады. Бір қызығы, мұнда да бәрі анық емес)) Ал, не қалайсыз? Конференцияда басқатырғыштар жасау үшін адамдарға орын беру керек. Мысалы, итератор біраз итерациядан кейін қол жеткізе алатын барлық нәрсені жою үшін келесі codeты алайық:
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);
Жарайды, мұнда бәрі жұмыс істейді. Бірақ біз Java 8 екенін есте ұстаймыз. Сондықтан codeты жеңілдетуге тырысайық:
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);
Ол шынымен әдемі болды ма? Дегенмен, java.lang.IllegalStateException болады . Оның себебі... Java тіліндегі қате. Ол түзетілген, бірақ JDK 9-да. Мұнда OpenJDK-дегі тапсырманың сілтемесі берілген: Iterator.forEachRemaining vs. Iterator.remove . Әрине, бұл бұрыннан талқыланған: неге iterator.forEachRemaining тұтынушы ламбдасындағы элементті жоймайды? Басқа әдіс Stream API арқылы тікелей:
List<String> names = new ArrayList(Arrays.asList("John", "Sara", "Jack"));
Stream<String> stream = names.stream();
stream.forEach(obj -> System.out.println(obj));

қорытындылар

Жоғарыдағы барлық материалдан көргеніміздей, цикл for-each loopитератордың үстіндегі «синтаксистік қант» болып табылады. Дегенмен, ол қазір көптеген жерлерде қолданылады. Сонымен қатар, кез келген өнімді сақтықпен пайдалану керек. Мысалы, зиянсыз адам forEachRemainingжағымсыз тосынсыйларды жасыра алады. Бұл тағы бір рет бірлік сынақтары қажет екенін дәлелдейді. Жақсы сынақ сіздің codeыңызда мұндай пайдалану жағдайын анықтай алады. Тақырып бойынша не көруге/оқуға болады: #Вячеслав
Пікірлер
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION