CodeGym /Kursy /JAVA 25 SELF /Interfejsy funkcyjne: Predicate, Consumer, Supplier, Func...

Interfejsy funkcyjne: Predicate, Consumer, Supplier, Function

JAVA 25 SELF
Poziom 49 , Lekcja 0
Dostępny

1. Wchodzimy głębiej w interfejs funkcyjny

Interfejs funkcyjny to interfejs, w którym jest DOKŁADNIE jedna metoda abstrakcyjna (czyli niezaimplementowana). Dzięki temu Java rozumie: „aha, tutaj można wstawić lambdę!” albo odwołanie do metody.

Aby nikt się nie pomylił, w takich interfejsach zwykle umieszcza się adnotację @FunctionalInterface. Nie jest obowiązkowa, ale jeśli ją dodasz i przypadkowo napiszesz drugą metodę abstrakcyjną, kompilator od razu zaprotestuje.

Przykład:

@FunctionalInterface
interface MyAction {
    void run();
}
MyAction action = () -> System.out.println("Pozdrowienia z lambdy!");
action.run(); // Wypisze: Pozdrowienia z lambdy!

Po co to jest potrzebne?

  • Pozwala używać wyrażeń lambda i odwołań do metod zamiast tworzenia klas anonimowych (mniej boilerplate’u!).
  • Daje kompilatorowi do zrozumienia, że interfejs jest przeznaczony do programowania funkcyjnego.

Ciekawostka: W standardowej bibliotece Javy jest już kilkadziesiąt takich interfejsów – nie trzeba na nowo wymyślać koła!

2. Przegląd standardowych interfejsów funkcyjnych

W pakiecie java.util.function znajdziesz dziesiątki interfejsów funkcyjnych. Omówimy cztery najpopularniejsze (mają najwyższą „frekwencję” wśród interfejsów Javy).

Interfejs Co przyjmuje Co zwraca Do czego zwykle służy
Predicate<T>
T
boolean
Sprawdzanie warunku (filtrowanie)
Consumer<T>
T
void
Wykonanie działania na obiekcie
Supplier<T>
nic
T
Pobranie/wygenerowanie obiektu
Function<T, R>
T
R
Przekształcenie T w R

Predicate<T>

Opis: Funkcja, która przyjmuje obiekt typu T i zwraca true lub false. Typowy przykład: filtrowanie listy. Kluczowa metoda — test.

Predicate<String> isLong = s -> s.length() > 5;
System.out.println(isLong.test("Java"));       // false
System.out.println(isLong.test("Functional")); // true

Consumer<T>

Opis: Przyjmuje obiekt typu T i wykonuje na nim działanie, nic nie zwraca. Kluczowa metoda — accept.

Consumer<String> printer = s -> System.out.println("Drukuję: " + s);
printer.accept("Hello, world!"); // Drukuję: Hello, world!

Supplier<T>

Opis: Nic nie przyjmuje, zwraca obiekt typu T. Można go traktować jako „generator” wartości. Kluczowa metoda — get.

Supplier<Double> randomSupplier = () -> Math.random();
System.out.println(randomSupplier.get()); // Na przykład: 0.1234567

Function<T, R>

Opis: Przyjmuje obiekt typu T i zwraca obiekt typu R. Typowy przykład: przekształcanie danych. Kluczowa metoda — apply.

Function<String, Integer> stringToLength = s -> s.length();
System.out.println(stringToLength.apply("Java")); // 4

W skrócie: UnaryOperator, BinaryOperator, BiFunction

  • UnaryOperator<T> – to to samo, co Function<T, T>: przyjmuje i zwraca ten sam typ.
  • BinaryOperator<T> – to to samo, co BiFunction<T, T, T>: przyjmuje dwa T, zwraca jedno T.
  • BiFunction<T, U, R> – przyjmuje dwa różne typy, zwraca trzeci.
UnaryOperator<Integer> square = x -> x * x;
BinaryOperator<Integer> sum = (a, b) -> a + b;
BiFunction<String, Integer, String> repeat = (s, n) -> s.repeat(n);

3. Przykłady użycia

Zobaczmy, jak te interfejsy pojawiają się w prawdziwych zadaniach, a szczególnie – w kolekcjach i Stream API.

Przekazywanie do metod kolekcji i Stream API

Przykład 1: Predicate i filtrowanie

List<String> words = List.of("java", "stream", "lambda", "code");
List<String> longWords = words.stream()
    .filter(word -> word.length() > 4) // Predicate<String>
    .toList();
System.out.println(longWords); // [stream, lambda]

Przykład 2: Consumer i forEach

words.forEach(word -> System.out.println("Słowo: " + word)); // Consumer<String>

Przykład 3: Function i map

List<Integer> lengths = words.stream()
    .map(word -> word.length()) // Function<String, Integer>
    .toList();
System.out.println(lengths); // [4, 6, 6, 4]

Przykład 4: Supplier i generowanie wartości

Supplier<String> greetingSupplier = () -> "Cześć, Java!";
System.out.println(greetingSupplier.get()); // Cześć, Java!

Porównanie z klasami anonimowymi

Kiedyś trzeba było pisać tak:

Predicate<String> isShort = new Predicate<String>() {
    @Override
    public boolean test(String s) {
        return s.length() < 5;
    }
};

Z lambdami jest o wiele przyjemniej:

Predicate<String> isShort = s -> s.length() < 5;

4. Praktyka: piszemy wyrażenia lambda dla każdego interfejsu

Zaimplementujemy małą aplikację – listę użytkowników. Każdy użytkownik będzie reprezentowany przez klasę User:

public class User {
    private final String name;
    private final int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() { return name; }
    public int getAge() { return age; }

    @Override
    public String toString() {
        return name + " (" + age + ")";
    }
}

Utwórzmy listę użytkowników:

List<User> users = List.of(
    new User("Anna", 23),
    new User("Boris", 17),
    new User("Vika", 31),
    new User("Gosha", 15)
);

Predicate: filtrowanie dorosłych

Predicate<User> isAdult = user -> user.getAge() >= 18;
List<User> adults = users.stream()
    .filter(isAdult)
    .toList();

System.out.println("Dorośli: " + adults); // Dorośli: [Anna (23), Vika (31)]

Consumer: drukowanie użytkowników

Consumer<User> printUser = user -> System.out.println("Użytkownik: " + user);
adults.forEach(printUser);

Supplier: generowanie użytkowników

Supplier<User> randomUserSupplier = () -> {
    String[] names = {"Dima", "Katya", "Lyosha"};
    int randomAge = 10 + (int)(Math.random() * 30);
    String randomName = names[(int)(Math.random() * names.length)];
    return new User(randomName, randomAge);
};

User randomUser = randomUserSupplier.get();
System.out.println("Losowy użytkownik: " + randomUser);

Function: pobieranie imienia użytkownika

Function<User, String> getName = user -> user.getName();
List<String> names = users.stream()
    .map(getName)
    .toList();

System.out.println("Imiona: " + names); // Imiona: [Anna, Boris, Vika, Gosha]

5. Przydatne niuanse

Użycie w Stream API: filter, map, forEach i inne

Złóżmy wszystko w całość i napiszmy łańcuch przekształceń:

users.stream()
     .filter(user -> user.getAge() >= 18)         // Predicate<User>
     .map(user -> user.getName().toUpperCase())    // Function<User, String>
     .forEach(name -> System.out.println("Dorosły: " + name)); // Consumer<String>

Wynik:

Dorosły: ANNA
Dorosły: VIKA

Tabela-ściągawka: co gdzie przekazać

Gdzie używane Jaki interfejs jest potrzebny Przykład użycia
filter (Stream)
Predicate<T>
filter(u -> u.getAge() > 18)
map (Stream)
Function<T, R>
map(u -> u.getName())
forEach (Stream, List)
Consumer<T>
forEach(u -> System.out.println(u))
generate (Stream)
Supplier<T>
Stream.generate(() -> ...)

Dlaczego warto znać interfejsy funkcyjne?

  • Stanowią podstawę wszystkich wyrażeń lambda w Javie.
  • Pozwalają pisać uniwersalny, wielokrotnego użytku i zwięzły kod.
  • Ułatwiają pracę z kolekcjami, strumieniami i zadaniami asynchronicznymi.

Kiedy używać którego interfejsu?

  • Predicate – gdy trzeba sprawdzić warunek (filtrować, wyszukiwać).
  • Consumer – gdy trzeba coś zrobić z obiektem (wypisać, zapisać, wysłać).
  • Supplier – gdy trzeba otrzymać lub wygenerować obiekt (fabryki, generatory).
  • Function – gdy trzeba przekształcić obiekt z jednego typu na inny.

6. Typowe błędy przy pracy z interfejsami funkcyjnymi

Błąd nr 1: Niewłaściwy wybór interfejsu. Czasem początkujący mylą Predicate i Function – na przykład próbują zwracać boolean z Function, a nie z Predicate. Zapamiętaj: Predicate zawsze zwraca boolean, Function – dowolny inny typ.

Błąd nr 2: Nieużywanie standardowych interfejsów. Często tworzy się własne interfejsy w rodzaju „Checker” z metodą boolean check(T t) zamiast używać Predicate. Lepiej korzystać ze standardowych – są powszechnie wspierane i czynią kod bardziej zrozumiałym dla innych programistów.

Błąd nr 3: Zbyt skomplikowana lambda. Jeśli lambda zamienia się w miniopowiadanie na 10 linii, warto wynieść ją do osobnej metody lub klasy. Lambda to zwięzłość i czytelność.

Błąd nr 4: Zapomniana adnotacja @FunctionalInterface. Jeśli piszesz własny interfejs funkcyjny – nie zapomnij o adnotacji. Zabezpieczy przed przypadkowymi błędami (np. dodaniem drugiej metody abstrakcyjnej).

Błąd nr 5: Używanie zmiennego stanu wewnątrz lambdy. Jeśli lambda zmienia zewnętrzne zmienne lub kolekcje, może to prowadzić do nieoczekiwanych błędów, zwłaszcza podczas pracy z wątkami. Lepiej unikać efektów ubocznych.

Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION