JavaRush /Blog Java /Random-PL /Popularny na temat wyrażeń lambda w Javie. Z przykładami ...
Стас Пасинков
Poziom 26
Киев

Popularny na temat wyrażeń lambda w Javie. Z przykładami i zadaniami. Część 1

Opublikowano w grupie Random-PL
Dla kogo jest ten artykuł?
  • Dla tych, którzy myślą, że dobrze znają już Java Core, ale nie mają pojęcia o wyrażeniach lambda w Javie. A może słyszałeś już coś o lambdach, ale bez szczegółów.
  • dla tych, którzy mają pewną wiedzę na temat wyrażeń lambda, ale nadal boją się ich używać i nie są w stanie ich używać.
Jeśli nie należysz do żadnej z tych kategorii, ten artykuł może okazać się nudny, niepoprawny i ogólnie „niefajny”. W takim przypadku możesz przejść obok lub, jeśli jesteś dobrze zorientowany w temacie, zasugeruj w komentarzach, jak mógłbym ulepszyć lub uzupełnić artykuł. Materiał nie ma żadnej wartości akademickiej, a tym bardziej nowości. Wręcz przeciwnie: postaram się w nim opisać skomplikowane (dla niektórych) rzeczy w możliwie najprostszy sposób. Do napisania zainspirowała mnie prośba o wyjaśnienie API strumieniowego. Przemyślałem to i doszedłem do wniosku, że bez zrozumienia wyrażeń lambda niektóre moje przykłady dotyczące „strumieni” będą niezrozumiałe. Zacznijmy więc od lambd. Popularny na temat wyrażeń lambda w Javie.  Z przykładami i zadaniami.  Część 1 - 1Jaka wiedza jest wymagana, aby zrozumieć ten artykuł:
  1. Znajomość programowania obiektowego (zwanego dalej OOP), a mianowicie:
    • wiedza czym są klasy i obiekty, jaka jest między nimi różnica;
    • wiedza czym są interfejsy, czym różnią się od klas, jakie jest pomiędzy nimi powiązanie (interfejsy i klasy);
    • wiedza czym jest metoda, jak ją nazwać, czym jest metoda abstrakcyjna (lub metoda bez implementacji), jakie są parametry/argumenty metody, jak je tam przekazać;
    • modyfikatory dostępu, metody/zmienne statyczne, metody/zmienne końcowe;
    • dziedziczenie (klasy, interfejsy, wielokrotne dziedziczenie interfejsów).
  2. Znajomość Java Core: generyczne, kolekcje (listy), wątki.
Cóż, zaczynajmy.

Trochę historii

Wyrażenia lambda pojawiły się w Javie z programowania funkcjonalnego, a tam z matematyki. W połowie XX wieku w Ameryce na Uniwersytecie Princeton pracował niejaki Alonzo Church, który bardzo lubił matematykę i wszelkiego rodzaju abstrakcje. To Alonzo Church wymyślił rachunek lambda, który początkowo był zbiorem abstrakcyjnych idei i nie miał nic wspólnego z programowaniem. W tym samym czasie na tym samym Uniwersytecie Princeton pracowali matematycy, tacy jak Alan Turing i John von Neumann. Wszystko się połączyło: Church wymyślił system rachunku lambda, Turing opracował swoją abstrakcyjną maszynę liczącą, znaną obecnie jako „maszyna Turinga”. Cóż, von Neumann zaproponował schemat architektury komputerów, który stanowił podstawę współczesnych komputerów (i obecnie nazywa się go „architekturą von Neumanna”). Idee Alonzo Churcha nie zyskały wówczas takiej sławy, jak prace jego kolegów (z wyjątkiem dziedziny „czystej” matematyki). Jednak nieco później ideą Churcha zainteresował się niejaki John McCarthy (również absolwent Uniwersytetu Princeton, w chwili pisania tej historii pracownik Massachusetts Institute of Technology). Na ich podstawie w 1958 roku stworzył pierwszy funkcjonalny język programowania Lisp. A 58 lat później idee programowania funkcjonalnego wyciekły do ​​Javy jako numer 8. Nie minęło nawet 70 lat... Tak naprawdę nie jest to najdłuższy okres czasu na zastosowanie idei matematycznej w praktyce.

