CodeGym/Blog Java/Random-PL/Wyjaśnienie wyrażeń lambda w Javie. Z przykładami i zadan...
John Squirrels
Poziom 41
San Francisco

Wyjaśnienie wyrażeń lambda w Javie. Z przykładami i zadaniami. Część 1

Opublikowano w grupie Random-PL
Dla kogo jest ten artykuł?
  • To jest dla ludzi, którzy myślą, że znają już dobrze Java Core, ale nie mają pojęcia o wyrażeniach lambda w Javie. A może słyszeli coś o wyrażeniach lambda, ale brakuje im szczegółów
  • Jest przeznaczony dla osób, które mają pewną wiedzę na temat wyrażeń lambda, ale nadal są nimi zniechęcone i nie są przyzwyczajone do ich używania.
Wyjaśnienie wyrażeń lambda w Javie.  Z przykładami i zadaniami.  Część 1 - 1Jeśli nie pasujesz do żadnej z tych kategorii, ten artykuł może okazać się nudny, wadliwy lub ogólnie nie dla ciebie. W takim przypadku nie krępuj się przejść do innych rzeczy lub, jeśli jesteś dobrze zorientowany w temacie, proszę o sugestie w komentarzach, jak mógłbym poprawić lub uzupełnić artykuł. Materiał nie twierdzi, że ma jakąkolwiek wartość akademicką, nie mówiąc już o nowości. Wręcz przeciwnie: postaram się opisać rzeczy skomplikowane (dla niektórych osób) możliwie najprościej. Prośba o wyjaśnienie Stream API zainspirowała mnie do napisania tego. Pomyślałem o tym i zdecydowałem, że niektóre z moich przykładów strumieniowych byłyby niezrozumiałe bez zrozumienia wyrażeń lambda. Zaczniemy więc od wyrażeń lambda. Co musisz wiedzieć, aby zrozumieć ten artykuł?
  1. Powinieneś rozumieć programowanie obiektowe (OOP), a mianowicie:

    • klasy, obiekty i różnice między nimi;
    • interfejsy, czym różnią się od klas oraz relacje między interfejsami a klasami;
    • metody, jak je wywoływać, metody abstrakcyjne (czyli metody bez implementacji), parametry metod, argumenty metod i sposób ich przekazywania;
    • modyfikatory dostępu, metody/zmienne statyczne, metody/zmienne finalne;
    • dziedziczenie klas i interfejsów, wielokrotne dziedziczenie interfejsów.
  2. Znajomość Java Core: typy generyczne (generics), kolekcje (listy), wątki.
Cóż, przejdźmy do tego.

Trochę historii

Wyrażenia lambda pojawiły się w Javie z programowania funkcyjnego, a tam z matematyki. W Stanach Zjednoczonych w połowie XX wieku Alonzo Church, który bardzo lubił matematykę i wszelkiego rodzaju abstrakcje, pracował na Uniwersytecie Princeton. To Alonzo Church wynalazł rachunek lambda, który początkowo był zbiorem abstrakcyjnych idei całkowicie niezwiązanych z programowaniem. W tym samym czasie na Uniwersytecie Princeton pracowali matematycy, tacy jak Alan Turing i John von Neumann. Wszystko się połączyło: Church wymyślił rachunek lambda. Turing opracował swoją abstrakcyjną maszynę obliczeniową, znaną obecnie jako „maszyna Turinga”. A von Neumann zaproponował architekturę komputerową, która stanowiła podstawę nowoczesnych komputerów (obecnie nazywana „architekturą von Neumanna”). W tym czasie kościół Alonzo Jego idee nie stały się tak znane jak prace jego kolegów (z wyjątkiem dziedziny czystej matematyki). Jednak nieco później pomysłami Churcha zainteresował się John McCarthy (również absolwent Uniwersytetu Princeton, aw czasie naszej historii pracownik Massachusetts Institute of Technology). W 1958 roku stworzył pierwszy funkcjonalny język programowania, LISP, oparty na tych pomysłach. A 58 lat później idee programowania funkcjonalnego przedostały się do Javy 8. Nie minęło nawet 70 lat... Szczerze mówiąc, nie jest to najdłuższy czas potrzebny na zastosowanie idei matematycznej w praktyce. pracownik Massachusetts Institute of Technology) zainteresował się ideami Churcha. W 1958 roku stworzył pierwszy funkcjonalny język programowania, LISP, oparty na tych pomysłach. A 58 lat później idee programowania funkcjonalnego przedostały się do Javy 8. Nie minęło nawet 70 lat... Szczerze mówiąc, nie jest to najdłuższy czas potrzebny na zastosowanie idei matematycznej w praktyce. pracownik Massachusetts Institute of Technology) zainteresował się ideami Churcha. W 1958 roku stworzył pierwszy funkcjonalny język programowania, LISP, oparty na tych pomysłach. A 58 lat później idee programowania funkcjonalnego przedostały się do Javy 8. Nie minęło nawet 70 lat... Szczerze mówiąc, nie jest to najdłuższy czas potrzebny na zastosowanie idei matematycznej w praktyce.

Sedno sprawy

Wyrażenie lambda jest rodzajem funkcji. Możesz uznać to za zwykłą metodę Java, ale z charakterystyczną zdolnością do przekazywania do innych metod jako argumentu. Zgadza się. Możliwe stało się przekazywanie metodom nie tylko liczb, ciągów znaków i kotów, ale także innych metod! Kiedy możemy tego potrzebować? Byłoby to pomocne, gdybyśmy np. chcieli przekazać jakąś metodę wywołania zwrotnego. To znaczy, jeśli potrzebujemy metody, którą wywołujemy, aby mieć możliwość wywołania innej metody, którą do niej przekazujemy. Innymi słowy, mamy więc możliwość przekazania jednego wywołania zwrotnego w pewnych okolicznościach i innego wywołania zwrotnego w innych. I tak, aby nasza metoda, która odbiera nasze wywołania zwrotne, je wywołała. Sortowanie to prosty przykład. Załóżmy, że piszemy sprytny algorytm sortowania, który wygląda tak:
public void mySuperSort() {
    // We do something here
    if(compare(obj1, obj2) > 0)
    // And then we do something here
}
W ifinstrukcji wywołujemy compare()metodę, przekazując dwa obiekty do porównania i chcemy wiedzieć, który z tych obiektów jest „większy”. Zakładamy, że „większy” występuje przed „mniejszym”. Piszę „większy” w cudzysłowach, ponieważ piszemy uniwersalną metodę, która będzie wiedziała, jak sortować nie tylko rosnąco, ale także malejąco (w tym przypadku „większy” obiekt będzie faktycznie „mniejszym” obiektem , i wzajemnie). Aby ustawić określony algorytm dla naszego sortowania, potrzebujemy mechanizmu, który przekaże go do naszej mySuperSort()metody. W ten sposób będziemy mogli „kontrolować” naszą metodę, gdy zostanie wywołana. Oczywiście moglibyśmy napisać dwie oddzielne metody — mySuperSortAscend()imySuperSortDescend()— do sortowania w porządku rosnącym i malejącym. Możemy też przekazać metodzie jakiś argument (na przykład zmienną logiczną; jeśli to prawda, posortuj w porządku rosnącym, a jeśli fałsz, to w porządku malejącym). Ale co, jeśli chcemy posortować coś skomplikowanego, na przykład listę tablic ciągów? Skąd nasza mySuperSort()metoda będzie wiedziała, jak posortować te tablice ciągów? Według rozmiaru? Przez łączną długość wszystkich słów? Być może alfabetycznie na podstawie pierwszego ciągu w tablicy? A co, jeśli w niektórych przypadkach musimy posortować listę tablic według rozmiaru tablicy, aw innych według łącznej długości wszystkich słów w każdej tablicy? Spodziewam się, że słyszałeś już o komparatorach i że w tym przypadku po prostu przekażemy naszej metodzie sortowania obiekt komparatora, który opisuje pożądany algorytm sortowania. Bo normasort()metoda jest zaimplementowana w oparciu o tę samą zasadę co mySuperSort(), której użyję sort()w moich przykładach.
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);
Wynik:
Dota GTA5 Halo
if then else
I really love Java
Tutaj tablice są sortowane według liczby słów w każdej tablicy. Tablica zawierająca mniej słów jest uważana za „mniejszą”. Dlatego jest na pierwszym miejscu. Tablica zawierająca więcej słów jest uważana za „większą” i umieszczana na końcu. Jeśli przekażemy metodzie inny komparator sort(), taki jak sortByCumulativeWordLength, to otrzymamy inny wynik:
if then else
Dota GTA5 Halo
I really love Java
Teraz tablice are są sortowane według całkowitej liczby liter w słowach tablicy. W pierwszej tablicy jest 10 liter, w drugiej — 12, aw trzeciej — 15. Jeśli mamy tylko jeden komparator, to nie musimy deklarować dla niego osobnej zmiennej. Zamiast tego możemy po prostu utworzyć anonimową klasę w momencie wywołania metody sort(). Coś takiego:
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;
    }
});
Otrzymamy taki sam wynik jak w pierwszym przypadku. Zadanie 1. Przepisz ten przykład tak, aby tablice były sortowane nie w kolejności rosnącej liczby słów w każdej tablicy, ale w kolejności malejącej. To wszystko już wiemy. Wiemy, jak przekazywać obiekty do metod. W zależności od tego, czego w danym momencie potrzebujemy, możemy przekazać różne obiekty do metody, która następnie wywoła metodę, którą zaimplementowaliśmy. To nasuwa pytanie: po co nam tutaj wyrażenie lambda?  Ponieważ wyrażenie lambda jest obiektem, który ma dokładnie jedną metodę. Jak „obiekt metody”. Metoda spakowana w obiekcie. Ma po prostu nieco nieznaną składnię (ale o tym później). Spójrzmy jeszcze raz na ten kod:
arrays.sort(new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
});
Tutaj bierzemy naszą listę tablic i wywołujemy jej sort()metodę, do której jedną compare()metodą przekazujemy obiekt komparatora (jego nazwa nie ma dla nas znaczenia — w końcu to jedyna metoda tego obiektu, więc nie możemy się pomylić). Ta metoda ma dwa parametry, z którymi będziemy pracować. Jeśli pracujesz w IntelliJ IDEA, prawdopodobnie widziałeś, jak oferuje znaczne zagęszczenie kodu w następujący sposób:
arrays.sort((o1, o2) -> o1.length - o2.length);
Zmniejsza to sześć linii do jednej krótkiej. 6 linii jest zapisywanych jako jedna krótka. Coś zniknęło, ale gwarantuję, że nie było to nic ważnego. Ten kod będzie działał dokładnie tak samo, jak w przypadku klasy anonimowej. Zadanie 2. Zgadnij, jak przepisać rozwiązanie do zadania 1, używając wyrażenia lambda (przynajmniej poproś IntelliJ IDEA, aby przekonwertował twoją anonimową klasę na wyrażenie lambda).

