1. Wprowadzenie
Jeśli krótko: wyrażenie lambda – to sposób, aby szybko utworzyć implementację interfejsu funkcyjnego „w locie”, bez deklarowania oddzielnej klasy ani klasy anonimowej. To jak mała metoda bez nazwy, którą można przekazać jako argument albo zapisać w zmiennej.
Przez długi czas Java była „klasycznym” językiem obiektowym (OOP). Jeśli chcieliście przekazać kawałek zachowania, na przykład co zrobić po kliknięciu przycisku, trzeba było pisać klasy anonimowe:
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick() {
System.out.println("Przycisk naciśnięty!");
}
});
Od Javy 8 pojawiła się magia lambd:
button.setOnClickListener(() -> System.out.println("Przycisk naciśnięty!"));
Gdzie () -> System.out.println("Przycisk naciśnięty!") – to właśnie wyrażenie lambda.
Formalnie: wyrażenie lambda to zwięzła forma zapisu implementacji jedynej metody abstrakcyjnej interfejsu funkcyjnego.
Dlaczego to ważne?
- Przekazywać zachowanie jako wartość (np. co zrobić z każdym elementem listy).
- Pisać kod zwięzły i czytelny.
- Korzystać ze współczesnych API Javy: Stream API, obsługa zdarzeń, zadania asynchroniczne i wiele więcej.
2. Składnia wyrażeń lambda
Ogólny zapis
(parametry) -> wyrażenie
// albo
(parametry) -> { blok kodu }
Przykłady dla różnych interfejsów
1. Bez parametrów (np. Runnable):
Runnable r = () -> System.out.println("Cześć, lambda!");
r.run();
2. Jeden parametr (np. Consumer):
Consumer<String> printer = s -> System.out.println("Przekazano: " + s);
printer.accept("Java");
Jeśli parametr jest tylko jeden i jego typ da się wywnioskować, można pominąć nawiasy.
Typ String to typ zmiennej (s), którą tam przekazujemy.
3. Wiele parametrów (np. Comparator):
Comparator<Integer> cmp = (a, b) -> a - b;
System.out.println(cmp.compare(10, 5)); // 5
Typ Integer to typ zmiennych (a, b), które tam przekazujemy.
4. Wieloliniowe ciało (potrzebne klamry i return, jeśli jest jawny zwrot):
Function<Integer, Integer> square = x -> {
int result = x * x;
return result;
};
System.out.println(square.apply(6)); // 36
Pierwszy Integer to typ wyniku funkcji, drugi Integer to typ parametru.
Skróty i zwięzłość
- Jeśli ciało to pojedyncze wyrażenie, można pominąć nawiasy i return.
- Jeśli nie ma parametrów – piszemy puste nawiasy: () -> ...
- Jeśli parametr jest jeden – można bez nawiasów: x -> ...
- Jeśli parametrów jest więcej niż jeden – potrzebne są nawiasy: (a, b) -> ...
Tabela podsumowująca
Zobaczmy, jak tę samą ideę zapisać lambdą i klasą anonimową:
| Interfejs | Wyrażenie lambda (przykład) | Równoważny zapis z klasą anonimową |
|---|---|---|
|
|
|
|
|
|
|
|
|
Przykład z własnym interfejsem
Załóżmy, że mamy interfejs funkcyjny:
@FunctionalInterface
interface Operation {
int apply(int a, int b);
}
Dawniej implementowano tak:
Operation sum = new Operation() {
@Override
public void apply(int a, int b) {
return a + b;
}
};
A teraz – nowocześnie:
Operation sum = (a, b) -> a + b;
System.out.println(sum.apply(3, 5)); // 8
Przykłady dla standardowych interfejsów
Runnable:
Runnable hello = () -> System.out.println("Hello from thread!");
new Thread(hello).start();
Comparator:
List<String> list = Arrays.asList("jabłko", "banan", "kiwi");
list.sort((a, b) -> a.length() - b.length());
System.out.println(list);
Function:
Function<String, Integer> parse = s -> Integer.parseInt(s);
System.out.println(parse.apply("123")); // 123
3. Zakres i przechwytywanie zmiennych
Zmienne z kontekstu zewnętrznego (effectively final)
Wyrażenia lambda mogą korzystać ze zmiennych z otaczającej metody. Ale jest zasada: takie zmienne muszą być effectively final – czyli albo jawnie oznaczone jako final, albo po prostu niezmieniane po inicjalizacji.
Przykład:
String prefix = "Wynik: ";
Function<Integer, String> f = x -> prefix + (x * 2);
// prefix jest tutaj „zamrożony” – po tym nie wolno go już zmieniać
System.out.println(f.apply(5)); // Wynik: 10
Jeśli spróbujemy zmienić prefix po tym, jak został użyty w lambdzie, kompilator zgłosi błąd.
Dlaczego tak?
Wyrażenie lambda może zostać wywołane po wyjściu z metody, w której zadeklarowano zmienną. Aby uniknąć „magicznych” błędów, Java pozwala używać tylko niezmiennych zmiennych.
Różnice względem klas anonimowych
W klasach anonimowych obowiązuje ta sama zasada: zmienne z zewnętrznej metody muszą być final/effectively final. Są jednak niuanse związane z zakresem: wewnątrz klasy anonimowej this odnosi się do samej klasy anonimowej, a w lambdzie – do klasy zewnętrznej.
public class Demo {
public void test() {
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println(this); // Wypisze: Demo$1 (klasa anonimowa)
}
};
Runnable r2 = () -> System.out.println(this); // Wypisze: Demo (klasa zewnętrzna)
r1.run();
r2.run();
}
}
Lambda i pola klasy
Wyrażenie lambda może odwoływać się do pól klasy zewnętrznej bez ograniczeń:
public class Counter {
private int base = 10;
public void printSum(int x) {
Function<Integer, Integer> sum = y -> base + y + x;
System.out.println(sum.apply(5));
}
}
Tutaj base – można zmieniać (to pole klasy).
x – musi być effectively final.
4. Praktyka: napiszmy kilka wyrażeń lambda
Przykład: filtrowanie listy liczb
List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5, 6);
nums.stream()
.filter(n -> n % 2 == 0)
.forEach(n -> System.out.println("Parzysta: " + n));
Więcej o Stream API poznasz na 30. poziomie :P
Przykład: funkcja przekształcenia napisu
Function<String, String> capitalize = s -> s.toUpperCase();
System.out.println(capitalize.apply("java")); // JAVA
Przykład: własny interfejs funkcyjny
@FunctionalInterface
interface StringTransformer {
String transform(String s);
}
StringTransformer exclaim = s -> s + "!";
System.out.println(exclaim.transform("Cześć")); // Cześć!
Przykład: użycie zmiennych z kontekstu zewnętrznego
int factor = 2;
List<Integer> numbers = Arrays.asList(1, 2, 3);
numbers.forEach(n -> System.out.println(n * factor));
// factor nie można zmienić po tym!
5. Typowe błędy przy pracy z wyrażeniami lambda
Błąd nr 1: próba zmiany zmiennej przechwyconej przez lambdę.
Taki kod się nie skompiluje – zmienna musi być effectively final:
int sum = 0;
List<Integer> numbers = Arrays.asList(1, 2, 3);
numbers.forEach(n -> sum += n); // Błąd: sum nie jest final!
Jeśli trzeba akumulować wartości – użyj tablicy albo obiektu typu wrapper.
Błąd nr 2: zamieszanie z zakresem this.
Wewnątrz wyrażenia lambda this odnosi się do klasy zewnętrznej, a nie do samej lambdy (w przeciwieństwie do klasy anonimowej).
Błąd nr 3: brak nawiasów klamrowych i return w wieloliniowym wyrażeniu lambda.
Jeśli ciało lambdy to nie jedno wyrażenie, potrzebne są klamry i return:
Function<Integer, Integer> square = x -> {
int y = x * x;
return y;
};
Błąd nr 4: nieprawidłowe określenie typu wyrażenia lambda.
Wyrażenie lambda zawsze implementuje interfejs funkcyjny. Nie można po prostu napisać:
var f = x -> x + 1; // Błąd! Nie wiadomo, jakiego typu interfejs.
Trzeba jawnie wskazać typ:
Function<Integer, Integer> f = x -> x + 1;
GO TO FULL VERSION