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


240

У мене є питання щодо використання 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(або навпаки). Я думаю, що другий варіант є більш читабельним (справа смаку, звичайно). Але чи є якась "реальна" причина, чому слід віддати перевагу?


6
Зрештою, ні, це не має значення.
fge

50
Або добре. Ідіть із тим, що вам здається, читабельніше. (Не хвилюйтесь, будьте щасливі.)
Брайан Гец

3
Я вважаю за краще t -> tпросто тому, що це більш лаконічно.
Девід Конрад

3
Питання трохи не пов’язане, але чи знає хто, чому дизайнери мови змушують ідентифікатор () повертати екземпляр Функції замість того, щоб мати параметр типу T і повертати його, щоб метод міг використовуватися з посиланнями на метод?
Кирило Рахман

Я зауважу, що є корисність спілкуватися зі словом "ідентичність", оскільки це має важливе значення в інших областях функціонального програмування.
orbfish

Відповіді:


312

Що стосується поточної реалізації JRE, Function.identity()завжди буде повертати один і той же екземпляр, тоді як кожне виникнення identifier -> identifierне тільки створюватиме власний екземпляр, але навіть матиме окремий клас реалізації. Детальніше дивіться тут .

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

Тож використання Function.identity()замість цього x -> xможе заощадити деяку пам’ять, але це не повинно впливати на ваше рішення, якщо ви дійсно вважаєте, що x -> xце читабельніше, ніж Function.identity().

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


5
Гарна відповідь. У мене є деякі сумніви щодо налагодження. Чим це може бути корисно? Це малоймовірно, щоб отримати слід виключення стека, що включає x -> xкадр. Ви пропонуєте встановити точку перерви для цієї лямбда? Зазвичай поставити точку перелому в лямбді з одновиразом (принаймні в Eclipse) не так просто ...
Tagir Valeev,

14
@Tagir Valeev: ви можете налагодити код, який отримує довільну функцію, і вступити в метод застосування цієї функції. Тоді ви можете опинитися у вихідному коді лямбда-виразу. У разі явного лямбда-виразу ви знаєте, звідки походить ця функція, і ви зможете розпізнати, в якому місці рішення пройти, хоча функція ідентифікації була прийнята. При використанні Function.identity()ця інформація втрачається. Тоді ланцюжок викликів може допомогти у простих випадках, але подумайте, наприклад, багатопотокове оцінювання, коли оригінальний ініціатор не знаходиться у сліді стека…
Holger

2
Цікаво в цьому контексті: blog.codefx.org/java/instance-non-capturing-lambdas
Вім Деблауве

13
@Wim Deblauwe: Цікаво, але я завжди бачив це навпаки: якщо заводський метод не вказав прямо в своїй документації, що він повертатиме новий екземпляр при кожному виклику, ви не можете припустити, що він буде. Тож це не повинно дивуватися, якщо цього немає. Зрештою, це одна велика причина використання заводських методів замість new. new Foo(…)гарантує створення нового екземпляра точного типу Foo, тоді як Foo.getInstance(…)може повернути наявний екземпляр (підтип) Foo
Holger

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()методу.


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

@shmosel Я не можу сказати багато про різницю в продуктивності, тому ви можете мати рацію. Але якщо продуктивність не є проблемою, я залишатимусь з нею, i -> iоскільки моя мета - зіставити Integer на int (що mapToIntдосить добре підказує) не явно викликати intValue()метод. Як досягти цього картографування, насправді не так важливо. Тож давайте погоджуємося не погоджуватися, але дякую за вказівку на можливу різницю у виконанні, мені потрібно буде десь уважніше подивитися на це.
Пшемо

44

З джерела JDK :

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

Отже, ні, доки це синтаксично правильно.


8
Цікаво, чи це визнає недійсною відповідь, що стосується лямбда, що створює об'єкт, - чи це конкретна реалізація.
orbfish

28
@orbfish: це абсолютно відповідає. Кожне виникнення t->tвихідного коду може створювати один об'єкт, а реалізація - Function.identity()це одне . Таким чином, усі сайти, що викликають виклики identity(), поділять цей об’єкт, тоді як усі сайти, явно використовують лямбда-вираз t->t, створюватимуть власний об’єкт. Метод Function.identity()не є особливим у будь-якому разі, коли ви створюєте заводський метод, що інкапсулює загальновживаний лямбда-вираз і викликає цей метод замість повторення лямбда-виразу, ви можете зберегти деяку пам’ять, враховуючи поточну реалізацію .
Холгер

Я здогадуюсь, що це тому, що компілятор оптимізує створення нового t->tоб'єкта щоразу, коли метод викликається і переробляє той самий кожен раз, коли метод викликається?
Даніель Грей

1
@DanielGray рішення приймається під час виконання. Компілятор вставляє invokedynamicінструкцію, яка пов'язана при першому її виконанні, виконуючи так званий метод завантаження, який у випадку лямбда-виразів розташований у LambdaMetafactory. Ця реалізація вирішує повернути ручку конструктору, заводському методу або коду, який завжди повертає один і той же об'єкт. Він також може вирішити повернути посилання на вже існуючу обробку (що в даний час не відбувається).
Холгер

@Holger Ви впевнені, що цей заклик до ідентичності не буде накреслений, а потім може бути мономорфізований (і знову накреслений)?
JasonN
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.