Lambda ·
‹ Previous Next ›
⏱ 5 min read Modified: 2026-09-08

Специализации функциональных интерфейсов в Java

Этот цикл выглядит безобидно, но за десять миллионов итераций он создаёт десять миллионов лишних объектов:

Function<Integer, Integer> square = x -> x * x;

long sum = 0;
for (int i = 0; i < 10_000_000; i++) {
    sum += square.apply(i);   // int -> Integer -> int на каждом шаге
}

Метод apply объявлен как R apply(T t) и умеет работать только с объектами, поэтому каждый int сначала упаковывается в Integer, а затем тут же распаковывается обратно. Достаточно заменить тип переменной на IntUnaryOperator с методом int applyAsInt(int) — и упаковка исчезает полностью. Именно ради таких случаев в JDK существуют специализации функциональных интерфейсов.

Специализации функциональных интерфейсов (functional interface specializations) — это варианты базовых интерфейсов пакета java.util.function, рассчитанные на другое количество аргументов (бинарные специализации) или на примитивные типы int, long, double вместо объектов (примитивные специализации). Всего пакет java.util.function содержит 43 интерфейса: 9 обобщённых (объектных) и 34 примитивных.

Зачем нужны специализации: цена автоупаковки

Дженерики в Java не работают с примитивными типами: запись Function<int, int> не компилируется. Поэтому любой обобщённый функциональный интерфейс — Predicate, Consumer, Function, Supplier — вынужден принимать и возвращать объекты. Когда в него передают число, включается автоупаковка (autoboxing), а на выходе — автораспаковка (unboxing).

Что это стоит на практике:

  • каждый упакованный int — отдельный объект в куче (в 64-битной HotSpot это порядка 16 байт плюс 4–8 байт на ссылку против 4 байт у примитива);
  • миллионы короткоживущих Integer нагружают сборщик мусора;
  • массив ссылок на Integer раскидан по куче, тогда как int[] лежит одним непрерывным блоком — теряется локальность данных и эффективность кеша процессора.

Примитивные специализации решают проблему на уровне сигнатуры: их абстрактные методы объявлены сразу над примитивами.

// Упаковка на каждом вызове
Function<Integer, Integer> squareBoxed = x -> x * x;

// Ни одной упаковки: int на входе, int на выходе
IntUnaryOperator square = x -> x * x;

int sum = IntStream.rangeClosed(1, 100)
                   .map(square)   // IntUnaryOperator
                   .sum();        // 338350

Важно

Кеш Integer покрывает только диапазон от минус 128 до 127 — на этих значениях Integer.valueOf() отдаёт готовый объект. Всё, что выходит за диапазон, создаётся заново при каждой упаковке. Поэтому «упаковка почти бесплатна» работает лишь на маленьких числах и не спасает в реальных вычислениях.

Бинарные специализации функциональных интерфейсов

Базовые интерфейсы Predicate<T>, Consumer<T> и Function<T, R> принимают один аргумент. Для двух аргументов в java.util.function есть бинарные специализации (binary specializations) с приставкой Bi-. Бинарным аналогом UnaryOperator выступает BinaryOperator.

Интерфейс Функциональный дескриптор Абстрактный метод Описание
BiConsumer<T, U> (T, U) → void void accept(T, U) Принимает на вход два объекта и ничего не возвращает.
BiFunction<T, U, R> (T, U) → R R apply(T, U) Принимает на вход два объекта и возвращает результат типа R.
BiPredicate<T, U> (T, U) → boolean boolean test(T, U) Принимает на вход два объекта и возвращает значение boolean.
BinaryOperator<T> (T, T) → T T apply(T, T) Частный случай BiFunction<T, T, T>: оба аргумента и результат одного типа.
BiFunction<String, Integer, String> repeat = (s, n) -> s.repeat(n);
System.out.println(repeat.apply("ab", 3));        // ababab

BiPredicate<String, Integer> isLonger = (s, n) -> s.length() > n;
System.out.println(isLonger.test("Java", 3));     // true

BiConsumer<String, Integer> printer = (k, v) -> System.out.println(k + " = " + v);
printer.accept("age", 30);                        // age = 30

BinaryOperator<Integer> max = BinaryOperator.maxBy(Comparator.naturalOrder());
System.out.println(max.apply(7, 42));             // 42

