JavaRush /Курсы /JAVA 25 SELF /Композиция функций: compose, andThen

Композиция функций: compose, andThen

JAVA 25 SELF
49 уровень , 1 лекция
Открыта

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

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

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: Побочные эффекты в функциях.
Функции и предикаты лучше держать «чистыми» (без побочных эффектов), иначе композиция становится опасной и непредсказуемой.

1
Задача
JAVA 25 SELF, 49 уровень, 1 лекция
Недоступна
Стандартизация логинов для Тайного Сообщества 🕵️‍♂️
Стандартизация логинов для Тайного Сообщества 🕵️‍♂️
1
Задача
JAVA 25 SELF, 49 уровень, 1 лекция
Недоступна
Магические преобразования чисел в Школе Чародеев 🧙‍♀️
Магические преобразования чисел в Школе Чародеев 🧙‍♀️
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