W3docs

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

Преобразование значений одного типа в другой в Java с помощью интерфейса Function и методов andThen/compose.

Function<T, R> — функциональный интерфейс для задачи «превратить этот T в R»: один входной параметр, один результат, побочные эффекты не предполагаются. Именно такую форму принимает Stream.map, именно её ожидает Optional.map, именно её требует Map.computeIfAbsent, и именно её принимает любой метод JDK, который «преобразует одно в другое». Один абстрактный метод, три-четыре полезных метода по умолчанию и небольшая алгебра (andThen, compose, identity) для составления цепочек преобразований без написания промежуточных лямбд.

Интерфейс

@FunctionalInterface
public interface Function<T, R> {
  R apply(T t);                                                    // the only abstract method

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

apply(T) — это SAM (единственный абстрактный метод). Каждая лямбда или ссылка на метод, оказывающаяся на позиции Function<T, R>, реализует именно его.

Function<String, Integer> length = String::length;
int n = length.apply("hello");                  // 5

Как правило, вы предоставляете вызов apply методам stream.map(length) или optional.map(length). Знать имя метода важно, когда вы пишете код, который принимает Function<T, R> и должен вызвать его самостоятельно.

andThen и compose — два способа объединять функции в цепочку

Два метода по умолчанию создают новую Function, объединяя получатель с другой функцией. Они отличаются только направлением:

Function<String, String>  trim     = String::trim;
Function<String, Integer> length   = String::length;

Function<String, Integer> trimThenLength = trim.andThen(length);     // f.andThen(g): g(f(x))
Function<String, Integer> sameThing      = length.compose(trim);     // g.compose(f): g(f(x))

Оба строят один и тот же конвейер s -> length(trim(s)). Разница в том, который из них лучше читается в месте вызова:

  • andThen читается слева направо, в том же порядке, в котором течут данные. trim.andThen(length).andThen(asString) означает «trim, затем length, затем asString».
  • compose читается справа налево, как принято в математической записи: f ∘ g означает «сначала применить g, затем f». length.compose(trim) — «length после trim».

В прикладном коде andThen почти всегда понятнее — код читается сверху вниз и слева направо, и конвейер «слева направо» соответствует этому порядку. compose удобен, когда у вас уже есть финальная функция и нужно добавить предварительную обработку в начало без переписывания всей цепочки.

Оба метода ленивы в том смысле, что не выполняют никаких вычислений в момент композиции; они просто возвращают новую Function, чей метод apply вызывает составляющие функции в нужном порядке.

Function.identity() — преобразование-заглушка

Function<T, T> id = Function.identity();      // t -> t

identity() возвращает один и тот же экземпляр при каждом вызове (синглтон-лямбда), поэтому его использование не требует никаких аллокаций. Единственное место, где он по-настоящему незаменим, — маппер ключа или значения в Collectors.toMap, когда нужно передать Function, хотя значением является «сам элемент»:

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

Без Function.identity() пришлось бы писать p -> p, что при каждом вызове создаёт новую лямбду и читается хуже.

Тонкий момент: identity() работает только тогда, когда типы входных и выходных данных совпадают. Как только обобщённый тип расширяется (Function<? super T, ? extends R>), компилятор может потребовать явной записи лямбды. Это граничный случай, но стоит помнить о нём, когда вывод типов вызывает ошибки.

Function<T, R> против UnaryOperator<T>

UnaryOperator<T> — специализация для случая, когда входной и выходной типы одинаковы:

UnaryOperator<String> upper = String::toUpperCase;       // String -> String
Function<String, String> sameShape = String::toUpperCase;

Оба являются допустимыми экземплярами Function<String, String>UnaryOperator<T> расширяет Function<T, T>. Разница на уровне API: List.replaceAll, Map.replaceAll и Comparator.thenComparing(UnaryOperator) объявляют UnaryOperator<T>, потому что «заменить каждый элемент преобразованным значением того же типа» — это именно такая форма. Передайте ссылку на метод, и компилятор выберет правильный вариант.

BiFunction<T, U, R> — два входных параметра

Форма с двумя аргументами:

BiFunction<String, Integer, String> repeat = String::repeat;
String s = repeat.apply("ab", 3);             // "ababab"

BiFunction поддерживает andThen, но не compose — асимметрия намеренная, поскольку предобработка двухаргументной функции потребовала бы двух параметров compose.

JDK использует BiFunction<K, V, V> для Map.merge и BiFunction<K, V, V_NEW> для Map.compute. BinaryOperator<T> — частный случай, когда все три параметра типа равны T (входные и выходной одного типа), который рассматривается в главе BinaryOperator.

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

Function<Integer, String> выполняет упаковку int при каждом вызове. В пакете есть три семейства, позволяющие избежать этого:

// 1. Primitive in, object out — "IntFunction<R>"
IntFunction<String>     fromInt   = i -> "n=" + i;

// 2. Object in, primitive out — "ToIntFunction<T>"
ToIntFunction<String>   strLen    = String::length;
ToDoubleFunction<Item>  price     = Item::price;

// 3. Primitive in, primitive out — "IntToLongFunction", "IntUnaryOperator", etc.
IntToLongFunction       square    = i -> (long) i * i;
IntUnaryOperator        doubleIt  = i -> i * 2;
DoubleUnaryOperator     halve     = d -> d / 2.0;

Наименования читаются как предложение:

  • IntX — работает с int.
  • ToIntX — возвращает int.
  • IntToLongX — принимает int, возвращает long.

Stream.mapToInt(ToIntFunction) — мост из упакованного Stream<T> в IntStream. Как только вы перешли на IntStream, каждое преобразование использует IntUnaryOperator или IntToLongFunction — без каких-либо затрат на упаковку.

Пример: композиция, identity и примитивная специализация

Программа ниже создаёт две Function, компонует их с помощью andThen и compose, чтобы показать эквивалентность, использует Function.identity() внутри Collectors.toMap и сравнивает упакованную Function<Integer, Integer> с примитивным IntUnaryOperator на нагрузке, достаточной для ощущения затрат на упаковку.

java— editable, runs on the server

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

  • trim.andThen(upper) и upper.compose(trim) дали одинаковую String для одинакового входного значения. Они отличаются только тем, какое название естественнее звучит в месте записи — andThen соответствует потоку данных слева направо, compose — математической нотации «f после g».
  • Более длинная цепочка trim.andThen(upper).andThen(length) изменила тип результата по пути — с String на Integer. Конвейер компонуется типобезопасно; компилятор отследил String -> String -> String -> Integer за вас.
  • Function.identity() вошёл в Collectors.toMap(Person::name, Function.identity()) в роли маппера значений. Лямбда p -> p тоже сработала бы, но identity() — синглтон без аллокаций, и его использование выражает намерение явно («значение и есть person»).
  • Упакованная Function<Integer, Integer> платит за две упаковки Integer при каждом вызове; примитивный IntUnaryOperator — ничего. Одиночный прогрев может показать похожее время — JIT хорошо устраняет короткоживущие упаковки — но под реальным давлением аллокаций (большие кучи, параллельная сборка мусора, ускользающие значения) примитивный вариант остаётся устойчивым. Используйте его в горячих конвейерах, обрабатывающих миллионы значений.
  • BiFunction.andThen(Function) объединил двухаргументную функцию с одноаргументным продолжением. Метода BiFunction.compose не существует — предобработка двух входных параметров потребовала бы двух аргументов compose, чего API намеренно избегает.

Что дальше

Function<T, R> и Predicate<T> — оба «чистые» формы: вход, выход, побочные эффекты не ожидаются. В следующей главе, Java Consumer и Supplier, рассматриваются два интерфейса, выходящие за рамки этой чистоты: Consumer<T> принимает входное значение и ничего не возвращает (побочный эффект — вывод, логирование, сохранение), а Supplier<T> ничего не принимает и возвращает результат (ленивое значение по умолчанию, фабрика, генератор случайных чисел). Они завершают четырёхстороннюю таксономию, которую вы видели в обзоре встроенных интерфейсов.

Практика

Практика
У вас есть `Function<String, String> trim = String::trim;` и `Function<String, Integer> length = String::length;`. Вы хотите получить `Function<String, Integer>`, которая сначала обрезает строку, а затем берёт длину. Какое выражение строит её наиболее естественно?
У вас есть `Function<String, String> trim = String::trim;` и `Function<String, Integer> length = String::length;`. Вы хотите получить `Function<String, Integer>`, которая сначала обрезает строку, а затем берёт длину. Какое выражение строит её наиболее естественно?
Was this page helpful?