JavaRush /Java блогу /Random-KY /Java тилиндеги ламбда туюнтмалары боюнча популярдуу. Миса...
Стас Пасинков
Деңгээл
Киев

Java тилиндеги ламбда туюнтмалары боюнча популярдуу. Мисалдар жана тапшырмалар менен. 1-бөлүк

Группада жарыяланган
Бул макала кимге арналган?
  • Java Coreду жакшы билем деп ойлогондор үчүн, бирок Javaдагы ламбда туюнтмалары жөнүндө эч кандай түшүнүгү жок. Же, балким, сиз буга чейин ламбдалар жөнүндө бир нерсе уккандырсыз, бирок чоо-жайы жок.
  • лямбда туюнтмаларын түшүнгөн, бирок дагы эле коркуп, аларды колдонуудан адаттан тыш болгондор үчүн.
Эгер сиз бул категориялардын бирине кирбесеңиз, анда сиз бул макаланы кызыксыз, туура эмес жана жалпысынан “салкын эмес” деп таба аласыз. Бул учурда, же өтүп кетүүгө тартынбаңыз, же эгер сиз теманы жакшы билсеңиз, комментарийлерде макаланы кантип жакшыртсам же толуктайм деп сунуштаңыз. Материал эч кандай академиялык мааниге ээ эмес, жаңылыгы аз. Тескерисинче, тескерисинче: анда мен татаал (айрымдар үчүн) нерселерди мүмкүн болушунча жөнөкөй сүрөттөөгө аракет кылам. Мен агым api тус!нд!ру суранымын жазуга шабыттанды. Мен бул жөнүндө ойлонуп, лямбда туюнтмаларын түшүнбөстөн, "агымдар" жөнүндөгү кээ бир мисалдарым түшүнүксүз болот деп чечтим. Ошентип, ламбдалардан баштайлы. Java тorндеги ламбда туюнтмалары боюнча популярдуу.  Мисалдар жана тапшырмалар менен.  1-1-бөлүкБул макаланы түшүнүү үчүн кандай бorм керек:
  1. Объектке багытталган программалоону (мындан ары - ООП) түшүнүү, атап айтканда:
    • класстар жана an objectтер кандай экенин, алардын ортосунда кандай айырма бар экенин билүү;
    • интерфейстер деген эмне, алар класстардан эмнеси менен айырмаланат, алардын ортосунда кандай байланыш бар (интерфейстер жана класстар);
    • метод деген эмне, аны кантип аташ керек, абстракттуу метод деген эмне (же ишке ашыруусу жок ыкма), методдун параметрлери/аргументтери кандай, аларды кантип ал жакка өткөрүү керектиги жөнүндө билүү;
    • кирүү модификаторлору, статикалык методдор/өзгөрмөлөр, акыркы методдор/өзгөрмөлөр;
    • тукум куучулук (класстар, интерфейстер, интерфейстердин көп мурастоосу).
  2. Java Core бorми: генериктер, жыйнактар ​​(тизмелер), жиптер.
Мейли, баштайлы.

Бир аз тарых

Ламбда туюнтмалары Java тorне функционалдык программалоодон, ал эми математикадан келген. 20-кылымдын орто ченинде Америкада Принстон университетинде математиканы жана ар кандай абстракцияларды абдан жакшы көргөн Алонцо чиркөөсү иштеген. Бул Алонзо чиркөөсү биринчи абстракттуу идеялардын жыйындысы болгон жана программалоо менен эч кандай байланышы жок ламбда эсептөөсүн ойлоп тапкан. Ошол эле учурда ошол эле Принстон университетинде Алан Тюринг жана Жон фон Нейман сыяктуу математиктер иштешкен. Баары чогулду: Черч ламбда эсептөө системасын ойлоп тапты, Тьюринг өзүнүн абстракттуу эсептөө машинасын иштеп чыкты, азыр “Тюринг машинасы” деп аталат. Ооба, фон Нейман заманбап компьютерлердин негизин түзгөн компьютерлердин архитектурасынын диаграммасын сунуш кылган (жана азыр “фон Нейман архитектурасы” деп аталат). Ал кезде Алонзо Черчтин идеялары кесиптештеринин эмгеги сыяктуу атак-даңкка ээ болгон эмес («таза» математика чөйрөсүн кошпогондо). Бирок, бир аз убакыт өткөндөн кийин, белгилүү бир Жон Маккарти (ошондой эле окуя учурунда Принстон университетинин бүтүрүүчүсү - Массачусетс технологиялык институтунун кызматкери) Черчтин идеяларына кызыгып калган. Алардын негизинде 1958-жылы биринчи функционалдык программалоо тorн Lisp түзгөн. Ал эми 58 жылдан кийин функционалдык программалоо идеялары Java тorне 8-сан катары агып кирди. Арадан 70 жыл да өткөн жок... Чынында, бул математикалык идеяны практикада колдонуу үчүн эң узак убакыт эмес.

Маңызы

Ламбда туюнтмасы ушундай функция. Сиз муну Javaдагы кадимки метод деп ойлосоңуз болот, бир гана айырмасы, аны аргумент катары башка методдорго өткөрүп берсе болот. Ооба, ыкмаларга сандарды, саптарды жана мышыктарды гана эмес, башка ыкмаларды да өткөрүү мүмкүн болуп калды! Бул бизге качан керек болушу мүмкүн? Мисалы, биз кандайдыр бир кайра чалууну өткөргүбүз келсе. Биз ага өткөн башка ыкманы чакыра алуу үчүн биз чакырган ыкма керек. Башкача айтканда, биз кээ бир учурларда бир кайра чалууну, ал эми башка учурларда башканы өткөрүү мүмкүнчүлүгүнө ээ болушубуз үчүн. Биздин кайра чалууларыбызды кабыл алган методубуз аларды чакырат. Жөнөкөй мисал сорттоо болуп саналат. Төмөнкүдөй көрүнгөн кандайдыр бир татаал сорттоо жаздык дейли:
public void mySuperSort() {
    // ... do something here
    if(compare(obj1, obj2) > 0)
    // ... and here we do something
}
Кайда, ifбиз метод деп атайбыз compare(), ал жерге биз салыштырган эки an objectти өткөрүп беребиз жана бул an objectтердин кайсынысы "чоң" экенин билгибиз келет. «Көбүрөөк» болгонду «кичинекей» дегендин алдына коёбуз. Мен тырмакчага "көбүрөөк" деп жаздым, анткени биз өсүү боюнча гана эмес, кемүү тартибинде да иреттей ала турган универсалдуу ыкманы жазып жатабыз (мында "көбүрөөк" an objectиси кичирээк жана тескерисинче болот) . Эрежени так кантип сорттогубуз келсе, аны кандайдыр бир жол менен биздин mySuperSort(). Бул учурда, биз аны чакырып жатканда, кандайдыр бир жол менен "башкаруу" алат. Албетте, өсүү жана кемүү тартибинде сорттоо mySuperSortAsc()үчүн эки өзүнчө ыкманы жаза аласыз . mySuperSortDesc()Же методдун ичинде кандайдыр бир параметрди өткөрүңүз (мисалы, booleanэгерде true, өсүү тартибинде жана эгерде falseазаюу тартибинде болсо). Бирок биз кандайдыр бир жөнөкөй структураны эмес, мисалы, сап массивдеринин тизмесин сорттоону кааласакчы? Биздин ыкма mySuperSort()бул сап массивдерин кантип сорттоо керектигин кайдан билет? Өлчөмгө? Сөздөрдүн жалпы узундугу боюнча? Балким, алфавит боюнча, массивдеги биринчи сапка жараша? Бирок, кээ бир учурларда массивдердин тизмесин массивдин өлчөмү боюнча, ал эми башка учурда массивдеги сөздөрдүн жалпы узундугу боюнча сорттоо керек болсочу? Менин оюмча, сиз буга чейин салыштыргычтар жөнүндө уккансыз жана мындай учурларда биз жөн гана салыштыруучу an objectти сорттоо методубузга өткөрүп беребиз, анда биз сорттогубуз келген эрежелерди сүрөттөйбүз. Стандарттык ыкма sort()ошол эле принцип боюнча ишке ашырылгандыктан, mySuperSort()мисалдарда мен стандартты колдоном sort().
String[] array1 = {"Mother", "soap", "frame"};
String[] array2 = {"I", "Very", "I love", "java"};
String[] array3 = {"world", "work", "May"};