Porozmawiajmy o interfejsach

Zasadniczo interfejs jest po prostu listą metod abstrakcyjnych. Kiedy tworzymy klasę, która implementuje jakiś interfejs, nasza klasa musi implementować metody zawarte w interfejsie (lub musimy uczynić klasę abstrakcyjną). Istnieją interfejsy z wieloma różnymi metodami (na przykład  List) i są interfejsy z tylko jedną metodą (na przykład Comparatorlub Runnable). Istnieją interfejsy, które nie mają jednej metody (tak zwane interfejsy znaczników, takie jak Serializable). Interfejsy, które mają tylko jedną metodę, są również nazywane interfejsami funkcjonalnymi . W Javie 8 są one nawet oznaczone specjalną adnotacją:@FunctionalInterface. To właśnie te interfejsy jednometodowe są odpowiednie jako typy docelowe dla wyrażeń lambda. Jak powiedziałem powyżej, wyrażenie lambda to metoda opakowana w obiekt. A kiedy mijamy taki obiekt, zasadniczo mijamy tę pojedynczą metodę. Okazuje się, że nie obchodzi nas, jak nazywa się ta metoda. Jedyne, co ma dla nas znaczenie, to parametry metody i oczywiście treść metody. Zasadniczo wyrażenie lambda jest implementacją funkcjonalnego interfejsu. Wszędzie tam, gdzie widzimy interfejs z pojedynczą metodą, anonimową klasę można przepisać jako lambda. Jeśli interfejs ma więcej lub mniej niż jedną metodę, wówczas wyrażenie lambda nie zadziała i zamiast tego użyjemy klasy anonimowej lub nawet instancji zwykłej klasy. Teraz nadszedł czas, aby zagłębić się trochę w lambdy. :)

Składnia

Ogólna składnia jest mniej więcej taka:
(parameters) -> {method body}
Oznacza to, że nawiasy otaczają parametry metody, „strzałkę” (utworzoną przez myślnik i znak większości), a następnie treść metody w nawiasach klamrowych, jak zawsze. Parametry odpowiadają parametrom określonym w metodzie interfejsu. Jeśli kompilator potrafi jednoznacznie określić typy zmiennych (w naszym przypadku wie, że pracujemy z tablicami łańcuchowymi, bo nasz Listobiekt jest typowany za pomocą String[]), to nie trzeba wskazywać ich typów.
Jeśli są niejednoznaczne, wskaż rodzaj. IDEA pokoloruje go na szaro, jeśli nie będzie potrzebny.
Możesz przeczytać więcej w tym samouczku Oracle i gdzie indziej. Nazywa się to „ wpisywaniem docelowym ”. Możesz nazwać zmienne, jak chcesz — nie musisz używać tych samych nazw, które są określone w interfejsie. Jeśli nie ma parametrów, po prostu wskaż puste nawiasy. Jeśli jest tylko jeden parametr, po prostu podaj nazwę zmiennej bez nawiasów. Teraz, gdy rozumiemy parametry, czas omówić treść wyrażenia lambda. Wewnątrz nawiasów klamrowych piszesz kod tak samo, jak w przypadku zwykłej metody. Jeśli twój kod składa się z pojedynczej linii, możesz całkowicie pominąć nawiasy klamrowe (podobnie jak instrukcje if i pętle for). Jeśli twoja jednowierszowa lambda coś zwraca, nie musisz dołączać areturnoświadczenie. Ale jeśli używasz nawiasów klamrowych, musisz jawnie dołączyć returninstrukcję, tak jak w zwykłej metodzie.

Przykłady

