JavaRush /Java блогы /Random-KK /Java тіліндегі ламбда өрнектері туралы танымал. Мысалдар ...
Стас Пасинков
Деңгей
Киев

Java тіліндегі ламбда өрнектері туралы танымал. Мысалдар мен тапсырмалар арқылы. 1 бөлім

Топта жарияланған
Бұл мақала кімге арналған?
  • Java Core-ді жақсы білемін деп ойлайтындар үшін, бірақ Java тіліндегі лямбда өрнектері туралы түсінігі жоқ. Немесе, мүмкін, сіз ламбдалар туралы бірдеңе естіген шығарсыз, бірақ егжей-тегжейсіз.
  • лямбда өрнектері туралы біраз түсінігі бар, бірақ әлі де қорқатын және оларды пайдалану әдеттен тыс адамдар үшін.
Егер сіз осы санаттардың біріне жатпасаңыз, бұл мақаланы қызықсыз, дұрыс емес және әдетте «керемет емес» деп табуыңыз мүмкін. Бұл жағдайда өтіп кетуге болады, немесе егер сіз тақырыпты жақсы білетін болсаңыз, түсініктемелерде мақаланы қалай жақсартуға немесе толықтыруға болатынын ұсыныңыз. Материал ешқандай академиялық құндылықты талап етпейді, жаңашылдық аз. Керісінше, керісінше: онда мен күрделі (кейбіреулер үшін) заттарды мүмкіндігінше қарапайым сипаттауға тырысамын. Маған жазуға ағындық api түсіндіріп беру өтініші түрткі болды. Мен бұл туралы ойладым және ламбда өрнектерін түсінбестен, «ағындар» туралы кейбір мысалдарым түсініксіз болады деп шештім. Сонымен, ламбдалардан бастайық. Java тіліндегі ламбда өрнектері туралы танымал.  Мысалдар мен тапсырмалар арқылы.  1 - 1 бөлімБұл мақаланы түсіну үшін қандай білім қажет:
  1. Объектілі-бағытталған бағдарламалауды (бұдан әрі - OOP) түсіну, атап айтқанда:
    • кластар мен an objectілердің қандай екенін, олардың арасындағы айырмашылық неде екенін білу;
    • интерфейстер дегеніміз не, олардың кластардан айырмашылығы, олардың арасындағы байланыс қандай (интерфейстер мен класстар) туралы білім;
    • әдіс деген не, оны қалай атауға болады, абстрактілі әдіс дегеніміз не (немесе іске асырылуы жоқ әдіс), әдістің параметрлері/аргументтері қандай, оларды сол жерге қалай беру керек;
    • қатынас модификаторлары, статикалық әдістер/айнымалылар, соңғы әдістер/айнымалылар;
    • мұрагерлік (класстар, интерфейстер, интерфейстердің бірнеше мұрагерлік).
  2. Java Core туралы білім: генериктер, жинақтар (тізімдер), ағындар.
Кәне, бастайық.

Кішкене тарих

Lambda өрнектері Java-ға функционалдық бағдарламалаудан, ал математикадан келді. 20 ғасырдың ортасында Америкада Принстон университетінде белгілі бір Алонцо шіркеуі жұмыс істеді, ол математика мен абстракцияның барлық түрлерін жақсы көреді. Ламбда есептеуін ойлап тапқан Алонзо шіркеуі болды, ол бастапқыда кейбір дерексіз идеялардың жиынтығы болды және бағдарламалауға ешқандай қатысы жоқ. Сол кезде сол Принстон университетінде Алан Тюринг пен Джон фон Нейман сияқты математиктер жұмыс істеді. Барлығы біріктірілді: Черч ламбда есептеу жүйесін ойлап тапты, Тьюринг өзінің дерексіз есептеу машинасын жасады, қазір «Тьюринг машинасы» деп аталады. Фон Нейман қазіргі заманғы компьютерлердің негізін құрайтын (және қазір «фон Нейман архитектурасы» деп аталады) компьютерлер архитектурасының диаграммасын ұсынды. Ол кезде Алонзо Черчтің идеялары оның әріптестерінің жұмысы сияқты үлкен атаққа ие болмады («таза» математика саласын қоспағанда). Алайда, біраз уақыттан кейін белгілі бір Джон Маккарти (сонымен бірге Принстон университетінің түлегі, оқиға кезінде Массачусетс технологиялық институтының қызметкері) Черчтің идеяларына қызығушылық танытты. Солардың негізінде 1958 жылы ол Lisp атты алғашқы функционалды программалау тілін жасады. Ал 58 жылдан кейін функционалдық бағдарламалау идеялары Java тіліне 8 саны болып шықты. Тіпті 70 жыл да өткен жоқ... Шындығында, бұл математикалық идеяны тәжірибеде қолданудың ең ұзақ кезеңі емес.

мәні

Ламбда өрнегі осындай функция болып табылады. Мұны Java тіліндегі кәдімгі әдіс ретінде қарастыруға болады, жалғыз айырмашылығы - оны басқа әдістерге аргумент ретінде беруге болады. Иә, әдістерге сандарды, жолдарды және мысықтарды ғана емес, сонымен қатар басқа әдістерді де беру мүмкін болды! Бұл бізге қашан қажет болуы мүмкін? Мысалы, егер біз кері қоңырауды өткізгіміз келсе. Біз оған өтетін басқа әдісті шақыра алу үшін бізге шақыратын әдіс қажет. Яғни, бізде кейбір жағдайларда бір кері қоңырауды, ал басқаларында екіншісін жіберу мүмкіндігі бар. Біздің кері қоңырауларымызды қабылдайтын әдісіміз оларды шақырады. Қарапайым мысал - сұрыптау. Мынадай күрделі сұрыптауды жаздық делік:
public void mySuperSort() {
    // ... do something here
    if(compare(obj1, obj2) > 0)
    // ... and here we do something
}
Мұнда ifбіз әдіс деп атаймыз compare(), онда біз салыстыратын екі нысанды береміз және осы нысандардың қайсысы «үлкен» екенін білгіміз келеді. Біз «кіші» дегеннің алдына «көп» дегенді қоямыз. Мен тырнақшаға «көп» деп жаздым, өйткені біз өсу бойынша ғана емес, кему ретімен де сұрыптауға болатын әмбебап әдісті жазып жатырмыз (бұл жағдайда «көп» мәні бойынша кішірек нысан болады және керісінше) . Ережені дәл қалай сұрыптағымыз келетінін орнату үшін оны қандай да бір түрде біздің mySuperSort(). Бұл жағдайда біз әдісті шақыру кезінде қандай да бір жолмен «басқара» аламыз. Әрине, өсу және кему ретімен сұрыптаудың mySuperSortAsc()екі бөлек әдісін жазуға болады. mySuperSortDesc()Немесе әдіс ішінде кейбір параметрді жіберіңіз (мысалы, booleanегер true, өсу ретімен сұрыптаңыз және falseкему ретімен болса). Бірақ қандай да бір қарапайым құрылымды емес, мысалы, жол массивтерінің тізімін сұрыптағымыз келсе ше? Біздің әдіс mySuperSort()осы жол массивтерін сұрыптауды қалай біледі? Өлшемге? Сөздердің жалпы ұзындығы бойынша? Мүмкін алфавит бойынша, массивтің бірінші жолына байланысты? Бірақ кейбір жағдайларда массивтер тізімін массив өлшемі бойынша, ал басқа жағдайда массивтегі сөздердің жалпы ұзындығы бойынша сұрыптау қажет болса ше? Менің ойымша, сіз компараторлар туралы бұрыннан естідіңіз және мұндай жағдайларда біз жай ғана салыстырмалы нысанды сұрыптау әдісімізге өткіземіз, онда біз сұрыптауды қалайтын ережелерді сипаттаймыз. Стандартты әдіс 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іні өткізетін әдістің ішінде біз іске асыруды жазған әдіс деп аталады. . Сұрақ туындайды: ламбда өрнектерінің оған қандай қатысы бар? Ламбданың бір әдісті қамтитын нысан екенін ескере отырып. Бұл әдіс нысаны сияқты. Нысанға оралған әдіс. Олардың сәл ерекше синтаксисі бар (бірақ бұл туралы кейінірек). Осы жазбаны тағы бір қарастырайық
arrays.sort(new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
});
Мұнда біз тізімді аламыз arraysжәне оның әдісі деп атаймыз sort(), мұнда біз бір әдіспен салыстыру an objectісін береміз compare()(оның қалай аталатыны біз үшін маңызды емес, өйткені бұл нысандағы жалғыз, біз оны жіберіп алмаймыз). Бұл әдіс келесі параметрмен жұмыс істейтін екі параметрді алады. Егер сіз IntelliJ IDEA жүйесінде жұмыс істесеңіз , оның сізге бұл codeты айтарлықтай қысқарту үшін қалай ұсынатынын көрген боларсыз:
arrays.sort((o1, o2) -> o1.length - o2.length);
Осылайша алты жол бір қысқаға айналды. 6 жол бір қысқа жолға қайта жазылды. Бірдеңе жоғалып кетті, бірақ мен маңызды ештеңе жоғалмағанына кепілдік беремін және бұл code анонимді сыныппен бірдей жұмыс істейді. 2-тапсырма . Ламбдаларды пайдаланып 1-есептің шешімін қалай қайта жазу керектігін анықтаңыз (соңғы шара ретінде IntelliJ IDEA-дан анонимді сыныпты ламбдаға айналдыруды сұраңыз).

Интерфейстер туралы сөйлесейік

Негізінде интерфейс – бұл дерексіз әдістердің тізімі ғана. Класс құрып, ол интерфейстің қандай да бір түрін жүзеге асыратынын айтқан кезде, біз өз сыныбымызда интерфейсте көрсетілген әдістердің іске асырылуын жазуымыз керек (немесе, соңғы шара ретінде, оны жазбай, классты абстрактілі етіп жасау керек). ). Көптеген әртүрлі әдістермен интерфейстер бар (мысалы List), тек бір әдіспен интерфейстер бар (мысалы, бірдей Comparator немесе Runnable). Мүлде бір әдісі жоқ интерфейстер бар (маркер интерфейстері деп аталатындар, мысалы, Serializable). Бір ғана әдісі бар интерфейстерді функционалды интерфейстер деп те атайды . Java 8-де олар тіпті арнайы @FunctionalInterface annotationсымен белгіленген . Бұл лямбда өрнектері арқылы пайдалануға жарамды бір әдіспен интерфейстер. Жоғарыда айтқанымдай, лямбда өрнегі нысанға оралған әдіс болып табылады. Біз мұндай нысанды бір жерден өткізгенде, біз, шын мәнінде, осы бір ғана әдісті өткіземіз. Бұл әдістің қалай аталатыны бізге маңызды емес екені белгілі болды. Біз үшін маңызды нәрсе - бұл әдіс қабылдайтын параметрлер және шын мәнінде әдіс codeының өзі. Ламбда өрнегі, негізінен. функционалдық интерфейсті жүзеге асыру. Бір әдіспен интерфейсті көретін болсақ, бұл лямбда көмегімен мұндай анонимді классты қайта жазуға болатынын білдіреді. Егер интерфейсте бірнеше әдіс болса, онда лямбда өрнегі бізге сәйкес келмейді және біз анонимді классты немесе тіпті әдеттегі классты қолданамыз. Ламбдаларды қазудың уақыты келді. :)

Синтаксис

Жалпы синтаксис келесідей:
(параметры) -> {тело метода}
Яғни, жақша, олардың ішінде әдіс параметрлері, «көрсеткі» (бұл қатардағы екі таңба: минус және одан үлкен), содан кейін әдістің негізгі бөлігі әдеттегідей бұйра жақшаларда болады. Параметрлер әдісті сипаттау кезінде интерфейсте көрсетілгендерге сәйкес келеді. Егер айнымалылар түрін компилятор анық анықтай алатын болса (біздің жағдайда жолдар массивтерімен жұмыс істейтініміз белгілі, өйткені ол Listжолдар массивтері арқылы дәл теріледі), онда айнымалылар типі String[]қажет емес. жазылуы.
Егер сенімді болмасаңыз, түрін көрсетіңіз, қажет болмаса, IDEA оны сұр түспен бөлектейді.
Сіз , мысалы, Oracle оқулығында көбірек оқи аласыз . Бұл «мақсатты теру» деп аталады . Айнымалыларға интерфейсте көрсетілгендер емес, кез келген атаулар берілуі мүмкін. Егер параметрлер жоқ болса, онда жай жақшалар. Бір ғана параметр болса, тек жақшасыз айнымалы атауы. Біз параметрлерді сұрыптадық, енді лямбда өрнегі денесі туралы. Бұйра жақшалардың ішіне әдеттегі әдіс сияқты codeты жазыңыз. Егер сіздің бүкіл codeыңыз тек бір жолдан тұрса, сізге бұйра жақшаларды мүлде жазудың қажеті жоқ (ifs және ілмектер сияқты). Егер ламбда бір нәрсені қайтарса, бірақ оның денесі бір жолдан тұрса, оны returnжазудың қажеті жоқ. Бірақ егер сізде бұйра жақшалар болса, онда әдеттегі әдіс сияқты, сіз анық жазуыңыз керек return.

Мысалдар

1-мысал.
() -> {}
Ең қарапайым нұсқа. Ал ең мағынасыз :) Өйткені ол ештеңе жасамайды. 2-мысал.
() -> ""
Сондай-ақ қызықты нұсқа. Ол ешнәрсені қабылдамайды және бос жолды қайтарады ( returnқажетсіз ретінде алынып тасталды). Дәл солай, бірақ return:
() -> {
    return "";
}
3-мысал. Ламбдаларды қолданатын сәлем әлемі
() -> System.out.println("Hello world!")
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 өрнегін түрдегі нысан ретінде сақтай аламыз 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(). Интерфейс бойынша іске қосу әдісі параметр ретінде ештеңені қабылдамайды. Және ол ештеңе қайтармайды (void). Сондықтан, осылайша жазғанда, ештеңе қабылдамайтын немесе қайтармайтын қандай да бір әдіспен нысан жасалады. run()Бұл интерфейстегі әдіске әбден сәйкес келеді Runnable. Сондықтан біз бұл лямбда өрнегін сияқты айнымалыға қоя алдық Runnable. 4-мысал
() -> 42
Қайтадан ол ештеңені қабылдамайды, бірақ 42 санын қайтарады. Бұл лямбда өрнегі түрдегі айнымалыға орналастырылуы мүмкін 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 нысанын, аты және бүтін жасы бар жолды қабылдаймыз. Әдістің өзінде біз өткен атау мен жасты мысыққа орнаттық. catБіздің айнымалы анықтамалық тип болғандықтан, лямбда өрнегі сыртындағы Cat нысаны өзгереді (ол ішіне берілген атау мен жасты алады). Ұқсас ламбда қолданатын сәл күрделірек нұсқасы:
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 нысанында алдымен бір күй болды, бірақ ламбда өрнегін қолданғаннан кейін күй өзгерді . Ламбда өрнектері генериктермен жақсы жұмыс істейді. Ал егер бізге сыныпты жасау керек болса Dog, мысалы, ол да іске асырады WithNameAndAge, онда әдісте main()біз лямбда өрнегін мүлде өзгертпей Dog-пен бірдей әрекеттерді жасай аламыз. 3-тапсырма . Санды қабылдайтын және логикалық мәнді қайтаратын әдіспен функционалды интерфейсті жазыңыз. trueБерілген сан 13-ке қалдықсыз бөлінетін болса қайтаратын ламбда өрнегі түріндегі осындай интерфейстің орындалуын жазыңыз 4-тапсырма . Екі жолды қабылдайтын және бірдей жолды қайтаратын әдіспен функционалды интерфейсті жазыңыз. Ең ұзын жолды қайтаратын лямбда түрінде осындай интерфейстің іске асырылуын жазыңыз. 5-тапсырма . Үш бөлшек сандарды қабылдайтын әдіспен функционалды интерфейсті жазыңыз: a, b, cжәне бірдей бөлшек санды қайтарады. Дискриминантты қайтаратын ламбда өрнегі түрінде осындай интерфейстің іске асырылуын жазыңыз. Кім ұмытты, D = b^2 - 4ac . 6-тапсырма . 5-тапсырмадағы функционалды интерфейсті пайдаланып, операцияның нәтижесін беретін лямбда өрнегін жазыңыз a * b^c. Java тіліндегі ламбда өрнектері туралы танымал. Мысалдар мен тапсырмалар арқылы. 2-бөлім.
Пікірлер
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION