JavaRush /Java блогу /Random-KY /For and For-Each цикли: мен кантип итерациялаганым, кайта...
Viacheslav
Деңгээл

For and For-Each цикли: мен кантип итерациялаганым, кайталанган, бирок кайталанган эмес.

Группада жарыяланган

Киришүү

Циклдер программалоо тилдеринин негизги структураларынын бири болуп саналат. Мисалы, Oracle веб-сайтында " Сабак: Тилдин негиздери " бөлүмү бар, анда циклдерде " The for Statement " өзүнчө сабак бар. Негиздерди жаңырталы: Цикл үч туюнтмадан (эсептөөдөн) турат: инициализация (инициализация), шарт (токтотуу) жана өсүү (өсүү):
For and For-Each цикли: мен кантип кайталаганым, кайталаганым, бирок кайталанбаганым тууралуу окуя - 1
Кызыгы, алардын баары милдеттүү эмес, демек, биз кааласак, жаза алабыз:
for (;;){
}
Ырас, бул учурда биз чексиз цикл алабыз, анткени Биз циклден чыгуунун (токтотуунун) шартын тактабайбыз. Инициализация туюнтмасы бүт цикл аткарылганга чейин бир гана жолу аткарылат. Циклдин өзүнүн масштабы бар экенин дайыма эстен чыгарбоо керек. Бул инициализация , токтотуу , көбөйтүү жана цикл денеси бир эле өзгөрмөлөрдү көрөрүн билдирет . Тармал кашаалардын жардамы менен масштабды аныктоо дайыма оңой. кашаанын ичиндегилердин баары кашаалардын сыртында көрүнбөйт, бирок кашаалардын сыртындагылардын баары кашаалардын ичинде көрүнүп турат. Инициализация жөн гана туюнтма. Мисалы, өзгөрмөнү инициализациялоонун ордуна, сиз жалпысынан эч нерсе кайтарбай турган ыкманы чакырсаңыз болот. Же биринчи чекиттин алдында бош орун калтырып, аны өткөрүп жибериңиз. Төмөнкү туюнтма токтотуу шартын көрсөтөт . Чындык болсо , цикл аткарылат. Ал эми false болсо , жаңы итерация башталbyte. Эгерде сиз төмөнкү сүрөттү карасаңыз, компиляция учурунда ката алабыз жана IDE даттанат: циклдеги биздин туюнтма жеткorксиз. Биз циклде бир дагы кайталоо болбой тургандыктан, биз дароо чыгабыз, анткени жалган:
For and For-Each цикли: мен кантип кайталаганым, кайталаганым, бирок кайталанбаганым жөнүндөгү жомок - 2
Аяктоо билдирүүсүндөгү туюнтмага көз салып туруу керек : бул сиздин тиркемеңизде чексиз циклдер болобу же жокпу, түздөн-түз аныктайт. Көбөйтүү эң жөнөкөй туюнтма. Ал циклдин ар бир ийгorктүү итерациясынан кийин аткарылат. Жана бул сөз айкашын да өткөрүп жиберсе болот. Мисалы:
int outerVar = 0;
for (;outerVar < 10;) {
	outerVar += 2;
	System.out.println("Value = " + outerVar);
}
Мисалдан көрүнүп тургандай, циклдин ар бир итерациясын 2 кадам менен көбөйтөбүз, бирок мааниси outerVar10дон аз болгондо гана. Мындан тышкары, көбөйтүү операторундагы туюнтма иш жүзүндө жөн гана туюнтма болгондуктан, ал баарын камтышы мүмкүн. Демек, өсүштүн ордуна азайтууну колдонууга эч ким тыюу салbyte, б.а. наркын төмөндөтүү. Сиз ар дайым өсүү жазууну көзөмөлдөө керек. +=адегенде көбөйтүүнү, анан тапшырманы аткарат, бирок жогорудагы мисалда тескерисин жазсак, чексиз цикл алабыз, анткени өзгөрмө outerVarэч качан өзгөртүлгөн маанини алbyte: бул учурда ал =+тапшырмадан кийин эсептелет. Айтмакчы, бул көрүнүш көбөйтүү менен бирдей ++. Мисалы, бизде цикл бар болчу:
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 and For-Each цикли: мен кантип кайталаганым, кайталаганым, бирок кайталанбаганым жөнүндөгү жомок - 3

Ар бир цикл үчүн

