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