Litera lambda na tle innych znaków symbolizująca wyrażenia lambda

Wyrażenia lambda w języku Java

Wraz z ósmym wydaniem wersji Javy wprowadzono do niej wiele istotnych funkcjonalności. Wśród nich można wyróżnić wyrażenia lambda skrótowo nazywane lambdami. Za ich pomocą programiści mogą pisać kod w sposób bardziej funkcyjny. Ten sposób programowania umożliwia tworzenie kodu o bardziej zwięzłej składni. Zastosowanie lambd sprawia, że nasz kod jest bardziej czytelny i łatwiejszy w utrzymaniu.

Czym jest lambda

Lambda jest instancją interfejsu funkcyjnego. Interfejs funkcyjny to interfejs, który definiuje tylko jedną abstrakcyjną metodę. Metoda abstrakcyjna to taka metoda, która nie posiada implementacji. Spójrzmy na przykład takiego interfejsu.

public interface FileFilter {
    boolean accept(File file);
}

Ten bardzo popularny interfejs pochodzi z pakietu java.io. Używany jest do ograniczania ilości pobieranych obiektów typu File. Jak widzimy, interfejs ten posiada tylko jedną abstrakcyjną metodę jaką jest metoda accept.

Przed wprowadzeniem lambd taki interfejs mógł być zaimplementowany na dwa sposoby. Pierwszy z nich to utworzenie konkretnej klasy, która implementuje ten interfejs. Spójrzmy na przykład takiej implementacji.

public class TxtFileFilter implements FileFilter {

    @Override
    public boolean accept(File file) {
        return file.getName().endsWith(".txt");
    }
}

Obiekt powyższej klasy służy do ograniczania pobieranych plików tylko do plików tekstowych. W tym celu w metodzie accept sprawdzamy czy dany plik posiada rozszerzenie txt.

Poniższy przykład pokazuje wykorzystanie klasy TxtFileFilter. Używamy jej do pobrania i wypisania nazw plików tekstowych znajdujących się w głównym katalogu dysku C.

import java.io.File;

public class TxtFileFilterTest {

    public static void main(String[] args) {
        TxtFileFilter txtFileFilter = new TxtFileFilter();
        File directory = new File("c:\\");
        File[] txtFiles = directory.listFiles(txtFileFilter);
        for (File txtFile: txtFiles) {
            System.out.println(txtFile.getName());
        }
    }
}

Drugim ze sposobów implementacji interfejsu FileFilter jest użycie klasy anonimowej. Klasa anonimowa to klasa nie posiadająca nazwy. Za jej pomocą tworzymy pojedynczy obiekt implementujący dany interfejs. Najczęściej implementacja takiej klasy znajduje się tuż przed miejscem jej użycia.

Spójrzmy zatem jak będzie wyglądał przykład implementacji filtra plików tekstowych z użyciem takiej klasy.

import java.io.File;
import java.io.FileFilter;

public class TxtFileFilterTest {

    public static void main(String[] args) {

        // anonymous class
        FileFilter txtFileFilter = new FileFilter() {
            @Override
            public boolean accept(File file) {
                return file.getName().endsWith(".txt");
            }
        };

        File directory = new File("c:\\");
        File[] txtFiles = directory.listFiles(txtFileFilter);
        for (File txtFile : txtFiles) {
            System.out.println(txtFile.getName());
        }
    }
}

Obydwa sposoby tworzenia implementacji interfejsu FileFilter zawierają bardzo rozbudowaną składnię. Wyrażenia lambda pozwalają znacznie skrócić i uprościć taki nadmiarowy zapis. Poznajmy zatem składnie lambd i zobaczmy jak uprości się kod z powyższych przykładów.

Składnia wyrażeń lambda

Składnia wyrażenia lambda jest bardzo prosta i przedstawia ją poniższy rysunek.

Składnia wyrażenia lambda

Widzimy, że wyrażenie to składa się z trzech elementów. Pierwszym z nich jest lista parametrów. Po niej następuje operator lambda czyli strzałka. Po operatorze tym mamy ciało wyrażenia lambda.

Lambda z jednym parametrem

Spójrzmy jak przedstawia się wyrażenie lambda z pojedynczym parametrem.