Esencja

Wyrażenie lambda jest taką funkcją. Można o tym myśleć jak o zwykłej metodzie w Javie, z tą tylko różnicą, że można ją przekazać innym metodom jako argument. Tak, możliwe stało się przekazywanie do metod nie tylko liczb, ciągów znaków i kotów, ale także innych metod! Kiedy możemy tego potrzebować? Na przykład, jeśli chcemy przekazać jakieś wywołanie zwrotne. Metoda, którą wywołujemy, jest nam potrzebna, aby móc wywołać inną metodę, którą jej przekazujemy. Oznacza to, że w niektórych przypadkach mamy możliwość przesłania jednego wywołania zwrotnego, a w innych innego. A nasza metoda, która akceptowałaby nasze wywołania zwrotne, wywołałaby je. Prostym przykładem jest sortowanie. Załóżmy, że piszemy skomplikowane sortowanie, które wygląda mniej więcej tak:
public void mySuperSort() {
    // ... zrób coś tutaj
    if(compare(obj1, obj2) > 0)
    // ...i tu coś robimy
}
Where ifnazywamy metodą compare(), przekazujemy tam dwa obiekty, które porównujemy i chcemy się dowiedzieć, który z tych obiektów jest „większy”. Przedstawimy ten, który jest „więcej”, przed tym, który jest „mniejszy”. Napisałem „więcej” w cudzysłowie, ponieważ piszemy uniwersalną metodę, która będzie mogła sortować nie tylko w porządku rosnącym, ale i malejącym (w tym przypadku „więcej” będzie obiektem, który jest zasadniczo mniejszy i odwrotnie) . Aby ustawić regułę określającą dokładnie sposób sortowania, musimy w jakiś sposób przekazać ją do naszego pliku mySuperSort(). W takim przypadku będziemy mogli w jakiś sposób „kontrolować” naszą metodę w trakcie jej wywoływania. Oczywiście możesz napisać dwie osobne metody mySuperSortAsc()sortowania mySuperSortDesc()w kolejności rosnącej i malejącej. Lub przekaż jakiś parametr wewnątrz metody (na przykład booleanif truesortuj w kolejności rosnącej, a jeśli falsew kolejności malejącej). Co jednak, jeśli chcemy posortować nie jakąś prostą strukturę, ale na przykład listę tablic ciągów znaków? Skąd nasza metoda będzie mySuperSort()wiedzieć, jak sortować te tablice ciągów? Na wymiar? Według całkowitej długości słów? Być może alfabetycznie, w zależności od pierwszego wiersza w tablicy? Co jednak, jeśli w niektórych przypadkach będziemy musieli posortować listę tablic według rozmiaru tablicy, a w innym przypadku według całkowitej długości słów w tablicy? Myślę, że słyszałeś już o komparatorach i że w takich przypadkach po prostu przekazujemy obiekt komparatora do naszej metody sortowania, w której opisujemy zasady, według których chcemy sortować. Ponieważ metoda standardowa sort()jest realizowana na tej samej zasadzie, co mySuperSort()w przykładach, posłużę się metodą standardową sort().
String[] array1 = {"Matka", "mydło", "rama"};
String[] array2 = {"I", "Bardzo", "Kocham", "java"};
String[] array3 = {"świat", "praca", "Móc"};

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);
Wynik:
  1. mama umyła ramę
  2. pokój Pracy może
  3. Naprawdę kocham Javę
Tutaj tablice są sortowane według liczby słów w każdej tablicy. Tablicę zawierającą mniej słów uważa się za „mniejszą”. Dlatego pojawia się na początku. Ten, w którym jest więcej słów, jest uważany za „więcej” i kończy się na końcu. Jeżeli do metody sort()przekażemy inny komparator (sortByWordsLength)to wynik będzie inny:
  1. pokój Pracy może
  2. mama umyła ramę
  3. Naprawdę kocham Javę
