Java 8 Lambdas上的反射类型推断

时间:2014-02-19 17:04:56

标签: java generics reflection lambda java-8

我正在Java 8中尝试新的Lambdas,我正在寻找一种方法来使用lambda类的反射来获取lambda函数的返回类型。我特别感兴趣的是lambda实现了一个通用的超接口。在下面的代码示例中,MapFunction<F, T>是通用的超接口,我正在寻找一种方法来找出绑定到泛型参数T的类型。

虽然Java在编译器之后抛弃了许多泛型类型信息,但是泛型超类和通用超接口的子类(和匿名子类)确实保留了该类型信息。通过反射,可以访问这些类型。在下面的示例(案例1)中,反射告诉我MyMapper的{​​{1}}实现将MapFunction绑定到泛型类型参数java.lang.Integer

即使对于本身是通用的子类,也有一些方法可以找出与泛型参数绑定的内容,如果已知其他参数。考虑以下示例中的案例2 T IdentityMapperF绑定到同一类型。当我们知道这一点时,如果我们知道参数类型T(我的情况就是这样),我们知道类型F

现在的问题是,如何才能实现Java 8 lambdas的类似功能?由于它们实际上不是通用超接口的常规子类,因此上述方法不起作用。 具体来说,我是否可以确定TparseLambda绑定到java.lang.Integer,而TidentityLambda绑定到FT?< / p> PS:从理论上讲,应该可以对lambda代码进行反编译,然后使用嵌入式编译器(如JDT)并进入其类型推理。我希望有一种更简单的方法可以做到这一点; - )

/**
 * 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;
}

5 个答案:

答案 0 :(得分:17)

如何将lambda代码映射到接口实现的确切决策留给实际的运行时环境。原则上,实现相同原始接口的所有lambda都可以像MethodHandleProxies那样共享一个运行时类。对特定lambdas使用不同的类是由实际的LambdaMetafactory实现执行的优化,而不是用于帮助调试或反射的功能。

因此,即使您在lambda接口实现的实际运行时类中找到更详细的信息,它也将是当前使用的运行时环境的工件,它可能在不同的实现中甚至在当前环境的其他版本中都不可用。 / p>

如果lambda是Serializable,您可以使用serialized form包含实例化接口类型的method signature这一事实来将实际类型变量值拼接在一起。

答案 1 :(得分:14)

这目前可以解决,但只是以一种漂亮的hackie方式,但我先解释一下:

编写lambda时,编译器会插入一个指向LambdaMetafactory的动态调用指令和一个带有lambda主体的私有静态合成方法。常量池中的合成方法和方法句柄都包含泛型类型(如果lambda使用类型或在示例中是显式的)。

现在在运行时调用LambdaMetaFactory并使用实现功能接口的ASM生成类,然后该方法的主体使用传递的任何参数调用私有静态方法。然后使用Unsafe.defineAnonymousClass将其注入原始类(请参阅John Rose post),以便它可以访问私有成员等。

不幸的是,生成的类没有存储通用签名(它可以),所以你不能使用通常的反射方法来解决擦除问题

对于普通类,您可以使用Class.getResource(ClassName + ".class")检查字节码,但对于使用Unsafe定义的匿名类,您运气不好。但是,您可以使用JVM参数将LambdaMetaFactory转储出来:

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

通过查看转储的类文件(使用javap -p -s -v),可以看到它确实调用了静态方法。但问题仍然是如何从Java本身获取字节码。

不幸的是,它变成了hackie:

使用反射我们可以调用Class.getConstantPool,然后访问MethodRefInfo以获取类型描述符。然后我们可以使用ASM来解析它并返回参数类型。把它们放在一起:

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

更新了jonathan的建议

现在理想情况下LambdaMetaFactory生成的类应该存储泛型类型签名(我可能会看到我是否可以向OpenJDK提交补丁)但是目前这是我们能做的最好的。上面的代码有以下问题:

  • 它使用未记录的方法和类
  • 它非常容易受到JDK中的代码更改
  • 的影响
  • 它不保留泛型类型,因此如果您传递List&lt; String&gt;进入一个lambda它将作为List
  • 出现

答案 2 :(得分:11)

参数化类型信息仅在运行时可用于绑定的代码元素 - 即,特别编译为类型。 Lambdas做同样的事情,但是当你的Lambda被去掉一个方法而不是一个类型时,没有类型来捕获这些信息。

请考虑以下事项:

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

f0f1都会保留其通用类型信息,正如您所期望的那样。但由于它们是未被绑定的方法,已被删除到Function<Object,Object>f2f3没有。

答案 3 :(得分:11)

我最近添加了对解析TypeTools的lambda类型参数的支持。例如:

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

已解析的类型args符合预期:

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

处理传递的lambda:

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

注意:底层实现使用@danielbodart概述的ConstantPool方法,该方法已知可用于Oracle JDK和OpenJDK(可能还有其他方法)。

答案 4 :(得分:7)

我找到了一种为可序列化的lambdas做这种方法的方法。我的所有lambdas都是可序列化的,这就是可行的。

谢谢,Holger,指点我SerializedLambda

泛型参数在lambda的合成静态方法中捕获,可以从那里检索。使用SerializedLambda

中的信息可以找到实现lambda的静态方法

步骤如下:

  1. 通过为所有可序列化的lambdas自动生成的写入替换方法获取SerializedLambda
  2. 找到包含lambda实现的类(作为合成静态方法)
  3. 获取合成静态方法的java.lang.reflect.Method
  4. Method
  5. 获取通用类型

    更新:显然,这不适用于所有编译器。我已经使用Eclipse Luna(作品)和Oracle javac(不起作用)的编译器进行了尝试。


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