Метод String.repeat() доступен начиная с Java 11. Интерфейсов на три и более аргумента (TriFunction и подобных) в стандартной библиотеке нет — при необходимости их объявляют самостоятельно.

Примитивные специализации функциональных интерфейсов

Примитивные специализации существуют для трёх типов: int, long и double (плюс BooleanSupplier для boolean). Ниже — полный список по группам.

Специализации Predicate

Интерфейс Абстрактный метод Описание
IntPredicate boolean test(int) Принимает на вход значение типа int и возвращает значение типа boolean.
LongPredicate boolean test(long) Принимает на вход значение типа long и возвращает значение типа boolean.
DoublePredicate boolean test(double) Принимает на вход значение типа double и возвращает значение типа boolean.

Как и Predicate<T>, эти интерфейсы имеют default-методы and(), or() и negate() для комбинирования условий.

Специализации Consumer

Интерфейс Абстрактный метод Описание
IntConsumer void accept(int) Принимает на вход значение типа int и ничего не возвращает.
LongConsumer void accept(long) Принимает на вход значение типа long и ничего не возвращает.
DoubleConsumer void accept(double) Принимает на вход значение типа double и ничего не возвращает.

Специализации Function

У Function<T, R> примитивным может быть вход, выход или и то и другое — отсюда три подгруппы.

Принимающие примитив, возвращающие объект

Интерфейс Абстрактный метод Описание
IntFunction<R> R apply(int) Принимает на вход значение типа int и возвращает объект типа R.
LongFunction<R> R apply(long) Принимает на вход значение типа long и возвращает объект типа R.
DoubleFunction<R> R apply(double) Принимает на вход значение типа double и возвращает объект типа R.

Принимающие объект, возвращающие примитив

Интерфейс Абстрактный метод Описание
ToIntFunction<T> int applyAsInt(T) Принимает на вход объект типа T и возвращает значение типа int.
ToLongFunction<T> long applyAsLong(T) Принимает на вход объект типа T и возвращает значение типа long.
ToDoubleFunction<T> double applyAsDouble(T) Принимает на вход объект типа T и возвращает значение типа double.

Принимающие и возвращающие примитив

Интерфейс Абстрактный метод Описание
IntToDoubleFunction double applyAsDouble(int) Принимает на вход значение типа int и возвращает значение типа double.
IntToLongFunction long applyAsLong(int) Принимает на вход значение типа int и возвращает значение типа long.
LongToDoubleFunction double applyAsDouble(long) Принимает на вход значение типа long и возвращает значение типа double.
LongToIntFunction int applyAsInt(long) Принимает на вход значение типа long и возвращает значение типа int.
DoubleToIntFunction int applyAsInt(double) Принимает на вход значение типа double и возвращает значение типа int.
DoubleToLongFunction long applyAsLong(double) Принимает на вход значение типа double и возвращает значение типа long.

Пар IntToIntFunction или DoubleToDoubleFunction в JDK нет — их роль выполняют IntUnaryOperator и DoubleUnaryOperator.

Специализации Supplier

Интерфейс Абстрактный метод Описание
BooleanSupplier boolean getAsBoolean() Не принимает аргументов и возвращает значение типа boolean.
IntSupplier int getAsInt() Не принимает аргументов и возвращает значение типа int.
LongSupplier long getAsLong() Не принимает аргументов и возвращает значение типа long.
DoubleSupplier double getAsDouble() Не принимает аргументов и возвращает значение типа double.

Обратите внимание: у Supplier<T> метод называется get(), а у примитивных версий — getAsInt(), getAsLong(), getAsDouble(), getAsBoolean(). Разные имена нужны, чтобы избежать конфликта стирания типов.

Специализации UnaryOperator

Интерфейс Абстрактный метод Описание
IntUnaryOperator int applyAsInt(int) Принимает и возвращает значение типа int.
LongUnaryOperator long applyAsLong(long) Принимает и возвращает значение типа long.
DoubleUnaryOperator double applyAsDouble(double) Принимает и возвращает значение типа double.

Специализации BinaryOperator