Teraz tablice są sortowane według całkowitej liczby liter w słowach takiej tablicy. W pierwszym przypadku jest ich 10, w drugim 12, a w trzecim 15. Jeśli użyjemy tylko jednego komparatora, to nie możemy stworzyć dla niego osobnej zmiennej, a po prostu stworzyć obiekt anonimowej klasy bezpośrednio przy czas wywołania metody sort(). Tak:
String[] array1 = {"Matka", "mydło", "rama"};
String[] array2 = {"I", "Bardzo", "Kocham", "java"};
String[] array3 = {"świat", "praca", "Móc"};

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;
    }
});
Wynik będzie taki sam jak w pierwszym przypadku. Zadanie 1 . Przepisz ten przykład tak, aby sortował tablice nie w kolejności rosnącej według liczby słów w tablicy, ale w kolejności malejącej. To wszystko już wiemy. Wiemy jak przekazywać obiekty do metod, możemy przekazać ten lub inny obiekt do metody w zależności od tego, czego w danej chwili potrzebujemy, a wewnątrz metody, w której przekazujemy taki obiekt, zostanie wywołana metoda, dla której pisaliśmy implementację . Powstaje pytanie: co mają z tym wspólnego wyrażenia lambda? Biorąc pod uwagę, że lambda jest obiektem zawierającym dokładnie jedną metodę. To jest jak obiekt metody. Metoda zawinięta w obiekt. Mają po prostu nieco nietypową składnię (ale o tym później). Przyjrzyjmy się jeszcze raz temu wpisowi
arrays.sort(new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
});
Tutaj bierzemy naszą listę arraysi wywołujemy jej metodę sort(), gdzie przekazujemy obiekt komparatora za pomocą jednej metody compare()(nie ma dla nas znaczenia, jak się ona nazywa, ponieważ jest jedyna w tym obiekcie, nie przegapimy tego). Metoda ta przyjmuje dwa parametry, z którymi będziemy dalej pracować. Jeśli pracujesz w IntelliJ IDEA , prawdopodobnie widziałeś, jak oferuje ci ten kod do znacznego skrócenia:
arrays.sort((o1, o2) -> o1.length - o2.length);
W ten sposób sześć linijek zamieniło się w jedną krótką. 6 linijek przepisano w jedną krótką. Coś zniknęło, ale gwarantuję, że nie zniknęło nic ważnego, a ten kod będzie działał dokładnie tak samo, jak z klasą anonimową. Zadanie 2 . Dowiedz się, jak przepisać rozwiązanie problemu 1 za pomocą wyrażeń lambda (w ostateczności poproś IntelliJ IDEA o przekształcenie twojej anonimowej klasy w lambdę).

Porozmawiajmy o interfejsach

Zasadniczo interfejs to po prostu lista abstrakcyjnych metod. Kiedy tworzymy klasę i mówimy, że będzie ona implementować jakiś interfejs, musimy napisać w naszej klasie implementację metod, które są wymienione w interfejsie (lub w ostateczności nie pisać tego, ale uczynić klasę abstrakcyjną ). Istnieją interfejsy z wieloma różnymi metodami (na przykład List), istnieją interfejsy z tylko jedną metodą (na przykład ten sam Komparator lub Runnable). Istnieją interfejsy, które w ogóle nie posiadają jednej metody (tzw. interfejsy znacznikowe, np. Serializable). Interfejsy posiadające tylko jedną metodę nazywane są także interfejsami funkcjonalnymi . W Javie 8 są one nawet oznaczone specjalną adnotacją @FunctionalInterface . To interfejsy z jedną metodą, które nadają się do użycia w wyrażeniach lambda. Jak powiedziałem powyżej, wyrażenie lambda jest metodą opakowaną w obiekt. A kiedy gdzieś przekazujemy taki obiekt, w rzeczywistości przekazujemy tę jedną metodę. Okazuje się, że nie ma dla nas znaczenia, jak nazwie się tę metodę. Dla nas ważne są tylko parametry, jakie przyjmuje ta metoda, a właściwie sam kod metody. Zasadniczo wyrażenie lambda jest. implementacja interfejsu funkcjonalnego. Tam, gdzie widzimy interfejs z jedną metodą, oznacza to, że taką anonimową klasę możemy przepisać za pomocą lambdy. Jeśli interfejs ma więcej/mniej niż jedną metodę, to wyrażenie lambda nam nie będzie odpowiadać i skorzystamy z klasy anonimowej, lub nawet zwykłej. Czas zagłębić się w lambdy. :)

