CodeGym /Kursy /JAVA 25 SELF /Kompozycja funkcji: compose, andThen

Kompozycja funkcji: compose, andThen

JAVA 25 SELF
Poziom 49, Lekcja 1
Dostępny

1. Pojęcie kompozycji funkcji

Trochę teorii (ale bez zanudzania)

W matematyce kompozycja funkcji to sytuacja, gdy wynik jednej funkcji staje się wejściem dla innej. Jeśli mamy funkcje f i g, to kompozycja g(f(x)) oznacza: najpierw stosujemy f do x, potem wynik podstawiamy do g.

W programowaniu idea jest taka sama: chcemy budować złożone przekształcenia z prostych, aby nie pisać jednej gigantycznej funkcji od wszystkiego. To czyni kod elastycznym, łatwym do ponownego użycia i czytelnym.

Wyobraź sobie taśmę produkcyjną ciastek: najpierw ciasto (f), potem krem (g), potem posypka (h). Cały proces — to h(g(f(skladniki))).

Dlaczego kompozycja jest ważna?

  • Kompozycja pozwala składać program z małych funkcji–„klocków”.
  • Łatwiejsze ponowne użycie: jeden „klocek” można wstawić w różne miejsca bez duplikowania.
  • Elastyczność: aby zmienić jeden etap, wymieniamy odpowiednią funkcję — reszty nie ruszamy.
  • Czytelność i testowalność: małe funkcje są łatwiejsze do czytania, sprawdzania i utrzymania.

2. Metody compose i andThen w interfejsie Function

Interfejs Function: przypomnienie

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);

    // Metody kompozycji:
    default <V> Function<V, R> compose(Function<? super V, ? extends T> before)
    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after)
}
  • compose: najpierw wykonywana jest funkcja przekazana do compose, a potem — bieżąca.
  • andThen: najpierw wykonywana jest bieżąca funkcja, a potem — ta, która została przekazana do andThen.

Schemat wizualny

// Załóżmy, że mamy dwie funkcje:
Function<String, Integer> parse = s -> Integer.parseInt(s);
Function<Integer, Integer> square = x -> x * x;

// compose: square.compose(parse) == x -> square.apply(parse.apply(x))
"5" --parse--> 5 --square--> 25

// andThen: parse.andThen(square) == x -> square.apply(parse.apply(x))
"5" --parse--> 5 --square--> 25

// Ale kolejność ma znaczenie, jeśli typy są różne!

Przykład: konwersja łańcucha na liczbę, a następnie do kwadratu

import java.util.function.Function;

public class ComposeAndThenDemo {
    public static void main(String[] args) {
        // Funkcja: zamienia tekst na liczbę
        Function<String, Integer> parse = s -> Integer.parseInt(s);

        // Funkcja: podnosi liczbę do kwadratu
        Function<Integer, Integer> square = x -> x * x;

        // Połączmy: najpierw parsujemy, potem podnosimy do kwadratu
        Function<String, Integer> parseThenSquare = parse.andThen(square);

        System.out.println(parseThenSquare.apply("7")); // 49

        // A jeśli zamienimy kolejność?
        // square.compose(parse) — taki sam rezultat (dla tych funkcji)
        Function<String, Integer> squareOfParsed = square.compose(parse);

        System.out.println(squareOfParsed.apply("8")); // 64
    }
}

Kiedy kolejność ma znaczenie?

Jeśli typy funkcji się nie zgadzają, kolejność staje się krytyczna. Na przykład:

Function<String, String> addPrefix = s -> "User: " + s;
Function<String, Integer> length = s -> s.length();

Function<String, Integer> composed = addPrefix.andThen(length);
System.out.println(composed.apply("Alice")); // "User: Alice" -> 11

// A tak:
// length.andThen(addPrefix) — błąd kompilacji!
// length zwraca Integer, a addPrefix przyjmuje String.

Tabela: różnica między compose i andThen

f
g
f.compose(g)
f.andThen(g)
T → R
V → T
V → R: f(g(x))
T → V: g(f(x))

3. Kompozycja predykatów i innych interfejsów

Predicate<T>: and, or, negate

Funkcyjny interfejs Predicate<T> to funkcja zwracająca boolean. Do łączenia predykatów są specjalne metody:

  • and: logiczne I (&&)
  • or: logiczne LUB (||)
  • negate: logiczne NIE (!)

Przykład: złożone warunki filtrowania

Załóżmy, że mamy klasę użytkownika:

public class User {
    String name;
    int age;

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

Teraz napiszmy różne predykaty:

import java.util.function.Predicate;

Predicate<User> isAdult = user -> user.age >= 18;
Predicate<User> nameStartsWithA = user -> user.name.startsWith("A");

// Łączymy: pełnoletni i imię na "A"
Predicate<User> adultAndA = isAdult.and(nameStartsWithA);

// Pełnoletni lub imię na "A"
Predicate<User> adultOrA = isAdult.or(nameStartsWithA);

// Niepełnoletni
Predicate<User> notAdult = isAdult.negate();

Teraz możemy użyć tych predykatów przy filtrowaniu, np. za pomocą Stream API:

import java.util.List;
import java.util.stream.Collectors;

List<User> users = List.of(
    new User("Alice", 20),
    new User("Bob", 17),
    new User("Anna", 15),
    new User("Mike", 22)
);

List<User> filtered = users.stream()
    .filter(adultAndA)
    .collect(Collectors.toList());

// Do filtered trafi tylko Alice (pełnoletnia i imię na "A")

Kompozycja Consumer, Function, Supplier

  • Consumer<T>: metoda andThen — pozwala wykonać dwie operacje jedna po drugiej.
  • Function<T, R>: kompozycję omówiliśmy wyżej.
  • Supplier<T>: bezpośrednio się nie łączy, ale może być używany wewnątrz innych funkcji.

Przykład: Consumer<T>.andThen

import java.util.function.Consumer;

Consumer<String> print = s -> System.out.println("Otrzymano: " + s);
Consumer<String> printUpper = s -> System.out.println("Wielkimi literami: " + s.toUpperCase());

Consumer<String> combined = print.andThen(printUpper);

combined.accept("hello");
// Wypisze:
// Otrzymano: hello
// Wielkimi literami: HELLO

4. Praktyka: łańcuchy przekształceń i filtrowania

Zadanie 1: Zbudować łańcuch przekształceń Function

Załóżmy, że w naszej aplikacji przechowujemy użytkowników jako łańcuchy "Imię,Wiek", na przykład "Alice,20". Trzeba:

  • Przekształcić łańcuch w obiekt User
  • Pobrać wiek
  • Sprawdzić, czy użytkownik jest pełnoletni
import java.util.function.Function;
import java.util.function.Predicate;

Function<String, User> stringToUser = str -> {
    String[] parts = str.split(",");
    return new User(parts[0], Integer.parseInt(parts[1]));
};
Function<User, Integer> getAge = user -> user.age;
Predicate<Integer> isAdultAge = age -> age >= 18;

// Połączymy: tekst -> User -> wiek -> predykat
Function<String, Integer> stringToAge = stringToUser.andThen(getAge);

String input = "Bob,19";
int age = stringToAge.apply(input);
System.out.println("Wiek: " + age); // 19

System.out.println("Pełnoletni? " + isAdultAge.test(age)); // true

Zadanie 2: Łączyć kilka Predicate do filtrowania

Załóżmy, że chcemy wybrać użytkowników, którzy są starsi niż 18 i których imię zaczyna się na „A” lub „M”.

Predicate<User> isAdult = user -> user.age > 18;
Predicate<User> nameStartsWithA = user -> user.name.startsWith("A");
Predicate<User> nameStartsWithM = user -> user.name.startsWith("M");

Predicate<User> filter = isAdult.and(nameStartsWithA.or(nameStartsWithM));

List<User> filtered = users.stream()
    .filter(filter)
    .collect(Collectors.toList());

Zadanie 3: Wieloetapowe przekształcenie Function

Załóżmy, że trzeba: pobrać łańcuch, przyciąć spacje, zamienić na wielkie litery, dodać prefiks "USER: ".

Function<String, String> trim = String::trim;
Function<String, String> toUpper = String::toUpperCase;
Function<String, String> addPrefix = s -> "USER: " + s;

// Budujemy łańcuch
Function<String, String> pipeline = trim.andThen(toUpper).andThen(addPrefix);

System.out.println(pipeline.apply("   user   ")); // USER: VASYA

5. Typowe błędy przy kompozycji funkcji

Błąd nr 1: Pomylenie kolejności compose/andThen.
Początkujący często mylą, co wykonywane jest pierwsze, a co — drugie. Zapamiętaj: f.compose(g) — najpierw g, potem f; f.andThen(g) — najpierw f, potem g.

Błąd nr 2: Niezgodność typów.
Jeśli typ wyniku jednej funkcji nie zgadza się z typem parametru drugiej — kompilator nie pozwoli ich połączyć. Na przykład nie da się zrobić Function<Integer, String>.andThen(Function<Double, Boolean>).

Błąd nr 3: Zbyt skomplikowane łańcuchy.
Czasem kusi, by zmieścić całą logikę biznesową w jednym łańcuchu i otrzymać „spaghetti”. Dziel je na mniejsze funkcje i nadawaj im zrozumiałe nazwy.

Błąd nr 4: Efekty uboczne w funkcjach.
Funkcje i predykaty najlepiej utrzymywać „czyste” (bez efektów ubocznych), w przeciwnym razie kompozycja staje się ryzykowna i nieprzewidywalna.

1
Zadanie
JAVA 25 SELF, poziom 49, lekcja 1
Niedostępne
Standardyzacja loginów dla Tajnego Stowarzyszenia 🕵️‍♂️
Standardyzacja loginów dla Tajnego Stowarzyszenia 🕵️‍♂️
1
Zadanie
JAVA 25 SELF, poziom 49, lekcja 1
Niedostępne
Magiczne przekształcenia liczb w Szkole Czarodziejów 🧙‍♀️
Magiczne przekształcenia liczb w Szkole Czarodziejów 🧙‍♀️
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION