Функциональное программирование в 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 автоматическим ускорением.
- Путать неизменяемую ссылку и неизменяемое значение.
Вопросы на собеседовании
- Что такое чистая и ссылочно прозрачная функция?
- Как Java передаёт функцию как значение?
- Зачем функциональному стилю иммутабельность?
- Что означает ленивость Stream API?
- Как выразить функцию, которая определена не для всех входов?
- Где оставлять побочные эффекты в прикладном коде?
Связанные заметки
Функциональные интерфейсы · ФИ стандартной библиотеки Java · Лямбда выражения · Stream API · Класс Optional · Immutable классы и данные