Lambda ·
‹ Предыдущий Следующий ›
⏱ 5 минут чтения Обновлено: 2026-09-07

Интерфейс Function в Java

Что напечатает эта строка: f1.andThen(f2).compose(f3).compose(f4).apply("Compose"), если каждая функция дописывает к строке свою цифру? Интуиция подсказывает Compose1234, а на консоли появится Compose4312. Порядок вызова в цепочке — главный источник ошибок при работе с Function, и ниже мы разберём его по шагам.

java.util.function.Function<T, R> — это встроенный функциональный интерфейс из пакета java.util.function, добавленный в Java SE 8. Он принимает аргумент типа T и возвращает результат типа R, то есть описывает преобразование одного значения в другое — того же или другого типа.

Что такое интерфейс Function

Объявление интерфейса в JDK выглядит так:

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}

У него ровно один абстрактный метод — apply(), поэтому интерфейс помечен аннотацией @FunctionalInterface и его можно реализовать лямбда-выражением или ссылкой на метод.

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

T -> R

Читается это так: «на вход — значение типа T, на выход — значение типа R». Типичные задачи: String в Integer, сущность в DTO, идентификатор в объект, число в отформатированную строку.

Актуально ли это сегодня

Интерфейс Function не менялся с Java 8: сигнатуры apply, andThen, compose и identity одинаковы в Java 8, 11, 17, 21 и 25. Всё, что описано в уроке, работает в любом актуальном LTS-релизе без оговорок.

Метод apply(): первый пример

apply() — единственный абстрактный метод интерфейса. Именно его тело вы пишете в лямбде, и именно он выполняет преобразование:

import java.util.function.Function;

public class FunctionExample1 {
    public static void main(String[] args) {
        Function<Double, Long> function = d -> Math.round(d);
        System.out.println(function.apply(5.7)); // 6
    }
}

Здесь срабатывают сразу три неявных преобразования, о которых полезно знать:

  • литерал 5.7 (тип double) автоматически упаковывается в Double, потому что apply() принимает объект;
  • внутри лямбды Double распаковывается обратно в double для вызова Math.round(double);
  • результат типа long упаковывается в Long, так как второй параметр типа объявлен как Long.

Ту же лямбду можно записать ссылкой на метод — компилятор сам выберет перегрузку Math.round(double), потому что целевой тип требует возврата Long:

Function<Double, Long> function = Math::round;
System.out.println(function.apply(5.7)); // 6

andThen и compose: порядок вызова

Кроме apply() интерфейс объявляет два метода по умолчанию, которые склеивают функции в цепочку:

default <V> Function<T, V> andThen(Function<? super R, ? extends V> after);
default <V> Function<V, R> compose(Function<? super V, ? extends T> before);

Разница между ними только в порядке выполнения:

Метод Что выполняется первым Эквивалентная запись Направление чтения
f.andThen(g) сначала f, затем g x -> g.apply(f.apply(x)) слева направо, как читается код
f.compose(g) сначала g, затем f x -> f.apply(g.apply(x)) справа налево, как в математике

Классический пример, который часто дают на собеседовании:

import java.util.function.Function;

public class FunctionExample2 {
    public static void main(String[] args) {
        Function<String, String> f1 = s -> s + "1";
        Function<String, String> f2 = s -> s + "2";
        Function<String, String> f3 = s -> s + "3";
        Function<String, String> f4 = s -> s + "4";

        System.out.println(f1.andThen(f2).compose(f3).compose(f4).apply("Compose")); // Compose4312
        System.out.println(f1.andThen(f2).andThen(f3).apply("AndThen"));             // AndThen123
    }
}

Первая строка разбирается по шагам так. Каждый compose «подкладывает» функцию в начало цепочки, поэтому последняя присоединённая функция срабатывает первой:

Шаг Функция Почему именно она Результат
1 f4 последний compose — выполняется раньше всех Compose4
2 f3 предыдущий compose Compose43
3 f1 исходная функция цепочки Compose431
4 f2 andThen — всегда после f1 Compose4312

Вторая строка проще: andThen добавляет функции в конец, поэтому порядок совпадает с порядком записи — f1, f2, f3, и результат равен AndThen123.

Мнемоника

Две записи всегда дают одну и ту же функцию: g.compose(f) эквивалентно f.andThen(g). Если сомневаетесь в порядке — перепишите цепочку через andThen, её читать слева направо гораздо проще. Оба метода бросают NullPointerException, если передать им null: внутри стоит Objects.requireNonNull.

Function.identity()

У интерфейса есть один статический метод:

static <T> Function<T, T> identity()

Он возвращает функцию, которая отдаёт свой аргумент без изменений — тождественное преобразование T -> T:

import java.util.function.Function;

public class FunctionExample3 {
    public static void main(String[] args) {
        Function<String, String> f = Function.identity();
        System.out.println(f.apply("Some Value")); // Some Value
    }
}

Сам по себе такой вызов бесполезен. Смысл identity() появляется там, где API требует передать функцию, а вам нужен исходный объект «как есть» — например, когда сам объект становится ключом в Map:

import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

public class IdentityExample {
    public static void main(String[] args) {
        List<String> words = List.of("Java", "Function", "Lambda");

        Map<String, Integer> lengthByWord = words.stream()
                .collect(Collectors.toMap(Function.identity(), String::length));

        System.out.println(lengthByWord); // {Java=4, Lambda=6, Function=8}
    }
}

Где Function применяется на практике

В стандартной библиотеке Function встречается чаще любого другого функционального интерфейса. Самые частые точки входа:

  • Stream.map(Function<? super T, ? extends R>) — преобразование каждого элемента потока;
  • Optional.map(Function<? super T, ? extends U>) — преобразование значения, если оно есть;
  • Map.computeIfAbsent(K, Function<? super K, ? extends V>) — вычисление значения для отсутствующего ключа;
  • Collectors.toMap и Collectors.groupingBy — извлечение ключа из элемента;
  • Comparator.comparing(Function<? super T, ? extends U>) — сравнение по вычисляемому полю.
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

public class FunctionInApi {
    public static void main(String[] args) {
        Function<String, Integer> length = String::length;

        // 1. Stream.map принимает Function
        List<Integer> lengths = List.of("Java", "Function", "Lambda")
                .stream()
                .map(length)
                .toList(); // Stream.toList() доступен с Java 16
        System.out.println(lengths); // [4, 8, 6]

        // 2. Map.computeIfAbsent: Function вычисляет значение для нового ключа
        Map<Character, List<String>> byFirstLetter = new HashMap<>();
        for (String word : List.of("Java", "Jar", "Lambda")) {
            byFirstLetter.computeIfAbsent(word.charAt(0), k -> new ArrayList<>()).add(word);
        }
        System.out.println(byFirstLetter); // {J=[Java, Jar], L=[Lambda]}
    }
}

BiFunction, UnaryOperator и примитивные варианты

Базовый Function<T, R> работает только с объектами и принимает ровно один аргумент. Для остальных случаев в пакете java.util.function есть готовые специализации:

Интерфейс Дескриптор Абстрактный метод Когда использовать
Function<T, R> T -> R apply обычное преобразование объекта в объект
BiFunction<T, U, R> (T, U) -> R apply нужны два аргумента
UnaryOperator<T> T -> T apply тип аргумента и результата совпадают
BinaryOperator<T> (T, T) -> T apply свёртка двух значений одного типа
IntFunction<R>, LongFunction<R>, DoubleFunction<R> int -> R apply аргумент — примитив, результат — объект
ToIntFunction<T>, ToLongFunction<T>, ToDoubleFunction<T> T -> int applyAsInt и аналоги аргумент — объект, результат — примитив
IntUnaryOperator, IntToDoubleFunction и др. int -> int, int -> double applyAsInt, applyAsDouble и аргумент, и результат — примитивы

Совет по производительности

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

Function, Supplier, Consumer и Predicate

Function — лишь один из четырёх базовых функциональных интерфейсов пакета java.util.function. Различать их проще всего по наличию входа и выхода:

Интерфейс Дескриптор Метод Задача
Function<T, R> T -> R apply принять значение и вернуть результат
Supplier<T> () -> T get только поставить значение, без входа
Consumer<T> T -> void accept только принять значение, без результата
Predicate<T> T -> boolean test проверить условие

Формально Predicate<T> можно заменить на Function<T, Boolean>, но так делать не стоит: Predicate возвращает примитивный boolean без упаковки и даёт методы and(), or(), negate(). Полный перечень интерфейсов пакета собран в уроке про специализации функциональных интерфейсов.

На чём чаще всего спотыкаются

  • Путают compose и andThen. Самая частая ошибка и в коде, и на собеседовании. Запомните: compose выполняет переданную функцию до текущей, andThenпосле.
  • Ожидают, что Function изменит исходный объект. Функция ничего не меняет на месте — она возвращает новое значение. Строка function.apply(x); без присваивания результата бессмысленна.
  • Пытаются бросить проверяемое исключение. Метод apply() объявлен без throws, поэтому IOException внутри лямбды не скомпилируется — придётся ловить его и заворачивать в непроверяемое.
  • Берут объектную версию для чисел. Function<Integer, Integer> в цикле на миллион итераций создаёт миллион упаковок; IntUnaryOperator не создаёт ни одной.
  • Забывают про null. Function ничего не знает о null: если apply() получит null, а лямбда вызовет метод на аргументе, будет NullPointerException. Проверку нужно писать самому либо использовать Optional.map.

Официальное описание интерфейса со всеми сигнатурами доступно в Javadoc java.util.function.Function.

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

Чем Function отличается от UnaryOperator?

UnaryOperator наследуется от Function, у которой тип аргумента и тип результата совпадают. Любой UnaryOperator строк можно передать туда, где ждут Function из String в String, но не наоборот. Берите UnaryOperator, когда преобразование не меняет тип: сигнатура метода становится короче и понятнее.

Как передать в Function два аргумента?

Есть два пути. Штатный - взять BiFunction с методом apply от двух параметров. Второй - карринг, когда одна функция возвращает другую: Function sum = a -> b -> a + b, и тогда sum.apply(2).apply(3) вернёт 5. Карринг удобен, если первый аргумент известен заранее, а второй появляется позже.

Можно ли бросить проверяемое исключение внутри Function?

Нет. Метод apply() объявлен без throws, поэтому проверяемое исключение внутри лямбды даст ошибку компиляции. Варианты решения: обработать исключение прямо в лямбде, обернуть его в RuntimeException либо объявить собственный функциональный интерфейс, чей метод объявлен с throws.

Зачем нужен Function.identity(), если можно написать t -> t?

Поведение одинаковое, но Function.identity() каждый раз возвращает один и тот же общий экземпляр, а лямбда t -> t порождает отдельный объект на каждом месте вызова. Плюс запись Collectors.toMap(Function.identity(), ...) читается однозначно, тогда как t -> t в длинной цепочке легко принять за забытое преобразование.

Что быстрее: Function с Integer или IntUnaryOperator?

Для чисел быстрее IntUnaryOperator. Объектная версия на каждом вызове упаковывает int в Integer и распаковывает обратно, создавая лишние объекты и нагрузку на сборщик мусора. Примитивная специализация работает с int напрямую. На десятке элементов разница незаметна, на миллионе - вполне ощутима.

Видео объяснение

Предпочитаете видеоформат? Посмотрите этот урок с примерами и объяснениями.

Комментарии

Зарегистрируйтесь или войдите, чтобы иметь возможность оставить комментарий.