(String param) -> System.out.println("Single parameter: " + param);

Powyższe wyrażenie posiada jeden parametr typu String o nazwie param. Ciało metody wypisuje na konsolę wartość przekazanego parametru. Wyrażenie to można zapisać w jeszcze prostszy sposób pomijając typ parametru.

(param) -> System.out.println("Single parameter: " + param);

Widzimy, że w powyższym kodzie nie ma już typu String. Kompilator języka Java sam wywnioskuje typ parametru. Zrobi to na podstawie kontekstu w którym znajduje się lambda.

Jeśli mamy tylko jeden parametr w wyrażeniu lambda to nawiasy są opcjonalne. Oznacza to, że powyższy przykład możemy wyrazić następująco:

param -> System.out.println("Single parameter: " + param);

Lambda z wieloma parametrami

Jeśli mamy więcej niż jeden parametr to poszczególne parametry rozdzielamy przecinkiem. Poniższy przykład pokazuje wyrażenie lambda z dwoma parametrami.

(param1, param2) -> System.out.println("First parameter: " + param1 + 
                                       "Second parameter: " + param2);

Ciało wyrażenia lambda

Ciało wyrażenia lambda znajdujące się po prawej stronie operatora -> reprezentuje metodę wykonywaną przez lambdę. W powyższych przykładach omawialiśmy przykłady gdy ciało wyrażenia miało tylko jedną linię kodu. Jeśli ciało musi zawierać więcej linii to wymagane są nawiasy klamrowe. Spójrzmy jak wygląda zapis lambdy w takim przypadku.

(param1, param2) -> {
    System.out.println("First parameter: " + param1);
    System.out.println("Second parameter: " + param2);
};

Ciało wyrażenia lambda podobnie jak zwykła metoda może zwracać wartość. W tym celu należy posłużyć się słowem kluczowym return jak w poniższym przykładzie.

(param) ->  {
    return param.toUpperCase();
};

Zwróćmy uwagę na nawiasy klamrowe. Jeśli lambda zwraca wartość używając słowa kluczowego return nawiasy klamrowe są wymagane.

Istnieje jednak przypadek w którym ciało lambdy może zwracać wartość i nie musimy posługiwać się słowem kluczowym return. Jeśli całe wyrażenie lambda zwraca obliczaną wartość możemy słowo return pominąć. Oznacza to, że wyrażenie

(p1, p2) ->  {
    return p1 < p2;
};

może zostać zapisane w następujący sposób

(p1, p2) ->  p1 < p2;

Kompilator sam wywnioskuje, że wyrażenie p1 < p2 jest zwracają wartością.

Zamiana klasy anonimowej na lambdę

Posiadając już wiedzę na temat składni wyrażeń lambda wróćmy do naszego przykładu z filtrem plików tekstowych. Wcześniej implementowaliśmy filtr używając konkretnej klasy lub klasy anonimowej. Teraz użyjemy wyrażeniami lambda.

import java.io.File;
import java.io.FileFilter;

public class LambdaTxtFileFilterTest {

    public static void main(String[] args) {
        // lambda expression
        FileFilter txtFileFilter = file -> file.getName().endsWith(".txt");

        File directory = new File("c:\\");
        File[] txtFiles = directory.listFiles(txtFileFilter);
        for (File txtFile : txtFiles) {
            System.out.println(txtFile.getName());
        }
    }
}

W powyższym przykładzie widzimy jak kod programu po zastosowaniu wyrażenia lambda się uprościł. Kod, który wcześniej zajmował wiele linii lub znajdował się w osobnej klasie teraz zmieścił się w jednej linii. Utworzone wyrażenie lambda zostało przypisane do zmiennej typu FileFilter. Następnie zostało przekazane jako argument do metody listFiles.

Referencje do metod

Wiemy już jak wygląda składnia lambd i jak ich użycie wpływa na czytelność kodu. Wiemy również, że lambdy to instancje interfejsów funkcyjnych. Jednak bardzo często taka implementacja to tylko wywołanie metody z istniejącej już klasy. W takim przypadku zamiast użyć całego wyrażenia lambda możemy odwołać się tylko do nazwy metody. Taki rodzaj odwołania do metody nazywany jest referencją do metody (ang. method reference).