List<String[]> arrays = new ArrayList<>();
arrays.add(array1);
arrays.add(array2);
arrays.add(array3);

Comparator<String[]> sortByLength = new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
};

Comparator<String[]> sortByWordsLength = new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        int length1 = 0;
        int length2 = 0;
        for (String s : o1) {
            length1 += s.length();
        }
        for (String s : o2) {
            length2 += s.length();
        }
        return length1 - length2;
    }
};

arrays.sort(sortByLength);
Натыйжа:
  1. апам рамканы жууду
  2. тынчтык эмгек май
  3. Мен Javaны чындап жакшы көрөм
Бул жерде массивдер ар бир массивдеги сөздөрдүн саны боюнча иргелет. Азыраак сөздөрү бар массив "кичинекей" деп эсептелет. Ошон үчүн ал башында келет. Кайсы жерде көп сөз бар болсо, ошол "көп" деп эсептелет жана аягында бүтөт. Эгерде методго sort()дагы бир салыштыргычты өткөрүп берсек (sortByWordsLength), анда натыйжа башкача болот:
  1. тынчтык эмгек май
  2. апам рамканы жууду
  3. Мен Javaны чындап жакшы көрөм
Эми массивдер мындай массивдин сөздөрүндөгү тамгалардын жалпы саны боюнча иргелет. Биринчисинде 10 тамга, экинчисинде 12, үчүнчүсүндө 15 тамга бар. Эгерде биз бир гана салыштыргычты колдонсок, анда биз ал үчүн өзүнчө өзгөрмө түзө албайбыз, жөн гана анонимдүү класстын an objectин түзөбүз. ыкманы чакыруу убактысы sort(). Болжол менен ушундай:
String[] array1 = {"Mother", "soap", "frame"};
String[] array2 = {"I", "Very", "I love", "java"};
String[] array3 = {"world", "work", "May"};

List<String[]> arrays = new ArrayList<>();
arrays.add(array1);
arrays.add(array2);
arrays.add(array3);

arrays.sort(new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
});
Натыйжа биринчи учурдагыдай эле болот. 1-тапшырма . Бул мисалды массивдерди массивдеги сөздөрдүн санынын өсүү тартибинде эмес, кемүү тартибинде иреттей тургандай кылып кайра жазыңыз. Мунун баарын биз билебиз. Биз an objectтерди методдорго кантип өткөрүүнү билебиз, биз тигил же бул an objectти азыркы учурда эмнеге муктаж болгонубузга жараша методго өткөрө алабыз жана мындай an objectти өткөргөн методдун ичинде биз ишке ашырууну жазган ыкма деп аталат. . Суроо туулат: ламбда туюнтмаларынын ага кандай тиешеси бар? Ламбда так бир ыкманы камтыган an object экенин эске алсак. Бул метод an objectиси сыяктуу. Объектке оролгон ыкма. Алар жөн гана бир аз адаттан тыш синтаксиске ээ (бирок бул тууралуу кийинчерээк). Келгиле, бул жазууну дагы бир карап көрөлү
arrays.sort(new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
});
Бул жерде биз тизмебизди алып arrays, анын ыкмасы деп атайбыз sort(), анда биз бир эле ыкма менен салыштыруучу an objectти өткөрөбүз compare()(анын эмне деп аталганы биз үчүн маанилүү эмес, анткени бул an objectте жалгыз, биз аны өткөрүп жибербейбиз). Бул ыкма эки параметрди талап кылат, биз кийинки менен иштейбиз. Эгер сиз IntelliJ IDEAда иштесеңиз , анда ал сизге бул codeду бир кыйла кыскартуу үчүн кантип сунуштаганын көргөн чыгарсыз:
arrays.sort((o1, o2) -> o1.length - o2.length);
Ошентип алты сап бир кыска сапка айланды. 6 сап бир кыска сапка кайра жазылды. Бир нерсе жок болуп кетти, бирок мен эч кандай маанилүү нерсе жок болуп кетпегенине кепилдик берем жана бул code анонимдүү класстагыдай эле иштейт. 2-тапшырма . Ламбдаларды колдонуу менен 1-маселенин чечorшин кантип кайра жазууга болорун табыңыз (акыркы чара катары IntelliJ IDEAдан анонимдүү классыңызды ламбдага айлантууну сураныңыз).

