Java 8 Lambdas 上的反射类型推理

2022-08-31 19:58:08

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

虽然Java在编译器之后丢弃了大量的泛型类型信息,但泛型超类和泛型超接口的子类(和匿名子类)确实保留了该类型信息。通过反射,这些类型是可访问的。在下面的示例(案例 1)中,反射告诉我,绑定到泛型类型参数 的实现。MyMapperMapFunctionjava.lang.IntegerT

即使对于本身就是泛型的子类,如果其他一些子类是已知的,也有一些方法可以找出绑定到泛型参数的子类。考虑以下示例中的情况 2,其中两者绑定到同一类型。当我们知道这一点时,如果我们知道参数类型,我们就知道类型(在我的情况下,我们知道)。IdentityMapperFTFT

现在的问题是,我如何才能为Java 8 lambdas实现类似的东西?由于它们实际上不是泛型超接口的常规子类,因此上述方法不起作用。具体来说,我能弄清楚绑定到 和 绑定与 和 相同吗?parseLambdajava.lang.IntegerTidentityLambdaFT

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

答案 1

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

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

如果 lambda 是,则可以使用序列化表单包含实例化接口类型的方法签名这一事实,将实际的类型变量值放在一起。Serializable


答案 2

目前可以解决这个问题,但只能以一种非常黑客的方式解决,但让我先解释一些事情:

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

现在,在运行时调用 ,并使用 ASM 生成一个类,该类实现函数接口,然后使用传递的任何参数调用方法的函数接口和方法主体。然后使用它注入原始类(参见John Rose post),以便它可以访问私有成员等。LambdaMetaFactoryUnsafe.defineAnonymousClass

不幸的是,生成的类不存储通用签名(它可以),因此您无法使用通常的反射方法来绕过擦除

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

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

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

不幸的是,这是它变得黑客的地方:

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

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

使用乔纳森的建议进行更新

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

  • 它使用未记录的方法和类
  • 它极易受到JDK中代码更改的影响。
  • 它不会保留泛型类型,因此,如果您将List<String>传递到lambda中,它将作为List出来

推荐