Kurs Java

Przegląd interfejsów funkcyjnych

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:
@FunctionalInterface
public interface Supplier<T> {

    T 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:
public interface Item {     

    String getDescription();
} 
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?
public class Start {     

    public static void main(String[] args) {
    
        Item item = () -> "This is description for MovieItem";
        String description = item.getDescription();
        System.out.println(description);
    }  
}      
Wynik wykonania kodu:
Java 8 Wynik wykonania kodu - Interfejs Supplier 2
Tak skonstruowane wyrażenie lambda jest implementacją interfejsu Supplier o dokładnie określonym parametrze typu. Dlatego też możemy je zapisać w postaci:
import java.util.function.Supplier;

public class Start {

    public static void main (String[] args) {
        
        Supplier<String> descriptionSupplier = () -> "This is description for MovieItem";
        System.out.println(descriptionSupplier.get());
    }
}    
Wynik wykonania kodu:
Java 8 Wynik wykonania kodu - Metoda forEach
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ść.

import java.util.function.Supplier;

public class Start {

    public static void main (String[] args) {

        Supplier<String> descriptionSupplier = () -> "This is description for MovieItem";
        useSupplier(descriptionSupplier);
    }

    public static void useSupplier(Supplier<String> descriptionSupplier) {
        System.out.println(descriptionSupplier.get());
    }
}
Wynik wykonania kodu:
Java 8 - Data i czas przed Javą 8
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ę:
Błędny przykład interfejsu Supplier
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:
@FunctionalInterface
public interface Consumer<T> {

    void accept(T t);
    
    ...
}
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.
public interface Item {     

    void printDescription(Integer id);
} 
W ten sposób skonstruujemy wyrażenie lambda:
public class Start {

    public static void main(String[] args) {

        Item item = id -> System.out.println(id);
        item.printDescription(10);
    }
}    
Wynik wykonania kodu:
Java 8 - Data i czas przed Javą 8
A tak będzie wyglądało to samo wyrażenie lambda przypisane do bazowego interfejsu Consumer:
import java.util.function.Consumer;

public class Start {

    public static void main(String[] args) {

        Consumer<Integer> printConsumer = id -> System.out.println(id);
        useConsumer(printConsumer);
    }

    static void useConsumer(Consumer<Integer> consumer) {
        consumer.accept(10);
    }
}
Wynik wykonania kodu:
Java 8 - Data i czas przed Javą 8
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ą:
import java.util.function.Consumer;

public class Start {

    public static void main(String[] args) {

        Integer initialId = 2;

        Consumer<Integer> printConsumer = id -> {

            Integer idNumberShift = 5;
            System.out.println(initialId + idNumberShift + id);
        };
        useConsumer(printConsumer);
    }

    static void useConsumer(Consumer<Integer> consumer) {
        consumer.accept(10);
    }
}
Wynik wykonania kodu:
Java 8 - Data i czas przed Javą 8
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.
@FunctionalInterface
public interface Predicate<T> {

    boolean test(T t);
    
    ...
}
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:
import java.util.function.Predicate;

public class Start {

    public static void main(String[] args) {

        Predicate<String> testPredicate = name -> name.length() > 3;
        usePredicate(testPredicate);
    }

    static void usePredicate(Predicate<String> predicate) {
        System.out.println(predicate.test("JavAPPa"));
    }
}
Wynik wykonania kodu:
Java 8 - Data i czas przed Javą 8
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.
import java.util.function.Predicate;

public class Start {

    public static void main(String[] args) {

        Predicate<String> testPredicate = name -> {

            name += " is a very useful portal!";
            name += " Don't you think so?";
            System.out.println(name);
            
            return name.length() > 21;
        };
        usePredicate(testPredicate);
    }

    static void usePredicate(Predicate<String> predicate) {
        System.out.println(predicate.test("JavAPPa"));
    }
}

Wynik wykonania kodu:
Java 8 - Data i czas przed Javą 8

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.
@FunctionalInterface
public interface Function<T, R> {

    R apply(T t);

    ...
}
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ę.
import java.util.function.Function;

public class Start {

    public static void main(String[] args) {

        Function<Integer, String> functionApplier = value -> String.valueOf(value);
        useFunction(functionApplier);
    }

    static void useFunction(Function<Integer, String> function) {
        System.out.println(function.apply(7));
    }
}
Wynik wykonania kodu:
Java 8 - Data i czas przed Javą 8
Oczywiście tak jak zawsze, tutaj również możesz użyć instrukcji blokowej.
import java.util.function.Function;

public class Start {

    public static void main(String[] args) {

        Function<Integer, String> functionApplier = value -> {

            if(value > 1) {
                return String.valueOf(value);
            }

            return String.valueOf(0);
        };
        useFunction(functionApplier);
    }

    static void useFunction(Function<Integer, String> function) {
        System.out.println(function.apply(7));
    }
}
Wynik wykonania kodu:
Java 8 - Data i czas przed Javą 8
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.
Zdjęcie autora
Autor: Jarek Klimas
Data: 03 stycznia 2024
Labele: Backend, Podstawowy, Java
Masz pytanie dotyczące tego rozdziału? Zadaj je nam!
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.
kursjava@javappa.com

Stale się rozwijamy, a więc bądź na bieżąco!
Na ten adres będziemy przesyłać informacje o ważniejszych aktualizacjach, a także o nowych materiałach pojawiających się na stronie.
Polub nas na Facebooku:
Nasi partnerzy: stackshare
Javappa to również profesjonalne usługi programistyczne oparte o technologie JAVA. Jeśli chesz nawiązać z nami kontakt w celu uzyskania doradztwa bądź stworzenia aplikacji webowej powinieneś poznać nasze doświadczenia.
Kliknij O nas .


Pozycjonowanie stron: Grupa TENSE