gpt4 book ai didi

java - 使用 lambda 而不是显式匿名内部类时的不同泛型行为

转载 作者:行者123 更新时间:2023-12-02 10:27:57 58 4
gpt4 key购买 nike

上下文

我正在从事一个严重依赖泛型类型的项目。其关键组件之一是所谓的 TypeToken ,它提供了一种在运行时表示泛型类型并对其应用一些实用函数的方法。为了避免 Java 的类型删除,我使用大括号表示法 ( {} ) 创建一个自动生成的子类,因为这使得类型可具体化。

什么 TypeToken基本上是

这是 TypeToken 的强简化版本这比原始实现更宽松。但是,我正在使用这种方法,因此我可以确保真正的问题不在于这些实用程序函数之一。

public class TypeToken<T> {

private final Type type;
private final Class<T> rawType;

private final int hashCode;


/* ==== Constructor ==== */

@SuppressWarnings("unchecked")
protected TypeToken() {
ParameterizedType paramType = (ParameterizedType) this.getClass().getGenericSuperclass();
this.type = paramType.getActualTypeArguments()[0];

// ...
}

当它起作用时

基本上,这种实现几乎在所有情况下都能完美运行。
处理大多数类型没有问题。以下示例完美运行:
TypeToken<List<String>> token = new TypeToken<List<String>>() {};
TypeToken<List<? extends CharSequence>> token = new TypeToken<List<? extends CharSequence>>() {};

由于它不检查类型,因此上面的实现允许编译器允许的每种类型,包括 TypeVariables。
<T> void test() {
TypeToken<T[]> token = new TypeToken<T[]>() {};
}

在这种情况下, typeGenericArrayType拿着 TypeVariable作为其组件类型。这完全没问题。

使用 lambdas 时的奇怪情况

但是,当您初始化 TypeToken 时在 lambda 表达式中,事情开始发生变化。 (类型变量来自上面的 test函数)
Supplier<TypeToken<T[]>> sup = () -> new TypeToken<T[]>() {};

在这种情况下, type仍然是 GenericArrayType ,但它持有 null作为其组件类型。

但是如果你正在创建一个匿名内部类,事情又开始发生变化:
Supplier<TypeToken<T[]>> sup = new Supplier<TypeToken<T[]>>() {
@Override
public TypeToken<T[]> get() {
return new TypeToken<T[]>() {};
}
};

在这种情况下,组件类型再次拥有正确的值 (TypeVariable)

产生的问题
  • lambda 示例中的 TypeVariable 会发生什么?为什么类型推断不尊重泛型类型?
  • 显式声明和隐式声明的示例有什么区别?类型推断是唯一的区别吗?
  • 如何在不使用样板显式声明的情况下解决此问题?这在单元测试中变得尤为重要,因为我想检查构造函数是否抛出异常。

  • 澄清一下:这不是一个与程序“相关”的问题,因为我根本不允许不可解析的类型,但这仍然是一个我想了解的有趣现象。

    我的研究

    更新 1

    同时,我对这个话题做了一些研究。在 Java Language Specification §15.12.2.2我发现了一个可能与它有关的表达式 - “与适用性相关”,提到“隐式类型的 lambda 表达式”作为异常(exception)。显然,这是错误的章节,但该表达式在其他地方使用,包括关于类型推断的章节。

    但说实话:我还没有真正弄清楚所有这些运营商喜欢什么 :=Fi0是什么让我们很难详细了解它。如果有人能澄清一下,如果这可能是对奇怪行为的解释,我会很高兴。

    更新 2

    我再次考虑了这种方法并得出结论,即使编译器会删除该类型,因为它与“适用性无关”,也没有理由将组件类型设置为 null而不是最慷慨的类型,Object。我想不出语言设计者决定这样做的单一原因。

    更新 3

    我刚刚用最新版本的 Java 重新测试了相同的代码(我之前使用过 8u191)。令我遗憾的是,这并没有改变任何东西,尽管 Java 的类型推断得到了改进......

    更新 4

    几天前,我在官方 Java 错误数据库/跟踪器中请求了一个条目,它刚刚被接受。由于审查我的报告的开发人员将优先级 P4 分配给错误,可能需要一段时间才能修复它。您可以找到报告 here .

