Встроенные функциональные интерфейсы 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 — acceptBiSupplier не существует — 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.IntToLongX—intна входе,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 stringsFunction.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 (примитивный), суммируя небольшой список.
Что следует вынести из запуска:
- Четыре главных интерфейса чётко соответствуют четырём видам работы: преобразование (
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, охватывает оператор :: и его четыре формы (статический, связанный экземплярный, несвязанный экземплярный, конструктор), а также объясняет, когда ссылка на метод понятнее лямбды, а когда — наоборот.