Appa Notka.
Ten rozdział jest udostępniany za darmo z okazji premiery Javy 15 (GA), która nastąpiła we wrześniu.
Rozdział będzie dostępny bezpłatnie do odwołania.
Interfejsy funkcyjne zostały wprowadzone do Javy 8 w ramach nowego pakietu
java.util.function.
Dzielą się na cztery podstawowe kategorie, których skrótowe opisy można zdefiniować w następujący sposób:
- Supplier - nie przyjmuje żadnego obiektu na wejściu, ale zwraca obiekt (dostawca)
- Consumer - przyjmuje obiekt na wejściu, ale niczego nie zwraca (konsumer)
- Predicate - przyjmuje obiekt na wejściu i zwraca wartość PRAWDA lub FAŁSZ (predykat)
- Function - przyjmuje obiekt na wejściu i zwraca obiekt na wyjściu (funkcja)
Teraz przejdziemy do dokładnego omówienia wszystkich typów wraz z ich rozszerzeniami, ponieważ zgodnie z tym, co napisaliśmy we wstępie,
powyższe kategorie są jedynie podstawowym podziałem, a z nich wywodzi się sporo interfejsów funkcyjnych o dodatkowych możliwościach.
Interfejs funkcyjny Supplier
Pierwszym omawianym przez nas interfejsem jest
Supplier, którego użycie sprowadza się do wykorzystania prostej metody
get. Pamiętajmy, że
skoro jest to interfejs funkcyjny, to zawiera on tylko jedną metodę abstrakcyjną i
w tym przypadku jest to właśnie metoda
get:
Widzimy, że metoda nie przyjmuje żadnych parametrów, ale zwraca obiekt określonego typu. Jakiego typu?
Oczywiście to już zależy od tego, jakiego użyjemy
parametru typu.
Tak wygląda definicja nowego (od Javy 8) interfejsu. Co jednak, gdybyśmy sami chcieli stworzyć interfejs funkcyjny?
Jak może on wyglądać? Wróćmy do interfejsu, o którym wspominaliśmy już w tym kursie:
Ten interfejs jest interfejsem funkcyjnym
Supplier, ponieważ posiada jedną metodę,
która nie przyjmuje argumentów, ale zwraca rezultat. W tym przypadku jest to obiekt typu
String.
I teraz najważniejsze. Pamiętasz nasze pierwsze
wyrażenie lambda, przygotowane w rozdziale
Wyrażenia lambda - Starter?
Wynik wykonania kodu:
Tak skonstruowane wyrażenie lambda jest implementacją interfejsu
Supplier o dokładnie określonym
parametrze typu. Dlatego też możemy je zapisać w postaci:
Wynik wykonania kodu:
Skoro ma ono określony typ, to możemy go użyć tak, jak pozwalają na to reguły Javy.
Na przykład możemy wykorzystać go jako
typ parametru metody.
W ten sposób możemy w
jednym miejscu zadeklarować wykonanie kodu metody, by następnie
w zupełnie innym miejscu realnie go wykonać.
Wykonanie polega na wywołaniu metody
get interfejsu
Supplier.
Ma to sens, ponieważ jak wiemy, mamy tu do czynienia z dostawcą (z ang. supplier) i tak też działa metoda, która nie ma parametrów, a jedynie zwraca (dostarcza)
konkretną wartość.
Wynik wykonania kodu:
Appa Notka. Metoda useSupplier jest metodą statyczną, ponieważ chcemy ją uruchomić
szybko i sprawnie (bez dodatkowego kodu) w ramach statycznej metody main. Oczywiście nic nie stoi na przeszkodzie, aby zmienić
sygnaturę metody useSupplier na niestatyczną, następnie stworzyć obiekt klasy Start i na tym obiekcie wywołać
tak zmienioną metodę. Nie stanowi to wartości edukacyjnej w ramach omawianego tematu. Niemniej w ramach ćwiczenia możesz zmodyfikować kod tak, by poruszać się w kontekście obiektu.
Niezwykle ważne jest, aby zrozumieć to, że
kluczowe jest wyrażenie lambda znajdujące się po prawej stronie operatora przypisania.
Musi ono pasować do typu interfejsu funkcyjnego zmiennej określonej po lewej stronie.
Tak skonstruowane wyrażenie lambda (bez parametrów i z jednym zwracanym obiektem, w tym przypadku stringiem) będzie pasowało do interfejsu z jedną metodą abstrakcyjną, która nie przyjmuje argumentów, a zwraca
obiekt typu
String. W innym przypadku kod nie skompiluje się:
Po prostu wyrażenie lambda po prawej stronie nie jest typu interfejsu
Supplier. Wyrażenie przyjmuje na wejściu jeden parametr
i zwraca też jedną wartość, tak więc jest to wyrażenie typu
Function. O tym jednak powiemy sobie nieco później.
Najpierw zobaczmy, jak wygląda
Consumer.
Interfejs funkcyjny Consumer
Interfejs funkcyjny
Consumer posiada jedną metodę abstrakcyjną
accept i wygląda tak:
Kropki oznaczają występowanie innych metod, jednak one nas nie interesują, ponieważ nie są abstrakcyjne.
Interfejs
Item z poprzedniego przykładu można bardzo łatwo zmienić tak, by stał się interfejsem
funkcyjnym typu
Consumer. Wystarczy, że zamiast metody
getDescription
wprowadzimy na przykład metodę
printDescription, która będzie przyjmowała argument, ale nie będzie
niczego zwracała.
W ten sposób skonstruujemy wyrażenie lambda:
Wynik wykonania kodu:
A tak będzie wyglądało to samo wyrażenie lambda przypisane do bazowego interfejsu
Consumer:
Wynik wykonania kodu:
Dodatkowo przekazujemy tutaj obiekt consumera do metody
useConsumer i dopiero tam wywołujemy kod
zdefiniowany wczesniej w postaci wyrażenia lambda.
Oczywiście kod naszego wyrażenia lambda moze być bardziej rozbudowany (podobnie zresztą jak w przypadku suppliera oraz implementacji innych interfejsów funkcyjnych).
Wtedy całość kodu obejmujemy klamrami, tworząc jedną instrukcję blokową:
Wynik wykonania kodu:
Na uwagę zasługuje tutaj fakt, że dzięki zastosowaniu wyrażenia lambda możemy w naszym algorytmie skorzystać ze zmiennej
initialId
zainicjowanej w metodzie
main, a wykonanie obliczeń i tak zostanie wykonane dopiero po uruchomieniu metody
accept,
w zupełnie innym miejscu programu.
Interfejs funkcyjny Predicate
Interfejs funkcyjny
Predicate to bardzo ciekawy przypadek, gdyż metodę abstrakcyjną
test, która zarówno przyjmuje parametr na wejściu,
jak i zwraca wartość na wyjściu. Co więcej, wartość ta może być tylko wartością logiczną PRAWDA lub FAŁSZ.
Oczywiście sami także możemy przygotować własny interfejs, który będzie spełniał podane założenia, ale to pozostawiamy
Ci do wykonania w ramach ćwiczenia. Tak więc po przeczytaniu tego paragrafu stwórz interfejs, który będzie miał jedną metodę
abstrakcyjną przyjmującą na wejściu obiekt, np. typu
String i zwracającą na wyjściu wartość
boolean.
My natomiast pokażemy teraz jak zaimplementować taki przypadek z użyciem domyślnego interfejsu funkcyjnego
Predicate:
Wynik wykonania kodu:
Appa Notka. Zwróć uwagę, że w przypadku interfejsów funkcyjnych
zwracających wartość, dla których wyrażenie lambda NIE zawiera instrukcji blokowej (wielolinijkowego kodu), nie stosujemy słowa kluczowego return.
Od razu wpisujemy to, co ma zostać zwrócone. W naszym przykładzie zwrócony zostanie wynik boolean ze sprawdzenia, czy długość tekstu zmiennej name
jest większa niż trzy (name -> name.length() > 3). W przypadku gdy stosujemy instrukcję blokową, wymagane jest użycie na jej końcu słowa return.
Taki przykład pokazujemy poniżej.
Załóżmy teraz, że nasz algorytm jest nieco bardziej rozbudowany i wymaga użycia kilku linii kodu. W takiej sytuacji używamy instrukcji blokowej,
a na końcu w celu zwrócenia wartości, musimy użyć słowa kluczowego
return.
Wynik wykonania kodu:
Interfejs funkcyjny Function
Wreszcie dotarliśmy do najsilniejszego interfejsu funkcyjnego. Jego moc bierze się stąd, że posiada on metodę,
która pozwala przyjąć wartość na wejściu, zwraca wartość na wyjściu, a do tego te wartości mogą być praktycznie dowolnego typu.
Zobaczmy teraz, jak będzie wyglądała implementacja takiego interfejsu za pomocą wyrażenia lambda.
To, co powinno zwrócić Twoją szczególną uwagę, to
oznaczenie dwóch typów.
Pierwszy oznacza typ obiektu przyjmowanego w postaci parametru, a drugi typ obiektu zwracanego przez funkcję.
Wynik wykonania kodu:
Oczywiście tak jak zawsze, tutaj również możesz użyć instrukcji blokowej.
Wynik wykonania kodu:
Ogólnie rzecz biorąc, tego typu instrukcje blokowe są skonstruowane podobnie jak metody, dlatego też możemy tu w prosty sposób implementować dosyć
złożone algorytmy. Warto pamiętać, aby po zamknięciu instrukcji dodać
średnik. Inaczej kod nam się nie skompiluje.
W ten sposób przebrnęliśmy przez cztery podstawowe kategorie interfejsów funkcyjnych. To jednak nie koniec, gdyż
tak naprawdę powyższe interfejsy ciągle mają swoje ograniczenia. Na przykład możesz się zastanawiać, co się dzieje w przypadku gdy
potrzebujemy przekazać dwa parametry do wyrażenia lambda? Wszystkie dotychczasowe przykłady operowały na maksymalnie jednym parametrze.
Odpowiedź na to pytanie znajdziesz w kolejnym rozdziale, gdzie przedstawiamy wariacje wymienionych do tej pory interfejsów funkcyjnych.
Autor: Jarek Klimas
Data: 03 stycznia 2024
Labele: Backend, Podstawowy, Java
Czy informacje, które otrzymałeś, były pomocne?
Jeśli tak, zapraszam Cię do podarowania mi kawy.
Masz pytanie dotyczące prezentowanego materiału?
Coś jest dla Ciebie niejasne i Twoje wątpliwości przeszkadzają Ci w pełnym zrozumieniu treści?
Napisz do nas maila, a my chętnie znajdziemy odpowiednie rozwiązanie.
Najciekawsze pytania wraz z odpowiedziami będziemy publikować pod rozdziałem.
Nie czekaj. Naucz się programować jeszcze lepiej.