    对 Tom Hawtin 的大喊大叫 - 提到这可能是 Java SE 本身的一个基本错误的主旨。然而,由于 Mike Strobel 令人印象深刻的背景知识,他的报告可能比我的更详细。但是,当我撰写报告时,Strobel 的答案尚未公布。

    最佳答案

    tldr:

    1. There is a bug in javac that records the wrong enclosing method for lambda-embedded inner classes. As a result, type variables on the actual enclosing method cannot be resolved by those inner classes.
    2. There are arguably two sets of bugs in the java.lang.reflect API implementation:
      • Some methods are documented as throwing exceptions when nonexistent types are encountered, but they never do. Instead, they allow null references to propagate.
      • The various Type::toString() overrides currently throw or propagate a NullPointerException when a type cannot be resolved.


    答案与通常在使用泛型的类文件中发出的泛型签名有关。

    通常,当您编写具有一个或多个泛型父类(super class)型的类时,Java 编译器会发出 Signature包含类父类(super class)型的完全参数化泛型签名的属性。我已经 written about these before ,但简短的解释是:没有它们,除非您碰巧拥有源代码,否则不可能将泛型类型用作泛型类型。由于类型删除,有关类型变量的信息在编译时丢失。如果该信息不作为额外元数据包含在内,则 IDE 和您的编译器都不会知道类型是泛型的,并且您不能这样使用它。编译器也不能发出必要的运行时检查来强制类型安全。
    javac将为签名包含类型变量或参数化类型的任何类型或方法发出泛型签名元数据,这就是为什么您能够获取匿名类型的原始泛型父类(super class)型信息的原因。例如,这里创建的匿名类型:
    TypeToken<?> token = new TypeToken<List<? extends CharSequence>>() {};

    ...包含此 Signature :
    LTypeToken<Ljava/util/List<+Ljava/lang/CharSequence;>;>;

    由此, java.lang.reflection API 可以解析关于您的(匿名)类的通用父类(super class)型信息。

    但我们已经知道,当 TypeToken 时,这工作得很好。用具体类型参数化。让我们看一个更相关的例子,它的类型参数包含一个类型变量:
    static <F> void test() {
    TypeToken sup = new TypeToken<F[]>() {};
    }

    在这里,我们得到以下签名:
    LTypeToken<[TF;>;

    有道理吧?现在,让我们看看 java.lang.reflect API 能够从这些签名中提取通用父类(super class)型信息。如果我们查看 Class::getGenericSuperclass() ,我们看到它做的第一件事是调用 getGenericInfo() .如果我们之前没有调用过这个方法,一个 ClassRepository被实例化:
    private ClassRepository getGenericInfo() {
    ClassRepository genericInfo = this.genericInfo;
    if (genericInfo == null) {
    String signature = getGenericSignature0();
    if (signature == null) {
    genericInfo = ClassRepository.NONE;
    } else {
    // !!! RELEVANT LINE HERE: !!!
    genericInfo = ClassRepository.make(signature, getFactory());
    }
    this.genericInfo = genericInfo;
    }
    return (genericInfo != ClassRepository.NONE) ? genericInfo : null;
    }

    这里的关键部分是调用 getFactory() , 扩展为:
    CoreReflectionFactory.make(this, ClassScope.make(this))
    ClassScope是我们关心的一点:这为类型变量提供了一个解析范围。给定一个类型变量名称,在作用域中搜索匹配的类型变量。如果未找到,则搜索“外部”或封闭范围:
    public TypeVariable<?> lookup(String name) {
    TypeVariable<?>[] tas = getRecvr().getTypeParameters();
    for (TypeVariable<?> tv : tas) {
    if (tv.getName().equals(name)) {return tv;}
    }
    return getEnclosingScope().lookup(name);
    }

    最后,这一切的关键(来自 ClassScope ):
    protected Scope computeEnclosingScope() {
    Class<?> receiver = getRecvr();

    Method m = receiver.getEnclosingMethod();
    if (m != null)
    // Receiver is a local or anonymous class enclosed in a method.
    return MethodScope.make(m);

    // ...
    }

    如果在类本身中找不到类型变量(例如 F )(例如匿名 TypeToken<F[]> ),则下一步是搜索 封闭方法 .如果我们查看反汇编的匿名类,我们会看到这个属性:
    EnclosingMethod: LambdaTest.test()V

    该属性的存在意味着 computeEnclosingScope将产生 MethodScope用于泛型方法 static <F> void test() .自 test声明类型变量 W ,我们在搜索封闭范围时找到它。

    那么,为什么它在 lambda 中不起作用呢?

    要回答这个问题,我们必须了解 lambdas 是如何编译的。 lambda 的主体 gets moved合成静态方法。在我们声明 lambda 的地方,一个 invokedynamic指令被发出,这会导致 TypeToken我们第一次点击该指令时生成的实现类。

    在这个例子中,为 lambda 体生成的静态方法看起来像这样(如果反编译):
    private static /* synthetic */ Object lambda$test$0() {
    return new LambdaTest$1();
    }

    ...哪里 LambdaTest$1是你的匿名类(class)。让我们分解它并检查我们的属性:
    Signature: LTypeToken<TW;>;
    EnclosingMethod: LambdaTest.lambda$test$0()Ljava/lang/Object;

    就像我们在 lambda 之外实例化匿名类型的情况一样,签名包含类型变量 W . 但是EnclosingMethod指合成方法 .

    合成方法 lambda$test$0()不声明类型变量 W .此外, lambda$test$0()没有被 test() 包围,所以 W的声明在里面是不可见的。你的匿名类有一个父类(super class)型,它包含一个你的类不知道的类型变量,因为它超出了范围。

    当我们拨打 getGenericSuperclass()LambdaTest$1 的范围层次结构不包含 W ,因此解析器无法解析它。由于代码的编写方式,这个未解析的类型变量导致 null被放置在泛型父类(super class)型的类型参数中。

    请注意,如果您的 lambda 实例化了一个类型,它做了 不是 引用任何类型变量(例如, TypeToken<String> )那么你就不会遇到这个问题。

    结论

    (i) javac 中存在错误. Java 虚拟机规范 §4.7.7 (“ EnclosingMethod 属性”)指出:

    It is the responsibility of a Java compiler to ensure that the method identified via the method_index is indeed the closest lexically enclosing method of the class that contains this EnclosingMethod attribute. (emphasis mine)



    目前, javac似乎在 lambda 重写器运行后确定封闭方法,因此, EnclosingMethod属性指的是一个在词法范围内从未存在过的方法。如 EnclosingMethod报告了实际的词法封闭方法,该方法上的类型变量可以由 lambda 嵌入的类解析,并且您的代码将产生预期的结果。

    可以说,签名解析器/reifier 默默地允许 null 也是一个错误。要传播到 ParameterizedType 中的类型参数(正如@tom-hawtin-tackline 指出的那样,它具有诸如 toString() 抛出 NPE 之类的辅助效果)。

    我的 bug reportEnclosingMethod问题现已上线。

    (ii) 可以说 java.lang.reflect 中存在多个错误及其支持的 API。

    方法 ParameterizedType::getActualTypeArguments()被记录为抛出 TypeNotPresentException当“任何实际类型参数引用不存在的类型声明”时。该描述可以说涵盖了类型变量不在范围内的情况。 GenericArrayType::getGenericComponentType()当“底层数组类型的类型引用不存在的类型声明”时,应该抛出类似的异常。目前,两者似乎都没有抛出 TypeNotPresentException在任何情况下。

    我还认为各种 Type::toString覆盖应该只填写任何未解析类型的规范名称,而不是抛出 NPE 或任何其他异常。

    我已针对这些与反射相关的问题提交了错误报告,一旦链接公开,我将发布该链接。

    解决方法?

    如果您需要能够引用由封闭方法声明的类型变量,那么您不能使用 lambda 来做到这一点;您将不得不退回到更长的匿名类型语法。但是,lambda 版本应该适用于大多数其他情况。您甚至应该能够引用由封闭类声明的类型变量。例如,这些应该始终有效:
    class Test<X> {
    void test() {
    Supplier<TypeToken<X>> s1 = () -> new TypeToken<X>() {};
    Supplier<TypeToken<String>> s2 = () -> new TypeToken<String>() {};
    Supplier<TypeToken<List<String>>> s3 = () -> new TypeToken<List<String>>() {};
    }
    }

    不幸的是,鉴于此错误自首次引入 lambdas 以来显然已存在,并且尚未在最新的 LTS 版本中修复,您可能不得不假设该错误在修复后很长时间仍保留在客户的 JDK 中,假设它得到了完全固定。

    关于java - 使用 lambda 而不是显式匿名内部类时的不同泛型行为,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53039980/

    58 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com