Menu

Wyrażenia lambda w Javie: zwięzły kod funkcyjny

Czym jest wyrażenie lambda w Javie, składnia ze strzałką, jak lambda implementuje interfejs funkcyjny, referencje do metod i przechwytywanie zmiennych.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Zachowanie, które można przekazywać

Wyrażenie lambda to zwięzły blok kodu, który możesz przekazać metodzie, zapisać w zmiennej albo zwrócić, tak jak liczbę czy napis. Przed lambdami przekazanie zachowania oznaczało napisanie całej klasy (albo rozwlekłej klasy anonimowej) tylko po to, żeby opakować jedną metodę. Lambda sprowadza to do istoty: parametrów i ciała.

Lambda zawsze implementuje interfejs funkcyjny, czyli interfejs z dokładnie jedną metodą abstrakcyjną (pojawiły się na końcu strony o interfejsach). Kompilator z kontekstu ustala, o który interfejs chodzi, i dopasowuje twoją lambdę do tej jedynej metody.

x -> x * 2 to cała implementacja apply. Bez new, bez ciała klasy, bez nazwy metody: wszystko to dostarcza interfejs.

Składnia ze strzałką

Każda lambda ma postać parametry -> ciało. Poszczególne części zmieniają się zależnie od potrzeb:

() -> 42                       // bez parametrów
x -> x + 1                     // jeden parametr, nawiasy opcjonalne
(x, y) -> x + y                // dwa lub więcej parametrów wymagają nawiasów
(int x, int y) -> x + y        // typy są opcjonalne: zwykle są wnioskowane
x -> {                          // ciało blokowe wymaga klamer i return
    int doubled = x * 2;
    return doubled + 1;
}

Ciało z jednym wyrażeniem (x -> x + 1) niejawnie zwraca swoją wartość, bez słowa kluczowego return. Gdy tylko użyjesz klamer, piszesz zwykły blok i musisz jawnie użyć return, jeśli metoda coś zwraca. Częsty błąd to mieszanie obu form: x -> { x + 1 } się nie kompiluje, bo blok potrzebuje instrukcji (return x + 1;).

Lambdy zastępują klasy anonimowe

Najlepiej widać, co daje lambda, porównując „przed” i „po”. Sortowanie z własnym Comparator wyglądało kiedyś tak:

// przed: klasa anonimowa
names.sort(new Comparator<String>() {
    public int compare(String a, String b) {
        return a.length() - b.length();
    }
});

To samo jako lambda to jedna czytelna linia:

Comparator to interfejs funkcyjny (jego jedyna metoda abstrakcyjna to compare), więc lambda pasuje od razu. Cała ceremonia (new, klasa, sygnatura metody) znika i zostaje tylko logika porównania.

Skrzynka z narzędziami java.util.function

Rzadko musisz deklarować własny interfejs funkcyjny. Pakiet java.util.function zawiera najczęstsze kształty, a prawie wszystkie API biblioteki Javy je przyjmują:

  • Function<T, R>: przyjmuje T, zwraca R (apply)
  • Predicate<T>: przyjmuje T, zwraca boolean (test)
  • Supplier<T>: niczego nie przyjmuje, zwraca T (get)
  • Consumer<T>: przyjmuje T, niczego nie zwraca (accept)

To interfejsy generyczne: Function<String, Integer> wykorzystuje generyki z poprzedniej strony, żeby zachować bezpieczeństwo typów. Wybierz ten, którego kształt pasuje do tego, co twój kod ma przyjmować i zwracać.

Referencje do metod

Gdy lambda jedynie wywołuje jedną istniejącą metodę, możesz zastąpić ją referencją do metody z użyciem ::. To ta sama wartość, zapisana bardziej bezpośrednio:

Referencje do metod występują w kilku odmianach: String::toUpperCase (metoda instancji wywoływana na każdym argumencie), Math::max (metoda statyczna), System.out::println (metoda konkretnego obiektu) i ArrayList::new (konstruktor). Sięgaj po nie tylko wtedy, gdy czytają się jasno. Jeśli musisz się zastanawiać, która forma pasuje, zwykła lambda też jest w porządku.

Przechwytywanie zmiennych

Lambda może używać zmiennych lokalnych z otaczającej ją metody, ale tylko wtedy, gdy są final lub effectively final: przypisane raz i nigdy niezmieniane. Java przechwytuje wartość w chwili tworzenia lambdy, więc zmienna, która mogłaby się później zmienić, byłaby niejednoznaczna.

Jeśli gdziekolwiek ponownie przypiszesz factor, kompilator odrzuci lambdę z komunikatem „variable used in lambda expression should be final or effectively final”. Gdy naprawdę potrzebujesz modyfikowalnego wspólnego stanu, przechwyć zamiast tego obiekt: pole, element tablicy albo AtomicInteger, bo referencja pozostaje stała, nawet gdy zmienia się zawartość. Pamiętaj, że lambdy, w odróżnieniu od klas anonimowych, nie tworzą własnego zasięgu: this wewnątrz lambdy odnosi się do otaczającej instancji, a nie do samej lambdy.

Dalej: strumienie

Lambdy to budulec, a nie cel sam w sobie. Prawdziwą korzyść przynoszą w Streams API, gdzie łączysz w łańcuch operacje takie jak filter, map i reduce (każda przyjmuje lambdę), żeby wyrazić przekształcenia danych jako czytelny potok zamiast plątaniny pętli. O tym jest następna strona.

Najczęściej zadawane pytania

Czym jest wyrażenie lambda w Javie?

Lambda to krótki sposób zapisania instancji interfejsu funkcyjnego, czyli interfejsu z jedną metodą abstrakcyjną. Zamiast całej klasy anonimowej piszesz parametry -> ciało. Kompilator dopasowuje lambdę do jedynej metody interfejsu, więc Runnable r = () -> System.out.println("hi"); to kompletny Runnable. Lambdy pozwalają przekazywać zachowanie tak jak dane.

Czym różni się lambda od referencji do metody w Javie?

Obie tworzą instancję interfejsu funkcyjnego. Lambda ma jawne parametry i ciało (s -> s.toUpperCase()), a referencja do metody to skrót dla lambdy, która tylko wywołuje jedną istniejącą metodę (String::toUpperCase). Używaj referencji do metody, gdy lambda jedynie przekazywałaby argumenty do jednej nazwanej metody: jest krótsza i lepiej się czyta.

Dlaczego zmienne używane w lambdzie w Javie muszą być final lub effectively final?

Lambda może przechwytywać zmienne lokalne z otaczającej metody, ale tylko wtedy, gdy nigdy nie zmieniają się po przypisaniu: to właśnie znaczy „effectively final”. Java przechwytuje wartość, a nie żywą referencję do zmiennej, więc pozwolenie na ponowne przypisanie byłoby niejednoznaczne i niebezpieczne przy wielu wątkach. Jeśli potrzebujesz modyfikowalnego wspólnego stanu, użyj zamiast tego pola albo opakowania w postaci tablicy lub AtomicInteger.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