Келгиле, интерфейстер жөнүндө сүйлөшөлү

Негизи интерфейс бул абстракттуу ыкмалардын тизмеси. Биз класс түзүп, ал кандайдыр бир интерфейсти ишке ашырат деп айтканда, биз классыбызга интерфейсте саналган ыкмаларды ишке ашырууну жазышыбыз керек (же акыркы чара катары аны жазбай, классты абстракттуу кылышыбыз керек). ). Көптөгөн ар кандай ыкмалар менен интерфейстер бар (мисалы List), бир гана методу бар интерфейстер бар (мисалы, ошол эле Comparator же Runnable). Дегеле бир ыкмасы жок интерфейстер бар (маркердик интерфейстер деп аталат, мисалы, Serializable). Бир гана ыкмасы бар интерфейстер функционалдык интерфейстер деп да аталат . Java 8де алар атүгүл атайын @FunctionalInterface annotationсы менен белгиленген . Бул ламбда туюнтмалары менен колдонууга ылайыктуу болгон бир ыкма менен интерфейстер. Мен жогоруда айткандай, ламбда туюнтмасы an objectке оролгон ыкма. Жана биз бир жерден мындай an objectти өткөрүп жатканда, биз, чынында, бул бир гана ыкма менен өтөбүз. Көрсө, бул ыкма кандай аталса, биз үчүн маанилүү эмес экен. Биз үчүн маанилүү болгон нерсе бул ыкма кабыл алган параметрлер жана чындыгында методдун codeунун өзү. Ламбда туюнтмасы, негизинен. функционалдык интерфейсти ишке ашыруу. Бир метод менен интерфейсти көргөндө, бул биз lambda аркылуу анонимдүү классты кайра жаза алабыз дегенди билдирет. Эгерде интерфейсте бирден көп/азыраак ыкма болсо, анда ламбда туюнтмасы бизге туура келбейт жана биз анонимдүү классты, ал тургай кадимки классты колдонобуз. Ламбдаларды казууга убакыт жетти. :)

Синтаксис

Жалпы синтаксис төмөнкүдөй:
(параметры) -> {тело метода}
Башкача айтканда, кашаа, алардын ичинде ыкма параметрлери, "жебе" (бул эки белги: минус жана чоңураак), андан кийин методдун негизги бөлүгү дайыма эле тармал кашааларда болот. Параметрлер методду сүрөттөп жатканда интерфейсте көрсөтүлгөнгө туура келет. Эгерде өзгөрмөлөрдүн түрү компилятор тарабынан так аныктала турган болсо (биздин учурда, биз саптардын массивдери менен иштеп жатканыбыз анык белгилүү, анткени ал Listсаптардын массивдери менен так терилген), анда өзгөрмөлөрдүн түрү String[]керек эмес. жазылуу.
Ишенбесеңиз, түрүн көрсөтүңүз жана IDEA керек болбосо, аны боз түс менен бөлүп көрсөтөт.
Мисалы, сиз Oracle окуу куралынан көбүрөөк окуй аласыз . Бул "максат терүү" деп аталат . Өзгөрмөлөргө интерфейсте көрсөтүлгөндөй эле эмес, каалаган аттар берorши мүмкүн. Эгерде эч кандай параметрлер жок болсо, анда жөн гана кашаа. Эгер бир гана параметр болсо, кашаасыз өзгөрмөнүн аты жөн гана. Биз параметрлерди иреттеп алдык, эми ламбда туюнтмасынын өзү жөнүндө. Тармал кашаанын ичине кадимки ыкмадагыдай codeду жазыңыз. Эгер бүт codeуңуз бир саптан турса, анда такыр тармал кашааларды жазуунун кереги жок (ifs жана циклдер сыяктуу). Эгерде сиздин ламбдаңыз бир нерсени кайтарып берсе, бирок анын денеси бир саптан турса, анда аны returnжазуунун кереги жок. Бирок, эгерде сизде тармал кашаа болсо, анда кадимки ыкмадагыдай эле, ачык жазуу керек return.