Przykład 1.
() -> {}
Najprostszy przykład. I najbardziej bez sensu :), bo to nic nie daje. Przykład 2.
() -> ""
Inny ciekawy przykład. Nic nie pobiera i zwraca pusty ciąg znaków ( returnjest pomijany, ponieważ jest niepotrzebny). Tutaj to samo, ale z return:
() -> {
    return "";
}
Przykład 3. „Witaj, świecie!” za pomocą lambd
() -> System.out.println("Hello, World!")
Nic nie pobiera i nic nie zwraca (nie możemy tego umieścić returnprzed wywołaniem metody System.out.println(), ponieważ println()typem zwracanym przez metodę jest void). Po prostu wyświetla powitanie. Jest to idealne rozwiązanie do implementacji interfejsu Runnable. Poniższy przykład jest bardziej kompletny:
public class Main {
    public static void main(String[] args) {
        new Thread(() -> System.out.println("Hello, World!")).start();
    }
}
Lub tak:
public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(() -> System.out.println("Hello, World!"));
        t.start();
    }
}
Możemy też zapisać wyrażenie lambda jako Runnableobiekt, a następnie przekazać je konstruktorowi Thread:
public class Main {
    public static void main(String[] args) {
        Runnable runnable = () -> System.out.println("Hello, World!");
        Thread t = new Thread(runnable);
        t.start();
    }
}
Przyjrzyjmy się bliżej momentowi zapisania wyrażenia lambda do zmiennej. Interfejs Runnablemówi nam, że jego obiekty muszą mieć public void run()metodę. Zgodnie z interfejsem runmetoda nie przyjmuje żadnych parametrów. I nic nie zwraca, tzn. zwracany typ to void. W związku z tym ten kod utworzy obiekt z metodą, która niczego nie pobiera ani nie zwraca. To doskonale pasuje do metody Runnableinterfejsu run(). Dlatego mogliśmy umieścić to wyrażenie lambda w Runnablezmiennej.  Przykład 4.
() -> 42
Ponownie nic nie bierze, ale zwraca liczbę 42. Takie wyrażenie lambda można umieścić w Callablezmiennej, ponieważ ten interfejs ma tylko jedną metodę, która wygląda mniej więcej tak:
V call(),
gdzie  V jest zwracanym typem (w naszym przypadku  int). W związku z tym możemy zapisać wyrażenie lambda w następujący sposób:
Callable<Integer> c = () -> 42;
Przykład 5. Wyrażenie lambda obejmujące kilka wierszy
() -> {
    String[] helloWorld = {"Hello", "World!"};
    System.out.println(helloWorld[0]);
    System.out.println(helloWorld[1]);
}
Ponownie, jest to wyrażenie lambda bez parametrów i voidtypu zwracanego (ponieważ nie ma returninstrukcji).  Przykład 6
x -> x
Tutaj bierzemy xzmienną i zwracamy ją. Pamiętaj, że jeśli istnieje tylko jeden parametr, możesz pominąć otaczające go nawiasy. Tutaj to samo, ale z nawiasami:
(x) -> x
A oto przykład z wyraźną instrukcją return:
x -> {
    return x;
}
Lub tak z nawiasami i instrukcją return:
(x) -> {
    return x;
}
Lub z wyraźnym wskazaniem typu (a więc z nawiasami):
(int x) -> x
Przykład 7
x -> ++x
Bierzemy xi zwracamy, ale dopiero po dodaniu 1. Możesz przepisać tę lambdę tak:
x -> x + 1
W obu przypadkach pomijamy nawiasy wokół parametru i treści metody wraz z instrukcją return, ponieważ są one opcjonalne. Wersje z nawiasami i instrukcją return podano w Przykładzie 6. Przykład 8
(x, y) -> x % y
Bierzemy xi yzwracamy resztę z dzielenia xprzez y. Nawiasy wokół parametrów są tutaj wymagane. Są opcjonalne tylko wtedy, gdy istnieje tylko jeden parametr. Oto z wyraźnym wskazaniem typów:
(double x, int y) -> x % y
Przykład 9
(Cat cat, String name, int age) -> {
    cat.setName(name);
    cat.setAge(age);
}
Bierzemy Catobiekt, Stringimię i wiek całkowity. W samej metodzie używamy przekazanego imienia i wieku do ustawienia zmiennych na kocie. Ponieważ nasz catobiekt jest typem referencyjnym, zostanie zmieniony poza wyrażeniem lambda (otrzyma przekazaną nazwę i wiek). Oto nieco bardziej skomplikowana wersja, która używa podobnej lambdy:
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 +
                '}';
    }
}
Wynik:
Cat{name='null', age=0}
Cat{name='Smokey', age=3}
Jak widać Catobiekt miał jeden stan, a następnie stan ten uległ zmianie po zastosowaniu wyrażenia lambda. Wyrażenia lambda doskonale łączą się z rodzajami. A jeśli musimy stworzyć Dogklasę, która również implementuje HasNameAndAge, możemy wykonać te same operacje na Dogmetodzie main() bez zmiany wyrażenia lambda. Zadanie 3. Napisz funkcjonalny interfejs z metodą, która pobiera liczbę i zwraca wartość logiczną. Napisz implementację takiego interfejsu w postaci wyrażenia lambda, które zwraca true, jeśli przekazana liczba jest podzielna przez 13. Zadanie 4.Napisz funkcjonalny interfejs z metodą, która pobiera dwa ciągi znaków i również zwraca ciąg znaków. Napisz implementację takiego interfejsu w postaci wyrażenia lambda, które zwraca dłuższy łańcuch znaków. Zadanie 5. Napisz interfejs funkcjonalny z metodą, która pobiera trzy liczby zmiennoprzecinkowe: a, b i c oraz zwraca liczbę zmiennoprzecinkową. Napisz implementację takiego interfejsu w postaci wyrażenia lambda, które zwraca wyróżnik. Jeśli zapomniałeś, to D = b^2 — 4ac. Zadanie 6. Korzystając z interfejsu funkcjonalnego z Zadania 5, napisz wyrażenie lambda, które zwraca wynik a * b^c. Wyjaśnienie wyrażeń lambda w Javie. Z przykładami i zadaniami. Część 2
Komentarze
  • Popularne
  • Najnowsze
  • Najstarsze
Musisz się zalogować, aby dodać komentarz
Ta strona nie ma jeszcze żadnych komentarzy