Lambda wyrażenia

Osoby rozumiejące motywacje i mechanizmy za lambda wyrażeniami, mogą przejrzeć część omawiającą, na pewno warto obejrzeć Przyklad7 i Przyklad8 i przejść od razu do zadania (na końcu).

0-1. Wstęp

Pamiętacie assertThrows z JUnit?

    assertThrows( () -> testowanaMetoda(1,0) );

to w środku to właśnie lambda wyrażenie (ang. lambda expression).
BTW: Lambda λ to symbol anonimowej funkcji (na pewno pamiętacie z Podstaw matematyki, np. λx.x+2 :)
Symbol pochodzi z rachunku lambda (https://pl.wikipedia.org/wiki/Rachunek_lambda).

Ale po co to w ogóle tu jest? Nie mogłoby być tak?

    assertThrows( testowanaMetoda(1,0) );

No właśnie nie mogłoby... patrz Przyklad0

Z jakich dokładnie części to się wszystko składa?

patrz Przyklad1

BTW: interfejs funkcyjny z funkcją bezparametrową używany jest nie tylko w JUnit. Również w wątkach pojawia się interfejs Runnable z jedną metodą void run()

    new Thread( () -> System.out.println("biegnę!") ).start();

<Ogólnie wszędzie tam gdzie chcemy wykonać jakiś kod być może wielokrotnie ale w innej sytuacji>

Lambda wyrażenie oczywiście może przyjmować argumenty i obliczać jakiś wynik, ale o tym za chwilę.

2-3. Jak to się ma do klas i obiektów?

Lambda wyrażenie formalnie jest obiektem nowej (anonimowej) klasy implementującej (tą jedną metodą) swój interfejs.
Patrz Przyklad2 i Przyklad3.
Ale tak naprawdę w skompilowanym kodzie z lambda-wyrażeniem nie pojawia się żadna klasa... Porównaj (po skompilowaniu całego projektu):

   ls -l out/production/*/p01/ocochodzi/przyklad1/*
   ls -l out/production/*/p23/cotojest/przyklad3/*

Dla ciekawskich:

   javap -p -v out/production/*/p01/ocochodzi/przyklad1/Przyklad1.class | less

Widać metodę prywatną lambda$main$0 (na dole),
w main:

   0: invokedynamic #7,  0              // InvokeDynamic #0:rob:()Lp01/ocochodzi/przyklad1/JakisKod;

i na samym dole

BootstrapMethods:
  0: #53 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #60 ()V
      #61 REF_invokeStatic p01/ocochodzi/przyklad1/Przyklad1.lambda$main$0:()V
      #60 ()V
InnerClasses:
  public static final #69= #65 of #67;    // Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles

Czyli ten obiekt i ta anonimowa klasa są robione magicznie (czyli wywołaniem wewnętrznej metody LambdaMetafactory.metafactory) z dodatkowej prywatnej metody.

4-8. Co z tym można zrobić?

patrz Przyklad4

patrz Przyklad5

patrz Przyklad6

patrz Przyklad7

Ogólna składnia lambda wyrażenia to jedno z:

parametry -> wynik
parametry -> { instrukcje }

gdzie parametry to jedno z:

()
nazwa
(nazwa₁, ..., nazwaₙ)
(Typ₁ nazwa₁, ..., Typₙ nazwaₙ)

a wynik to wyrażenie (może być typu void)

W bibliotece standardowej przydają się do:

patrz Przyklad8

BTW: Trzy odsłony odwołania do metody

Większość interfejsów funkcyjnych z biblioteki standardowej opisana jest w pakiecie
java.util.function

Tam jest strasznie dużo interfejsów, ale to przez typy bazowe (i dążenie do efektywności).
Tak naprawdę interfejsy są kilku rodzajów:

Dodatkowo

Część z tych interfejsów ma dodatkowe metody statyczne i domyślne, wspomagające tworzenie odp. funkcji.

Warto też obejrzeć oficjalny tutorial Javy na temat lambda wyrażeń. Jest tam jeden spójny przykład: wyciągnij niektóre (Predicate) elementy z kolekcji, przerób je (Function) i obsłuż (Consumer), w wersji stopniowo coraz bardziej abstrakcyjnej.

Zadanie

Zadanie będzie o interfejsie Porównywacz (wszelkie podobieństwo nieprzypadkowe :)
Trzeba samemu przez to przejść, żeby potem świadomie z tych wszystkich funkcji korzystać. Dla zaawansowanych proponuję "szybką ścieżkę": punkty 1, 4,5, 7,8,9 (ale przeczytać trzeba wszystko).

  1. Przerób dany algorytm sortujący (wraz z porównywaczem) na wersję generyczną.

  2. Posortuj, używając załączonej metody SortowaniePrzezWybieranie oraz lambda wyrażenia, podane samochody wg:

    • ceny
    • liczby miejsc
    • alfabetycznie po nazwie marki i modelu (tym razem możesz po prostu użyć łączenia napisów)

    Możesz oczywiście używać metod Integer::compare i String::compareTo

To trochę nudne, bo trzeba wyciągać dane z jednego obiektu, wyciągać dane z drugiego obiektu i potem je porównywać. Pomóżmy sobie trochę:

  1. Zrób gdzieś metodę, która połączy wyciąganie z porównywawaniem... czyli robimy nowy interfejs "wyciągnij int z samochodu" (np. IntWyciagacz) oraz metodę porownywaczZIntWyciagacza, która zamieni wyciągacz w porównywacz samochodów :) Czy to powinna być metoda statyczna czy obiektowa? Użyj jej do (wygodniejszego) sortowania samochodów wg ceny, wg liczby osób.

  2. Uogólnij Wyciągacz na typ O i W (wyciągacz wyciąga typ wyciągnięty W z obiektu O) oraz zrób generyczną metodę zamieniacz, która z Wyciągacza

  3. Wykorzystaj utworzoną metodę zamieniacz do posortowania samochodów wg ceny oraz wg marki i modelu

Teraz posortuj samochody wg pary marka, cena, ale bez używania łączenia napis+liczba! To byłoby oszukiwanie :)

  1. Napisz odpowiedni porównywacz ręcznie. Możesz to zrobić jako lambda wyrażenie, albo jako osobna metoda. W tej sytuacji użyj odwołania do metody (method reference) w wywołaniu sortowania.

  2. Napisz metodę kolejno, która bierze Porownywacz

  3. Umieść odpowiednie wersje metod zamieniacz i kolejno w interfejsie Porownywacz jako metody default i/lub static. Skorzystaj z nich do sortowania wg ceny oraz wg pary marka + cena.

A teraz zobacz, jak to się robi naprawdę:

  1. Skorzystaj z metody sort w klasie list, używając odpowiednich metod z interfejsu Comparator.
    Zauważ, że są tam metody podobne do naszej porownywaczZIntWyciagacza,
    inne z kolei bazują na porównaniu właściwym dla danej klasy (interfejs Comparable).
    Zadanie można rozwiązać na parę sposobów. Nie krępuj się :)

Na koniec przejrzyj pozostałe metody z interfejsu Comparator i zastanów się jak mogą być zaimplementowane...