Мисалдар

Мисал 1.
() -> {}
Эң жөнөкөй вариант. Жана эң маанисиз :).Анткени ал эч нерсе кылbyte. Мисал 2.
() -> ""
Ошондой эле кызыктуу вариант. Ал эч нерсени кабыл алbyte жана бош сапты кайтарат ( returnкерексиз деп калтырылган). Ошол эле, бирок менен return:
() -> {
    return "";
}
Мисал 3. Ламбдаларды колдонуу менен салам дүйнөсү
() -> System.out.println("Hello world!")
Эч нерсе алbyte, эч нерсе кайтарbyte ( returnчакыруунун алдына коё албайбыз System.out.println(), анткени методдогу кайтаруу түрү println() — void)жөн гана экранда жазууну көрсөтөт. Интерфейсти ишке ашыруу үчүн идеалдуу Runnable. Ушул эле мисал толукраак:
public class Main {
    public static void main(String[] args) {
        new Thread(() -> System.out.println("Hello world!")).start();
    }
}
Же бул сыяктуу:
public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(() -> System.out.println("Hello world!"));
        t.start();
    }
}
Же биз lambda туюнтмасын типтеги an object катары сактап Runnable, анан аны конструкторго өткөрүп бере алабыз thread’а:
public class Main {
    public static void main(String[] args) {
        Runnable runnable = () -> System.out.println("Hello world!");
        Thread t = new Thread(runnable);
        t.start();
    }
}
Келгиле, ламбда туюнтмасын өзгөрмөгө сактоо учурун кененирээк карап чыгалы. Интерфейс Runnableанын an objectилеринин методу болушу керектигин айтат public void run(). Интерфейс боюнча, иштетүү ыкмасы эч нерсени параметр катары кабыл алbyte. Анан эч нерсе кайтарbyte (void). Ошондуктан, бул жол менен жазып жатканда, эч нерсе кабыл албаган же кайтарбаган кандайдыр бир ыкма менен an object түзүлөт. run()Бул интерфейстеги ыкмага абдан шайкеш келет Runnable. Ошондуктан биз бул ламбда туюнтмасын сыяктуу өзгөрмөгө киргизе алдык Runnable. Мисал 4
() -> 42
Дагы, ал эч нерсени кабыл алbyte, бирок 42 санын кайтарат. Бул лямбда туюнтмасын type өзгөрмөсүнө жайгаштырса болот Callable, анткени бул интерфейс бир гана ыкманы аныктайт, ал төмөнкүдөй көрүнөт:
V call(),
VКайтаруучу маанинин түрү кайда (биздин учурда ) int. Демек, биз мындай ламбда туюнтмасын төмөнкүдөй сактай алабыз:
Callable<Integer> c = () -> 42;
Мисал 5. Ламбда бир нече сапта
() -> {
    String[] helloWorld = {"Hello", "world!"};
    System.out.println(helloWorld[0]);
    System.out.println(helloWorld[1]);
}
Кайрадан, бул параметрсиз ламбда туюнтмасы жана анын кайтаруу түрү void(анткени return). Мисал 6
x -> x
Бул жерде биз бир нерсени өзгөрмөгө алып х, аны кайтарабыз. Эгер бир гана параметр кабыл алынса, анын айланасындагы кашааларды жазуунун кереги жок экенин эске алыңыз. Ошол эле, бирок кашаа менен:
(x) -> x
Бул жерде ачык-айкын варианты бар return:
x -> {
    return x;
}
Же ушул сыяктуу, кашаа менен жана return:
(x) -> {
    return x;
}
Же түрүн ачык көрсөтүү менен (жана, тиешелүүлүгүнө жараша, кашаа менен):
(int x) -> x
Мисал 7
x -> ++x
Биз аны кабыл алып х, кайра кайтарабыз, бирок 1көбүрөөк. Сиз ошондой эле төмөнкүдөй кайра жаза аласыз:
x -> x + 1
Эки учурда тең параметрдин, методдун жана сөздүн айланасында кашааларды көрсөтпөйбүз return, анткени бул зарыл эмес. кашаа жана кайтаруу менен опциялар 6-мисалда сүрөттөлгөн. 8-мисал
(x, y) -> x % y
Биз кээ бир бөлүгүн кабыл алабыз хжана убөлүүнүн калган бөлүгүн xменен кайтарабыз y. Параметрлердин тегерегиндеги кашаалар бул жерде мурунтан эле талап кылынат. Алар бир гана параметр болгондо гана милдеттүү болуп саналат. Бул сыяктуу түрлөрүн ачык көрсөтүү менен:
(double x, int y) -> x % y
Мисал 9
(Cat cat, String name, int age) -> {
    cat.setName(name);
    cat.setAge(age);
}
Биз Cat an objectисин, аталышы жана бүтүн сан жашы бар сапты кабыл алабыз. Методдун өзүндө биз өткөн атын жана жашын Cat деп койдук. catБиздин өзгөрмө шилтеме түрү болгондуктан, lambda туюнтмасынын сыртындагы Cat an objectи өзгөрөт (ал ичине өткөн ысымды жана жашты алат). Окшош ламбданы колдонгон бир аз татаалыраак version:
public class Main {
    public static void main(String[] args) {
        // create a cat and print to the screen to make sure it's "blank"
        Cat myCat = new Cat();
        System.out.println(myCat);

        // create lambda
        Settable<Cat> s = (obj, name, age) -> {
            obj.setName(name);
            obj.setAge(age);
        };

        // call the method, to which we pass the cat and the lambda
        changeEntity(myCat, s);
        // display on the screen and see that the state of the cat has changed (has a name and age)
        System.out.println(myCat);
    }

