Есть ли выигрыш в производительности при использовании ссылочного синтаксиса метода вместо лямбда-синтаксиса в Java 8?

56

Пропускают ли ссылки на методы накладные расходы на лямбда-оболочку? Могут ли они в будущем?

Согласно Учебному руководству по Java о методах :

Иногда ... лямбда-выражение делает только вызов существующего метода. В этих случаях часто проще обратиться к существующему методу по имени. Ссылки на методы позволяют вам сделать это; это компактные, легко читаемые лямбда-выражения для методов, которые уже имеют имя.

Я предпочитаю лямбда-синтаксис синтаксису ссылки на метод по нескольким причинам:

Лямбды понятнее

Несмотря на заявления Oracle, я нахожу лямбда-синтаксис более легким для чтения, чем сокращенная ссылка на метод объекта, поскольку синтаксис ссылки на метод неоднозначен:

Bar::foo

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

x -> Bar.foo(x)

Или вы вызываете метод экземпляра с нулевым аргументом для x?

x -> x.foo()

Синтаксис ссылки на метод может заменять любой из них. Он скрывает то, что на самом деле делает ваш код.

Лямбды безопаснее

Если вы ссылаетесь на Bar :: foo как на метод класса, а Bar позже добавляет метод экземпляра с тем же именем (или наоборот), ваш код больше не будет компилироваться.

Вы можете использовать лямбды последовательно

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

Заключение

Если за это не снижается производительность, у меня возникает соблазн отключить предупреждение в моей среде IDE и последовательно использовать лямбда-синтаксис, не добавляя случайную ссылку на метод в мой код.

PS

Ни здесь, ни там, но в моих снах ссылки на методы объекта выглядят больше так и применяют invoke-dynamic к методу непосредственно к объекту без лямбда-обертки:

_.foo()
GlenPeterson
источник
2
Это не отвечает на ваш вопрос, но есть и другие причины для использования замыканий. На мой взгляд, многие из них намного перевешивают крошечные издержки при вызове дополнительной функции.
9
Я думаю, что вы беспокоитесь о производительности преждевременно. По моему мнению, производительность должна быть вторичным фактором при использовании ссылки на метод; это все о выражении ваших намерений. Если вам нужно передать функцию куда-нибудь, почему бы просто не передать функцию вместо какой-либо другой функции, которая ей делегирует? Это кажется странным для меня; как будто вы не совсем понимаете, что функции - это первоклассные вещи, им нужен анонимный сопровождающий для их перемещения. Но чтобы ответить на ваш вопрос более прямо, я не удивлюсь, если ссылку на метод можно будет реализовать как статический вызов.
Доваль
7
.map(_.acceptValue())Если я не ошибаюсь, это очень похоже на синтаксис Scala. Может быть, вы просто пытаетесь использовать не тот язык.
Джорджио
2
«Синтаксис ссылки на метод не будет работать на методах, возвращающих void» - как это так? Я передаю , 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методом .

Заключение

Прежде всего, сравните со ссылкой на метод, использование лямбда-выражения вызовет только еще один вызов метода для генерирующего метода из лямбды.

JasonMing
источник
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) или неограниченные экземпляры метода instance ( Integer::intValue) являются «простейшими» или наиболее «удобными», в том смысле, что они могут оптимально обрабатываться метафорическим вариантом «быстрого пути» без десагеринга . Это преимущество полезно указать в метафакторных вариантах TLE:

Устраняя аргументы там, где они не нужны, файлы классов становятся меньше. А опция быстрого пути понижает планку для виртуальной машины для внедрения операции лямбда-преобразования, позволяя рассматривать ее как операцию «упаковки» и облегчая оптимизацию распаковки.

Естественно, ссылка на метод захвата экземпляра ( obj::myMethod) должна предоставлять ограниченный экземпляр в качестве аргумента для дескриптора метода для вызова, что может означать необходимость десагеринга с использованием методов «моста».

Заключение

Я не совсем уверен, о какой лямбда-оболочке вы намекаете, но, хотя конечный результат использования пользовательских лямбда-ссылок или ссылок на методы одинаков, способ, которым достигается, кажется, совсем другим, и может быть другим в будущем, если это не так сейчас. Следовательно, я полагаю, что более вероятно, чем нет, что метаданные могут обрабатывать ссылки на методы более оптимальным образом.

ХИК
источник
1
Это наиболее актуальный ответ, поскольку он затрагивает внутренние механизмы, необходимые для создания лямбды. Как человек, который фактически отладил процесс создания лямбды (чтобы выяснить, как генерировать стабильные идентификаторы для данной ссылки лямбда / метода, чтобы их можно было удалить из карт), я обнаружил, что довольно удивительно, как много работы сделано для создания экземпляр лямбды. Oracle / OpenJDK используют ASM для динамической генерации классов, которые при необходимости закрываются над любой переменной, указанной в вашей лямбде (или квалификатором экземпляра ссылки на метод) ...
Ajax
1
... В дополнение к закрытию переменных, в частности, лямбда-выражения также создают статический метод, который внедряется в декларирующий класс, чтобы у созданной лямбда-функции было что-то, что нужно вызвать для сопоставления с функциями, которые вы хотите вызвать. Ссылка на метод с квалификатором экземпляра примет этот экземпляр в качестве параметра этого метода; Я не проверял, пропускает ли ссылка this :: method в закрытом классе это закрытие, но я проверял, что статические методы фактически пропускают промежуточный метод (и просто создают объект, соответствующий цели вызова на позвоните на сайт).
Аякс
1
Предположительно, закрытому методу также не потребуется виртуальная диспетчеризация, тогда как все более общедоступное необходимо будет закрыть над экземпляром (с помощью сгенерированного статического метода), чтобы он мог вызывать виртуальный экземпляр в реальном экземпляре, чтобы убедиться, что он замечает переопределения. TL; DR: ссылки на методы закрываются при меньшем количестве аргументов, поэтому они меньше просачиваются и требуют меньше динамической генерации кода.
Аякс
3
Последний спам-комментарий ... Динамическое поколение классов довольно тяжеловесно. Возможно, это все же лучше, чем загрузка анонимного класса в загрузчик классов (поскольку самые тяжелые части выполняются только один раз), но вы бы очень удивились, насколько много работы уходит на создание лямбда-экземпляра. Было бы интересно увидеть реальные тесты лямбда-выражений по сравнению с методами и анонимными классами. Обратите внимание, что две лямбды или ссылки на методы, которые ссылаются на одни и те же вещи, но в разных местах, создают разные классы во время выполнения (даже внутри одного и того же метода, сразу друг за другом).
Аякс
@Ajax А как насчет этого? blog.soat.fr/2015/12/benchmark-java-lambda-vs-classe-anonyme .
Вальфрат
0

При использовании лямбда-выражений есть одно довольно серьезное последствие, которое может повлиять на производительность.

Когда вы объявляете лямбда-выражение, вы создаете замыкание поверх локальной области видимости.

Что это значит и как это влияет на производительность?

Ну, я рад, что ты спросил. Это означает, что каждое из этих маленьких лямбда-выражений является небольшим анонимным внутренним классом, и это означает, что оно содержит ссылку на все переменные, которые находятся в той же области видимости лямбда-выражения.

Это также означает thisссылку на экземпляр объекта и все его поля. В зависимости от времени фактического вызова лямбды, это может привести к довольно значительной утечке ресурсов, поскольку сборщик мусора не может отпустить эти ссылки, пока объект, удерживающий лямбду, еще жив ...

Роланд Тепп
источник
То же самое относится и к нестатическим ссылкам на методы.
Аякс
12
Java лямбды не являются строго замыканиями. И при этом они не анонимные внутренние классы. Они НЕ содержат ссылку на все переменные в области видимости, где они объявлены. Они ТОЛЬКО несут ссылку на переменные, на которые они фактически ссылаются. Это также относится к thisобъекту, на который можно сослаться. Следовательно, лямбда не пропускает ресурсы, просто имея для них область видимости; он держится только за те объекты, которые ему нужны. С другой стороны, анонимный внутренний класс может пропускать ресурсы, но не всегда делает это. См. Этот код для примера: a.blmq.us/2mmrL6v
squid314
Этот ответ просто неправильный
Стефан Райх
Спасибо @StefanReich, это было очень полезно!
Роланд Тепп