Składnia takich odwołań składa się z nazwy klasy lub obiektu, dwóch znaków dwukropka ( :: ) i nazwy metody którą chcemy wywołać.

Istnieją trzy warianty referencji do metod, co pokazuje poniższa tabela.

Rodzaj referencji Składnia
Referencja do metody statycznej Klasa::metodaStatyczna
Referencja do instancji metody określonego obiektu obiekt::metoda
Referencja do instancji metody obiektu określonego typu Klasa::metoda

Spójrzmy na poniższe przykłady z objaśnieniami, które pomogą zrozumieć w jaki sposób używać referencji do metod.

Referencja do metody statycznej

Stwórzmy przykładową klasę, która będzie zawierać metodę statyczną.

public class NumberValidator {

    public static boolean isPositive(int number) {
        return number > 0;
    }
}

Ta prosta klasa posiada statyczną metodę isPositiveNumber. Metoda ta sprawdza czy liczba podana jako argument jest liczbą dodatnią.

Teraz zdefiniujmy przykładowy interfejs funkcyjny, którego zadaniem jest sprawdzanie operacji logicznych. W naszym przypadku wykorzystamy go do współpracy z klasą NumberValidator.

interface Predicate {
    boolean test(int value);
}

Zobaczmy teraz jak będzie wyglądał kod przed i po użyciu referencji do metody statycznej.

public class StaticMethodReference {

    public static void main(String[] args) {
        // lambda expression
        Predicate predicate = (number) -> NumberValidator.isPositive(number);
        System.out.println(predicate.test(10));

        // method reference
        Predicate referencePredicate = NumberValidator::isPositive;
        System.out.println(referencePredicate.test(10));
    }
}

W przykładzie tworzymy implementację interfejsu Predicate w postaci wyrażenia lambda. W tym celu wykorzystujemy statyczną metodę isPositive z klasy NumberValidator. Zdefiniowana lambda jest przypisana do zmiennej predicate. Tak przypisana lambda może być wywoływana w innych miejscach programu. W powyższym kodzie wywołujemy lambdę i wypisujemy wynik jej działania na konsolę.

W kolejnych liniach również tworzymy implementację interfejsu Predicate. Tym razem jednak posługujemy się operatorem :: aby użyć referencji do metody. Jak widzimy taki zapis jest znacznie krótszy niż zdefiniowane wcześniej wyrażenie lambda.

Referencja do instancji metody określonego obiektu

Drugim typem referencji to metod jest referencja do instancji metody określonego obiektu. W tym przypadku w pierwszej kolejności musimy utworzyć obiekt. A następnie wywołać metodę tego obiektu za pomocą referencji do metody.

Aby zademonstrować ten typ referencji użyjemy zdefiniowanego w Javie interfejsu Function. Deklaracja tego interfejsu funkcyjnego przedstawia się następująco:

public interface Function<T, R> {
    R apply(T t);
}

Interfejs ten jest interfejsem generycznym posiadającym metodę apply. Metoda apply tego interfejsu opisuje funkcję, w której argument wejściowy i wyjściowy są różnego typu. Litera T reprezentuje typ argumentu wejściowego. Natomiast litera R typ wyniku zwracanego przez metodę.

Stwórzmy prostą klasę, która będzie posiadać metodę spełniającą założenia interfejsu Function.

public class CharactersCounter {

    public Integer count(String input) {
        return input.length();
    }
}

Powyższa klasa CharactersCounter posiada pojedynczą metodę count. Metoda przyjmuje argument typu String. Wartość zwracana z metody jest typu Integer. Ta prosta metoda zwraca ilość znaków dla przekazanego ciągu znaków bazując na metodzie length klasy String.

Spójrzmy na przykład, który pokazuje w jaki sposób wyrażenie lambda może zostać zamienione na referencję do metody.

import java.util.function.Function;

public class ObjectMethodReference {

    public static void main(String[] args) {

        CharactersCounter counter = new CharactersCounter();

        // lambda expression
        Function<String, Integer> lambdaFunction = (text) -> counter.count(text);
        System.out.println(lambdaFunction.apply("java"));

        // method reference
        Function<String, Integer> referenceFunction = counter::count;
        System.out.println(referenceFunction.apply("java"));
    }
}