Интерфейс Абстрактный метод Описание
IntBinaryOperator int applyAsInt(int, int) Принимает на вход два значения типа int и возвращает значение типа int.
LongBinaryOperator long applyAsLong(long, long) Принимает на вход два значения типа long и возвращает значение типа long.
DoubleBinaryOperator double applyAsDouble(double, double) Принимает на вход два значения типа double и возвращает значение типа double.

Специализации BiConsumer

Интерфейс Абстрактный метод Описание
ObjIntConsumer<T> void accept(T, int) Принимает на вход объект типа T и значение типа int, ничего не возвращает.
ObjLongConsumer<T> void accept(T, long) Принимает на вход объект типа T и значение типа long, ничего не возвращает.
ObjDoubleConsumer<T> void accept(T, double) Принимает на вход объект типа T и значение типа double, ничего не возвращает.

Порядок аргументов фиксирован: сначала объект, затем примитив. Такая сигнатура удобна для аккумуляторов — например, в трёхаргументном IntStream.collect().

ObjIntConsumer<StringBuilder> append = StringBuilder::append;

StringBuilder sb = new StringBuilder("x=");
append.accept(sb, 42);
System.out.println(sb);   // x=42

Специализации BiFunction

Интерфейс Абстрактный метод Описание
ToIntBiFunction<T, U> int applyAsInt(T, U) Принимает на вход два объекта и возвращает значение типа int.
ToLongBiFunction<T, U> long applyAsLong(T, U) Принимает на вход два объекта и возвращает значение типа long.
ToDoubleBiFunction<T, U> double applyAsDouble(T, U) Принимает на вход два объекта и возвращает значение типа double.

Симметричных IntBiFunction или BiPredicate над примитивами в JDK нет: два примитивных аргумента покрывают только IntBinaryOperator и его аналоги.

Как расшифровать имя интерфейса по его названию

Имена в java.util.function строятся по строгому шаблону, и, зная его, сигнатуру любого из 43 интерфейсов можно восстановить по названию.

Элемент имени Что означает Пример Сигнатура
Int / Long / Double в начале Тип входного параметра IntPredicate boolean test(int)
To + тип Тип возвращаемого значения ToIntFunction<T> int applyAsInt(T)
Тип + To + тип И вход, и выход примитивные IntToDoubleFunction double applyAsDouble(int)
Obj + тип Объект плюс примитив ObjIntConsumer<T> void accept(T, int)
Bi в начале Два объектных аргумента BiFunction<T, U, R> R apply(T, U)
UnaryOperator / BinaryOperator Все типы совпадают IntBinaryOperator int applyAsInt(int, int)

Имя абстрактного метода тоже подчиняется правилу: если результат примитивный, к глаголу добавляется суффикс As + тип — applyAsInt, applyAsLong, applyAsDouble, getAsInt, getAsBoolean. У методов test и accept суффикса нет: их возвращаемый тип (boolean и void) и так фиксирован.

Где специализации встречаются в Stream API

Чаще всего эти интерфейсы попадаются не в объявлениях переменных, а в сигнатурах методов Stream, IntStream, Map и List. Таблица ниже помогает понять, какую лямбду от вас ждёт компилятор.

Метод Ожидаемый интерфейс Что происходит
Stream<T>.mapToInt() ToIntFunction<T> Переход от потока объектов к IntStream
IntStream.map() IntUnaryOperator Преобразование int в int без упаковки
IntStream.mapToObj() IntFunction<R> Возврат от IntStream к Stream<R>
IntStream.filter() IntPredicate Отбор примитивов по условию
IntStream.forEach() IntConsumer Терминальная операция без результата
IntStream.reduce() IntBinaryOperator Свёртка потока в одно число
IntStream.collect() Supplier, ObjIntConsumer, BiConsumer Ручная сборка результата
Map<K, V>.forEach() BiConsumer<K, V> Обход пар «ключ — значение»
Map<K, V>.merge() BiFunction<V, V, V> Слияние старого и нового значения
List<E>.replaceAll() UnaryOperator<E> Замена каждого элемента на месте
Arrays.setAll(int[], ...) IntUnaryOperator Заполнение массива по индексу
List<String> words = List.of("Java", "Stream", "API");

// ToIntFunction<String>: объект -> примитив
int totalLength = words.stream()
                       .mapToInt(String::length)
                       .sum();                       // 13

// IntPredicate: фильтрация без упаковки
long evenCount = IntStream.rangeClosed(1, 10)
                          .filter(n -> n % 2 == 0)
                          .count();                  // 5

// IntBinaryOperator: свёртка
int factorial = IntStream.rangeClosed(1, 5)
                         .reduce(1, (a, b) -> a * b); // 120

// IntFunction<String>: примитив -> объект
List<String> labels = IntStream.rangeClosed(1, 3)
                               .mapToObj(n -> "#" + n)
                               .toList();             // [#1, #2, #3]

Полезно знать

Ссылка на метод вроде Integer::parseInt подходит сразу под несколько интерфейсов — и под ToIntFunction<String>, и под Function<String, Integer>. Компилятор выбирает по целевому типу, и во втором случае вы молча получаете упаковку результата. Если тип пишете вы сами, выбирайте примитивную версию.

Где чаще всего спотыкаются

  • Примитивные интерфейсы не наследуют обобщённые. IntBinaryOperator не является наследником BiFunction, а IntPredicate — наследником Predicate. Это независимые типы, и переменную одного нельзя присвоить переменной другого.
  • У IntFunction, ToIntFunction и ToIntBiFunction нет default-методов. Привычные andThen() и compose() есть у Function, IntUnaryOperator и DoubleUnaryOperator, но не у «смешанных» специализаций — цепочку придётся собирать лямбдой вручную.
  • Специализаций для float, byte, short и char не существует. Для них используют int- и double-версии, полагаясь на расширяющее преобразование.
  • UnaryOperator<T> и Function<T, T> взаимозаменяемы не в обе стороны. UnaryOperator<String> можно передать туда, где ждут Function<String, String>, но не наоборот: UnaryOperator наследует Function, а не равен ему.
  • Забытый boxed(). Чтобы из IntStream получить Stream<Integer> и собрать его в List<Integer>, нужен вызов boxed() либо mapToObj().

Правило выбора

Пишете обычный код, где лямбда вызывается десятки раз, — берите обобщённый интерфейс, он читается проще. Обрабатываете большие коллекции чисел, массивы или потоки из тысяч и миллионов элементов — берите примитивную специализацию и IntStream/LongStream/DoubleStream. Полный список сигнатур всегда есть в Javadoc пакета java.util.function.

Часто задаваемые вопросы

Чем IntFunction отличается от ToIntFunction?

IntFunction<R> принимает примитив и возвращает объект: R apply(int). ToIntFunction<T> — наоборот, принимает объект и возвращает примитив: int applyAsInt(T). Правило простое: префикс типа перед именем описывает аргумент, а приставка To — результат.

Почему в java.util.function нет специализаций для float, byte, short и char?

Полное покрытие всех восьми примитивов дало бы сотни интерфейсов, поэтому авторы JDK ограничились int, long и double. Значения byte, short и char расширяются до int без потери точности, а float — до double. Единственное исключение для boolean — интерфейс BooleanSupplier.

Можно ли присвоить переменную IntBinaryOperator переменной BinaryOperator?

Нет. Примитивные специализации не наследуют обобщённые интерфейсы — это параллельные, никак не связанные типы. Саму лямбду (a, b) -> a + b компилятор выведет под любой из них, но готовую переменную придётся обернуть вручную: BinaryOperator<Integer> op = (a, b) -> intOp.applyAsInt(a, b);.

Зачем BooleanSupplier, если есть Supplier<Boolean>?

BooleanSupplier объявляет boolean getAsBoolean() и отдаёт примитив, тогда как Supplier<Boolean> возвращает объект и допускает null — при автораспаковке это обернётся NullPointerException. Примитивная версия и быстрее, и безопаснее по контракту.

Насколько сильно автоупаковка замедляет код?

На единичных вызовах разница незаметна и не стоит усложнения. Она проявляется на сотнях тысяч и миллионах итераций: каждый упакованный int — отдельный объект в куче (около 16 байт плюс ссылка), лишняя работа сборщику мусора и потеря локальности данных. Кеш Integer покрывает лишь значения от минус 128 до 127.

Video Explanation

Prefer video format? Watch this lesson with examples and explanations.

Comments

Please log in or register to have a possibility to add comment.