Java 8 #3: przegląd interfejsów funkcyjnych

Rodzaje interfejsów funkcyjnych

W poprzednim wpisie postarałem się wytłumaczyć, że wyrażenie lambda można z łatwością przekształcić na znaną już Ci klasę anonimową i na odwrót. Nie znaczy to jednak, że obie te struktury są dokładnie tym samym. Będąc bardziej ścisłym wyrażenie lambda jest udostępnioną możliwością przekształcenia fragmentu kodu na obiekt. Wzorcem dla każdego z tych wyrażeń na co je przekształcić jest interfejs funkcyjny. W wielu przypadkach nie ma potrzeby pisania swojego własnego interfejsu funkcyjnego, ponieważ lambda, którą chcesz użyć dotyczy klas już dawno będących częścią JDK. W takich przypadkach twórcy języka udostępnili wiele generycznych interfejsów funkcyjnych, które ułatwią Ci pracę z lambdami.

Wszystkie interfejsy funkcyjne znajdziesz w pakiecie java.util.function. Wyróżnia się kilka podstawowych kategorii interfejsów funkcyjnych*:

Interfejs Supplier

Zadaniem tego typu interfejsów jest produkowanie (dostarczanie) danych. Możesz go rozumieć jako metodę, która nie przyjmuje żadnego parametru ale zawraca jakieś wartości.

Implementacja interfejsu w JDK 8:

@FunctionalInterface
public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}

Przykładowe użycie:

int age = 20;
Supplier<String> ageInfo = () -> "Ania age is " + age;
List<String> namesWithAges = new ArrayList<>();
namesWithAges.add(ageInfo.get());

Przypisanie tutaj zostało zastosowane tylko dla pokazania, że faktycznie jest to interfejs supplier. Wartość age nie jest parametrem lambdy, traktuj ją jak finalną zmienną klasową w przypadku standardowych metod.

Interfejs Consumer

W przeciwieństwie do interfejsu Supplier, ten rodzaj interfejsu skupia się na przetwarzaniu dostarczonej z zewnątrz wartości jednocześnie bez zwracania jakiegokolwiek rezultatu.

Fragment implementacji interfejsu w JDK 8:

@FunctionalInterface
public interface Consumer<T> {

    /**
     * Performs this operation on the given argument.
     *
     * @param t the input argument
     */
    void accept(T t);

Świetnym przykładem wykorzystania interfejsu kosumenta jest nowo dodana do JDK 8 metoda forEach, która umożliwia łatwą iterację po kolekcjach:

List<String> names = new ArrayList<>();
names.add("Ania");
names.add("Tomek");
names.add("Janusz");
Consumer<String> action = name -> System.out.println("Name is " + name);
names.forEach(action);

Teraz spytasz się, jak ten zapis nic nie zwraca, skoro ewidentnie widać przypisanie do name. Nie jest to przypisanie wartości zwracanej, ale uproszczenie zapisu lambdy. Ta funkcja przecież nic nie zwraca, bo System.out.println nie daje rezultatu, jedynie go wyświetla. Normalnie wyglądałby mniej więcej tak: Consumer action = (name) -> …). W tym przypadku można jednak opuścić nawiasy.

Interfejs Predicate

Predykat to kolejny ciekawy interfejs, który umożliwia sprawdzenie, czy dany warunek zachodzi. Funkcja ta waliduje jakiś obiekt, po czym zwraca prawdę albo fałsz.

Fragment implementacji interfejsu w JDK 8:

@FunctionalInterface
public interface Predicate<T> {

    /**
     * Evaluates this predicate on the given argument.
     *
     * @param t the input argument
     * @return {@code true} if the input argument matches the predicate,
     * otherwise {@code false}
     */
    boolean test(T t);

Skorzystam teraz z poprzedniej listy imion oraz wcześniej omawianej metody foreach.

names.forEach(nameToCheck -> {
	Predicate<String> checkName = (name) -> name.equals("Ania");
	
	if (checkName.test(nameToCheck)) {
		System.out.println("I found " + nameToCheck);
	}
});

Zawartość forEach jest oczywiście lambdą (jak widzisz można je zagnieżdżać). Za pomocą metody equals mój predykat weryfikuje czy sprawdzane imię to Ania”. Następnie korzystam z niego, walidują mój warunek poprzez metodę test.

Interfejs Function

Interfejs typu Function jest jednym z najczęściej używanych. Powodem jest to, że łączy on strukturę zarówno intefejsów Supplier, jak i Concumer. Jest on odpowiedzią na potrzebę stworzenia funkcji, która będzie zarówno przyjmować jakiś parametr, jak i go przetwarzać i zwracać rezultat, będący innym obiektem.

Fragment implementacji interfejsu w JDK 8:

@FunctionalInterface
public interface Function<T, R> {

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);

Świetnym przykładem wykorzystania tego interfejsu jest wyliczenie długości stringa za pomocą metody lenght.

names.forEach(nameToCheck -> {
	Function<String, Integer> func = name -> name.length();
	Integer nameLength = func.apply(nameToCheck);
	System.out.println("Length of name " + nameLength);
});

Udało się w ten prosty sposób napisać krótki kod, który pracuje na obiekcie typu String, ale zwraca wartość typu Integer (długość napisu). Wszystko dzięki prostej lambdzie, użytej w metodzie apply. Podobnym interfejsem funkcyjnym jest do Function jest dziedziczony po nim interfejs Operator.

Interfejs Operator

Interfejs ten niestety nie występuje w formie ogólnej tak jak poprzednie. Zatem zademonstruję tu najprostszy z nich, zwany UnaryOperator, czyli operator jednoargumentowy. Reprezentuje on operację na pojedynczym parametrze, która daje wynik tego samego typu. Przykładem operatora jednoargumentowego jest znana Ci operacja inkrementacji (np. i++). Masz tu tylko jeden operand (czyli zmienną i).

@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {

    /**
     * Returns a unary operator that always returns its input argument.
     *
     * @param <T> the type of the input and output of the operator
     * @return a unary operator that always returns its input argument
     */
    static <T> UnaryOperator<T> identity() {
        return t -> t;
    }
}

UnaryOperator znaleźć można wśród chociażby metod z klasy String, takich jak np. toUpperCase. Powodem, dlaczego ta metoda, odpowiada strukturze tego typu operatora, jest jego sposób użycia. Zauważ, że jest ona użyta na konkretnej zmiennej typu String. Nie przyjmuje żadnego parametru, nie potrzebuje by wywołana z innej klasy (podobnie jak inkrementacja dla typów prostych).

UnaryOperator<String> operator = String::toUpperCase; // alternative to name -> name.toUpperCase()
names.replaceAll(operator);

Teraz wszystkie imiona będą zapisane z pomocą wielkich liter. W pierwszej linijce można zauważyć zapis, korzystający z podwójnego dwukropka. Jest to alternatywna implementacja, zwana referencjami do metod (ang. method reference), w stosunku do kodu z wykorzystaniem strzałki ( -> ). Możesz go zastosować, kiedy używasz prostej jednolinijkowej lambdy. Pamiętaj jednak, że ten zapis zataja argumenty funkcji użyte w lambdzie, także bezpieczniej jest używać zwykłej lambdy (szczególnie w przypadku, gdy korzystasz z większej liczby argumentów).

*Pełną listę znajdziesz: https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html

Kod do lekcji: https://github.com/developeronthego/java-jdk8/tree/master/src/main/java/java8/function

Może Ci się również spodoba

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *