Функциональное программирование в Java

Модель

Функциональный стиль рассматривает вычисление как преобразование значений. Функции передаются как данные, операции составляются в конвейеры, а изменяемое состояние и побочные эффекты ограничиваются.

Java остаётся объектно-ориентированным императивным языком, но Функциональные интерфейсы, Лямбда выражения и Stream API позволяют применять функциональные приёмы там, где они делают код яснее.

Ссылочная прозрачность и чистота

Чистая функция:

  • при одинаковых аргументах возвращает одинаковый результат;
  • не меняет внешнее наблюдаемое состояние.
BigDecimal applyDiscount(BigDecimal price, BigDecimal rate) {
    return price.multiply(BigDecimal.ONE.subtract(rate));
}

Результат вызова можно мысленно заменить самим значением, не изменив поведение программы — это ссылочная прозрачность.

Метод, который читает текущее время, случайное число, БД или изменяет аргумент, уже зависит от контекста:

Instant createTimestamp(Clock clock) {
    return clock.instant();
}

Передача Clock не делает I/O чистым, но превращает скрытую зависимость от глобального времени в явный вход и упрощает тест.

Функции как значения

Function<BigDecimal, BigDecimal> addTax =
        value -> value.multiply(new BigDecimal("1.20"));
 
Function<BigDecimal, BigDecimal> round =
        value -> value.setScale(2, RoundingMode.HALF_UP);
 
Function<BigDecimal, BigDecimal> pricePipeline =
        addTax.andThen(round);

Тип Function<T,R> хранит поведение, а andThen строит новую функцию. Исходные функции не меняются.

Иммутабельность

Неизменяемые значения снижают число возможных состояний:

record Line(String sku, int quantity, BigDecimal price) {
    Line {
        if (quantity <= 0) {
            throw new IllegalArgumentException("quantity");
        }
    }
 
    BigDecimal total() {
        return price.multiply(BigDecimal.valueOf(quantity));
    }
}

final-ссылка не гарантирует неизменяемость содержимого коллекции. Для снимка используют List.copyOf, а вложенные mutable-объекты также требуют защиты.

Декларативный конвейер

BigDecimal total = lines.stream()
        .filter(line -> line.quantity() > 0)
        .map(Line::total)
        .reduce(BigDecimal.ZERO, BigDecimal::add);

Код описывает «отфильтровать → преобразовать → свернуть». Stream ленив до терминальной операции и не должен повторно использоваться после завершения.

Цикл может быть яснее, если нужны сложные ветвления, ранний выход, checked-ошибки или детальная диагностика. Функциональный стиль — инструмент, а не требование заменить каждый for.

Полные и частичные функции

Функция divide(a, b) не определена при b == 0. Контракт должен выразить это:

Optional<BigDecimal> divide(BigDecimal left, BigDecimal right) {
    if (right.signum() == 0) {
        return Optional.empty();
    }
    return Optional.of(left.divide(right, MathContext.DECIMAL64));
}

Другой вариант — проверяемое доменное исключение или тип результата с ошибкой. Важно, чтобы сигнатура и документация не скрывали допустимые исходы.

Побочные эффекты на границах

Приложение не может быть полностью чистым: оно читает сообщения, пишет в БД, вызывает сеть. Полезное разделение:

вход/I/O → проверка → чистое преобразование → сохранение/I/O

Тогда сложные правила тестируются как функции над значениями, а эффекты остаются в небольшом оркестрационном слое.

Ловушки

  • Называть метод чистым, хотя он изменяет переданную коллекцию.
  • Использовать peek для важной бизнес-логики.
  • Прятать I/O внутри map.
  • Делать длинный Stream-конвейер с множеством вложенных условий.
  • Считать parallel stream автоматическим ускорением.
  • Путать неизменяемую ссылку и неизменяемое значение.

Вопросы на собеседовании

  1. Что такое чистая и ссылочно прозрачная функция?
  2. Как Java передаёт функцию как значение?
  3. Зачем функциональному стилю иммутабельность?
  4. Что означает ленивость Stream API?
  5. Как выразить функцию, которая определена не для всех входов?
  6. Где оставлять побочные эффекты в прикладном коде?

Связанные заметки

Функциональные интерфейсы · ФИ стандартной библиотеки Java · Лямбда выражения · Stream API · Класс Optional · Immutable классы и данные