Чи є корисність для використання синтаксису опорного методу замість лямбда-синтаксису на Java 8?


56

Чи пропускають посилання методів накладні покриття лямбда-обгортки? Чи можуть вони в майбутньому?

Відповідно до навчального посібника Java щодо посилань на методи :

Іноді ... лямбда-вираз не робить нічого, крім виклику існуючого методу. У цих випадках часто зрозуміліше посилатися на існуючий метод по імені. Довідки методів дозволяють вам це зробити; вони компактні, легко читаються лямбда-вирази для методів, які вже мають назву.

Я вважаю за краще синтаксис лямбда перед синтаксисом опорного методу з кількох причин:

Лямбди чіткіші

Незважаючи на твердження Oracle, я вважаю, що синтаксис лямбда легше читати, ніж короткий посилання на предметний метод, оскільки синтаксис опорного методу неоднозначний:

Bar::foo

Ви викликаєте статичний метод одного аргументу на класі x і передаєте його x?

x -> Bar.foo(x)

Або ви викликаєте метод екземпляра нульового аргументу на x?

x -> x.foo()

Синтаксис опорного методу може відповідати будь-якому. Він приховує, що насправді робить ваш код.

Лямбди безпечніші

Якщо ви посилаєтесь на Bar :: foo як на метод класу, а Bar згодом додає метод з тим самим іменем (або навпаки), ваш код більше не буде компілюватися.

Ламбда можна використовувати послідовно

Ви можете обернути будь-яку функцію в лямбда - так ви зможете послідовно використовувати один і той же синтаксис. Синтаксис опорного методу не працюватиме на методах, які беруть або повертають примітивні масиви, викидають перевірені винятки або мають однакове ім'я методу, що використовується як екземпляр та статичний метод (оскільки синтаксис опорного методу неоднозначний щодо того, який метод буде викликаний) . Вони не працюють, якщо ви перевантажували методи однаковою кількістю аргументів, але все одно не слід робити цього (див. Пункт 41 Джоша Блоха), тому ми не можемо протиставити це посиланням на методи.

Висновок

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

PS

Ні тут, ні там, але в мріях посилання на метод об'єкта виглядають більше так і застосовують динаміку виклику проти методу безпосередньо на об'єкті без лямбда-обгортки:

_.foo()

2
Це не відповідає на ваше запитання, але є й інші причини використання закриття. На мою думку, багато з них набагато переважають крихітні накладні витрати на додатковий виклик функції.

9
Я думаю, що ти переживаєш про продуктивність передчасно. На мій погляд, ефективність має бути вторинною увагою для використання методу посилання; справа в тому, щоб висловити свій намір. Якщо вам потрібно десь передати функцію, чому б просто не передати функцію замість якоїсь іншої функції, яка делегує їй? Мені це здається дивним; як ви не зовсім купуєте, що функції - це першокласні речі, їм потрібен анонімний каперон, щоб перемістити їх. Але щоб вирішити ваше питання більш прямо, я не здивуюсь, якщо посилання на метод може бути реалізовано як статичний виклик.
Довал

7
.map(_.acceptValue()): Якщо я не помиляюся, це дуже схоже на синтаксис Scala. Можливо, ви просто намагаєтесь використовувати неправильну мову.
Джорджіо

2
"Синтаксис опорного методу не працюватиме на методах, які повертають нікчемність" - Як так? Я передаю System.out::printlnна forEach()весь час ...?
Лукас Едер

3
"Синтаксис опорного методу не працюватиме на методах, які беруть або повертають примітивні масиви, викидають перевірені винятки ...." Це неправильно. Ви можете використовувати посилання на методи, а також лямбда-вирази для таких випадків, доки відповідний функціональний інтерфейс це дозволяє.
Стюарт Маркс

Відповіді:


12

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

Наприклад

public class InvokeTest {

    private static void invoke(final Runnable r) {
        r.run();
    }

    private static void target() {
        new Exception().printStackTrace();
    }

    @Test
    public void lambda() throws Exception {
        invoke(() -> target());
    }

    @Test
    public void methodReference() throws Exception {
        invoke(InvokeTest::target);
    }
}

Ви побачите, як на консолі виводиться стежка.

У lambda(), метод виклику target()є lambda$lambda$0(InvokeTest.java:20), який має відстежувану інформацію про лінію. Очевидно, що це лямбда, яку ви пишете, компілятор генерує для вас анонімний метод. І тоді, виклик методу лямбда - це щось подібне InvokeTest$$Lambda$2/1617791695.run(Unknown Source), тобто invokedynamicвиклик у JVM, це означає, що виклик пов'язаний із створеним методом .

У methodReference(), спосіб виклику target()є безпосередньо тим InvokeTest$$Lambda$1/758529971.run(Unknown Source), що означає, що виклик безпосередньо пов'язаний з InvokeTest::targetметодом .

Висновок

Перш за все, порівняння з методом-посиланням, використання лямбда-експресії спричинить лише ще один виклик методу до методу генерації від лямбда.


1
Відповідь нижче про метафабрикати трохи краща. Я додав кілька корисних коментарів, які стосуються його (а саме те, як такі програми, як Oracle / OpenJDK, використовують ASM для генерування об'єктів класу обгортки, необхідних для створення екземплярів лямбда / методу.
Ajax

29

Вся справа в метафабриці

По-перше, більшість посилань на метод не потребують знешкодження метафабрики лямбда, вони просто використовуються як опорний метод. У розділі "Ламбда-цукор" статті статті перекладу лямбдаських виразів ("TLE"):

Якщо всі рівні, приватні методи є кращими, ніж неприватні, статичні методи переважніші, ніж методи екземпляра, найкраще, якщо тіл лямбда виведено в найпотаємніший клас, у якому з'являється лямбда-вираз, підписи повинні відповідати підпису тіла лямбда, додатково аргументи повинні бути попередньо викладені на передній частині списку аргументів для захоплених значень, і взагалі не погіршують посилання методу. Однак є випадки винятків, коли нам, можливо, доведеться відхилятися від цієї базової стратегії.

Це далі висвітлено далі у тексті TLE "Ламбда-метафабрика":

metaFactory(MethodHandles.Lookup caller, // provided by VM
            String invokedName,          // provided by VM
            MethodType invokedType,      // provided by VM
            MethodHandle descriptor,     // lambda descriptor
            MethodHandle impl)           // lambda body

implАргумент ідентифікує метод лямбда, або обезцукрена тіло лямбда або метод , названий в якості посилання методи.

Статичні ( Integer::sum) або необмежені Integer::intValueпосилання методу екземпляра ( ) є "найпростішими" або найбільш "зручними", в тому сенсі, що з ними можна оптимально обробляти метафакторний варіант "швидкого шляху" без знешкодження . Ця перевага корисно вказується у «Металевих варіантах» TLE:

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

Звичайно, посилання на метод захоплення екземпляра ( obj::myMethod) має надати обмежений екземпляр як аргумент для обробки методу для виклику, що може означати необхідність знеструмлення методів "bridge".

Висновок

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


1
Це найбільш відповідна відповідь, оскільки вона насправді стосується внутрішніх механізмів, необхідних для створення лямбда. Як хтось, хто насправді налагодив процес створення лямбда (щоб зрозуміти, як генерувати стабільні ідентифікатори для даної посилання на лямбда / метод, щоб їх можна було видалити з карт), я вважав, що це дуже дивно, скільки роботи робиться для створення екземпляр лямбда. Oracle / OpenJDK використовують ASM для динамічного генерування класів, які, якщо потрібно, закривають будь-яку змінну, на яку посилається у вашій лямбда (або класифікатор екземпляра посилання на метод) ...
Ajax

1
... Крім закриття змінних, лямбдаси, зокрема, також створюють статичний метод, який вводиться до класу декларування, щоб у створеній лямбда є що викликати для відображення у функції (функції), до яких потрібно викликати. Посилання методу з класифікатором екземпляра буде приймати цей екземпляр як параметр цього методу; Я не перевіряв, чи пропускає це посилання :: методу в приватному класі це закриття, але я перевірив, що статичні методи насправді пропускають проміжний метод (і просто створюють об'єкт, щоб відповідати цілі виклику в сайт виклику).
Аякс

1
Імовірно, приватний метод також не потребуватиме віртуальної розсилки, тоді як будь-що більше публічного потрібно буде закрити над екземпляром (за допомогою генерованого статичного методу), щоб він міг викликативіртуальну інформацію про фактичний екземпляр, щоб переконатися, що він помічає відміни. TL; DR: Посилання методів закриваються на менше аргументів, тому вони менше просочуються та потребують менш динамічного генерування коду.
Аякс

3
Останній спам-коментар ... Динамічне покоління класу досить важке. Все ж, мабуть, краще, ніж завантажувати анонімний клас у завантажувач класів (оскільки найважчі частини виконуються лише один раз), але ви були б дуже здивовані лише тому, скільки роботи йде на створення лямбда-екземпляра. Цікаво було б побачити реальні орієнтири лямбдів проти посилань на методи проти анонімних класів. Зауважте, що два лямбда або посилання на методи, які посилаються на одні і ті ж речі, але в різних місцях створюють різні класи під час виконання (навіть у межах одного методу, відразу один за одним).
Аякс


0

Існує один досить серйозний наслідок при використанні лямбда-виразів, які можуть вплинути на продуктивність.

Коли ви оголошуєте лямбда-вираз, ви створюєте закриття над локальним діапазоном.

Що це означає і як це впливає на продуктивність?

Ну я радий, що ти запитав. Це означає, що кожен з цих маленьких лямбда-виразів є невеликим анонімним внутрішнім класом, і це означає, що він несе в собі посилання на всі змінні, що знаходяться в тій же області вираження лямбда.

Це означає також thisпосилання на екземпляр об'єкта та всіх його полів. Залежно від часу фактичного виклику лямбда, це може призвести до досить значного витоку ресурсів, оскільки збирач сміття не в змозі відпустити ці посилання до тих пір, поки об'єкт, який тримає лямбда, ще живий ...


Те саме стосується нестатичних посилань на метод.
Аякс

12
Ягняні лямбда не є суворо закритими. Також вони не є анонімними внутрішніми класами. Вони НЕ несуть посилання на всі змінні в області, де вони оголошені. Вони ТОЛЬКО несуть посилання на змінні, на які вони насправді посилаються. Це стосується також thisоб'єкта, на який можна посилатися. Тому лямбда не витікає ресурси, просто маючи для них можливості; він тримається лише за потрібні йому предмети. З іншого боку, анонімний внутрішній клас може просочувати ресурси, але не завжди це робить. Дивіться цей приклад для прикладу: a.blmq.us/2mmrL6v
squid314

Ця відповідь просто неправильна
Стефан Рейх

Дякую @StefanReich, це було дуже корисно!
Роланд Тепп
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.