Składnia

Ogólna składnia jest mniej więcej taka:
(параметры) -> {тело метода}
Oznacza to, że w nawiasach znajdują się parametry metody, „strzałka” (są to dwa znaki z rzędu: minus i większy), po czym treść metody jak zawsze jest ujęta w nawiasy klamrowe. Parametry odpowiadają parametrom określonym w interfejsie przy opisie metody. Jeżeli kompilator potrafi jasno określić typ zmiennych (w naszym przypadku wiadomo na pewno, że pracujemy z tablicami ciągów znaków, bo Listtypuje się je precyzyjnie poprzez tablice ciągów znaków), to typ zmiennych String[]nie musi być zostać napisanym.
Jeśli nie jesteś pewien, określ typ, a IDEA podświetli go na szaro, jeśli nie będzie potrzebny.
Więcej informacji można znaleźć na przykład w samouczku Oracle . Nazywa się to „wpisywaniem celu” . Zmiennym można nadać dowolne nazwy, niekoniecznie te określone w interfejsie. Jeśli nie ma parametrów, po prostu nawiasy. Jeśli jest tylko jeden parametr, wystarczy nazwa zmiennej bez nawiasów. Uporządkowaliśmy parametry, teraz o treści samego wyrażenia lambda. Wewnątrz nawiasów klamrowych napisz kod jak w przypadku zwykłej metody. Jeśli cały kod składa się tylko z jednej linii, nie musisz w ogóle pisać nawiasów klamrowych (jak w przypadku if i pętli). Jeśli twoja lambda coś zwraca, ale jej ciało składa się z jednej linii, returnnie ma potrzeby pisania. Ale jeśli masz nawiasy klamrowe, to, jak w zwykłej metodzie, musisz jawnie napisać return.

Przykłady