W pierwszym kroku tworzymy instancję klasy CharactersCounter. Dzięki temu będziemy mieli możliwość odwoływania się do instancji metody count.

W kolejnym kroku tworzymy wyrażenie lambda. Wyrażenie to wywołuje w swoim ciele metodę count.

Następnie widzimy jak możemy skrócić zapis stosując referencję do metody instancji obiektu.

Referencja do instancji metody dowolnego obiektu określonego typu

Ostatnim typem referencji jest referencja do dowolnego obiektu określonego typu. Ta metoda odwoływania się do metod jest bardziej zawiła niż dwie poprzednie. Dokumentacja Oracle również dość zdawkowo opisuje ten typ referencji.

Składnia tego typu referencji jest podobna do składni referencji do metody statycznej. W przypadku metody statycznej składnia ta ma następującą postać:

Klasa::metodaStatyczna

W przypadku referencji do instancji metody obiektu określonego typu składnia ta wygląda tak:

Klasa::metoda

Mimo, że wywołania te wyglądają podobnie to jednak sposób działania obydwóch rodzajów referencji się różni. Poniższy obrazek pokazuje referencję do metody statycznej oraz jej odpowiednik w postaci wyrażenia lambda.

Referencja do metody statycznej i analogiczne wyrażenie lambda

Widzimy, że wszystkie parametry lambdy przekazywane są do metody statycznej.

Spójrzmy na analogiczny obrazek dla referencji do instancji metody obiektu określonego typu.

Referencja do instancji metody dowolnego obiektu określonego typu i analogiczne wyrażenie lambda

W tym przypadku pierwszy z parametrów wyrażenia lambda jest obiektem z którego wywoływana jest metoda. Pozostałe parametry wyrażenia lambda przekazywane są do tej metody.

Rzućmy okiem na poniższy przykład by zobaczyć jak zamienić wyrażenie lambda na referencję do metody.

import java.util.function.Function;

public class ClassMethodReference {

    public static void main(String[] args) {

        // lambda expression
        convert("lambda", (text) -> text.toUpperCase());

        // method reference
        convert("lambda", String::toUpperCase);
    }

    private static void convert(String text, Function<String, String> function) {
        System.out.println(function.apply(text));
    }
}

Powyższa klasa definiuje statyczną metodę convert. Metoda ta posiada dwa argumenty wejściowe.

Pierwszym z nich jest łańcuch znaków. Drugim funkcja spełniająca założenia interfejsu funkcyjnego Function. W metodzie wywołujemy metodę interfejsu funkcyjnego przekazując do niej pierwszy argument. W ten sposób możemy na przekazanym łańcuchu znaków wykonywać różne transformacje. Wynik wykonania metody apply jest wypisywany na konsoli.

W metodzie main znajduje się wywołanie metody convert z użyciem lambdy oraz referencji do metody. Widzimy, że wyrażenie lambda posiada jeden parametr typu String. W ciele metody odwołujemy się do tego parametru i wywołujemy metodę toUpperCase. Używając referencji do metody posługujemy się nazwą klasy. W tym przypadku jest to klasa String. Wynika, to z tego, że pierwszym przekazywanym do lambdy parametrem był właśnie obiekt klasy String.

Referencje do konstruktorów

Także w odniesieniu do konstruktorów możemy użyć odwołania za pomocą operatora ::. Składnia takiego odwołania jest podobna jak przypadku referencji do metody statycznej. Jednak zamiast nazwy metody po operatorze :: używamy słowa new.

NazwaKlasy::new

Jeśli na przykład chcemy utworzyć obiekt klasy String to wówczas użyjemy następującej konstrukcji:

String::new

co jest równoważne następującemu wyrażeniu lambda:

() -> new String()

Spójrzmy na przykład, który pokazuje w jaki sposób wyrażenie lambda zostało zamienione na referencję do konstruktora.

import java.util.function.Function;

public class ConstructorReference {

    public static void main(String[] args) {

        // lambda expression
        Function<String,Integer> lambda = (parameter) -> new Integer(parameter);
        System.out.println(lambda.apply("100"));

        // constructor reference
        Function<String,Integer> reference = Integer::new;
        System.out.println(reference.apply("100"));
    }
}

