【问题标题】:Reflection type inference on Java 8 LambdasJava 8 Lambda 上的反射类型推断
【发布时间】:2026-01-24 10:00:01
【问题描述】:

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

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

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

现在的问题是,我如何才能为 Java 8 lambda 实现类似的功能?由于它们实际上不是通用超接口的常规子类,因此上述方法不起作用。 具体来说,我可以弄清楚parseLambdajava.lang.Integer 绑定到T,而identityLambda 将相同的绑定到FT

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

【问题讨论】:

  • 小注释:public MapFunction&lt;String, Integer&gt; parseLambda = (String str) -&gt; { return Integer.valueOf(str); }可以写成public MapFunction&lt;String, Integer&gt; parseLambda = str -&gt; Integer.valueOf(str);
  • 我认为不可能。似乎 Lambda 项目的作者出于某种原因试图阻止反射,因为堆栈跟踪没有提供有关方法名称的信息,甚至方法引用也没有通过反射提供方法名称。
  • @skiwi:我还没有编写任何 Java 8 代码,但我的理解是 public MapFunction&lt;String, Integer&gt; parseLambda = Integer::valueOf; 实际上是等效的。
  • @JoachimSauer 没错

标签: java generics reflection lambda java-8


【解决方案1】:

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

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

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

【讨论】:

  • 真是个坏主意。与不可序列化的 lambda 相比,序列化的 lambda 具有显着更高的性能成本,并且安全性较低(序列化本质上是一个公共隐藏的构造函数,用于处理程序员可能打算保持私有的行为和/或捕获的数据。)
  • 方法签名和实例化的接口类型不足以确定在 lambda 中调用哪个方法。此外,在尝试使用此 hack 时,我经常会收到编译错误“代码太大”。最后,可以通过使用反射获得第一个捕获的参数,而无需序列化 lambda。
  • @gouessej 这个问题从来都不是关于获得第一个捕获的参数,所以我看不出这部分评论的相关性。而且我不明白您的第一句话要说什么,当然,方法是通过它们的名称和签名来识别的。没有其他的。 “代码太大”错误是完全不同的事情。考虑到 Brian Goetz 的警告,你不应该感到惊讶。不建议将它用于生产代码,过度使用它,即使在一个类中,也会遇到严重的限制。有一种合成方法可以反序列化类中定义的所有 lambda。
  • @Holger 序列化 lambda 并不能帮助获得比在包含 lambda 中调用的实例的字段“arg$1”上使用普通反射更多的信息。当我使用您对 '(final String s) -> a.b(s)' 的最后建议时,序列化的 lambda 不包含 'a.b'。
  • @gouessej 当然不是。这绝不是本次问答的任务。
【解决方案2】:

目前这是可以解决的,但只能以一种非常老套的方式解决,但让我先解释一下:

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

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

不幸的是,生成的 Class 不存储通用签名(它可以)所以你不能使用通常的反射方法来绕过擦除

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

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

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

不幸的是,这就是它的难点:

使用反射,我们可以调用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);

更新了乔纳森的建议

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

  • 它使用未记录的方法和类
  • 它极易受到 JDK 中代码更改的影响
  • 它不保留泛型类型,因此如果将 List 传递给 lambda,它将以 List 的形式出现

【讨论】:

  • 哇,这看起来很严肃 ;-) 我上面概述的解决方案相当简单,但不幸的是,它不适用于 OpenJDK 编译器(或 Oracle 编译器),而仅适用于Eclipse 编译器——后者将通用方法签名存储在类文件中。 Method 对象可以使用该签名来获取泛型参数类型和泛型返回类型。向 OpenJDK 团队提交补丁,以类似的方式包含该签名将是完美的解决方案。有没有可能提出这样的建议?
  • 此示例适用于java.util.function.Function,但不适用于其他一些功能接口,因为成员 ref 位于不同的索引处。对于 Oracle JDK 和 Open JDK,可以通过 constantPool.getMemberRefInfoAt(constantPool.size() - 2) 可靠地获取成员 ref
  • @danielbodart “不幸的是,生成的类不存储通用签名(它可以)所以你不能使用通常的反射方法来绕过擦除” - 为什么?您可以反映异常内部类的泛型类型,但不能反映 lambda :( 在我看来这是一个错误。
  • @danielbodart 是否可以获得泛型类型,就像使用 getActualTypeArguments 使用类或匿名内部类一样?切换到 lambda 时,Tehre 没有任何信息。
  • @danielbodart 我编写了一个补丁,在使用编译器标志时将通用签名添加到合成 lambda 方法。我正在寻找它的支持者:mail.openjdk.java.net/pipermail/compiler-dev/2015-January/…
【解决方案3】:

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

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

解析的类型参数符合预期:

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(可能还有其他)。

【讨论】:

  • 等等,我还没有检查你的链接,但如果你能做到这一点,那么我爱你!
  • 我看了,但乔纳森,我正在寻找的是获得传递的 lambda 的泛型类型。我不确定我是否理解 API,或者它是否可以做到这一点。 interface Lamb { ... } (String str) -> {} ... 你明白我在追求什么吗?
  • 其实我追的不是方法String参数,而是泛型类型抛出并定义的异常类型。 MyInterface { void call() throws E; } ... () { doSomthingThatThrowsE() }
  • 我更新了我的帖子以包括如何从传递的 lambda 解析类型参数。回复:异常类型,你也许可以通过反射以某种方式得到它,但这不是 TypeTools 现在所做的。
  • 似乎获取此 lambda 表达式() -&gt; (List&lt;String&gt;)null 的返回类型只会得到java.util.List,它没有List 中的嵌套泛型类型。
【解决方案4】:

参数化类型信息仅在运行时可用于绑定的代码元素 - 即专门编译为类型的代码元素。 Lambda 做同样的事情,但是由于您的 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&lt;Object,Object&gt;f2f3 的未绑定方法。

【讨论】:

  • 如果Function&lt;Integer,String&gt; f3 = (Integer) i -&gt; String.valueOf(i); 怎么办?看来参数化类型还没有绑定。
  • 正确 - 转换参数仍然不会导致函数成为编译类型的一部分,因此它仍然被删除。您必须使函数成为某种类,例如 f0 和 f1 所示的匿名类和内部类。
【解决方案5】:

我找到了一种用于可序列化 lambda 的方法。我所有的 lambda 表达式都是可序列化的,这很有效。

感谢 Holger,将我指向 SerializedLambda

泛型参数在 lambda 的合成静态方法中捕获,并且可以从那里检索。使用来自SerializedLambda 的信息可以找到实现 lambda 的静态方法

步骤如下:

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

更新:显然,这不适用于所有编译器。我已经用 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");
}

【讨论】:

  • 请注意,SerializedLambda.getImplMethodSignature() 已经提供了 lambda 合成方法的类型签名。最好使用它,因为方法名称不能保证是唯一的。
  • 那么在 Oracle Javac 上发生了什么?
  • 仅供参考:Eclipse 编译器无意中支持了这一点。版本 4.5 M4 将在编译器选项“-genericsignature”(bugs.eclipse.org/bugs/show_bug.cgi?id=449063)的帮助下允许它
  • 如果其他人拼命寻找可靠识别给定方法引用的多个实例的方法,这个答案是纯金。方法 replaceMethod = clazz.getDeclaredMethod("writeReplace"); replaceMethod.setAccessible(true);对象 serializedForm = replaceMethod.invoke(function); if (serializedForm instanceof SerializedLambda) { return ((SerializedLambda)serializedForm).getImplMethodName(); }
  • 您显然需要使用可序列化的 lambdas 才能使其工作,但是,如果您可以将它们限制为该形式,这将满足您的需求:-)