1. Introducción
En este tutorial veremos una API importante introducida en Java 7 y extendida en nuevas versiones, java.lang.invoke.MethodHandles .
Aprenderemos qué son los identificadores de métodos, cómo crearlos y usarlos.
2. ¿Qué son los identificadores de método?
En la documentación de la API, el identificador del método tiene esta definición:
El identificador de método es una referencia ejecutable escrita al método base, constructor, campo u otra operación de bajo nivel con transformaciones adicionales de argumentos o valores de retorno.
En otras palabras, los identificadores de métodos son un mecanismo de bajo nivel para buscar, adaptar y llamar a métodos. Los identificadores de método son inmutables y no tienen estado de visualización.
Para crear y usar MethodHandle
, debe realizar 4 pasos:
- Crear un descriptor de búsqueda - búsqueda
- Declarar tipo de método
- Manejar método de búsqueda
- Manejar método de llamada
2.1. Método Manijas vs Reflexión
Los manejadores de métodos se introdujeron para funcionar junto con la API java.lang.reflect , como Se crean para diferentes propósitos y difieren en sus características.
En términos de rendimiento, la API MethodHandles puede ser mucho más rápida que la API Reflection, porque las comprobaciones de acceso se realizan en el momento de la creación en lugar de la ejecución . Si hay un administrador de seguridad, esta diferencia aumenta, porque La búsqueda de clases y la obtención de sus elementos están sujetas a controles adicionales.
Sin embargo, el rendimiento no es el único indicador de la optimización de una tarea, debe tenerse en cuenta que la API MethodHandles es más difícil de usar debido a la falta de mecanismos como la obtención de métodos de clase, la verificación de tokens de acceso, etc.
A pesar de esto, la API MethodHandles permite métodos de curry, cambiando el tipo y el orden de los parámetros.
Ahora, conociendo la definición y el propósito de la API MethodHandles, podemos trabajar con ellos. Comencemos buscando métodos.
3. Crear búsqueda
Lo primero que debemos hacer cuando queremos crear un identificador de método es obtener una búsqueda, el objeto de fábrica responsable de crear identificadores de método para métodos, constructores y campos visibles para la clase de búsqueda.
Con la API MethodHandles, puede crear un objeto de búsqueda con diferentes modos de acceso.
Cree una búsqueda que proporcione acceso a métodos públicos:
MethodHandles.Lookup publicLookup = MethodHandles.publicLookup();
Sin embargo, si necesitamos acceso a los métodos privados y protegidos, podemos usar el método de búsqueda () en su lugar:
MethodHandles.Lookup lookup = MethodHandles.lookup();
4. Crear un MethodType
Para crear un MethodHandle, el objeto de búsqueda debe establecerse en un tipo, y esto se puede hacer usando la clase MethodType.
En particular, un MethodType representa los argumentos y el tipo de retorno aceptado y devuelto por el identificador del método, o pasado y esperado por el código de llamada.
La estructura MethodType es simple, está formada por el tipo de retorno, junto con el número correspondiente de tipos de parámetros, que deben correlacionarse completamente entre el identificador del método y el código de llamada.
Al igual que MethodHandle, todas las instancias de MethodType son inmutables.
Veamos cómo definir un MethodType que define la clase java.util.List
como el tipo de retorno y la matriz Object como el tipo de entrada de datos:
MethodType mt = MethodType.methodType(List.class, Object[].class);
En caso de que el método devuelva un tipo de valor simple o nulo, usamos una clase que representa estos tipos (void.class, int.class …)
.
Defina un MethodType que devuelva un int y acepte un Object:
MethodType mt = MethodType.methodType(int.class, Object.class);
Puede comenzar a crear MethodHandle.
5. Método de búsqueda Handle
Después de establecer el tipo del método, para crear un MethodHandle, debe encontrarlo utilizando el objeto de búsqueda o publicLookup, que también devuelve la clase de origen y el nombre del método.
La búsqueda proporciona un conjunto de métodos que le permite encontrar el identificador del método de manera óptima, teniendo en cuenta el alcance del método. Considere los enfoques básicos, comenzando por el más simple.
5.1. Método Manija para métodos
Usando el método findVirtual()
, puede crear un MethodHandle para un método de instancia. Créelo basado en el método concat()
de la clase String
:
MethodType mt = MethodType.methodType(String.class, String.class); MethodHandle concatMH = publicLookup.findVirtual(String.class, "concat", mt);
5.2. Método de manejo para métodos estáticos
Para acceder al método estático, puede usar el método findStatic()
:
MethodType mt = MethodType.methodType(List.class, Object[].class); MethodHandle asListMH = publicLookup.findStatic(Arrays.class, "asList", mt);
En este caso, creamos un identificador de método para un método que convierte una matriz de tipo Object
en una List
.
5.3. Método Handle para constructores
Puede acceder al constructor utilizando el método findConstructor()
.
Cree un identificador de método con un comportamiento similar al constructor de la clase Integer con el parámetro String:
MethodType mt = MethodType.methodType(void.class, String.class); MethodHandle newIntegerMH = publicLookup.findConstructor(Integer.class, mt);
5.4. Método Handle para campos
Usando el identificador de método, también puede acceder a los campos.
Comencemos definiendo la clase Libro:
public class Book { String id; String title;
Como condición inicial, tenemos visibilidad directa entre el identificador de método y la propiedad declarada, por lo que podemos crear un identificador de método con un comportamiento similar al de un método get:
MethodHandle getTitleMH = lookup.findGetter(Book.class, "title", String.class);
Para obtener más información sobre la gestión de variables / campos, consulte el artículo de Java 9 Variable Handles Demystified , donde hablamos sobre la API java.lang.invoke.VarHandle introducida en Java 9.
5.5. Manejador de métodos para métodos privados
Puede crear un identificador de método para un método de tipo privado utilizando la API java.lang.reflect .
Comencemos creando un método privado para la clase Book:
private String formatBook() { return id + " > " + title; }
Ahora podemos crear un identificador de método con el comportamiento del método formatBook()
:
Method formatBookMethod = Book.class.getDeclaredMethod("formatBook"); formatBookMethod.setAccessible(true); MethodHandle formatBookMH = lookup.unreflect(formatBookMethod);
6. Manejar método de llamada
Una vez que hayamos creado nuestro identificador de método, continúe con el siguiente paso. La clase MethodHandle nos ofrece 3 formas diferentes de llamar al identificador del método: invoke()
, invokeWithArugments()
y invokeExact()
.
Comencemos con el método de invoke
.
6.1. Manejar método de llamada
Cuando se utiliza el método invoke()
, el número de argumentos (arity) es fijo, pero es posible realizar la conversión de tipos y empaquetar / desempaquetar los argumentos y tipos del valor de retorno.
Ahora veamos cómo invoke()
se puede usar con un argumento empaquetado:
MethodType mt = MethodType.methodType(String.class, char.class, char.class); MethodHandle replaceMH = publicLookup.findVirtual(String.class, "replace", mt); String output = (String) replaceMH.invoke("jovo", Character.valueOf('o'), 'a'); assertEquals("java", output);
En este caso, replaceMH
requiere argumentos char
, pero el método replaceMH
invoke()
descomprime el argumento Character antes de que se ejecute.
6.2. Llamar con argumentos
Invocar el método de manejo usando invokeWithArguments
tiene la menor restricción.
De hecho, además de verificar tipos y empaquetar / desempacar argumentos y valores de retorno, le permite hacer llamadas con un número variable de parámetros.
En la práctica, podemos crear una lista de enteros con una matriz de valores int de longitud desconocida:
MethodType mt = MethodType.methodType(List.class, Object[].class); MethodHandle asList = publicLookup.findStatic(Arrays.class, "asList", mt); List<Integer> list = (List<Integer>) asList.invokeWithArguments(1, 2); assertThat(Arrays.asList(1,2), is(list));
6.3. Llamar exacto
Si necesitamos que el identificador del método se ejecute de manera más restrictiva (por el conjunto de argumentos y su tipo), usamos el método invokeExact()
.
De hecho, no proporciona la capacidad de emitir tipos de una clase y requiere un conjunto fijo de argumentos.
Veamos cómo podemos agregar dos valores int usando el método handle:
MethodType mt = MethodType.methodType(int.class, int.class, int.class); MethodHandle sumMH = lookup.findStatic(Integer.class, "sum", mt); int sum = (int) sumMH.invokeExact(1, 11); assertEquals(12, sum);
En este caso, si pasamos un número que no es un int al método invokeExact
, cuando llamamos, obtenemos una WrongMethodTypeException
.
7. Trabajar con matrices
MethodHandles puede funcionar no solo con campos y objetos, sino también con matrices. Con la API asSpreader()
, puede crear un identificador de método que admita matrices como argumentos posicionales.
En este caso, el manejador del método toma una matriz, distribuyendo sus elementos como argumentos posicionales y, opcionalmente, la longitud de la matriz.
Veamos cómo obtener el identificador de método para verificar si los argumentos de la matriz son la misma cadena:
MethodType mt = MethodType.methodType(boolean.class, Object.class); MethodHandle equals = publicLookup.findVirtual(String.class, "equals", mt); MethodHandle methodHandle = equals.asSpreader(Object[].class, 2); assertTrue((boolean) methodHandle.invoke(new Object[] { "java", "java" }));
8. Aclaración del método de manejo
Una vez que se establece el identificador del método, puede refinarlo vinculando el argumento, sin llamar al método.
Por ejemplo, en Java 9, este truco se usa para optimizar la concatenación de cadenas.
Veamos cómo se puede hacer la concatenación adjuntando el sufijo a concatMH
:
MethodType mt = MethodType.methodType(String.class, String.class); MethodHandle concatMH = publicLookup.findVirtual(String.class, "concat", mt); MethodHandle bindedConcatMH = concatMH.bindTo("Hello "); assertEquals("Hello World!", bindedConcatMH.invoke("World!"));
9. Actualizaciones de Java 9
Java 9 introdujo varios cambios en la API MethodHandles para que sea más fácil de usar.
Las actualizaciones se refieren a 3 aspectos principales:
- Funciones de búsqueda : permiten buscar desde diferentes contextos y admiten métodos no abstractos en las interfaces.
- Operaciones con argumentos : mejora la funcionalidad de plegar, recopilar y distribuir argumentos.
- Combinaciones adicionales son agregar operaciones de bucle (
loop
, whileLoop
, doWhileLoop
, ...) y mejorar la gestión de excepciones con tryFinally
.
Estos cambios implicaron otras innovaciones útiles:
- Optimización mejorada del compilador JVM
- Instancia disminuida
- Concretización del uso de MethodHandles API
Una lista más detallada de cambios está disponible en la API de Javadoc MethodHandles .
10. Conclusión
En este artículo, nos reunimos con la API MethodHandles, y también aprendimos qué son los Manejadores de métodos y cómo usarlos.
También describimos cómo se asocia con la API Reflection. Dado que llamar a manejadores de métodos es una operación de bajo nivel, su uso se justifica solo si se ajustan exactamente a sus tareas.
Como de costumbre, todo el código fuente del artículo está disponible en Github .