Java 1.5тен баштап, Java иштеп чыгуучулары бизге for each loopOracle сайтында сүрөттөлгөн " The For-Each Loop " же 1.5.0 versionсы үчүн колдонмодогу дизайнды беришти . Жалпысынан алганда, бул төмөнкүдөй болот:
For and For-Each цикли: мен кантип кайталаганым, кайталаганым, бирок кайталанбаганым жөнүндөгү жомок - 4
Бул конструкциянын сыйкырдуу эмес экенине ынануу үчүн Java тorнин спецификациясынан (JLS) анын сүрөттөлүшүн окуй аласыз. Бул курулуш " 14.14.2. Өркүндөтүлгөн билдирүү үчүн " бөлүмүндө баяндалган . Көрүнүп тургандай, ар бир циклди массивдер жана java.lang.Iterable интерфейсин ишке ашыргандар менен колдонсо болот . Башкача айтканда, эгер чындап кааласаңыз, java.lang.Iterable интерфейсин ишке ашыра аласыз жана ар бир цикл үчүн классыңыз менен колдонсоңуз болот. Сиз дароо айтасыз: "Макул, бул кайталануучу an object, бирок массив an object эмес. Сорт." Жана сиз жаңылып каласыз, анткени... Java тorнде массивдер динамикалык түрдө түзүлгөн an objectтер. Тилдин спецификациясы бизге мындай дейт: " Java программалоо тorнде массивдер an object болуп саналат ." Жалпысынан алганда, массивдер JVM сыйкырдуу бир аз, анткени... массивдин ички структурасы кандай экени белгисиз жана Java Virtual Machine ичинде бир жерде жайгашкан. Ар бир кызыккан адам stackoverflow боюнча жоопторду окуй алат: " Жавада массив классы кантип иштейт? " Көрсө, биз массивди колдонбосок, анда Iterable ишке ашырган нерсени колдонушубуз керек . Мисалы:
List<String> names = Arrays.asList("John", "Sara", "Jack");
for (String name : names) {
	System.out.println("Name = " + name);
}
Бул жерде сиз коллекцияларды ( java.util.Collection ) колдонсок , ошонун аркасында так Iterable ала турганыбызды эстей аласыз . Эгерде an objectте Iterableди ишке ашырган класс болсо, ал итератор ыкмасы чакырылганда, ошол an objectтин мазмунун кайталай турган Итераторду берүүгө милдеттүү. Жогорудагы code, мисалы, byte codeго ээ болот (IntelliJ Ideaда сиз "Көрүү" -> "Байтcodeду көрсөтүү" кыла аласыз:
For and 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);
}

Итератор

Биз жогоруда көргөндөй, Iterable интерфейси кээ бир an objectтин мисалдары үчүн сиз мазмунду кайталай турган итераторду ала аласыз деп айтылат. Дагы бир жолу, бул SOLIDдин бирдиктүү жоопкерчorк принциби деп айтууга болот . Берorштер структурасы өзү өтүүгө түрткү бербеши керек, бирок аны камсыздай алат. Итератордун негизги ишке ашырылышы бул, адатта, тышкы класстын мазмунуна кирүү мүмкүнчүлүгү бар жана сырткы класста камтылган керектүү элементти камсыз кылган ички класс катары жарыяланат. 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 ). Ал кошуу ыкмаларын ишке ашырbyte, ошондуктан аны өзгөртүү мүмкүн эмес. Бул жөнүндө 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 элементин алат . Бул жерде баары жакшы болмок, бирок ийгorксиздик, дагы эле "нюанстар" бар) java.util.ConcurrentModificationExceptionhasNext() ал true кайтып келгенде гана пайда болот . Башкача айтканда, сиз коллекциянын өзү аркылуу акыркы элементти жок кылсаңыз, итератор түшпөйт. Көбүрөөк маалымат алуу үчүн " #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 and For-Each цикли: мен кантип кайталаганым, кайталаганым, бирок кайталанбаганым жөнүндөгү жомок - 6

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

Java 8дин чыгарылышы көптөгөн адамдардын жашоосун жеңилдетти. Биз ошондой эле an objectтердин мазмуну боюнча итерацияны этибарга алган жокпуз. Бул кантип иштээрин түшүнүү үчүн, бул жөнүндө бир нече сөз айтуу керек. 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 an objectи деп аталган нерсе киргизүүгө өтүп турганын көрөт, андан кийин -> кийинки туюнтма бул 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 керектөөчү ламбдадагы элементти алып салbyte? Дагы бир жолу 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