1. Przekazywanie zachowania zamiast danych
Po co w ogóle przekazywać funkcję jako parametr?
Wyobraź sobie: masz metodę, która sortuje listę. Ale skąd ma wiedzieć, jak dokładnie sortować? Alfabetycznie? Po długości? Po dacie urodzenia? Można by napisać osobną metodę dla każdego przypadku, ale szybko zamieni się to w koszmarne kopiuj-wklej.
Zamiast tego Java pozwala przekazywać zachowanie (czyli funkcję lub lambdę), która definiuje, jak sortować, filtrować, przekształcać i tak dalej. To sprawia, że kod jest elastyczny i wielokrotnie używalny.
Przykład: sortowanie z komparatorem
List<String> names = List.of("Anna", "Boris", "Vika");
// Przekazujemy zachowanie: jak porównywać elementy (po długości łańcucha)
names.stream()
.sorted((a, b) -> a.length() - b.length())
.forEach(System.out::println);
Przykład: filtrowanie z predykatem
List<String> names = List.of("Anna", "Boris", "Vika");
// Przekazujemy zachowanie: kogo zostawić (imię dłuższe niż 4 znaki)
names.stream()
.filter(name -> name.length() > 4)
.forEach(System.out::println);
W obu przypadkach nie „zaszywasz” logiki na sztywno w metodzie, tylko dajesz jej kawałek zachowania, który jest stosowany do każdego elementu.
Korzyści: mniej duplikacji, więcej elastyczności
Zamiast dziesiątek metod z podobnym kodem, ale różną logiką, piszesz jedną uniwersalną metodę, która przyjmuje „co robić” w postaci funkcji. To oszczędza czas, zmniejsza liczbę błędów i ułatwia testowanie.
2. Składnia przekazywania funkcji
Wyrażenia lambda jako argumenty
Najczęstszy sposób – w miejscu wywołania metody napisać lambdę ze strzałką ->:
list.forEach(item -> System.out.println(item));
Albo nawet krócej, jeśli odpowiednia metoda już istnieje – referencja do metody (::):
list.forEach(System.out::println);
Referencje do metod (method references)
Jeśli masz już odpowiednią metodę, możesz przekazać ją jako referencję :::
// Zwykła metoda
public static boolean isLongName(String name) {
return name.length() > 4;
}
// Przekazanie referencji do metody
names.stream()
.filter(MyClass::isLongName)
.forEach(System.out::println);
To działa, jeśli sygnatura metody odpowiada tej oczekiwanej przez interfejs funkcyjny (Predicate<T>, Function<T, R>, Comparator<T> itd.).
3. Przykłady ze standardowej biblioteki
Collections.sort i Comparator
List<String> names = new ArrayList<>(List.of("Anna", "Boris", "Vika"));
// Sortowanie po długości imienia
names.sort((a, b) -> a.length() - b.length());
System.out.println(names); // [Vika, Anna, Boris]
Stream.filter i Predicate
List<String> names = List.of("Anna", "Boris", "Vika");
// Zostawić tylko imiona zaczynające się na 'V'
names.stream()
.filter(name -> name.startsWith("V"))
.forEach(System.out::println); // Vika
Stream.map i Function
List<String> names = List.of("Anna", "Boris", "Vika");
// Zamienić imiona na wielkie litery
names.stream()
.map(String::toUpperCase)
.forEach(System.out::println);
Optional.ifPresent i Consumer
Optional<String> opt = Optional.of("Cześć!");
// Jeśli wartość jest obecna, wydrukuj ją
opt.ifPresent(s -> System.out.println("W tekście: " + s));
4. Praktyka: piszemy własne metody z funkcjami-parametrami
Czas zastosować wiedzę w praktyce! Napiszmy kilka metod, które przyjmują funkcję jako parametr i używają jej w środku.
Przykład 1: Metoda do obsługi elementów listy (forEach po swojemu)
Załóżmy, że mamy listę użytkowników (User). Chcemy wykonać nad każdym użytkownikiem jakieś działanie – na przykład wypisać imię, wysłać e‑mail, naliczyć bonusy itd. Zamiast „zaszywać” działanie na sztywno, przekażemy je jako parametr – Consumer<User>!
import java.util.List;
import java.util.function.Consumer;
class User {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
}
public class UserProcessor {
// Metoda przyjmująca Consumer<User>
public static void processUsers(List<User> users, Consumer<User> action) {
for (User user : users) {
action.accept(user);
}
}
public static void main(String[] args) {
List<User> users = List.of(
new User("Anna", 25),
new User("Boris", 30),
new User("Vika", 22)
);
// Wypisać imiona wszystkich użytkowników
processUsers(users, user -> System.out.println("Imię: " + user.name));
// Naliczyć bonus (dla przykładu tylko wypisać)
processUsers(users, user -> System.out.println(user.name + " otrzymał bonus!"));
}
}
Przykład 2: Metoda do filtrowania elementów (Predicate)
Napiszemy metodę, która zwraca tylko tych użytkowników, którzy spełniają warunek (np. tylko pełnoletnich):
import java.util.List;
import java.util.ArrayList;
import java.util.function.Predicate;
public class UserFilter {
public static List<User> filterUsers(List<User> users, Predicate<User> condition) {
List<User> result = new ArrayList<>();
for (User user : users) {
if (condition.test(user)) {
result.add(user);
}
}
return result;
}
public static void main(String[] args) {
List<User> users = List.of(
new User("Anna", 25),
new User("Boris", 17),
new User("Vika", 22)
);
// Filtrowanie tylko dorosłych
List<User> adults = filterUsers(users, user -> user.age >= 18);
adults.forEach(user -> System.out.println(user.name)); // Anna, Vika
}
}
Przykład 3: Metoda-przekształcacz (Function)
A teraz metoda, która z listy użytkowników robi listę ich imion (lub inne przekształcenia):
import java.util.List;
import java.util.ArrayList;
import java.util.function.Function;
public class UserMapper {
public static <R> List<R> mapUsers(List<User> users, Function<User, R> mapper) {
List<R> result = new ArrayList<>();
for (User user : users) {
result.add(mapper.apply(user));
}
return result;
}
public static void main(String[] args) {
List<User> users = List.of(
new User("Anna", 25),
new User("Boris", 17),
new User("Vika", 22)
);
// Uzyskujemy listę imion
List<String> names = mapUsers(users, user -> user.name);
System.out.println(names); // [Anna, Boris, Vika]
// Uzyskujemy listę wieków
List<Integer> ages = mapUsers(users, user -> user.age);
System.out.println(ages); // [25, 17, 22]
}
}
Przykład 4: Metoda-generator (Supplier)
Czasem trzeba uzyskać pewną wartość „na żądanie” – np. wygenerować losową liczbę, utworzyć obiekt, pobrać bieżący czas. Do tego nadaje się interfejs Supplier<T>.
import java.util.function.Supplier;
public class ValueGenerator {
public static int getValue(Supplier<Integer> supplier) {
return supplier.get();
}
public static void main(String[] args) {
// Pobieramy losową liczbę
int random = getValue(() -> (int)(Math.random() * 100));
System.out.println("Losowa liczba: " + random);
// Pobieramy bieżący czas w milisekundach
long time = getValue(System::currentTimeMillis);
System.out.println("Czas: " + time);
}
}
5. Jedna aplikacja: łączymy przykłady
Wyobraźmy sobie, że tworzymy prostą aplikację „Lista użytkowników”. Już potrafimy:
- Filtrować użytkowników według warunku (np. tylko dorosłych);
- Przekształcać użytkowników w coś (imiona, e-mail, wiek);
- Wykonywać działania nad każdym użytkownikiem (druk, naliczanie bonusów);
- Generować wartości na żądanie (np. tworzyć nowych użytkowników).
Teraz, używając tych metod, możemy budować elastyczne scenariusze przetwarzania danych, bez przepisywania kodu za każdym razem do nowego zadania.
import java.util.*;
import java.util.function.*;
public class UserApp {
static class User {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
public String toString() {
return name + " (" + age + ")";
}
}
// Uniwersalna metoda do przetwarzania użytkowników
static void processUsers(List<User> users, Consumer<User> action) {
for (User user : users) action.accept(user);
}
// Uniwersalny filtr
static List<User> filterUsers(List<User> users, Predicate<User> condition) {
List<User> result = new ArrayList<>();
for (User user : users) if (condition.test(user)) result.add(user);
return result;
}
// Uniwersalny przekształtnik
static <R> List<R> mapUsers(List<User> users, Function<User, R> mapper) {
List<R> result = new ArrayList<>();
for (User user : users) result.add(mapper.apply(user));
return result;
}
public static void main(String[] args) {
List<User> users = List.of(
new User("Anna", 25),
new User("Boris", 17),
new User("Vika", 22)
);
// 1. Wypisać wszystkich użytkowników
processUsers(users, user -> System.out.println("Użytkownik: " + user));
// 2. Znaleźć tylko dorosłych
List<User> adults = filterUsers(users, user -> user.age >= 18);
System.out.println("Dorośli: " + adults);
// 3. Uzyskać imiona dorosłych
List<String> adultNames = mapUsers(adults, user -> user.name);
System.out.println("Imiona dorosłych: " + adultNames);
// 4. Naliczyć bonus dorosłym
processUsers(adults, user -> System.out.println(user.name + " otrzymał bonus!"));
}
}
Dlaczego to lepsze niż „zwyczajne” podejście?
Taki sposób daje elastyczność: te same metody można wykorzystywać w zupełnie różnych scenariuszach, po prostu przekazując inne funkcje. Kod nie rozrasta się w nieskończone warianty „filtrujemy tak”, „drukujemy owak” – mamy wspólne narzędzia, które działają wszędzie. Dzięki temu jest bardziej zwięzły, czytelniejszy i mniej podatny na błędy. A do tego taki styl świetnie współgra z nowoczesnym Stream API, więc przechodząc do strumieni nie trzeba uczyć się od nowa – podejście jest to samo.
6. Typowe błędy przy przekazywaniu funkcji jako parametrów
Błąd nr 1: Sygnatura nie zgadza się z oczekiwanym interfejsem.
Jeśli metoda przyjmuje Predicate<User>, a przekazujesz lambdę, która zwraca nie boolean, lecz na przykład String, kompilator od razu powie „aj-aj-aj!” i nie pozwoli zbudować projektu. Sprawdź, że wartość zwracana i parametry zgadzają się z oczekiwanymi.
Błąd nr 2: Lambda używa zmiennych, które mogą się zmienić.
W Javie wyrażenia lambda mogą używać tylko zmiennych final lub effectively final z zewnętrznego kontekstu. Jeśli próbujesz zmienić taką zmienną wewnątrz lambdy – pojawi się błąd kompilacji.
Błąd nr 3: Mylisz interfejsy.
Czasem chcesz przekazać na przykład Consumer<T>, ale przez pomyłkę piszesz funkcję, która coś zwraca (np. Function<T, R>). Upewnij się, że twoja lambda zwraca dokładnie to, co trzeba (albo nic – dla Consumer).
Błąd nr 4: Zbyt złożone lambdy bezpośrednio w parametrach.
Jeśli lambda zajmuje więcej niż jedną–dwie linijki, lepiej wynieść ją do osobnej zmiennej lub metody. Inaczej kod stanie się nieczytelny i tylko ty będziesz w stanie się w nim połapać (i to nie zawsze).
GO TO FULL VERSION