Ponownie korzystamy tutaj z interfejsu Function. Tworzymy proste wyrażenie lambda, które zamienia łańcuch znaków na obiekt klasy Integer. W ciele lambdy wywołujemy konstruktora klasy Integer, który przyjmuje jako argument łańcuch znaków. Następnie wywołujemy metodę apply przekazując wartość 100. Wartość ta zostaje zamieniona na obiekt Integer. Wynik wykonania metody jest wypisywany na konsoli.

Następnie widzimy w jaki sposób lambda została uproszczona dzięki referencji do konstruktora. Został wywołany ten sam konstruktor klasy Integer, który został wywołany w wyrażeniu lambda. Jeśli klasa ma więcej konstruktorów to jaki zostanie wybrany zależy od kontekstu wywołania.

Jeśli chodzi o referencję do konstruktorów może ona również zostać użyta do tworzenia tablic. Na przykład następujące wyrażenie lambda

(size) -> new int[size]

może zostać zastąpione poniższą referencją do konstruktora

int[]::new

Parametrem konstruktora w tym przypadku jest rozmiar tablicy.

Zasięg zmiennych

W wyrażeniu lambda możemy się odwoływać do zmiennych lokalnych metody w której znajduje się lambda. Pokazuje to poniższy przykład, który używa zmiennej lokalnej localVariablew ciele metody.

import java.util.function.Function;

public class LambdaLocalScope {

  public void method() {
     Integer localVariable = new Integer(100);
     Function<Integer, Integer> lambda = param -> param + localVariable;
  }
}

W ciele wyrażenia lambda możemy także odwoływać się do zmiennych instancji klasy w której znajduje się lambda. Spójrzmy na poniższy przykład, który ilustruje ten przypadek.

import java.util.function.Function;

public class LambdaInstanceVariable {

    private Integer instanceVariable = new Integer(5);

    public void method() {
        Function<Integer, Integer> lambda = param -> param * instanceVariable;
    }
}

Z ciała wyrażenia lambda możemy także odwoływać się do zmiennych statycznych. Oto przykład takiego wywołania.

import java.util.function.Function;

public class LambdaStaticVariable {

    private static Integer staticVariable = new Integer(1);

    public static void main(String[] args) {
        Function<Integer, Integer> lambda = param -> param + staticVariable;
    }
}

W lambdzie możemy także odwoływać się do zmiennych statycznych innych klas.

Chciałem jeszcze zwrócić uwagę na jeszcze jeden ważny aspekt jeśli chodzi o zmienne lokalne i wyrażenia lambda. Przyjmuje się, że zmienne lokalne użyte w wyrażeniach lambda są traktowane jako stałe (ang. effective final). Kompilator nie pozwoli nam zmodyfikować takiej zmiennej zarówno w ciele lambdy jak i po nim. Spójrzmy na przykład, który pokazuje to zachowanie.

import java.util.function.Function;

public class LambdaEffectiveFinal {

    private void method() {
        int variable = 100;

        Function<Integer, Integer> lambda = parameter -> {
            // poniższa linia spowoduje błąd kompilacji
            // nie można zmienić lokalnej zmiennej w lambdzie
            int result = parameter * variable;
            // variable++;
            return result;
        };

        // poniższa linia również spowoduje błąd kompilacji
        // nie można zmiennić lokalnej zmiennej użytej w lambdzie
        // variable++;
    }
}

Powyższa klasa deklaruje w metodzie method zmienną lokalną o nazwie variable. Następnie mamy deklaracje wyrażenia lambda. W pierwszej linii tego wyrażenia mnożymy parametr lambdy przez zmienną variable. Wynik tej operacji przypisujemy do zmiennej result.

W kolejnej linii zmienna variable jest inkrementowana. Ostatnia linia lambdy zwraca wartość zmiennej result. Jak widzimy, linia z inkrementacją zmiennej variable jest poprzedzona znakiem komentarza. Jeśli usuniemy znak komentarza to program się nie skompiluje. Kompilator poinformuje nas o błędzie w następujący sposób:

Variable used in a lambda should be final or effectively final

Taki sam komunikat błędu otrzymamy jeśli spróbujemy zmienić wartość zmiennej variable poza ciałem lambdy.

Użycie słowa kluczowego this

W ciele wyrażenia lambda możemy posługiwać się słowem kluczowym this. Spójrzmy na poniższy przykład.

import java.util.function.Function;

public class LambdaWithThis {

    public void doCalculations() {
        Function<Integer, Long> lambda = (param) -> this.factorial(param);
    }

    long factorial(Integer n) {
        if (n <= 2) {
            return n;
        }
        return n * factorial(n - 1);
    }
}

Widzimy, że słowo this zostało tutaj użyte aby wywołać metodę factorial obliczającą silnię. Słowo this użyte w lambdzie odnosi się do instancji klasy w której się znajduje. W tym przypadku odnosi się do instancji klasy LambdaWithThis.

Użycie this w wyrażeniu lambda różni się od użycia this w klasie anonimowej. W klasie anonimowej this odnosi się do instancji tej klasy. Poniższy przykład obrazuje ten przypadek.

import java.util.function.Function;

public class AnonymousWithThis {

    public void doCalculations() {

        Function<Integer, Integer> anonymous = new Function<Integer, Integer>() {
            @Override
            public Integer apply(Integer number) {
                return this.fibonacci(number);
            }

            int fibonacci(int n) {
                if (n <= 1) {
                    return n;
                }
                return fibonacci(n - 1) + fibonacci(n - 2);
            }
        };
    }
}

Klasa anonimowa wywołuje metodę fibonacci obliczającą ciąg Fibonacciego. Widzimy, że metoda ta znajduje się we wnętrzu klasy anonimowej. Próba użycia słowa this z jakąkolwiek metodą spoza ciała klasy anonimowej spowoduje błąd kompilacji.

Adnotacja @FunctionalInterface

Wraz z wprowadzeniem wyrażeń lambda pojawiła się w Javie adnotacja @FunctionalInterface. Każdy predefiniowany interfejs funkcyjny w Javie opatrzony jest tą adnotacją. Spójrzmy na przykład w jaki sposób zdefiniowany jest interfejs Function znajdujący się w kodach źródłowych Javy.

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}

Użycie adnotacji zapewnienia sprawdzenie poprawności interfejsu funkcyjnego. Kompilator sprawdza czy interfejs posiada tylko jedną abstrakcyjną metodę. Jeśli będziemy próbowali dodać więcej niż jedną taką metodę zgłosi on następujący błąd:

Multiple non-overriding abstract methods found in interface

Korzystanie z adnotacji @FunctionalInterface nie jest obowiązkowe. Jednak zaleca się jej używanie kiedy tworzymy interfejs, który ma pełnić rolę interfejsu funkcyjnego. W ten sposób jasno komunikujemy cel interfejsu. Pozwalamy także kompilatorowi na sprawdzenie czy interfejs został poprawnie zdefiniowany.

Standardowe interfejsy funkcyjne

Poznaliśmy już wcześniej interfejs funkcyjny o nazwie Function. Interfejs ten znajduje się w pakiecie java.util.function. Oprócz niego twórcy Javy dostarczyli na ponad 40 innych interfejsów funkcyjnych. Zanim będziesz chciał utworzyć swój interfejs funkcyjny zerknij do tego pakietu. Być może znajdziesz taki interfejs, który spełni Twoje wymagania.

Nie sposób zapamiętać wszystkie interfejsy z pakietu java.util.function. Ale wystarczy, że zapamiętasz sześć najbardziej podstawowych. To wystarczy, aby zrozumieć jakie jest przeznaczenie pozostałych interfejsów. Spójrzmy zatem jak przedstawia się sześć najbardziej podstawowych interfejsów funkcyjnych.

Sześć podstawowych interfejsów funkcyjnych w języku java

Pierwszy z nich to znany nam już interfejs Function. Metoda tego interfejsu opisuje funkcję, w której argument wejściowy i wyjściowy są różnego typu. Oto przykład użycia tego interfejsu do konwersji wartości String do obiektu klasy Integer.

Function<String, Integer> stringToInteger = Integer::valueOf;