Przykład 1.
() -> {}
Najprostsza opcja. I ten najbardziej bezsensowny :).Bo nic nie daje. Przykład 2.
() -> ""
Również ciekawa opcja. Nie akceptuje niczego i zwraca pusty ciąg znaków ( returnpominięty jako niepotrzebny). To samo, ale z return:
() -> {
    return "";
}
Przykład 3. Witaj świecie przy użyciu lambd
() -> System.out.println("Hello world!")
Nic nie otrzymuje, nic nie zwraca (nie możemy umieścić returnprzed wywołaniem System.out.println(), ponieważ typ zwrotu w metodzie println() — void)po prostu wyświetla napis na ekranie. Idealny do implementacji interfejsu Runnable. Ten sam przykład jest pełniejszy:
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 obiekt typu Runnable, 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ć metodę public void run(). Zgodnie z interfejsem metoda run nie akceptuje niczego jako parametrów. I nic nie zwraca (void). Dlatego pisząc w ten sposób, zostanie utworzony obiekt za pomocą jakiejś metody, która niczego nie akceptuje ani nie zwraca. Co jest całkiem spójne z metodą run()w interfejsie Runnable. Dlatego mogliśmy umieścić to wyrażenie lambda w zmiennej takiej jak Runnable. Przykład 4
() -> 42
Ponownie niczego nie przyjmuje, ale zwraca liczbę 42. To wyrażenie lambda można umieścić w zmiennej typu Callable, ponieważ ten interfejs definiuje tylko jedną metodę, która wygląda mniej więcej tak:
V call(),
gdzie Vjest typem zwracanej wartości (w naszym przypadku int). W związku z tym możemy przechowywać takie wyrażenie lambda w następujący sposób:
Callable<Integer> c = () -> 42;
Przykład 5. Lambda w kilku liniach
() -> {
    String[] helloWorld = {"Hello", "world!"};
    System.out.println(helloWorld[0]);
    System.out.println(helloWorld[1]);
}
Ponownie jest to wyrażenie lambda bez parametrów i typu zwracanego void(ponieważ nie ma return). Przykład 6
x -> x
Tutaj bierzemy coś do zmiennej хi zwracamy to. Należy pamiętać, że jeśli zostanie zaakceptowany tylko jeden parametr, nawiasy wokół niego nie muszą być zapisywane. To samo, ale z nawiasami:
(x) -> x
A oto opcja z wyraźną opcją return:
x -> {
    return x;
}
Lub tak, z nawiasami i return:
(x) -> {
    return x;
}
Lub z wyraźnym wskazaniem typu (i odpowiednio w nawiasach):
(int x) -> x
Przykład 7
x -> ++x
Akceptujemy хi zwracamy, ale za 1więcej. Możesz też przepisać to w ten sposób:
x -> x + 1
W obu przypadkach nie umieszczamy nawiasów wokół parametru, treści metody i słowa return, ponieważ nie jest to konieczne. Opcje z nawiasami i zwrotem opisano w przykładzie 6. Przykład 8
(x, y) -> x % y
Akceptujemy część хi уzwracamy resztę podziału xprzez y. Nawiasy wokół parametrów są już tutaj wymagane. Są opcjonalne tylko wtedy, gdy istnieje tylko jeden parametr. W ten sposób 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);
}
Akceptujemy obiekt Cat, ciąg znaków z nazwą i wiekiem będącym liczbą całkowitą. W samej metodzie przekazujemy Catowi imię i wiek. catPonieważ nasza zmienna jest typem referencyjnym, obiekt Cat znajdujący się poza wyrażeniem lambda ulegnie zmianie (otrzyma przekazaną wewnątrz nazwę i wiek). Nieco bardziej skomplikowana wersja, która wykorzystuje podobną lambdę:
public class Main {
    public static void main(String[] args) {
        // utwórz kota i wydrukuj na ekranie, aby upewnić się, że jest „pusty”
        Cat myCat = new Cat();
        System.out.println(myCat);

        // utwórz lambdę
        Settable<Cat> s = (obj, name, age) -> {
            obj.setName(name);
            obj.setAge(age);
        };

        // wywołanie metody, której przekazujemy kota i lambdę
        changeEntity(myCat, s);
        // wyświetl na ekranie i zobacz, że stan kota się zmienił (ma imię i wiek)
        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 +
                '}';
    }
}
Wynik: Cat{name='null', age=0} Cat{name='Murzik', age=3} Jak widać, obiekt Cat początkowo miał jeden stan, lecz po użyciu wyrażenia lambda stan się zmienił . Wyrażenia lambda dobrze współpracują z wyrażeniami rodzajowymi. A jeśli potrzebujemy Dognp. stworzyć klasę, która będzie również implementować WithNameAndAge, to w metodzie main()możemy wykonać te same operacje z Dogiem, w ogóle nie zmieniając samego wyrażenia lambda. Zadanie 3 . Napisz interfejs funkcjonalny z metodą pobierającą liczbę i zwracającą wartość logiczną. Napisz implementację takiego interfejsu w postaci wyrażenia lambda, które zwraca, truejeśli przekazana liczba jest podzielna przez 13 bez reszty.Zadanie 4 . Napisz interfejs funkcjonalny z metodą, która pobiera dwa ciągi znaków i zwraca ten sam ciąg. Napisz implementację takiego interfejsu w postaci lambdy zwracającej najdłuższy ciąg znaków. Zadanie 5 . Napisz interfejs funkcjonalny za pomocą metody, która akceptuje trzy liczby ułamkowe: , ai zwraca tę samą liczbę ułamkową. Napisz implementację takiego interfejsu w postaci wyrażenia lambda zwracającego dyskryminator. Kto zapomniał, D = b^2 - 4ac . Zadanie 6 . Korzystając z interfejsu funkcjonalnego z zadania 5, napisz wyrażenie lambda zwracające wynik operacji . Popularny na temat wyrażeń lambda w Javie. Z przykładami i zadaniami. Część 2.bca * b^c
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION