1. Поняття композиції функцій
Трохи теорії (але без занудства)
У математиці композиція функцій — це коли результат однієї функції стає входом для іншої. Якщо у нас є функції f і g, то композиція g(f(x)) означає: спочатку застосовуємо f до x, потім результат підставляємо в g.
У програмуванні ідея та сама: ми хочемо складати складні перетворення з простих, щоб не писати одну гігантську функцію на всі випадки життя. Це робить код гнучким, повторно використовуваним і читабельним.
Уявіть конвеєр тістечок: спочатку тісто (f), потім крем (g), потім посипка (h). Увесь процес — це h(g(f(інгредієнти))).
Чому композиція важлива?
- Композиція дає змогу складати програму з малих функцій‑«цеглинок».
- Простіше повторно використовувати: одну «цеглинку» можна вставити в різні місця без дублювання.
- Гнучкість: щоб змінити один етап, замінюємо відповідну функцію — решту не чіпаємо.
- Читабельність і тестованість: малі функції легше читати, перевіряти й супроводжувати.
2. Методи compose і andThen в інтерфейсі Function
Інтерфейс Function: нагадування
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
// Методи для композиції:
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: спочатку виконується функція, яку ви передаєте в compose, а потім — поточна.
- andThen: спочатку виконується поточна функція, а потім — та, що передана в andThen.
Візуальна схема
// Нехай є дві функції:
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
// Але порядок має значення, якщо типи різні!
Приклад: перетворення рядка на число, далі — у квадрат
import java.util.function.Function;
public class ComposeAndThenDemo {
public static void main(String[] args) {
// Функція: перетворює рядок на число
Function<String, Integer> parse = s -> Integer.parseInt(s);
// Функція: зводить число у квадрат
Function<Integer, Integer> square = x -> x * x;
// Скомбінуємо: спочатку парсимо, потім зводимо у квадрат
Function<String, Integer> parseThenSquare = parse.andThen(square);
System.out.println(parseThenSquare.apply("7")); // 49
// А якщо поміняти місцями?
// square.compose(parse) — той самий результат (для цих функцій)
Function<String, Integer> squareOfParsed = square.compose(parse);
System.out.println(squareOfParsed.apply("8")); // 64
}
}
Коли порядок має значення?
Якщо типи функцій не збігаються, порядок стає критичним. Наприклад:
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
// А ось так:
// length.andThen(addPrefix) — помилка компіляції!
// length повертає Integer, а addPrefix приймає String.
Таблиця: різниця між compose та andThen
|
|
|
|
|---|---|---|---|
|
|
|
|
3. Композиція предикатів та інших інтерфейсів
Predicate<T>: and, or, negate
Функціональний інтерфейс Predicate<T> — це функція, яка повертає boolean. Для комбінування предикатів є спеціальні методи:
- and: логічне І (&&)
- or: логічне АБО (||)
- negate: логічне НЕ (!)
Приклад: складні умови фільтрації
Припустімо, у нас є клас користувача:
public class User {
String name;
int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
}
Тепер напишемо різні предикати:
import java.util.function.Predicate;
Predicate<User> isAdult = user -> user.age >= 18;
Predicate<User> nameStartsWithA = user -> user.name.startsWith("A");
// Комбінуємо: повнолітній і ім’я на "A"
Predicate<User> adultAndA = isAdult.and(nameStartsWithA);
// Повнолітній або ім’я на "A"
Predicate<User> adultOrA = isAdult.or(nameStartsWithA);
// Неповнолітній
Predicate<User> notAdult = isAdult.negate();
Тепер можна використовувати ці предикати у фільтрації, наприклад, за допомогою 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());
// У filtered потрапить тільки Alice (повнолітня і ім’я на "A")
Композиція Consumer, Function, Supplier
- Consumer<T>: метод andThen — дозволяє виконати дві операції підряд.
- Function<T, R>: композицію розглянули вище.
- Supplier<T>: безпосередньо не комбінується, але може використовуватись усередині інших функцій.
Приклад: Consumer<T>.andThen
import java.util.function.Consumer;
Consumer<String> print = s -> System.out.println("Отримано: " + s);
Consumer<String> printUpper = s -> System.out.println("У верхньому регістрі: " + s.toUpperCase());
Consumer<String> combined = print.andThen(printUpper);
combined.accept("hello");
// Виведе:
// Отримано: hello
// У верхньому регістрі: HELLO
4. Практика: ланцюжки перетворень і фільтрацій
Завдання 1: Скласти ланцюжок перетворень Function
Припустімо, у нашому застосунку ми зберігаємо користувачів як рядки «Ім’я,Вік», наприклад «Alice,20». Потрібно:
- Перетворити рядок на об’єкт User
- Отримати вік
- Перевірити, чи користувач повнолітній
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;
// Скомбінуємо: рядок -> User -> вік -> предикат
Function<String, Integer> stringToAge = stringToUser.andThen(getAge);
String input = "Bob,19";
int age = stringToAge.apply(input);
System.out.println("Вік: " + age); // 19
System.out.println("Повнолітній? " + isAdultAge.test(age)); // true
Завдання 2: Комбінувати кілька Predicate для фільтрації
Нехай нам потрібно обрати користувачів, які старші за 18 років і чиє ім’я починається з «A» або «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());
Завдання 3: Багатоступеневе перетворення Function
Нехай потрібно: отримати рядок, обрізати пробіли, перевести у верхній регістр, додати префікс «USER: ».
Function<String, String> trim = String::trim;
Function<String, String> toUpper = String::toUpperCase;
Function<String, String> addPrefix = s -> "USER: " + s;
// Складаємо ланцюжок
Function<String, String> pipeline = trim.andThen(toUpper).andThen(addPrefix);
System.out.println(pipeline.apply(" vasya ")); // USER: VASYA
5. Типові помилки під час композиції функцій
Помилка № 1: Переплутано порядок compose/andThen.
Початківці часто плутають, що виконується першим, а що — другим. Запам’ятайте: f.compose(g) — спочатку g, потім f; f.andThen(g) — спочатку f, потім g.
Помилка № 2: Невідповідність типів.
Якщо тип результату однієї функції не збігається з типом параметра іншої — компілятор не дозволить скомбінувати. Наприклад, не можна зробити Function<Integer, String>.andThen(Function<Double, Boolean>).
Помилка № 3: Надто складні ланцюжки.
Іноді хочеться вмістити всю бізнес‑логіку в один ланцюжок і отримати «локшину». Розбивайте на невеликі функції й давайте їм зрозумілі назви.
Помилка № 4: Побічні ефекти у функціях.
Функції та предикати краще тримати «чистими» (без побічних ефектів), інакше композиція стає небезпечною й непередбачуваною.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