Java 8 лямбда, Function.identity () или t-> t

241

У меня есть вопрос, касающийся использования Function.identity()метода.

Представьте себе следующий код:

Arrays.asList("a", "b", "c")
          .stream()
          .map(Function.identity()) // <- This,
          .map(str -> str)          // <- is the same as this.
          .collect(Collectors.toMap(
                       Function.identity(), // <-- And this,
                       str -> str));        // <-- is the same as this.

Есть ли причина, по которой вы должны использовать Function.identity()вместо str->str(или наоборот). Я думаю, что второй вариант более читабелен (вопрос вкуса, конечно). Но есть ли «реальная» причина, по которой следует отдавать предпочтение?

Przemysław Głębocki
источник
6
В конечном счете, нет, это не будет иметь значения.
августа
50
Либо в порядке. Идите с тем, что вы считаете более читабельным. (Не волнуйтесь, будьте счастливы.)
Брайан Гетц
3
Я бы предпочел t -> tпросто, потому что это более кратко.
Дэвид Конрад
3
Слегка несвязанный вопрос, но кто-нибудь знает, почему разработчики языка делают identity () возвращающим экземпляр Function вместо того, чтобы иметь параметр типа T и возвращать его, чтобы метод можно было использовать со ссылками на методы?
Кирилл Рахман
Я бы сказал, что полезно знакомиться со словом «идентичность», поскольку оно имеет важное значение в других областях функционального программирования.
orbfish

Ответы:

313

Начиная с текущей реализации JRE, Function.identity()всегда будет возвращаться один и тот же экземпляр, в то время как каждое вхождение identifier -> identifierне только создаст свой собственный экземпляр, но даже будет иметь отдельный класс реализации. Для более подробной информации смотрите здесь .

Причина в том, что компилятор генерирует синтетический метод, содержащий тривиальное тело этого лямбда-выражения (в случае x->xэквивалентного return identifier;), и указывает среде выполнения создать реализацию функционального интерфейса, вызывающего этот метод. Таким образом, среда выполнения видит только разные целевые методы, а текущая реализация не анализирует методы, чтобы выяснить, эквивалентны ли определенные методы.

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

Вы также можете учесть, что при компиляции с включенной отладочной информацией синтетический метод будет иметь атрибут отладки строки, указывающий на строку (и) исходного кода, содержащую лямбда-выражение, поэтому у вас есть шанс найти источник конкретного Functionэкземпляра во время отладки. , Напротив, при обнаружении экземпляра, возвращенного Function.identity()во время отладки операции, вы не будете знать, кто вызвал этот метод и передал экземпляр в операцию.

Holger
источник
5
Хороший ответ. У меня есть некоторые сомнения по поводу отладки. Чем это может быть полезно? Очень маловероятно, чтобы трассировка стека исключений включала x -> xфрейм. Вы предлагаете установить точку останова для этой лямбды? Обычно не так просто поместить точку останова в лямбду с одним выражением (по крайней мере, в Eclipse) ...
Тагир Валеев
14
@Tagir Valeev: вы можете отладить код, который получает произвольную функцию, и перейти к методу apply этой функции. Тогда вы можете получить исходный код лямбда-выражения. В случае явного лямбда-выражения вы будете знать, откуда взялась функция, и сможете распознать, в каком месте принять решение, хотя была сделана функция идентификации. При использовании Function.identity()эта информация теряется. Затем цепочка вызовов может помочь в простых случаях, но подумайте, например, о многопоточной оценке, когда первоначальный инициатор не находится в трассировке стека…
Хольгер,
2
Интересно в этом контексте: blog.codefx.org/java/instances-non-capturing-lambdas
Вим Deblauwe
13
@Wim Deblauwe: Интересно, но я бы всегда видел это наоборот: если метод фабрики явно не заявляет в своей документации, что он будет возвращать новый экземпляр при каждом вызове, вы не можете предполагать, что это произойдет. Так что не удивительно, если это не так. В конце концов, это одна из основных причин использования фабричных методов вместо new. new Foo(…)гарантирует создание нового экземпляра точного типа Foo, тогда как Foo.getInstance(…)может вернуть существующий экземпляр (подтип) Foo
Хольгер
93

В вашем примере нет большой разницы между str -> strи Function.identity()так как внутренне это просто t->t.

Но иногда мы не можем использовать, Function.identityпотому что мы не можем использовать Function. Посмотрите здесь:

List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);

это скомпилирует нормально

int[] arrayOK = list.stream().mapToInt(i -> i).toArray();

но если вы попытаетесь скомпилировать

int[] arrayProblem = list.stream().mapToInt(Function.identity()).toArray();

вы получите ошибку компиляции, так как mapToIntожидает ToIntFunction, что не связано с Function. Также ToIntFunctionнет identity()метода.

Pshemo
источник
3
См. Stackoverflow.com/q/38034982/14731 для другого примера, где замена i -> iна Function.identity()приведет к ошибке компилятора.
Гили
19
Я предпочитаю mapToInt(Integer::intValue).
Шмосел
4
@shmosel это нормально, но стоит упомянуть, что оба решения будут работать одинаково, так как mapToInt(i -> i)это упрощение mapToInt( (Integer i) -> i.intValue()). Используйте любую версию, которую вы считаете более понятной, для меня mapToInt(i -> i)лучше показывает намерения этого кода.
Пшемо
1
Я думаю, что при использовании ссылок на методы могут быть выигрыши в производительности, но это в основном лишь личные предпочтения. Я нахожу это более наглядным, потому что i -> iвыглядит как функция тождества, чего нет в данном случае.
шмосел
@shmosel Я не могу сказать много о разнице в производительности, поэтому вы можете быть правы. Но если производительность не является проблемой, я останусь с этим, i -> iтак как моя цель состоит в том, чтобы отобразить Integer в int (что mapToIntдовольно неплохо), чтобы не вызывать intValue()метод явно . Как будет достигнуто это отображение, не так уж важно. Так что давайте просто согласимся не соглашаться, но спасибо за указание на возможную разницу в производительности, мне нужно будет поближе взглянуть на это когда-нибудь.
Пшемо
44

Из источника JDK :

static <T> Function<T, T> identity() {
    return t -> t;
}

Так что нет, пока это синтаксически правильно.

JasonN
источник
8
Интересно, отменяет ли это ответ выше, касающийся лямбды, создающей объект, или это конкретная реализация?
orbfish
28
@orbfish: это совершенно точно. Каждое вхождение t->tв исходном коде может создать один объект и реализациюFunction.identity() - одно вхождение. Таким образом, все вызывающие сайты вызовов identity()будут совместно использовать этот один объект, в то время как все сайты, явно использующие лямбда-выражение t->t, создадут свой собственный объект. Этот метод Function.identity()ни в коем случае не является специальным, когда вы создаете фабричный метод, инкапсулирующий обычно используемое лямбда-выражение, и вызываете этот метод вместо повторения лямбда-выражения, вы можете сэкономить некоторую память, учитывая текущую реализацию .
Хольгер
Я предполагаю, что это потому, что компилятор оптимизирует создание нового t->tобъекта при каждом вызове метода и перезагружает один и тот же объект при каждом вызове метода?
Дэниел Грей,
1
@DanielGray решение принимается во время выполнения. Компилятор вставляет invokedynamicинструкцию, которая связывается при первом выполнении с помощью так называемого метода начальной загрузки, который в случае лямбда-выражений находится вLambdaMetafactory . Эта реализация решает вернуть дескриптор в конструктор, фабричный метод или код, всегда возвращающий один и тот же объект. Также может быть принято решение вернуть ссылку на уже существующий дескриптор (чего в настоящее время не происходит).
Хольгер
@Holger Вы уверены, что этот вызов идентичности не будет встроенным, а затем потенциально мономорфизированным (и снова встроенным)?
JasonN