Invocadoinámico 101

El lanzamiento de Java 7 de Oracle introdujo una nueva invokedynamicinstrucción de código de bytes para la máquina virtual Java (JVM) y un nuevo java.lang.invokepaquete de API para la biblioteca de clases estándar. Esta publicación le presenta esta instrucción y API.

El qué y el cómo de invocado dinámico

P: ¿Qué es invokedynamic?

A: invokedynamic es una instrucción de código de bytes que facilita la implementación de lenguajes dinámicos (para la JVM) mediante la invocación de métodos dinámicos. Esta instrucción se describe en la edición Java SE 7 de la especificación JVM.

Lenguajes dinámicos y estáticos

Un lenguaje dinámico (también conocido como lenguaje de tipado dinámico ) es un lenguaje de programación de alto nivel cuya verificación de tipo se realiza generalmente en tiempo de ejecución, una característica conocida como tipado dinámico . La verificación de tipos verifica que un programa sea seguro para los tipos : todos los argumentos de la operación tienen el tipo correcto. Groovy, Ruby y JavaScript son ejemplos de lenguajes dinámicos. (La @groovy.transform.TypeCheckedanotación hace que Groovy escriba check en el momento de la compilación).

Por el contrario, un lenguaje estático (también conocido como lenguaje de tipado estático ) realiza la verificación de tipos en tiempo de compilación, una característica conocida como tipado estático . El compilador verifica que un programa sea de tipo correcto, aunque puede diferir alguna verificación de tipo al tiempo de ejecución (piense en las conversiones y la checkcastinstrucción). Java es un ejemplo de lenguaje estático. El compilador de Java utiliza este tipo de información para producir código de bytes fuertemente tipado, que la JVM puede ejecutar de manera eficiente.

P: ¿Cómo se invokedynamicfacilita la implementación del lenguaje dinámico?

R: En un lenguaje dinámico, la verificación de tipos generalmente ocurre en tiempo de ejecución. Los desarrolladores deben aprobar los tipos adecuados o arriesgarse a fallas en el tiempo de ejecución. A menudo es el caso que java.lang.Objectes el tipo más preciso para un argumento de método. Esta situación complica la verificación de tipos, lo que afecta el rendimiento.

Otro desafío es que los lenguajes dinámicos generalmente ofrecen la capacidad de agregar campos / métodos y eliminarlos de las clases existentes. Como resultado, es necesario diferir la resolución de clase, método y campo al tiempo de ejecución. Además, a menudo es necesario adaptar la invocación de un método a un objetivo que tiene una firma diferente.

Estos desafíos han requerido tradicionalmente que el soporte de tiempo de ejecución ad hoc se construya sobre la JVM. Este soporte incluye clases de tipo contenedor, el uso de tablas hash para proporcionar una resolución dinámica de símbolos, etc. El código de bytes se genera con puntos de entrada al tiempo de ejecución en forma de llamadas a métodos utilizando cualquiera de las cuatro instrucciones de invocación de métodos:

  • invokestaticse utiliza para invocar staticmétodos.
  • invokevirtualse utiliza para invocar publicy protectedno staticmétodos a través del envío dinámico.
  • invokeinterfacees similar a invokevirtualexcepto que el método de envío se basa en un tipo de interfaz.
  • invokespecialse utiliza para invocar métodos de inicialización de instancias (constructores), así como privatemétodos y métodos de una superclase de la clase actual.

Este soporte de tiempo de ejecución afecta el rendimiento. El código de bytes generado a menudo requiere varias invocaciones de métodos JVM reales para una invocación de método de lenguaje dinámico. La reflexión se utiliza ampliamente y contribuye a la degradación del rendimiento. Además, las muchas rutas de ejecución diferentes hacen imposible que el compilador Just-In-Time (JIT) de la JVM aplique optimizaciones.

Para abordar el rendimiento deficiente, la invokedynamicinstrucción elimina el soporte de tiempo de ejecución ad hoc. En cambio, la primera llamada se inicia mediante la invocación de la lógica de tiempo de ejecución que selecciona de manera eficiente un método de destino, y las llamadas posteriores normalmente invocan el método de destino sin tener que reiniciar.

invokedynamictambién beneficia a los implementadores de lenguaje dinámico al admitir objetivos de sitios de llamadas que cambian dinámicamente: un sitio de llamadas , más específicamente, un sitio de llamadas dinámico es una invokedynamicinstrucción. Además, debido a que la JVM admite internamente invokedynamic, el compilador JIT puede optimizar mejor esta instrucción.

Controles de método

P: Entiendo que invokedynamicfunciona con identificadores de métodos para facilitar la invocación dinámica de métodos. ¿Qué es un identificador de método?

R: Un identificador de método es "una referencia directamente ejecutable con tipo a un método subyacente, constructor, campo u operación similar de bajo nivel, con transformaciones opcionales de argumentos o valores de retorno". En otras palabras, es similar a un puntero de función de estilo C que apunta a un código ejecutable, un objetivo , y que puede desreferenciarse para invocar este código. Los identificadores de método son descritos por la java.lang.invoke.MethodHandleclase abstracta .

P: ¿Puede proporcionar un ejemplo sencillo de creación e invocación de un control de método?

R: Consulte el Listado 1.

Listado 1. MHD.java(versión 1)

import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; public class MHD { public static void main(String[] args) throws Throwable { MethodHandles.Lookup lookup = MethodHandles.lookup(); MethodHandle mh = lookup.findStatic(MHD.class, "hello", MethodType.methodType(void.class)); mh.invokeExact(); } static void hello() { System.out.println("hello"); } }

Listado 1 describe un programa de demostración mango método que consiste en main()y hello()métodos de la clase. El objetivo de este programa es invocar hello()mediante un identificador de método.

main()La primera tarea es obtener un java.lang.invoke.MethodHandles.Lookupobjeto. Este objeto es una fábrica para crear identificadores de métodos y se utiliza para buscar objetivos como métodos virtuales, métodos estáticos, métodos especiales, constructores y accesores de campo. Además, depende del contexto de invocación de un sitio de llamada y hace cumplir las restricciones de acceso al control de método cada vez que se crea un identificador de método. En otras palabras, un sitio de llamada (como el main()método del Listado 1 que actúa como un sitio de llamada) que obtiene un objeto de búsqueda solo puede acceder a los destinos que son accesibles al sitio de llamada. El objeto de búsqueda se obtiene invocando el método de la java.lang.invoke.MethodHandlesclase MethodHandles.Lookup lookup().

publicLookup()

MethodHandlestambién declara un MethodHandles.Lookup publicLookup()método. A diferencia de lookup(), que se puede usar para obtener un identificador de método para cualquier método / constructor o campo accesible, publicLookup()se puede usar para obtener un identificador de método para un campo de acceso público o solo para un método / constructor de acceso público.

Después de obtener el objeto de búsqueda, MethodHandle findStatic(Class refc, String name, MethodType type)se llama al método de este objeto para obtener un identificador de método para el hello()método. El primer argumento que se pasa findStatic()es una referencia a la clase ( MHD) desde la que hello()se accede al método ( ), y el segundo argumento es el nombre del método. El tercer argumento es un ejemplo de un tipo de método , que "representa los argumentos y el tipo de retorno aceptado y devuelto por un identificador de método, o los argumentos y el tipo de retorno pasados ​​y esperados por un llamador de identificador de método". Está representado por una instancia de la java.lang.invoke.MethodTypeclase y se obtiene (en este ejemplo) llamando java.lang.invoke.MethodTypeal MethodType methodType(Class rtype)método de. Este método se llama porque hello()solo proporciona un tipo de retorno, que resulta servoid. Este tipo de retorno está disponible para methodType()pasar void.classa este método.

El identificador del método devuelto está asignado a mh. Este objeto luego se usa para llamar MethodHandleal Object invokeExact(Object... args)método, para invocar el identificador del método. En otras palabras, invokeExact()resulta en hello()ser llamado y helloescrito en el flujo de salida estándar. Debido a que invokeExact()se declara lanzar Throwable, lo he agregado throws Throwableal main()encabezado del método.

P: En su respuesta anterior, mencionó que el objeto de búsqueda solo puede acceder a aquellos objetivos que son accesibles al sitio de la llamada. ¿Puede proporcionar un ejemplo que demuestre el intento de obtener un identificador de método para un objetivo inaccesible?

R: Consulte el Listado 2.

Listado 2. MHD.java(versión 2)

import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; class HW { public void hello1() { System.out.println("hello from hello1"); } private void hello2() { System.out.println("hello from hello2"); } } public class MHD { public static void main(String[] args) throws Throwable { HW hw = new HW(); MethodHandles.Lookup lookup = MethodHandles.lookup(); MethodHandle mh = lookup.findVirtual(HW.class, "hello1", MethodType.methodType(void.class)); mh.invoke(hw); mh = lookup.findVirtual(HW.class, "hello2", MethodType.methodType(void.class)); } }

El Listado 2 declara HW(Hola, Mundo) y MHDclases. HWdeclara un publichello1()método de instancia y un privatehello2()método de instancia. MHDdeclara un main()método que intentará invocar estos métodos.

main()La primera tarea es crear una instancia HWen preparación para invocar hello1()y hello2(). A continuación, obtiene un objeto de búsqueda y utiliza este objeto para obtener un identificador de método para invocar hello1(). Esta vez, MethodHandles.Lookup's findVirtual()método se llama y el primer argumento pasado a este método es un Classobjeto que describe la HWclase.

Resulta que findVirtual()tendrá éxito, y la mh.invoke(hw);expresión subsiguiente se invocará hello1(), lo que dará como resultado hello from hello1una salida.

Porque hello1()es public, es accesible para el main()sitio de llamada al método. Por el contrario, hello2()no es accesible. Como resultado, la segunda findVirtual()invocación fallará con un IllegalAccessException.

Cuando ejecuta esta aplicación, debe observar el siguiente resultado:

hello from hello1 Exception in thread "main" java.lang.IllegalAccessException: member is private: HW.hello2()void, from MHD at java.lang.invoke.MemberName.makeAccessException(MemberName.java:507) at java.lang.invoke.MethodHandles$Lookup.checkAccess(MethodHandles.java:1172) at java.lang.invoke.MethodHandles$Lookup.checkMethod(MethodHandles.java:1152) at java.lang.invoke.MethodHandles$Lookup.accessVirtual(MethodHandles.java:648) at java.lang.invoke.MethodHandles$Lookup.findVirtual(MethodHandles.java:641) at MHD.main(MHD.java:27)

P: Los listados 1 y 2 usan los métodos invokeExact()y invoke()para ejecutar un identificador de método. ¿Cuál es la diferencia entre estos métodos?

R: Aunque invokeExact()y invoke()están diseñados para ejecutar un identificador de método (en realidad, el código de destino al que se refiere el identificador de método), difieren cuando se trata de realizar conversiones de tipos en argumentos y el valor de retorno. invokeExact()no realiza conversión automática de tipo compatible en argumentos. Sus argumentos (o expresiones de argumento) deben coincidir exactamente con el tipo de la firma del método, con cada argumento proporcionado por separado, o todos los argumentos proporcionados juntos como una matriz. invoke()requiere que sus argumentos (o expresiones de argumentos) sean una coincidencia de tipo compatible con la firma del método; se realizan conversiones de tipo automáticas, con cada argumento proporcionado por separado, o todos los argumentos proporcionados juntos como una matriz.

P: ¿Puede proporcionarme un ejemplo que muestre cómo invocar el captador y definidor de un campo de instancia?

R: Consulte el Listado 3.

Listado 3. MHD.java(versión 3)

import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; class Point { int x; int y; } public class MHD { public static void main(String[] args) throws Throwable { MethodHandles.Lookup lookup = MethodHandles.lookup(); Point point = new Point(); // Set the x and y fields. MethodHandle mh = lookup.findSetter(Point.class, "x", int.class); mh.invoke(point, 15); mh = lookup.findSetter(Point.class, "y", int.class); mh.invoke(point, 30); mh = lookup.findGetter(Point.class, "x", int.class); int x = (int) mh.invoke(point); System.out.printf("x = %d%n", x); mh = lookup.findGetter(Point.class, "y", int.class); int y = (int) mh.invoke(point); System.out.printf("y = %d%n", y); } }

El Listado 3 presenta una Pointclase con un par de campos de instancia enteros de 32 bits denominados xy y. Colocador de cada campo y getter se accede llamando MethodHandles.Lookup's findSetter()y findGetter()métodos, y el resultante MethodHandlese devuelve. Cada uno de findSetter()y findGetter()requiere un Classargumento que identifica la clase del campo, el nombre del campo y un Classobjeto que identifica la firma del campo.

El invoke()método se usa para ejecutar un setter o getter: detrás de escena, se accede a los campos de instancia a través de las JVM putfieldy las getfieldinstrucciones. Este método requiere que se pase como argumento inicial una referencia al objeto cuyo campo se está accediendo. Para las invocaciones de establecedores, también se debe pasar un segundo argumento, que consiste en el valor que se asigna al campo.

Cuando ejecuta esta aplicación, debe observar el siguiente resultado:

x = 15 y = 30

P: Su definición de identificador de método incluye la frase "con transformaciones opcionales de argumentos o valores de retorno". ¿Puede dar un ejemplo de transformación de argumentos?

R: He creado un ejemplo basado en el método de Mathclase de la double pow(double a, double b)clase. En este ejemplo, obtengo un identificador de método para el pow()método y transformo este identificador de método para que el segundo argumento pasado pow()sea ​​siempre 10. Consulte el Listado 4.