CodeGym /Kursy /JAVA 25 SELF /Przekazywanie funkcji jako parametrów: przykłady

Przekazywanie funkcji jako parametrów: przykłady

JAVA 25 SELF
Poziom 49, Lekcja 2
Dostępny

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).

1
Zadanie
JAVA 25 SELF, poziom 49, lekcja 2
Niedostępne
Parada Zwierząt: Ustawienie według imion 🐘
Parada Zwierząt: Ustawienie według imion 🐘
1
Zadanie
JAVA 25 SELF, poziom 49, lekcja 2
Niedostępne
Raport wskaźników "Monitoringu Gwiazd" 🔭
Raport wskaźników "Monitoringu Gwiazd" 🔭
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION