CodeGym /Java блог /Случаен /Обяснение на ламбда изрази в Java. С примери и задачи. Ча...
John Squirrels
Ниво
San Francisco

Обяснение на ламбда изрази в Java. С примери и задачи. Част 1

Публикувано в групата
За кого е тази статия?
  • Това е за хора, които смятат, че вече познават добре Java Core, но нямат представа за ламбда изразите в Java. Или може би са чували нещо за ламбда изрази, но подробностите липсват
  • Това е за хора, които имат известно разбиране за ламбда изразите, но все още са уплашени от тях и не са свикнали да ги използват.
Обяснение на ламбда изрази в Java.  С примери и задачи.  Част 1 - 1Ако не отговаряте на една от тези категории, може да намерите тази статия за скучна, недостатъчна or като цяло не е вашата чаша чай. В този случай не се колеbyteе да преминете към други неща or, ако сте добре запознати с темата, моля, направете предложения в коментарите How бих могъл да подобря or допълня статията. Материалът не претендира за академична стойност, камо ли за новост. Точно обратното: ще се опитам да опиша нещата, които са сложни (за някои хора) възможно най-просто. Молба за обяснение на Stream API ме вдъхнови да напиша това. Помислих за това и реших, че някои от моите примери за потоци биха бor неразбираеми без разбиране на ламбда изрази. Така че ще започнем с ламбда изрази. Какво трябва да знаете, за да разберете тази статия?
  1. Трябва да разбирате обектно-ориентираното програмиране (ООП), а именно:

    • класове, обекти и разликата между тях;
    • интерфейси, How се различават от класовете и връзката между интерфейси и класове;
    • методи, How да ги извикате, абстрактни методи (т.е. методи без имплементация), параметри на метода, аргументи на метода и How да ги подадете;
    • модификатори за достъп, статични методи/променливи, крайни методи/променливи;
    • наследяване на класове и интерфейси, множествено наследяване на интерфейси.
  2. Познаване на Java Core: генерични типове (generics), колекции (списъци), нишки.
Е, да се заемем с това.

Малко история

Ламбда изразите дойдоха в Java от функционалното програмиране, а там и от математиката. В САЩ в средата на 20 век Алонзо Чърч, който много обичаше математиката и всяHowви абстракции, работеше в Принстънския университет. Алонзо Чърч беше този, който изобрети ламбда смятането, което първоначално беше набор от абстрактни идеи, напълно несвързани с програмирането. Математици като Алън Тюринг и Джон фон Нойман работят в Принстънския университет по същото време. Всичко се събра: Чърч излезе с ламбда смятането. Тюринг разработва своята абстрактна изчислителна машина, сега известна като "машината на Тюринг". И фон Нойман предложи компютърна архитектура, която формира основата на съвременните компютри (сега наричана "архитектура на фон Нойман"). По това време църквата Алонзо Идеите му не стават толкова известни като трудовете на неговите колеги (с изключение на областта на чистата математика). Малко по-късно обаче Джон Маккарти (също възпитаник на Принстънския университет и по време на нашата история служител на Масачузетския технологичен институт) се заинтересува от идеите на Чърч. През 1958 г. той създава първия функционален език за програмиране, LISP, базиран на тези идеи. И 58 години по-късно идеите на функционалното програмиране изтекоха в Java 8. Не са минали дори 70 години... Честно казано, това не е най-дългото време, необходимо на една математическа идея да бъде приложена на практика. служител на Масачузетския технологичен институт) се заинтересува от идеите на Чърч. През 1958 г. той създава първия функционален език за програмиране, LISP, базиран на тези идеи. И 58 години по-късно идеите на функционалното програмиране изтекоха в Java 8. Не са минали дори 70 години... Честно казано, това не е най-дългото време, необходимо на една математическа идея да бъде приложена на практика. служител на Масачузетския технологичен институт) се заинтересува от идеите на Чърч. През 1958 г. той създава първия функционален език за програмиране, LISP, базиран на тези идеи. И 58 години по-късно идеите на функционалното програмиране изтекоха в Java 8. Не са минали дори 70 години... Честно казано, това не е най-дългото време, необходимо на една математическа идея да бъде приложена на практика.

Сърцевината на въпроса

Ламбда изразът е вид функция. Можете да го считате за обикновен Java метод, но с отличителната способност да бъде предаван на други методи като аргумент. Това е вярно. Стана възможно да се предават не само числа, низове и котки към методи, но и други методи! Кога може да имаме нужда от това? Би било полезно, например, ако искаме да предадем няHowъв метод за обратно извикване. Тоест, ако имаме нужда методът, който извикваме, да има способността да извиква друг метод, който му предаваме. С други думи, имаме способността да предадем едно обратно извикване при определени обстоятелства и различно обратно извикване при други. И така, че нашият метод, който получава нашите обратни извиквания, ги извиква. Сортирането е прост пример. Да предположим, че пишем няHowъв умен алгоритъм за сортиране, който изглежда така:

public void mySuperSort() { 
    // We do something here 
    if(compare(obj1, obj2) > 0) 
    // And then we do something here 
}
В ifоператора извикваме compare()метода, като предаваме два обекта за сравнение и искаме да знаем кой от тези обекти е "по-велик". Предполагаме, че "по-големият" идва преди "по-малкия". Слагам „по-голям“ в кавички, защото пишем универсален метод, който ще знае How да сортира не само във възходящ, но и в низходящ ред (в този случай „по-големият“ обект всъщност ще бъде „по-малкият“ обект , и обратно). За да зададем конкретния алгоритъм за нашето сортиране, имаме нужда от няHowъв механизъм, за да го предадем на нашия mySuperSort()метод. По този начин ще можем да "контролираме" нашия метод, когато бъде извикан. Разбира се, можем да напишем два отделни метода - mySuperSortAscend()иmySuperSortDescend()— за сортиране във възходящ и низходящ ред. Или можем да предадем няHowъв аргумент на метода (например булева променлива; ако е вярно, сортирайте във възходящ ред, а ако е невярно, тогава в низходящ ред). Но Howво ще стане, ако искаме да сортираме нещо сложно, като например списък с низови масиви? Как нашият mySuperSort()метод ще знае How да сортира тези масиви от низове? По размер? По кумулативната дължина на всички думи? Може би по азбучен ред въз основа на първия низ в масива? И Howво, ако трябва да сортираме списъка с масиви по размер на масива в някои случаи и по кумулативната дължина на всички думи във всеки масив в други случаи? Предполагам, че вече сте чували за компараторите и че в този случай ние просто ще предадем на нашия метод за сортиране обект за сравнение, който описва желания алгоритъм за сортиране. Тъй като стандартътsort()методът се изпълнява въз основа на същия принцип като mySuperSort(), който ще използвам sort()в моите примери.

String[] array1 = {"Dota", "GTA5", "Halo"}; 
String[] array2 = {"I", "really", "love", "Java"}; 
String[] array3 = {"if", "then", "else"}; 

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[]> sortByCumulativeWordLength = 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. Dota GTA5 Halo
  2. if then else
  3. I really love Java
Тук масивите са сортирани по броя на думите във всеки масив. Масив с по-малко думи се счита за "по-малък". Затова е на първо място. Масив с повече думи се счита за "по-голям" и се поставя в края. Ако предадем различен компаратор на sort()метода, като например sortByCumulativeWordLength, тогава ще получим различен резултат:

  1. if then else
  2. Dota GTA5 Halo
  3. I really love Java
Сега масивите са сортирани по общия брой букви в думите на масива. В първия масив има 10 букви, във втория — 12, а в третия — 15. Ако имаме само един компаратор, тогава не е нужно да декларираме отделна променлива за него. Вместо това можем просто да създадем анонимен клас точно в момента на извикване на sort()метода. Нещо като това:

String[] array1 = {"Dota", "GTA5", "Halo"}; 
String[] array2 = {"I", "really", "love", "Java"}; 
String[] array3 = {"if", "then", "else"}; 

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. Пренапишете този пример, така че да сортира масивите не във възходящ ред на броя на думите във всеки масив, а в низходящ ред. Ние вече знаем всичко това. Ние знаем How да предаваме обекти на методи. В зависимост от това, от което се нуждаем в момента, можем да предадем различни обекти на метод, който след това ще извика метода, който сме имплементирали. Това повдига въпроса: защо, за бога, имаме нужда от ламбда израз тук?  Тъй като ламбда изразът е обект, който има точно един метод. Като "обект на метод". Метод, пакетиран в обект. Просто има малко непознат синтаксис (но повече за това по-късно). Нека да погледнем отново този code:

arrays.sort(new Comparator<String[]>() { 
    @Override 
    public int compare(String[] o1, String[] o2) { 
        return o1.length - o2.length; 
    } 
});
Тук вземаме нашия списък с масиви и извикваме неговия sort()метод, към който предаваме обект за сравнение с един compare()метод (името му няма meaning за нас — в крайна сметка това е единственият метод на този обект, така че не можем да сгрешим). Този метод има два параметъра, с които ще работим. Ако работите в IntelliJ IDEA, вероятно сте видели, че предлага значително уплътняване на codeа, Howто следва:

arrays.sort((o1, o2) -> o1.length - o2.length);
Това намалява шест реда до един кратък. 6 реда са преписани като един кратък. Нещо изчезна, но гарантирам, че не е нещо важно. Този code ще работи точно по същия начин, Howто би работил с анонимен клас. Задача 2. Направете предположение How да пренапишете решението на Задача 1, като използвате ламбда израз (най-малкото помолете IntelliJ IDEA да преобразува вашия анонимен клас в ламбда израз).

Нека поговорим за интерфейсите

По принцип интерфейсът е просто списък от абстрактни методи. Когато създаваме клас, който имплементира няHowъв интерфейс, нашият клас трябва да имплементира методите, включени в интерфейса (or трябва да направим класа абстрактен). Има интерфейси с много различни методи (например  List), и има интерфейси само с един метод (например Comparatoror Runnable). Има интерфейси, които нямат нито един метод (така наречените маркерни интерфейси като Serializable). Интерфейсите, които имат само един метод, се наричат ​​още функционални интерфейси . В Java 8 те дори са маркирани със специална анотация:@FunctionalInterface. Именно тези интерфейси с един метод са подходящи като целеви типове за ламбда изрази. Както казах по-горе, ламбда изразът е метод, обвит в обект. И когато предаваме такъв обект, ние по същество предаваме този единствен метод. Оказва се, че не ни интересува How се казва методът. Единствените неща, които имат meaning за нас, са параметрите на метода и, разбира се, тялото на метода. По същество ламбда изразът е имплементацията на функционален интерфейс. Където и да видим интерфейс с един метод, анонимен клас може да бъде пренаписан като ламбда. Ако интерфейсът има повече or по-малко от един метод, тогава ламбда изразът няма да работи и instead of това ще използваме анонимен клас or дори екземпляр на обикновен клас. Сега е време да се поразровим малко в ламбдите. :)

Синтаксис

Общият синтаксис е нещо подобно:

(parameters) -> {method body}
Тоест, скоби около параметрите на метода, "стрелка" (образувана от тире и знак "по-голямо от") и след това тялото на метода в скоби, Howто винаги. Параметрите съответстват на посочените в интерфейсния метод. Ако типовете променливи могат да бъдат недвусмислено определени от компилатора (в нашия случай той знае, че работим с низови масиви, тъй като нашият Listобект е въведен с помощта на String[]), тогава не е нужно да посочвате техните типове.
Ако са двусмислени, посочете типа. IDEA ще го оцвети в сиво, ако не е необходимо.
Можете да прочетете повече в този урок на Oracle и другаде. Това се нарича " целево въвеждане ". Можете да наименувате променливите Howто искате — не е необходимо да използвате същите имена, посочени в интерфейса. Ако няма параметри, просто посочете празни скоби. Ако има само един параметър, просто посочете името на променливата без скоби. Сега, след като разбираме параметрите, е време да обсъдим тялото на ламбда израза. Вътре във фигурните скоби пишете code точно Howто бихте направor за обикновен метод. Ако вашият code се състои от един ред, тогава можете да пропуснете изцяло фигурните скоби (подобно на операторите if и циклите for). Ако вашата едноредова ламбда връща нещо, не е нужно да включвате areturnизявление. Но ако използвате фигурни скоби, тогава трябва изрично да включите returnизраз, точно Howто бихте направor в обикновен метод.

Примери

Пример 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();
    } 
}
Или можем дори да запазим ламбда израза като 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ни казва, че неговите обекти трябва да имат public void run()метод. Според интерфейса runметодът не приема параметри. И не връща нищо, т.е. типът му на връщане е void. Съответно този code ще създаде обект с метод, който не взема or връща нищо. Това напълно съответства на метода Runnableна интерфейса run(). Ето защо успяхме да поставим този ламбда израз в Runnableпроменлива.  Пример 4.

() -> 42
Отново не взема нищо, но връща числото 42. Такъв ламбда израз може да бъде поставен в променлива Callable, защото този интерфейс има само един метод, който изглежда по следния начин:

V call(),
където  V е върнатият тип (в нашия случай,  int). Съответно, можем да запазим ламбда израз, Howто следва:

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) -> x
И ето пример с явен оператор за връщане:

x -> { 
    return x;
}
Или като това със скоби и оператор за връщане:

(x) -> { 
    return x;
}
Или с изрично посочване на типа (и следователно със скоби):

(int x) -> x
Пример 7

x -> ++x
Ние го вземаме xи го връщаме, но само след като добавим 1. Можете да пренапишете тази ламбда така:

x -> x + 1
И в двата случая пропускаме скобите около параметъра и тялото на метода, заедно с returnоператора, тъй като те не са задължителни. Версии със скоби и израз за връщане са дадени в Пример 6. Пример 8

(x, y) -> 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обект, Stringиме и вътрешна възраст. В самия метод използваме предаденото име и възраст, за да зададем променливи на котката. Тъй като нашият catобект е референтен тип, той ще бъде променен извън ламбда израза (ще получи предаденото име и възраст). Ето една малко по-сложна version, която използва подобна ламбда:

public class Main { 

    public static void main(String[] args) { 
        // Create a cat and display it to confirm that it is "empty" 
        Cat myCat = new Cat(); 
        System.out.println(myCat);
 
        // Create a lambda 
        Settable<Cat> s = (obj, name, age) -> { 
            obj.setName(name); 
            obj.setAge(age); 

        }; 

        // Call a method to which we pass the cat and lambda 
        changeEntity(myCat, s); 

        // Display the cat on the screen and see that its state has changed (it has a name and age) 
        System.out.println(myCat); 

    } 

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

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

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

class Cat implements HasNameAndAge { 
    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='Smokey', age=3}
Както можете да видите, Catобектът имаше едно състояние и след това състоянието се промени, след като използвахме ламбда израза. Ламбда изразите се съчетават перфектно с генеричните. И ако трябва да създадем Dogклас, който също така прилага HasNameAndAge, тогава можем да извършим същите операции в Dogметода main() , без да променяме ламбда израза. Задача 3. Напишете функционален интерфейс с метод, който приема число и връща булева стойност. Напишете имплементация на такъв интерфейс като ламбда израз, който връща true, ако предаденото число се дели на 13. Задача 4.Напишете функционален интерфейс с метод, който приема два низа и също така връща низ. Напишете имплементация на такъв интерфейс като ламбда израз, който връща по-дългия низ. Задача 5. Напишете функционален интерфейс с метод, който приема три числа с плаваща запетая: a, b и c и също така връща число с плаваща запетая. Напишете имплементация на такъв интерфейс като ламбда израз, който връща дискриминанта. В случай, че сте забравor, това е D = b^2 — 4ac. Задача 6. Използвайки функционалния интерфейс от Задача 5, напишете ламбда израз, който връща резултата от a * b^c. Обяснение на ламбда изрази в Java. С примери и задачи. Част 2
Коментари
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION