Функциональные интерфейсы стандартной библиотеки

Базовая шестёрка

Пакет java.util.function содержит общие формы поведения:

ИнтерфейсSAMСмысл
Predicate<T>boolean test(T)проверить условие
Function<T,R>R apply(T)преобразовать значение
Consumer<T>void accept(T)выполнить побочный эффект
Supplier<T>T get()лениво предоставить значение
UnaryOperator<T>T apply(T)преобразование T → T
BinaryOperator<T>T apply(T,T)объединение двух T в один T
Predicate<String> valid = text -> text != null && !text.isBlank();
Function<String, Integer> length = String::length;
Consumer<String> log = message -> logger.info(message);
Supplier<Instant> now = clock::instant;
UnaryOperator<String> normalize = String::trim;
BinaryOperator<BigDecimal> add = BigDecimal::add;

Выбор точного интерфейса выражает назначение лучше, чем универсальный собственный SAM.

Predicate

Предикаты составляются через короткозамкнутые and, or, negate:

Predicate<Order> paid = Order::paid;
Predicate<Order> recent =
        order -> order.createdAt().isAfter(cutoff);
 
List<Order> selected = orders.stream()
        .filter(paid.and(recent))
        .toList();

Predicate.isEqual(target) строит null-safe-проверку через Objects.equals.

Function

Порядок композиции важен:

Function<String, String> trim = String::trim;
Function<String, Integer> parse = Integer::parseInt;
 
Function<String, Integer> trimThenParse = trim.andThen(parse);
Function<String, Integer> same = parse.compose(trim);
  • f.andThen(g) означает сначала f, затем g;
  • f.compose(g) означает сначала g, затем f;
  • Function.identity() возвращает аргумент без изменения.

Consumer и Supplier

Consumer обычно обозначает эффект: запись, логирование, накопление. andThen последовательно вызывает два consumer; если первый выбросил исключение, второй не выполнится.

Supplier выражает отложенное получение:

User user = cache.get(id)
        .orElseGet(() -> repository.load(id));

Поставщик вызывается только при необходимости, в отличие от заранее вычисленного аргумента orElse.

Bi-формы

Для двух аргументов есть BiPredicate<T,U>, BiFunction<T,U,R>, BiConsumer<T,U>.

BiFunction<BigDecimal, Integer, BigDecimal> multiply =
        (price, quantity) -> price.multiply(BigDecimal.valueOf(quantity));

BinaryOperator<T> — частный случай BiFunction<T,T,T>.

Примитивные специализации

Чтобы не создавать обёртки, библиотека содержит IntPredicate, LongSupplier, DoubleConsumer, IntFunction<R>, ToIntFunction<T> и другие.

ToIntFunction<String> length = String::length;
int total = names.stream().mapToInt(length).sum();

Специализации особенно важны в больших потоках чисел.

Ловушки

  • Использовать Consumer внутри map; map ожидает результат преобразования.
  • Путать порядок compose и andThen.
  • Вычислять дорогой fallback через orElse, когда нужен orElseGet.
  • Выбирать boxed-интерфейс на горячем числовом пути.
  • Делать Predicate с неожиданным изменением состояния.
  • Использовать BinaryOperator для неассоциативной операции в параллельном reduce.

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

  1. Чем Predicate, Function и Consumer отличаются по контракту?
  2. Когда использовать UnaryOperator вместо Function?
  3. В каком порядке выполняются compose и andThen?
  4. Зачем нужны primitive specializations?
  5. Чем BiFunction<T,T,T> отличается по смыслу от BinaryOperator<T>?
  6. Когда Supplier помогает избежать лишнего вычисления?

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

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