W3docs

Встроенные функциональные интерфейсы Java

Пакет java.util.function: Function, Predicate, Consumer, Supplier и их специализированные варианты.

Пакет java.util.function появился в Java 8, чтобы дать JDK — и вашему коду — единый словарь для лямбда-выражений. Без него каждый метод, принимающий функцию, должен был бы объявлять собственный одноразовый интерфейс (StringMapper, IntToBool, RowHandler, …), а лямбды, написанные для одного, нельзя было бы переиспользовать в другом. Пакет решает эту проблему с помощью 43 небольших интерфейсов, покрывающих формы, которые встречаются снова и снова: «принять одно, вернуть другое», «принять что-то, ответить да или нет», «принять что-то, выполнить действие», «дать мне что-то».

Если вы освоите только четыре интерфейса из этого пакета, пусть это будут Function, Predicate, Consumer и Supplier. Почти всё остальное — это их вариант: версии с двумя аргументами, примитивные специализации для обхода boxing, или вспомогательные методы для композиции.

Четыре главных интерфейса

Function<T, R>  f = t -> ...;       // takes a T, returns an R          — r = f.apply(t)
Predicate<T>    p = t -> ...;       // takes a T, returns a boolean      — boolean b = p.test(t)
Consumer<T>     c = t -> { ... };   // takes a T, returns nothing        — c.accept(t)
Supplier<T>     s = () -> ...;      // takes nothing, returns a T        — t = s.get()

Каждый из них аннотирован @FunctionalInterface и имеет одно абстрактное слово-метод (apply, test, accept, get). В работе со стримами вы редко вызываете эти методы напрямую — stream().filter(predicate).map(function).forEach(consumer) делает это за вас, — но знать имя метода важно, когда вы пишете код, принимающий Function<T, R> в качестве параметра и вызывающий его.

Эти формы соответствуют распространённым вопросам:

ВопросИнтерфейс
«Преобразовать X в Y?»Function<X, Y>
«Этот X подходит?»Predicate<X>
«Что-то сделать с этим X»Consumer<X>
«Дай мне X»Supplier<X>

Варианты с двумя аргументами

Когда операция требует двух входных значений, добавьте префикс Bi:

BiFunction<T, U, R>     f = (t, u) -> ...;     // two ins, one out                    — apply
BiPredicate<T, U>       p = (t, u) -> ...;     // two ins, a boolean                  — test
BiConsumer<T, U>        c = (t, u) -> { ... }; // two ins, no out                     — accept

BiSupplier не существует — Supplier по определению не принимает аргументов, поэтому «поставщик с двумя аргументами» был бы просто BiFunction.

Bi-варианты — это именно то, чего ожидают Map.forEach((k, v) -> ...), Map.merge и Map.compute:

Map<String, Integer> scores = new HashMap<>();
scores.forEach((name, score) -> System.out.println(name + "=" + score));   // BiConsumer
scores.merge("alice", 1, Integer::sum);                                       // BinaryOperator<Integer>

BinaryOperator<T> — это BiFunction<T, T, T>: одинаковый тип для обоих входных значений и результата. UnaryOperator<T> аналогично является Function<T, T>.

Примитивные специализации — как избежать издержек boxing

Function<Integer, Integer> работает, но при каждом вызове упаковывается входное и упаковывается выходное значение. В плотных циклах это ощутимая стоимость. Поэтому пакет предлагает примитивно-специализированные версии:

IntFunction<R>           f = i -> ...;        // int in, R out
IntPredicate             p = i -> ...;        // int in, boolean out
IntConsumer              c = i -> { ... };    // int in, void
IntSupplier              s = () -> 42;        // void in, int out
IntUnaryOperator         u = i -> i * 2;      // int in, int out
IntBinaryOperator        b = (a, c2) -> a + c2;

ToIntFunction<T>         f1 = t -> t.hashCode();         // T in, int out
ToIntBiFunction<T, U>    f2 = (t, u) -> t.hashCode() + u.hashCode();

IntToLongFunction        f3 = i -> (long) i * i;          // int in, long out
IntToDoubleFunction      f4 = i -> Math.sqrt(i);

Аналогичное семейство существует для Long и Double. Соглашение об именовании читается как предложение:

  • IntX — работает с int.
  • ToIntXпроизводит int.
  • IntToLongXint на входе, long на выходе.

В коде со стримами mapToInt(...) возвращает IntStream, терминальные операции которого (sum, average, min, max) возвращают примитивы без boxing — это одно из наиболее практически значимых преимуществ примитивных вариантов.

Композиция, встроенная в интерфейсы

Большинство интерфейсов оснащены методами default, позволяющими составлять новые функции без написания новых лямбд:

// Function: andThen (left-to-right), compose (right-to-left)
Function<String, String>  trim  = String::trim;
Function<String, Integer> len   = String::length;
Function<String, Integer> trimLen = trim.andThen(len);          // trim, then length
Function<String, Integer> sameThing = len.compose(trim);          // length applied after trim

// Predicate: and / or / negate
Predicate<String> notNull  = Objects::nonNull;
Predicate<String> notBlank = s -> !s.trim().isEmpty();
Predicate<String> useful   = notNull.and(notBlank);
Predicate<String> blank    = notBlank.negate();

// Consumer: andThen (run two consumers in sequence)
Consumer<String> log   = System.out::println;
Consumer<String> save  = s -> writeToFile(s);
Consumer<String> both  = log.andThen(save);

// Comparator (in java.util, not java.util.function, but the same idea):
Comparator<Person> byName = Comparator.comparing(Person::name);
Comparator<Person> ordered = byName.thenComparing(Person::age);

Есть также полезный статический фабричный метод: Predicate.not(p) — сокращение для p.negate(), которое читается более естественно в месте вызова:

list.removeIf(Predicate.not(String::isBlank));    // remove all blank strings

Function.identity и Predicate.isEqual — небольшие полезные статики

Два фабричных метода, которые часто встречаются в коде со стримами и которые стоит знать:

Function<T, T> id = Function.identity();          // t -> t — useful as a no-op map

Predicate<Object> isFoo = Predicate.isEqual("foo");  // o -> Objects.equals(o, "foo")

Function.identity() чаще всего используется как маппер ключа или значения в Collectors.toMap:

Map<String, Person> byName = people.stream()
    .collect(Collectors.toMap(Person::name, Function.identity()));

Predicate.isEqual редко бывает короче, чем s -> s.equals("foo"), но он безопасно сравнивает через Objects.equals, что важно, когда стрим может содержать null.

Подробный пример: четыре главных интерфейса, композиция и примитивные специализации

Программа ниже использует Function, Predicate, Consumer и Supplier, составляет несколько из них и сравнивает Function<Integer, Integer> (с boxing) и IntUnaryOperator (примитивный), суммируя небольшой список.

java— editable, runs on the server

Что следует вынести из запуска:

  • Четыре главных интерфейса чётко соответствуют четырём видам работы: преобразование (Function), проверка (Predicate), действие (Consumer), производство (Supplier). Имена их абстрактных методов (apply, test, accept, get) стоит запомнить.
  • trim.andThen(length) и notNull.and(notBlank) создали новые значения из старых без объявления вспомогательных методов. Это и есть алгебра композиции, которую интерфейсы несут в виде методов default.
  • Боксированный Function<Integer, Integer> заметно медленнее примитивного IntUnaryOperator, потому что каждый вызов выделяет два объекта Integer. В горячих путях — стрим-пайплайнах, обрабатывающих миллионы значений, — примитивные специализации полностью себя оправдывают.
  • Predicate.not(notBlank) читается более естественно, чем notBlank.negate() в месте вызова removeIf. Оба компилируются в одно и то же.

Что дальше

Теперь вы познакомились со стандартным словарём. Оставшийся вопрос об эргономике лямбд звучит так: «Если тело лямбды просто делегирует в один существующий метод, можно ли записать это короче?» Да — с помощью ссылок на методы. Следующая глава, Java Method References, охватывает оператор :: и его четыре формы (статический, связанный экземплярный, несвязанный экземплярный, конструктор), а также объясняет, когда ссылка на метод понятнее лямбды, а когда — наоборот.

Практика

Практика
Метод объявлен как `void each(Consumer<String> action)`. Какие из следующих вариантов являются допустимыми аргументами?
Метод объявлен как `void each(Consumer<String> action)`. Какие из следующих вариантов являются допустимыми аргументами?
Was this page helpful?