1. Wprowadzenie
Bądźmy szczerzy: pisanie klas anonimowych dla jednej–dwóch linijek kodu — to jak wynajmowanie ogromnej ciężarówki, żeby przewieźć z piekarni do sklepu jedną bułkę.
Na przykład, jeśli chcesz posortować listę napisów po długości, przed Java 8 trzeba było pisać tak:
List<String> list = Arrays.asList("jabłko", "banan", "kiwi");
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
Niby zadanie proste, a kodu — na pół ekranu. Szczególnie irytuje, gdy takich operacji jest dużo: kod staje się „hałaśliwy”, gubi się istota zadania. Programiści płakali, cierpieli, a potem wymyślili wyrażenia lambda. Już trochę je poznaliśmy, teraz będziemy powtarzać i pogłębiać naszą wiedzę.
Czym jest wyrażenie lambda
Wyrażenie lambda — to zwięzła forma zapisu implementacji interfejsu funkcyjnego, czyli interfejsu z jedną abstrakcyjną metodą (na przykład Comparator, Runnable, Consumer i wiele innych).
Prościej mówiąc, wyrażenie lambda pozwala napisać funkcję „w locie”, dokładnie tam, gdzie jest potrzebna, bez deklarowania osobnej klasy lub metody.
Ogólna składnia:
(parametry) -> { ciało }
Przykłady:
- (a, b) -> a + b — funkcja dodająca dwie liczby
- x -> x * x — funkcja podnosząca liczbę do kwadratu
- () -> System.out.println("Hello!") — funkcja bez parametrów
Powiązanie z interfejsami funkcyjnymi:
Wyrażenie lambda można przypisać do zmiennej typu interfejsu funkcyjnego albo przekazać jako argument do metody, która oczekuje takiego interfejsu.
2. Składnia wyrażeń lambda
Bez parametrów
Runnable r = () -> System.out.println("Witaj, świecie!");
r.run(); // Wypisze: Witaj, świecie!
Jeden parametr
Jeśli parametr jest jeden, można pominąć nawiasy:
Consumer<String> print = s -> System.out.println(s);
print.accept("Java jest super!");
Wiele parametrów
Nawiasy są obowiązkowe:
Comparator<String> cmp = (a, b) -> a.length() - b.length();
Ciało z jednego wyrażenia
Jeśli ciało składa się z jednego wyrażenia, nawiasy klamrowe i return nie są potrzebne:
Function<Integer, Integer> square = x -> x * x;
System.out.println(square.apply(5)); // 25
Ciało jako blok
Jeśli potrzebujesz kilku instrukcji, użyj nawiasów klamrowych i return (jeśli jest wartość zwracana):
Function<Integer, Integer> abs = x -> {
if (x < 0) {
return -x;
}
return x;
};
System.out.println(abs.apply(-3)); // 3
Typy parametrów
Najczęściej typów parametrów nie trzeba podawać — kompilator domyśli się z kontekstu. Ale jeśli chcesz, możesz określić je jawnie:
Comparator<String> cmp = (String a, String b) -> a.length() - b.length();
Lambda bez wartości zwracanej
Jeśli interfejs zwraca void, po prostu wpisz instrukcje:
list.forEach(s -> System.out.println("Element: " + s));
Tabela: Warianty składni wyrażeń lambda
| Co robimy | Przykład | Komentarz |
|---|---|---|
| Bez parametrów | |
Na przykład dla Runnable |
| Jeden parametr | |
Można bez nawiasów |
| Wiele parametrów | |
Nawiasy są obowiązkowe |
| Jedno wyrażenie | |
Bez return i nawiasów klamrowych |
| Blok kodu | |
Z return, jeśli jest wynik |
3. Zastosowanie: gdzie i jak używać wyrażeń lambda
Wyrażenia lambda najczęściej stosuje się tam, gdzie trzeba przekazać „zachowanie” — funkcję — jako argument. To była prawdziwa rewolucja dla kolekcji, strumieni (Stream API), zdarzeń i wielu innych obszarów.
Sortowanie listy
Przed Java 8:
list.sort(new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
Z wyrażeniem lambda:
list.sort((a, b) -> a.length() - b.length());
Tworzenie wątku (Thread)
Thread t = new Thread(() -> System.out.println("Wątek uruchomiony!"));
t.start();
Przetwarzanie kolekcji
list.forEach(s -> System.out.println(s.toUpperCase()));
Filtrowanie listy
List<String> longWords = list.stream()
.filter(s -> s.length() > 5)
.collect(Collectors.toList());
Przykład: Rozwijamy naszą aplikację edukacyjną
Załóżmy, że mamy listę użytkowników:
List<String> users = Arrays.asList("Alice", "Bob", "Charlie");
Wypisz wszystkich użytkowników, których imiona są dłuższe niż 4 znaki:
users.stream()
.filter(name -> name.length() > 4)
.forEach(name -> System.out.println("Użytkownik: " + name));
4. Zasięg zmiennych w wyrażeniach lambda
Wyrażenia lambda mogą używać zmiennych z otaczającej metody, ale są pewne niuanse!
Zmienne i lambdy
Lambda w Java może „przechwytywać” tylko takie zmienne, które po inicjalizacji już się nie zmieniają. Jeśli zmienna jest zadeklarowana jako final — wszystko jest oczywiste. Ale nawet jeśli słowo final nie jest napisane, kompilator sam sprawdza: czy wartość się zmienia, czy nie. Jeśli nie, uznaje ją za „jak gdyby final” i spokojnie dopuszcza w lambdzie.
Przykład:
int minLength = 4; // wartość nigdzie nie jest zmieniana
users.forEach(name -> {
if (name.length() > minLength) {
System.out.println(name);
}
});
Tutaj wszystko działa, ponieważ minLength pozostaje tą samą liczbą.
Jednak jeśli po użyciu w lambdzie spróbujesz przepisać minLength, otrzymasz błąd kompilacji:
int minLength = 4;
users.forEach(name -> {
if (name.length() > minLength) {
System.out.println(name);
}
});
minLength = 10; // Błąd! Lambda już "utrwaliła" wartość
W istocie zasada jest bardzo prosta: zmienna, która trafiła do lambdy, musi być niezmienna.
Różnica względem klas anonimowych
W klasach anonimowych i wyrażeniach lambda zmienne z zewnętrznej metody działają tak samo: tylko final/efektywnie final.
ALE!
W wyrażeniu lambda this odnosi się do obiektu zewnętrznego (bieżącej instancji klasy), a w klasie anonimowej — do instancji klasy anonimowej. To ważne, jeśli wewnątrz lambdy odwołujesz się do pól lub metod bieżącej klasy.
Przykład:
public class Example {
String name = "Klasa zewnętrzna";
void demo() {
Runnable r1 = new Runnable() {
String name = "Klasa anonimowa";
@Override
public void run() {
System.out.println(this.name); // "Klasa anonimowa"
}
};
Runnable r2 = () -> System.out.println(this.name); // "Klasa zewnętrzna"
r1.run();
r2.run();
}
}
5. Typowe błędy przy pracy z wyrażeniami lambda
Błąd nr 1: Użycie zmiennej, która nie jest final/nieefektywnie final. Jeśli postanowisz zmienić zmienną po tym, jak została użyta w lambdzie, kompilator natychmiast Cię powstrzyma. Robi się tak dla bezpieczeństwa: inaczej byłoby niejasne, jaką wartość zmiennej zastosować.
Błąd nr 2: Pomyłka z this. W wyrażeniu lambda this — to klasa zewnętrzna, a w klasie anonimowej — sama klasa anonimowa. Jeśli próbujesz wywołać metodę klasy zewnętrznej z lambdy, wszystko będzie działać, natomiast z klasy anonimowej — niekoniecznie (jeśli liczysz na kontekst klasy zewnętrznej).
Błąd nr 3: Lambda bez kontekstu. Wyrażenia lambda nie można użyć samodzielnie — trzeba je albo przypisać do zmiennej interfejsu funkcyjnego, albo przekazać tam, gdzie taki interfejs jest oczekiwany. Próba po prostu napisania x -> x + 1 poza kontekstem spowoduje błąd.
Błąd nr 4: Zbyt złożona lambda. Jeśli wyrażenie lambda robi się dłuższe niż 3–5 wierszy, trudno je czytać. W takich przypadkach lepiej wynieść logikę do osobnej metody.
GO TO FULL VERSION