Reflection type inference in Java 8 Lambdas

I experimented with the new Lambdas in Java 8, and I'm looking for a way to use reflection in lambda classes to get the return type of a lambda function. I am particularly interested in cases where a lambda implements a common superinterface. In the code example below, MapFunction<F, T> is a common superinterface, and I'm looking for a way to find out which type is bound to a common parameter T

While Java dumps a lot of common type information after the compiler, subclasses (and anonymous subclasses) of common superclasses and common superinterfaces saved this type information. Thanks to reflection, these types were available. In the example below (case 1), reflection tells me that the implementation of MyMapper MapFunction binds java.lang.Integer to a parameter of type type T

Even for subclasses, which are universal in themselves, there are certain ways to figure out what is associated with a common parameter, if some others are known. Consider example 2 in the example below, IdentityMapper , where both F and T bound to the same type. When we know this, we know the type F , if we know the type of the parameter T (which in my case we do).

The question is, how can I implement something similar for Java 8 lambdas? Since they are in fact not regular subclasses of the common superinterface, the method described above does not work. In particular, can I understand that parseLambda associates java.lang.Integer with T , and identityLambda associates the same with F and T ?

PS: Theoretically, you can decompile lambda code, and then use the built-in compiler (for example, JDT) and touch its type inference. I hope there is an easier way to do this :-)

 /** * The superinterface. */ public interface MapFunction<F, T> { T map(F value); } /** * Case 1: A non-generic subclass. */ public class MyMapper implements MapFunction<String, Integer> { public Integer map(String value) { return Integer.valueOf(value); } } /** * A generic subclass */ public class IdentityMapper<E> implements MapFunction<E, E> { public E map(E value) { return value; } } /** * Instantiation through lambda */ public MapFunction<String, Integer> parseLambda = (String str) -> { return Integer.valueOf(str); } public MapFunction<E, E> 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; } 
+42
java generics reflection lambda java-8
Feb 19 '14 at 17:04
source share
5 answers

I found a way to do this for serializable lambda. All my lambdas are serializable and it works.

Thanks Holger for pointing me to SerializedLambda .

General parameters are fixed in the synthetic static lambda method and can be obtained from there. Finding a static method that implements lambda is possible using information from SerializedLambda

The steps are as follows:

  • Get SerializedLambda using a record replacement method that is automatically generated for all serializable lambdas
  • Find a class containing an implementation of lambda (as a synthetic static method)
  • Get java.lang.reflect.Method for a synthetic static method
  • Get common types from this Method



UPDATE: This does not seem to work with all compilers. I tried it using the Eclipse Luna (works) compiler and Oracle javac (not working).




 // sample how to use public static interface SomeFunction<I, O> extends java.io.Serializable { List<O> applyTheFunction(Set<I> value); } public static void main(String[] args) throws Exception { SomeFunction<Double, Long> 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<java.lang.Long> // (the parameter, including *Double* as the generic set type) java.util.Set<java.lang.Double> 



 // 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"); } 
+4
Sep 02 '14 at 14:23
source share

The exact decision on how to map the lambda code to the implementation of the interface remains in the real runtime. Basically, all lambdas that implement the same raw interface can share the same runtime class, as MethodHandleProxies does. Using different classes for specific lambdas is an optimization performed by the actual implementation of LambdaMetafactory , but not a function designed to help debug or reflect.

Thus, even if you find more detailed information in a real runtime class for implementing a lambda interface, this will be an artifact of the runtime used, which may not be available in different implementations or even in other versions of your current environment.

If the lambda is Serializable , you can use the fact that the serialized form contains a signature of the type of the interface instance to minimize the values โ€‹โ€‹of the variables of the actual type.

+15
Mar 11 '14 at 18:21
source share

This can be solved now, but only in pretty hacks, but let me explain a few things first:

When you write lambda, the compiler introduces a dynamic call instruction pointing to LambdaMetafactory and a private static synthetic method with the body of a lambda. The synthetic method and method descriptor in the constant pool contain a common type (if lambda uses this type or is explicit, as in your examples).

Now, at runtime, LambdaMetaFactory is LambdaMetaFactory and a class is generated using ASM that implements the functional interface, and the method body then calls the private static method with any arguments passed. It is then introduced into the source class using Unsafe.defineAnonymousClass (see John Rose's post ) so that it can access private members, etc.

Unfortunately, the generated class does not preserve common signatures (it can), so you cannot use the usual reflection methods that allow you to bypass erasure

For a regular class, you can check the bytecode with Class.getResource(ClassName + ".class") , but for anonymous classes defined using Unsafe , you're out of luck. However, you can make LambdaMetaFactory reset them using the JVM argument:

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

By examining the dumped class file (using javap -p -s -v ), you can see that it actually calls the static method. But the problem remains how to get the bytecode from Java itself.

Unfortunately, these are hackies:

Using reflection, we can call Class.getConstantPool and then access MethodRefInfo to get the type descriptors. Then we can use ASM to parse this and return argument types. Putting it all together:

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

Updated with jonathan proposal

Now, ideally, the classes generated by LambdaMetaFactory should store generic signatures (I could see if I can provide the OpenJDK patch), but for now this is the best we can do. The above code has the following problems:

  • It uses undocumented methods and classes.
  • It is extremely vulnerable to code changes in the JDK
  • It does not save generic types, so if you pass List <String> to lambda it will exit as List
+11
Sep 01 '14 at 21:39
source share

Information with a parameterized type is available only at run time for elements of the associated code, that is, specially compiled into a type. Lambdas does the same, but since your Lambda is not attached to a method, not to a type, there is no type to capture this information.

Consider the following:

 import java.util.Arrays; import java.util.function.Function; public class Erasure { static class RetainedFunction implements Function<Integer,String> { public String apply(Integer t) { return String.valueOf(t); } } public static void main(String[] args) throws Exception { Function<Integer,String> f0 = new RetainedFunction(); Function<Integer,String> f1 = new Function<Integer,String>() { public String apply(Integer t) { return String.valueOf(t); } }; Function<Integer,String> f2 = String::valueOf; Function<Integer,String> f3 = i -> String.valueOf(i); for (Function<Integer,String> 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 and f1 retain general type information, as one would expect. But since they are unrelated methods that were erased before Function<Object,Object> , f2 and f3 , do not do this.

+10
Mar 03 '14 at 17:17
source share

I recently added support for resolving lambda type arguments for TypeTools . Example:

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

Arguments of allowed types as expected:

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

To process the passed lambda:

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

Note. The basic implementation uses the ConstantPool approach described by @danielbodart, which, as you know, works with Oracle JDK and OpenJDK (and, possibly, with others). A.

+7
Dec 08 '14 at 18:18
source share



All Articles