¿Existe algún beneficio de rendimiento al usar la sintaxis de referencia del método en lugar de la sintaxis lambda en Java 8?

56

¿Las referencias a métodos omiten la sobrecarga del contenedor lambda? ¿Podrían ellos en el futuro?

De acuerdo con el Tutorial de Java sobre referencias de métodos :

A veces ... una expresión lambda no hace más que llamar a un método existente. En esos casos, a menudo es más claro referirse al método existente por su nombre. Las referencias de métodos le permiten hacer esto; son expresiones lambda compactas y fáciles de leer para métodos que ya tienen un nombre.

Prefiero la sintaxis lambda a la sintaxis de referencia del método por varias razones:

Las lambdas son más claras

A pesar de las afirmaciones de Oracle, encuentro que la sintaxis lambda es más fácil de leer que la referencia breve al método de objeto porque la sintaxis de referencia del método es ambigua:

Bar::foo

¿Está llamando a un método estático de un argumento en la clase de x y pasándole x?

x -> Bar.foo(x)

¿O estás llamando a un método de instancia de argumento cero en x?

x -> x.foo()

La sintaxis de referencia del método podría sustituir a cualquiera de los dos. Oculta lo que su código está haciendo realmente.

Las lambdas son más seguras

Si hace referencia a Bar :: foo como un método de clase y luego Bar agrega un método de instancia con el mismo nombre (o viceversa), su código ya no se compilará.

Puedes usar lambdas constantemente

Puede ajustar cualquier función en un lambda, por lo que puede usar la misma sintaxis de manera consistente en todas partes. La sintaxis de referencia del método no funcionará en los métodos que toman o devuelven matrices primitivas, lanzan excepciones verificadas o tienen el mismo nombre de método utilizado como una instancia y un método estático (porque la sintaxis de referencia del método es ambigua sobre qué método se llamaría) . No funcionan cuando ha sobrecargado los métodos con el mismo número de argumentos, pero no debe hacerlo de todos modos (consulte el artículo 41 de Josh Bloch), por lo que no podemos compararlo con las referencias de métodos.

Conclusión

Si no hay una penalización de rendimiento por hacerlo, estoy tentado a desactivar la advertencia en mi IDE y usar la sintaxis lambda de manera consistente sin rociar la referencia de método ocasional en mi código.

PD

Ni aquí ni allá, pero en mis sueños, las referencias a métodos de objetos se parecen más a esto y aplican invoke-dynamic contra el método directamente en el objeto sin un contenedor lambda:

_.foo()
GlenPeterson
fuente
2
Esto no responde a su pregunta, pero hay otras razones para usar cierres. En mi opinión, muchos de ellos superan con creces la pequeña sobrecarga de una llamada de función adicional.
99
Creo que te preocupa el rendimiento prematuramente. En mi opinión, el rendimiento debería ser una consideración secundaria para usar una referencia de método; se trata de expresar tu intención. Si necesita pasar una función a alguna parte, ¿por qué no simplemente pasar la función en lugar de alguna otra función que le delegue? Me parece extraño como si no compraras que las funciones son cosas de primera clase, necesitan un acompañante anónimo para moverlas. Pero para abordar su pregunta más directamente, no me sorprendería si una referencia de método se puede implementar como una llamada estática.
Doval
77
.map(_.acceptValue()): Si no me equivoco, esto se parece mucho a la sintaxis de Scala. Tal vez solo estás tratando de usar el lenguaje incorrecto.
Giorgio
2
"La sintaxis de referencia del método no funcionará en los métodos que devuelven nulo" - ¿Cómo es eso? Estoy pasando System.out::printlna forEach()todo el tiempo ...?
Lukas Eder
3
"La sintaxis de referencia del método no funcionará en los métodos que toman o devuelven matrices primitivas, arrojan excepciones comprobadas ..." Eso no es correcto. Puede utilizar referencias de métodos y expresiones lambda para tales casos, siempre que la interfaz funcional en cuestión lo permita.
Stuart Marks el

Respuestas:

12

En muchos escenarios, creo que lambda y referencia de método son equivalentes. Pero la lambda ajustará el objetivo de invocación por el tipo de interfaz declarante.

Por ejemplo

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);
    }
}

Verá la salida de la consola del stacktrace.

En lambda(), el método que llama target()es lambda$lambda$0(InvokeTest.java:20), que tiene información de línea rastreable. Obviamente, esa es la lambda que escribes, el compilador genera un método anónimo para ti. Y luego, la persona que llama del método lambda es algo así como InvokeTest$$Lambda$2/1617791695.run(Unknown Source), es la invokedynamicllamada en JVM, significa que la llamada está vinculada al método generado .

En methodReference(), la llamada al método target()es directamente InvokeTest$$Lambda$1/758529971.run(Unknown Source), significa que la llamada está directamente vinculada al InvokeTest::targetmétodo .

Conclusión

Sobre todo, compárelo con referencia de método, el uso de la expresión lambda solo causará una llamada más al método de generación desde lambda.

JasonMing
fuente
1
La respuesta a continuación sobre la metafactory es un poco mejor. Agregué algunos comentarios útiles que son relevantes para él (es decir, cómo las implementaciones como Oracle / OpenJDK usan ASM para generar los objetos de clase de contenedor necesarios para crear instancias de manejo de lambda / método.
Ajax
29

Se trata de la metafactory

Primero, la mayoría de las referencias de métodos no necesitan ser desvanecidas por la metafábrica lambda, simplemente se usan como método de referencia. En la sección "Azúcar corporal Lambda" del artículo de la Traducción de las expresiones Lambda ("TLE"):

En igualdad de condiciones, los métodos privados son preferibles a los no privados, los métodos estáticos preferibles a los métodos de instancia, es mejor si los cuerpos lambda se insertan en la clase más interna en la que aparece la expresión lambda, las firmas deben coincidir con la firma del cuerpo de la lambda, extra los argumentos deben anteponerse al frente de la lista de argumentos para los valores capturados, y no desaprenderían en absoluto las referencias de métodos. Sin embargo, hay casos excepcionales en los que es posible que tengamos que desviarnos de esta estrategia de referencia.

Esto se resalta aún más abajo en "The Lambda Metafactory" de 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

El implargumento identifica el método lambda, ya sea un cuerpo lambda desugared o el método nombrado en una referencia de método.

Las referencias de un Integer::summétodo de instancia estático ( ) o sin límite ( Integer::intValue) son las 'más simples' o las más 'convenientes', en el sentido de que pueden ser manejadas de manera óptima por una variante metafábrica de 'vía rápida' sin el desvanecimiento . Esta ventaja se señala de manera útil en las "variantes metafactorias" de TLE:

Al eliminar argumentos donde no son necesarios, los archivos de clase se vuelven más pequeños. Y la opción de ruta rápida baja la barra para que la VM intrinsifique la operación de conversión lambda, lo que le permite ser tratada como una operación de "boxeo" y facilita las optimizaciones de unbox.

Naturalmente, una referencia de método de captura de instancias ( obj::myMethod) necesita proporcionar la instancia acotada como un argumento para el identificador del método para invocación, lo que puede significar la necesidad de desugar el uso de métodos 'puente'.

Conclusión

No estoy exactamente seguro de cuál es el 'envoltorio' lambda al que estás insinuando, pero a pesar de que el resultado final de usar tus lambdas o referencias de método definidas por el usuario es el mismo, la forma en que se alcanza parece ser bastante diferente, y puede ser diferente en el futuro si ese no es el caso ahora. Por lo tanto, supongo que es más probable que las referencias a métodos puedan ser manejadas de manera más óptima por la metafábrica.

hjk
fuente
1
Esta es la respuesta más relevante, ya que en realidad toca la maquinaria interna necesaria para crear una lambda. Como alguien que realmente depuró el proceso de creación de lambda (para descubrir cómo generar identificadores estables para una referencia de lambda / método dada para que puedan eliminarse de los mapas), me pareció bastante sorprendente cuánto trabajo se hace para crear un instancia de una lambda. Oracle / OpenJDK usa ASM para generar dinámicamente clases que, si es necesario, se cierran sobre cualquier variable referenciada en su lambda (o el calificador de instancia de una referencia de método) ...
Ajax
1
... Además de cerrar sobre variables, las lambdas en particular también crean un método estático que se inyecta en la clase de declaración para que la lambda creada tenga algo que llamar para asignar a las funciones que desea invocar. Una referencia de método con un calificador de instancia tomará esa instancia como parámetro de este método; No he probado si una referencia de este método :: en una clase privada omite este cierre, pero he probado que los métodos estáticos, de hecho, omiten el método intermedio (y simplemente crean un objeto para ajustarse al objetivo de invocación en el llamar al sitio).
Ajax
1
Presumiblemente, un método privado tampoco necesitará un despacho virtual, mientras que cualquier cosa más pública tendrá que cerrarse sobre la instancia (a través de un método estático generado) para que pueda invocar virtual en la instancia real para asegurarse de que detecte anulaciones. TL; DR: las referencias de métodos se cierran sobre menos argumentos, por lo que se filtran menos y requieren menos generación de código dinámico.
Ajax
3
Último comentario spam ... La generación de clase dinámica es bastante pesada. Probablemente sea mejor que cargar una clase anónima en el cargador de clases (ya que las partes más pesadas solo se hacen una vez), pero se sorprendería de cuánto trabajo se necesita para crear una instancia lambda. Sería interesante ver puntos de referencia reales de lambdas versus referencias de métodos versus clases anónimas. Tenga en cuenta que dos lambdas o referencias de métodos que hacen referencia a las mismas cosas, pero en diferentes lugares crean diferentes clases en tiempo de ejecución (incluso dentro del mismo método, uno detrás del otro).
Ajax
@Ajax ¿Qué pasa con este? blog.soat.fr/2015/12/benchmark-java-lambda-vs-classe-anonyme .
Walfrat el
0

Hay una consecuencia bastante grave cuando se usan expresiones lambda que pueden afectar el rendimiento.

Cuando declara una expresión lambda, está creando un cierre sobre el ámbito local.

¿Qué significa esto y cómo afecta el rendimiento?

Bueno, me alegra que lo hayas preguntado. Significa que cada una de esas pequeñas expresiones lambda es una pequeña clase interna anónima y eso significa que lleva consigo una referencia a todas las variables que están en el mismo alcance de la expresión lambda.

Esto significa también thisreferencia de la instancia del objeto y todos sus campos. Dependiendo del momento de la invocación real de la lambda, esto puede equivaler a una pérdida de recursos bastante significativa ya que el recolector de basura no puede soltar estas referencias mientras el objeto que se mantiene en una lambda todavía esté vivo ...

Roland Tepp
fuente
Lo mismo ocurre con las referencias de métodos no estáticos.
Ajax
12
Las lambdas de Java no son estrictamente cierres. Tampoco son clases internas anónimas. NO llevan una referencia a todas las variables en el alcance donde se declaran. SOLO llevan una referencia a las variables a las que realmente hacen referencia. Esto también se aplica al thisobjeto al que se podría hacer referencia. Una lambda, por lo tanto, no pierde recursos simplemente por tener un alcance para ellos; solo se aferra a los objetos que necesita. Por otro lado, una clase interna anónima puede perder recursos, pero no siempre lo hace. Consulte este código para ver un ejemplo: a.blmq.us/2mmrL6v
squid314
Esa respuesta es simplemente incorrecta
Stefan Reich
Gracias @StefanReich, ¡fue muy útil!
Roland Tepp