Inferencia de tipo de reflexión en Java 8 Lambdas

Estaba experimentando con los nuevos Lambdas en Java 8, y estoy buscando una forma de utilizar la reflexión en las clases lambda para obtener el tipo de retorno de una función lambda. Estoy especialmente interesado en casos donde la lambda implementa una superinterfaz genérica. En el siguiente ejemplo de código, MapFunction es la superinterfaz genérica, y estoy buscando una forma de averiguar qué tipo se une al parámetro genérico T

Si bien Java descarta una gran cantidad de información de tipo genérico después del comstackdor, las subclases (y las subclases anónimas) de superclases genéricas y superinterfaces genéricas conservaron esa información de tipo. A través de la reflexión, estos tipos fueron accesibles. En el ejemplo siguiente (caso 1) , la reflexión me dice que la implementación de MapFunction de MapFunction vincula java.lang.Integer al parámetro de tipo genérico T

Incluso para las subclases que son genéricas, existen ciertos medios para descubrir qué se une a un parámetro genérico, si se conocen otros. Considere el caso 2 en el ejemplo a continuación, IdentityMapper donde tanto F como T unen al mismo tipo. Cuando lo sabemos, conocemos el tipo F si conocemos el tipo de parámetro T (que en mi caso lo hacemos).

La pregunta es ahora, ¿cómo puedo darme cuenta de algo similar para Java 8 lambdas? Como en realidad no son subclases regulares de la superinterfaz genérica, el método descrito anteriormente no funciona. Específicamente, ¿puedo deducir que parseLambda vincula java.lang.Integer a T , y la identityLambda vincula lo mismo a F y T ?

PD: En teoría, debería ser posible descomstackr el código lambda y luego usar un comstackdor integrado (como el JDT) y tocar su tipo de inferencia. Espero que haya una manera más simple de hacer esto 😉

 /** * The superinterface. */ public interface MapFunction { T map(F value); } /** * Case 1: A non-generic subclass. */ public class MyMapper implements MapFunction { public Integer map(String value) { return Integer.valueOf(value); } } /** * A generic subclass */ public class IdentityMapper implements MapFunction { public E map(E value) { return value; } } /** * Instantiation through lambda */ public MapFunction parseLambda = (String str) -> { return Integer.valueOf(str); } public MapFunction identityLambda = (value) -> { return value; } public static void main(String[] args) { // case 1 getReturnType(MyMapper.class); // -> returns java.lang.Integer // case 2 getReturnTypeRelativeToParameter(IdentityMapper.class, String.class); // -> returns java.lang.String } private static Class getReturnType(Class implementingClass) { Type superType = implementingClass.getGenericInterfaces()[0]; if (superType instanceof ParameterizedType) { ParameterizedType parameterizedType = (ParameterizedType) superType; return (Class) parameterizedType.getActualTypeArguments()[1]; } else return null; } private static Class getReturnTypeRelativeToParameter(Class implementingClass, Class parameterType) { Type superType = implementingClass.getGenericInterfaces()[0]; if (superType instanceof ParameterizedType) { ParameterizedType parameterizedType = (ParameterizedType) superType; TypeVariable inputType = (TypeVariable) parameterizedType.getActualTypeArguments()[0]; TypeVariable returnType = (TypeVariable) parameterizedType.getActualTypeArguments()[1]; if (inputType.getName().equals(returnType.getName())) { return parameterType; } else { // some logic that figures out composed return types } } return null; } 

La decisión exacta de cómo asignar el código lambda a las implementaciones de interfaz se deja en el entorno de tiempo de ejecución real. En principio, todas las lambdas que implementan la misma interfaz sin MethodHandleProxies podrían compartir una única clase de tiempo de ejecución como MethodHandleProxies hace MethodHandleProxies . El uso de diferentes clases para lambdas específicos es una optimización realizada por la implementación real de LambdaMetafactory , pero no como una función destinada a ayudar a la depuración o Reflection.

Entonces, incluso si encuentra información más detallada en la clase de tiempo de ejecución real de una implementación de interfaz lambda, será un artefacto del entorno de tiempo de ejecución actualmente utilizado que podría no estar disponible en una implementación diferente o incluso en otras versiones de su entorno actual.

Si el lambda es Serializable , puede usar el hecho de que el formulario serializado contiene la firma del método del tipo de interfaz instanciada para confundir los valores reales de las variables de tipo.

Esto es posible de resolver actualmente, pero solo de una manera bastante hackie, pero permítanme primero explicar algunas cosas:

Cuando escribe un lambda, el comstackdor inserta una instrucción de invocación dinámica que apunta a la LambdaMetafactory y un método privado de síntesis estática con el cuerpo de la lambda. El método sintético y el método manejado en el conjunto constante contienen el tipo genérico (si el lambda usa el tipo o es explícito como en los ejemplos).

Ahora en el tiempo de ejecución se llama al LambdaMetaFactory y se genera una clase utilizando ASM que implementa la interfaz funcional y el cuerpo del método llama al método privado estático con cualquier argumento pasado. Luego se inyecta en la clase original utilizando Unsafe.defineAnonymousClass (consulte la publicación de John Rose ) para que pueda acceder a los miembros privados, etc.

Lamentablemente, la clase generada no almacena las firmas genéricas (podría) por lo que no puede usar los métodos de reflexión habituales que le permiten borrar el borrado

Para una clase normal puede inspeccionar el bytecode usando Class.getResource(ClassName + ".class") pero para clases anónimas definidas usando Unsafe no tiene suerte. Sin embargo, puede hacer que LambdaMetaFactory los LambdaMetaFactory con el argumento de JVM:

 java -Djdk.internal.lambda.dumpProxyClasses=/some/folder 

Al mirar el archivo de clase volcado (usando javap -p -s -v ), uno puede ver que efectivamente llama al método estático. Pero el problema sigue siendo cómo obtener el bytecode desde el propio Java.

Desafortunadamente, este es el lugar donde se piratea:

Usando la reflexión podemos llamar a Class.getConstantPool y luego acceder a MethodRefInfo para obtener los descriptores de tipo. Entonces podemos usar ASM para analizar esto y devolver los tipos de argumentos. Poniendolo todo junto:

 Method getConstantPool = Class.class.getDeclaredMethod("getConstantPool"); getConstantPool.setAccessible(true); ConstantPool constantPool = (ConstantPool) getConstantPool.invoke(lambda.getClass()); String[] methodRefInfo = constantPool.getMemberRefInfoAt(constantPool.size() - 2); int argumentIndex = 0; String argumentType = jdk.internal.org.objectweb.asm.Type.getArgumentTypes(methodRef[2])[argumentIndex].getClassName(); Class< ?> type = (Class< ?>) Class.forName(argumentType); 

Actualizado con la sugerencia de jonathan

Ahora, idealmente, las clases generadas por LambdaMetaFactory deberían almacenar las firmas genéricas (podría ver si puedo enviar un parche al OpenJDK), pero actualmente esto es lo mejor que podemos hacer. El código anterior tiene los siguientes problemas:

  • Utiliza métodos y clases no documentados
  • Es extremadamente vulnerable a los cambios de código en el JDK
  • No conserva los tipos generics, por lo que si pasa List a una lambda aparecerá como List

La información de tipo parametrizada solo está disponible en tiempo de ejecución para los elementos de código que están vinculados, es decir, comstackdos específicamente en un tipo. Los lambdas hacen lo mismo, pero como su Lambda se elimina con azucar a un método en lugar de a un tipo, no hay ningún tipo para capturar esa información.

Considera lo siguiente:

 import java.util.Arrays; import java.util.function.Function; public class Erasure { static class RetainedFunction implements Function { public String apply(Integer t) { return String.valueOf(t); } } public static void main(String[] args) throws Exception { Function f0 = new RetainedFunction(); Function f1 = new Function() { public String apply(Integer t) { return String.valueOf(t); } }; Function f2 = String::valueOf; Function f3 = i -> String.valueOf(i); for (Function f : Arrays.asList(f0, f1, f2, f3)) { try { System.out.println(f.getClass().getMethod("apply", Integer.class).toString()); } catch (NoSuchMethodException e) { System.out.println(f.getClass().getMethod("apply", Object.class).toString()); } System.out.println(Arrays.toString(f.getClass().getGenericInterfaces())); } } } 

f0 y f1 conservan su información de tipo genérico, como era de esperar. Pero como son métodos independientes que se han borrado a la Function , f2 y f3 no.

Recientemente agregué soporte para resolver argumentos tipo lambda para TypeTools . Ex:

 MapFunction fn = str -> Integer.valueOf(str); Class< ?>[] typeArgs = TypeResolver.resolveRawArguments(MapFunction.class, fn.getClass()); 

Los argumentos resueltos del tipo son los esperados:

 assert typeArgs[0] == String.class; assert typeArgs[1] == Integer.class; 

Para manejar una lambda pasada:

 public void call(Callable< ?> c) { // Assumes c is a lambda Class< ?> callableType = TypeResolver.resolveRawArguments(Callable.class, c.getClass()); } 

Nota: La implementación subyacente utiliza el enfoque ConstantPool delineado por @danielbodart que se sabe que funciona en Oracle JDK y OpenJDK (y posiblemente otros).

He encontrado una forma de hacerlo para lambdas serializables. Todas mis lambdas son serializables, para eso funciona.

Gracias, Holger, por indicarme la SerializedLambda .

Los parámetros generics se capturan en el método estático sintético de lambda y se pueden recuperar a partir de allí. Encontrar el método estático que implementa el lambda es posible con la información de SerializedLambda

Los pasos son los siguientes:

  1. Obtenga SerializedLambda a través del método de reemplazo de escritura que se genera automáticamente para todas las lambdas serializables
  2. Encuentre la clase que contiene la implementación lambda (como método estático sintético)
  3. Obtenga el método java.lang.reflect.Method para el método sintético estático
  4. Obtener tipos generics de ese Method

ACTUALIZACIÓN: Aparentemente, esto no funciona con todos los comstackdores. Lo he intentado con el comstackdor de Eclipse Luna (funciona) y el Oracle javac (no funciona).


 // sample how to use public static interface SomeFunction extends java.io.Serializable { List applyTheFunction(Set value); } public static void main(String[] args) throws Exception { SomeFunction lambda = (set) -> Collections.singletonList(set.iterator().next().longValue()); SerializedLambda sl = getSerializedLambda(lambda); Method m = getLambdaMethod(sl); System.out.println(m); System.out.println(m.getGenericReturnType()); for (Type t : m.getGenericParameterTypes()) { System.out.println(t); } // prints the following // (the method) private static java.util.List test.ClassWithLambdas.lambda$0(java.util.Set) // (the return type, including *Long* as the generic list type) java.util.List // (the parameter, including *Double* as the generic set type) java.util.Set 

 // getting the SerializedLambda public static SerializedLambda getSerializedLambda(Object function) { if (function == null || !(function instanceof java.io.Serializable)) { throw new IllegalArgumentException(); } for (Class< ?> clazz = function.getClass(); clazz != null; clazz = clazz.getSuperclass()) { try { Method replaceMethod = clazz.getDeclaredMethod("writeReplace"); replaceMethod.setAccessible(true); Object serializedForm = replaceMethod.invoke(function); if (serializedForm instanceof SerializedLambda) { return (SerializedLambda) serializedForm; } } catch (NoSuchMethodError e) { // fall through the loop and try the next class } catch (Throwable t) { throw new RuntimeException("Error while extracting serialized lambda", t); } } throw new Exception("writeReplace method not found"); } 

 // getting the synthetic static lambda method public static Method getLambdaMethod(SerializedLambda lambda) throws Exception { String implClassName = lambda.getImplClass().replace('/', '.'); Class< ?> implClass = Class.forName(implClassName); String lambdaName = lambda.getImplMethodName(); for (Method m : implClass.getDeclaredMethods()) { if (m.getName().equals(lambdaName)) { return m; } } throw new Exception("Lambda Method not found"); }