    private static <T extends WithNameAndAge>  void changeEntity(T entity, Settable<T> s) {
        s.set(entity, "Murzik", 3);
    }
}

interface WithNameAndAge {
    void setName(String name);
    void setAge(int age);
}

interface Settable<C extends WithNameAndAge> {
    void set(C entity, String name, int age);
}

class Cat implements WithNameAndAge {
    private String name;
    private int age;

    @Override
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Cat{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
Натыйжа: Cat{name='null', age=0} Cat{name='Murzik', age=3} Көрүнүп тургандай, Cat an objectинин башында бир абал бар болчу, бирок ламбда туюнтмасын колдонгондон кийин абал өзгөрдү . Lambda туюнтмалары генериктер менен жакшы иштейт. Ал эми биз классты түзүшүбүз керек болсо Dog, мисалы, ал да ишке ашыра турган болсо WithNameAndAge, анда методдо main()биз ламбда туюнтмасын такыр өзгөртпөстөн, Dog менен бир эле операцияларды жасай алабыз. 3-тапшырма . Сан алып, логикалык маанини кайтарган метод менен функционалдык интерфейсти жазыңыз. trueӨткөрүлгөн сан 13кө калдыксыз бөлүнсө, кайтаруучу ламбда туюнтмасы түрүндөгү мындай интерфейстин реализациясын жазыңыз 4-тапшырма . Эки сапты алып, ошол эле сапты кайтарган метод менен функционалдык интерфейсти жазыңыз. Эң узун сапты кайтарган ламбда түрүндөгү мындай интерфейстин реализациясын жазыңыз. 5-тапшырма . Функционалдык интерфейсти үч бөлчөк санды кабыл алган ыкма менен жазыңыз: a, b, cжана ошол эле бөлчөк санды кайтарат. Дискриминантты кайтарган ламбда туюнтмасы түрүндө мындай интерфейстин ишке ашырылышын жазыңыз. Ким унутуп калды, D = b^2 - 4ac . 6-тапшырма . 5-тапшырмадагы функционалдык интерфейсти колдонуп, операциянын жыйынтыгын берген ламбда туюнтмасын жазыңыз a * b^c. Java тorндеги ламбда туюнтмалары боюнча популярдуу. Мисалдар жана тапшырмалар менен. 2 бөлүк.
Комментарийлер
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION