【问题标题】:Generate lambda class for generic type variable using reflection使用反射为泛型类型变量生成 lambda 类
【发布时间】:2016-05-03 14:35:20
【问题描述】:

我正在尝试将 Java 接口用作 D 类型的一些高级包装器中的 mixins。

interface WrapsD {
    D getWrapped();
}

interface FeatureA extends WrapsD {
    default ...
}

interface FeatureB extends WrapsD  {
    default ...
}

abstract class DWrapperFactory<T extends WrapsD> {
    protected T doWrap(D d) {
        return () -> d; // <- does not work
    }
}

interface FeatureAB extends FeatureA, FeatureB {
}

class ProducingDWithFeatureAB extends DWrapperFactory<FeatureAB> {
    protected FeatureAB doWrap(D d) {
        return () -> d; // <- has to repeat this
    }
}

正如ProducingDWithFeatureAB 中所见,doWrap 必须在每个子类中实现,即使主体相同。 (Java 泛型为什么真的被破坏的另一个例子。)

由于其他原因我已经需要创建像 ProducingDWithFeatureAB 这样的具体类,并且 JRE 中存在用于合成 lambda 类的代码,所以应该可以使用反射只编写一次 doWrap。我想知道怎么做。

doWrap 过去是使用匿名内部类实现接口的,这更加生物模板。)

【问题讨论】:

  • 泛型类型在编译时由于类型擦除而被擦除。因此,在这种情况下,您要使用的通用方法/lambda 可能无法实现。
  • @callyalater 我已经必须为每个不同的T 创建一个子类,因此可以保证该类型可以通过反射获得。我只是想避免一次又一次地复制fun() 的实现,只是为了使类型工作......
  • 匿名本地类中要替换什么样的代码?
  • 你期望它的表现如何?
  • 您能否提供您尝试重构的示例代码?我看不到通用 T 可以如何由匿名类实现,除非使用未经检查的强制转换(然后问题实际上就在那里)。

标签: java generics lambda java-8


【解决方案1】:

这与泛型无关;您的通用示例只是混淆了真正的问题。

这是问题的核心:lambda 表达式需要一个作为函数接口的目标类型,并且编译器必须静态知道该目标类型。您的代码没有提供。例如,出于同样的原因,以下代码会出现同样的错误:

Object o = arg -> expr;

这里,Object 不是函数式接口,lambda 表达式只能在类型为(兼容的)函数式接口的上下文中使用。

泛型的使用使其更加混乱(而且我认为您自己也对泛型的工作方式感到困惑),但最终这将是它触底的地方。

【讨论】:

  • 是的,我明白这一切。问题是没有办法告诉编译器泛型类型变量T 是一个函数式接口。这个问题的根源在于 Java 泛型是使用类型擦除而不是模板来实现的。我可以保证T 将是一个功能接口,因此应该可以在运行时使用 relfection 生成正确的合成类。我只是想知道怎么做。
  • 不,这不是真正的问题——这只是你要绊倒的下一件事。即使有一种方法可以将T 约束为T extends FunctionalInterface,也只会让问题更进一步——编译器仍然必须静态地知道目标类型才能生成正确的实例化代码。并且该信息仍然被删除。
  • 是的,我也明白。正如我在问题的评论和修改后的问题中提到的,我不希望编译器能够为 lambda 生成字节码,而是我会在运行时使用反射和类元数据来合成 lambda 类将 T 作为具体类型。
【解决方案2】:

首先你要了解的,是一个form的方法

public Function<X,Y> fun() {
    return arg -> expr;
}

被脱糖等同于:

public Function<X,Y> fun() {
    return DeclaringClass::lambda$fun$0;
}
private static Y lambda$fun$0(X arg) {
    return expr;
}

XY 类型是从目标接口的功能签名派生的。虽然功能接口的实际实例是在运行时生成的,但您需要执行一个具体化的目标方法,该方法由编译器生成。

您可以反射性地为单个目标方法生成不同接口的实例,但它仍然要求所有这些功能接口具有相同的功能签名,例如从X 映射到Y,这会降低动态解决方案的实用性。

在您的情况下,所有目标接口确实具有相同的功能签名,这是可能的,但我必须强调,整个软件设计对我来说似乎是有问题的。

为了实现动态生成,我们必须如上所述对 lambda 表达式进行脱糖,并将捕获的变量 d 作为附加参数添加到目标方法。由于您的特定函数没有参数,因此它使捕获的 d 成为唯一的方法参数:

protected T doWrap(D d) {
    Class<T> type=getActualT();
    MethodHandles.Lookup l=MethodHandles.lookup();
    try
    {
        MethodType fType = MethodType.methodType(D.class);
        MethodType tType = fType.appendParameterTypes(D.class);
        return type.cast(LambdaMetafactory.metafactory(l, "getWrapped",
            tType.changeReturnType(type), fType,
            l.findStatic(DWrapperFactory.class, "lambda$doWrap$0", tType), fType)
            .getTarget().invoke(d));
    }
    catch(RuntimeException|Error t) { throw t; }
    catch(Throwable t) { throw new IllegalStateException(t); }
}
private static D lambda$doWrap$0(D d) {
    return d;
}

您必须实现方法getActualT(),它应该返回正确的类对象,如果DWrapperFactory 的实际子类是适当的可具体化类型,这是可能的,正如您所说。然后,doWrap 方法将动态生成一个适当的T 实例,并使用捕获的d 值调用脱糖的lambda 表达式的方法——所有这些都假设T 类型确实是一个函数式接口,这不能在编译时证明。

请注意,即使在运行时,LambdaMetafactory 也不会检查不变量是否成立,如果T 不是正确的功能接口(和WrapsD 的子类),您可能会在稍后抛出错误.


现在比较一下只是重复方法

protected SubtypeOfWrapsD doWrap(D d) {
    return () -> d;
}

在每个必须存在的可具体化类型中......

【讨论】:

  • 非常感谢您的解决方案和建议。我正在重新评估这种架构。可悲的是,它是对调用代码的更改最少的一种……
猜你喜欢
  • 2021-05-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-12-15
  • 2020-09-02
  • 1970-01-01
相关资源
最近更新 更多