1. Wyrażenia lambda i kolekcje
Przypomnijmy, jak wyglądało przetwarzanie kolekcji przed pojawieniem się wyrażeń lambda. Załóżmy, że mamy listę napisów i chcemy je wypisać na ekran:
List<String> list = Arrays.asList("kot", "pies", "jeż");
for (String s : list) {
System.out.println(s);
}
To proste, ale jeśli chcemy na przykład usunąć z listy wszystkie puste napisy, trzeba pisać pętlę z warunkiem, a czasem użyć iteratora (w przeciwnym razie będzie ConcurrentModificationException). Albo, powiedzmy, sortowanie po długości napisu:
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
Nawet w tak prostym zadaniu — już 5 linii kodu i masa „szumu” nawiasów. Jak to zoptymalizować? Odpowiedź już znacie: wyrażenia lambda.
Zastosowanie wyrażeń lambda w metodach kolekcji
Od Java 8 interfejsy kolekcji otrzymały nowe metody, które przyjmują interfejsy funkcyjne — co oznacza, że możemy przekazywać do nich wyrażenia lambda. Oto najpopularniejsze z nich:
- forEach(Consumer<T> action)
- removeIf(Predicate<T> filter)
- sort(Comparator<T> c)
- replaceAll(UnaryOperator<T> operator)
Przykład: forEach
Wypisać wszystkie elementy listy na ekran (stary sposób):
for (String s : list) {
System.out.println(s);
}
Teraz — z lambdą:
list.forEach(s -> System.out.println(s));
Albo jeszcze krócej, jeśli chcesz pobawić się w „guru Javy”:
list.forEach(System.out::println); // referencja do metody, omówimy później
Przykład: removeIf
Usunąć wszystkie puste napisy z listy:
List<String> animals = new ArrayList<>(Arrays.asList("kot", "", "pies", "jeż", ""));
animals.removeIf(s -> s.isEmpty());
System.out.println(animals); // [kot, pies, jeż]
Przykład: sort
Sortowanie listy po długości napisu:
List<String> animals = Arrays.asList("kot", "pies", "jeż", "słoń");
animals.sort((a, b) -> a.length() - b.length());
System.out.println(animals); // [kot, jeż, pies, słoń]
Przykład: replaceAll
Przekształcić wszystkie napisy na wielkie litery:
List<String> animals = new ArrayList<>(Arrays.asList("kot", "pies", "jeż"));
animals.replaceAll(s -> s.toUpperCase());
System.out.println(animals); // [KOT, PIES, JEŻ]
2. Stream API i wyrażenia lambda
Wraz z wydaniem Java 8 pojawiło się Stream API — potężne narzędzie do przetwarzania kolekcji w stylu funkcyjnym. Strumienie pozwalają filtrować, przekształcać, sortować i zbierać kolekcje za pomocą łańcuchów metod. I wszystkie te metody przyjmują wyrażenia lambda!
Ważne: Pełne omówienie Stream API będzie później, teraz — tylko podstawowe przykłady dla zrozumienia roli lambd.
Przykład: filtrowanie
Zostawić tylko napisy dłuższe niż 3 znaki:
List<String> animals = Arrays.asList("kot", "słoń", "jeż", "krokodyl");
animals.stream()
.filter(s -> s.length() > 3)
.forEach(System.out::println); // słoń, krokodyl
Przykład: przekształcenie (map)
Zamienić wszystkie napisy na wielkie litery:
List<String> animals = Arrays.asList("kot", "słoń", "jeż");
List<String> upper = animals.stream()
.map(s -> s.toUpperCase())
.collect(Collectors.toList());
System.out.println(upper); // [KOT, SŁOŃ, JEŻ]
Przykład: sortowanie
Uzyskać listę posortowaną po długości (nie zmieniając oryginalnej):
List<String> animals = Arrays.asList("kot", "słoń", "jeż", "krokodyl");
List<String> sorted = animals.stream()
.sorted((a, b) -> a.length() - b.length())
.collect(Collectors.toList());
System.out.println(sorted); // [kot, jeż, słoń, krokodyl]
3. Porównanie z klasami anonimowymi
Porównajmy, jak wyglądałby ten sam kod z klasą anonimową i z lambdą.
Sortowanie po długości napisu
Klasa anonimowa:
animals.sort(new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
Wyrażenie lambda:
animals.sort((a, b) -> a.length() - b.length());
Wniosek:
Wyrażenie lambda oszczędza mnóstwo linii i czyni kod bardziej czytelnym. Mniej nawiasów, mniej szumu — więcej sedna!
Usuwanie pustych napisów
Klasa anonimowa:
animals.removeIf(new Predicate<String>() {
@Override
public boolean test(String s) {
return s.isEmpty();
}
});
Wyrażenie lambda:
animals.removeIf(s -> s.isEmpty());
4. Praktyka: krótkie zadania z wyrażeniami lambda
Spróbujmy w praktyce zastosować wyrażenia lambda w mini-programie do pracy z listą użytkowników.
Przykład 1: Filtrowanie użytkowników po wieku
class User {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return name + " (" + age + ")";
}
}
List<User> users = Arrays.asList(
new User("Alisa", 17),
new User("Bob", 25),
new User("Charli", 15)
);
users.stream()
.filter(u -> u.age >= 18)
.forEach(System.out::println); // Bob (25)
Przykład 2: Sortowanie użytkowników po imieniu
List<User> users = Arrays.asList(
new User("Alisa", 17),
new User("Bob", 25),
new User("Charli", 15)
);
users.sort((u1, u2) -> u1.name.compareTo(u2.name));
System.out.println(users);
// [Alisa (17), Bob (25), Charli (15)]
Przykład 3: Przekształcenie listy użytkowników w listę imion
List<String> names = users.stream()
.map(u -> u.name)
.collect(Collectors.toList());
System.out.println(names); // [Alisa, Bob, Charli]
Przykład 4: Usunąć wszystkich niepełnoletnich
List<User> users = new ArrayList<>(Arrays.asList(
new User("Alisa", 17),
new User("Bob", 25),
new User("Charli", 15)
));
users.removeIf(u -> u.age < 18);
System.out.println(users); // [Bob (25)]
5. Przydatne niuanse
Cechy: zasięg i „efektywnie final” zmienne
Wyrażenie lambda może używać zmiennych z metody zewnętrznej, ale tylko jeśli są final lub „efektywnie final” (czyli nie są zmieniane po inicjalizacji).
int minAge = 18;
users.stream()
.filter(u -> u.age >= minAge)
.forEach(System.out::println);
Jeśli spróbujesz zmienić minAge po użyciu w lambdzie — kompilator zgłosi błąd.
Tabela: podstawowe metody kolekcji i strumieni z wyrażeniami lambda
| Metoda kolekcji/strumienia | Co robi | Typ wyrażenia lambda | Przykład |
|---|---|---|---|
|
Dla każdego elementu | |
|
|
Usuwa elementy spełniające warunek | |
|
|
Sortuje elementy | |
|
|
Zastępuje każdy element | |
|
|
Filtruje strumień | |
|
|
Przekształca elementy | |
|
|
Iteracja po strumieniu | |
|
|
Sortowanie w strumieniu | |
|
7. Typowe błędy
Błąd nr 1: Lambda jest zbyt długa. Jeśli w wyrażeniu lambda masz już 5 linii kodu, warunki, pętle i try-catch — najpewniej warto wynieść ten kod do osobnej metody. Lambdy są dobre do krótkiej logiki.
Błąd nr 2: Używanie zmiennych modyfikowalnych. Jeśli próbujesz wewnątrz lambdy zmienić zmienną z metody zewnętrznej (na przykład licznik), kompilator na to nie pozwoli. Zmienna musi być final albo efektywnie final.
Błąd nr 3: Zapominanie, że metody kolekcji/strumieni nie zawsze zmieniają oryginalną kolekcję. Na przykład stream().filter(...) nie zmienia listy źródłowej, lecz zwraca nowy strumień. Jeśli chcesz otrzymać kolekcję — użyj collect(Collectors.toList()).
Błąd nr 4: Wyrażenie lambda nie pasuje typem. Jeśli metoda przyjmuje na przykład Comparator<T>, a próbujesz przekazać lambdę z jednym parametrem (zamiast dwóch) — będzie błąd kompilacji.
Błąd nr 5: Utrata czytelności przy zagnieżdżonych lambdach. Jeśli masz łańcuch z map, filter, forEach, a w każdej lambdzie kolejna lambda — kod staje się nieczytelny. W takich przypadkach lepiej podzielić wyrażenia na osobne kroki albo wydzielić części do metod.
GO TO FULL VERSION