Drugim z interfejsów jest UnaryOperator. Metoda apply tego interfejsu opisuje funkcję w której argument wejściowy i wyjściowy są tego samego typu. Spójrzmy na bardzo prosty przykład użycia tego interfejsu do inkrementacji wartości typu Integer.

UnaryOperator<Integer> incrementer = i -> i + 1;

Trzeci z prezentowanych interfejsów to BinaryOperator. Metoda tego interfejsu przyjmuje dwa argumenty tego samego typu. Wynik zwracany z metody jest tego samego typu co argumenty wejściowe. Poniższy przykład ilustruje funkcję, która sumuje dwie liczby typu BigDecimal.

BinaryOperator<BigDecimal> adder = BigDecimal::add;

Kolejny czwarty już z interfejsów to Supplier. Metoda get tego interfejsu nie przyjmuje żadnych argumentów, natomiast zwraca wynik. Zobaczmy jak można wykorzystać ten interfejs to utworzenia obiektu klasy ArrayList.

Supplier<List> arryListSupplier = ArrayList::new;

Piąty z interfejsów to interfejs Consumer. Jego metoda accept posiada jeden argument i nie zwraca żadnej wartości. Oto prosty przykład użycia interfejsu Consumer do wypisania na konsoli argumentu wejściowego.

Consumer<String> consumer = System.out::println;

Ostatni z opisywanych interfejsów to Predicate. Posiada on metodę test, która przyjmuje argument i zwraca wyniki typu boolean. Poniższy przykład pokazuje użycie interfejsu Predicate. Sprawdzamy w nim czy przekazany argument jest liczbą dodatnią.

Predicate<Integer> isPositive = (number) -> number > 0;

Pozostałe interfejsy z pakietu java.util.function to warianty powyższych 6 interfejsów. Każdy z tych wariantów został opisany w jednej z poniższych tabel.

Warianty interfejsu Function

Interfejs Metoda
DoubleFunction<R> R apply(double value)
Przyjmuje argument typu long i zwraca wynik pewnego typu
IntFunction<R> R apply(int value)
Przyjmuje argument typu int i zwraca wynik pewnego typu
LongFunction<R> R apply(long value)
Przyjmuje argument typu long i zwraca wynik pewnego typu
DoubleToIntFunction int applyAsInt(double value)
Przyjmuje argument typu double i zwraca wynik typu int
DoubleToLongFunction long applyAsInt(double value)
Przyjmuje argument typu double i zwraca wynik typu long
IntToDoubleFunction double applyAsDouble(int value)
Przyjmuje argument typu int i zwraca wynik typu double
IntToLongFunction long applyAsLong(int value)
Przyjmuje argument typu int i zwraca wynik typu long
LongToIntFunction int applyAsInt(long value)
Przyjmuje argument typu long i zwraca wynik typu int
LongToDoubleFunction double applyAsDouble(long value)
Przyjmuje argument typu long i zwraca wynik typu double
ToDoubleFunction<T> double applyAsDouble(T value)
Przyjmuje argument pewnego typu i zwraca wynik typu double
ToIntFunction<T> int applyAsInt(T value)
Przyjmuje argument pewnego typu i zwraca wynik typu int
ToLongFunction<T> long applyAsLong(T value)
Przyjmuje argument pewnego typu i zwraca wynik typu long
BiFunction<T,U,R> R apply(T t, U u)
Przyjmuje argumenty dwóch różnych typów i zwraca wynik typu innego typu niż argumenty wejściowe
ToDoubleBiFunction<T,U> double applyAsDouble(T t, U u)
Przyjmuje argumenty dwóch różnych typów i zwraca wynik typu double
ToIntBiFunction<T,U> int applyAsDouble(T t, U u)
Przyjmuje argumenty dwóch różnych typów i zwraca wynik typu int
ToLongBiFunction<T,U> long applyAsDouble(T t, U u)
Przyjmuje argumenty dwóch różnych typów i zwraca wynik typu long

Warianty interfejsu UnaryOperator

Interfejs Metoda
DoubleUnaryOperator double applyAsDouble(double operand)
Przyjmuje argument typu double i zwraca wynik typu double
IntUnaryOperator int applyAsInt(int operand)
Przyjmuje argument typu int i zwraca wynik typu int
LongUnaryOperator long applyAsLong(long operand)
Przyjmuje argument typu long i zwraca wynik typu long

Warianty interfejsu BinaryOperator

Interfejs Metoda
DoubleBinaryOperator double applyAsDouble(double left, double right)
Przyjmuje dwa argumenty typu double i zwraca wynik typu double
IntBinaryOperator int applyAsDouble(int left, int right)
Przyjmuje dwa argumenty typu int i zwraca wynik typu int
LongBinaryOperator long applyAsDouble(long left, long right)
Przyjmuje dwa argumenty typu long i zwraca wynik typu long

Warianty interfejsu Supplier

Interfejs Metoda
DoubleSupplier double getAsDouble()
Zwraca wynik typu double
IntSupplier int getAsInt()
Zwraca wynik typu int
LongSupplier long getAsLong()
Zwraca wynik typu long
BooleanSupplier boolean getAsBoolean()
Zwraca wynik typu boolean

Warianty interfejsu Consumer

Interfejs Metoda
DoubleConsumer void accept(double value)
Przyjmuje argument typu boolean
IntConsumer void accept(int value)
Przyjmuje argument typu int
LongConsumer void accept(long value)
Przyjmuje argument typu long
BiConsumer<T,U> void accept(T t, U u)
Przyjmuje argumenty dwóch różnych typów
ObjDoubleConsumer<T> void accept(T t, double value)
Przyjmuje argument pewnego typu oraz argument typu double
ObjIntConsumer<T> void accept(T t, int value)
Przyjmuje argument pewnego typu oraz argument typu int
ObjLongConsumer<T> void accept(T t, long value)
Przyjmuje argument pewnego typu oraz argument typu long

Warianty interfejsu Predicate

Interfejs Metoda
DoublePredicate boolean test(double value)
Przyjmuje typu double i zwraca wyniku typu boolean
IntPredicate boolean test(int value)
Przyjmuje typu int i zwraca wyniku typu boolean
LongPredicate boolean test(long value)
Przyjmuje typu long i zwraca wyniku typu boolean
BiPredicate<T,U> boolean test(T t, U u)
Przyjmuje argumenty dwóch różnych typów i zwraca wyniku typu boolean

Dobre praktyki

Pisząc kod używający lambd warto stosować się do pewnych zaleceń, które sprawią, że kod będzie bardziej klarowny. Spójrzmy na poniższe zalecenia, które pomogą tworzyć prostszy i czytelniejszy kod.

  • Unikaj deklaracji typów parametrów jeśli kompilator jest sam w stanie je wywnioskować
  • Unikaj tworzenia bloków kodu w ciele lambdy
  • Unikaj nawiasów jeśli lista parametrów lambdy zawiera pojedynczy parametr
  • Unikaj słowa kluczowego return jeśli nie jest wymagane przy zwracaniu wyniku
  • Używaj referencji do metod i konstruktor jeśli ich zapis jest krótszy niż zwykłe wyrażenie lambda

Ćwiczenie do wykonania

  1. Napisz wyrażenie lambda, które przyjmuje dwa argumenty typu Integer i zwraca wartość większego z nich.
  2. Wykorzystując referencję do konstruktora utwórz obiekt klasy Boolean. Parametrem wejściowym metody jest łańcuch znaków zawierający wartość true lub false.

Materiały dodatkowe

Książki

  • Java. Efektywne programowanie. Wydanie III. Autor: Joshua Bloch.
  • Java. Receptury. Wydanie III. Autor: Ian F. Darwin.
  • Java 8. Przewodnik doświadczonego programisty. Autor: Cay S. Horstmann.

Zasoby sieciowe

Podsumowanie

W dzisiejszym artykule dowiedzieliśmy się czym są wyrażenia lambda. Poznaliśmy również korzyści jakie możemy osiągnąć dzięki ich zastosowaniu.

Lambdy mogą początkowo wydawać się skomplikowane, ale warto je poznać. Wysiłek włożony w ich naukę z pewnością się zwróci. Znając je kod będziemy pisać znacznie szybciej. Także jego analiza po pewnym czasie będzie szybsza ze względu na zwięzłość wyrażeń lambda.