当前位置: 首页 > 知识库问答 >
问题:

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

孙阳舒
2023-03-14

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

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

由于不检查类型,上面的实现允许编译器允许的每种类型,包括类型变量。

<T> void test() {
    TypeToken<T[]> token = new TypeToken<T[]>() {};
}

在这种情况下,类型是一个持有一个类型变量作为其组件类型的通用数组类型。这很好。

但是,当您在lambda表达式中初始化TypeToken时,事情开始发生变化。(类型变量来自上面的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)

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

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

同时,我也对这个话题做了一些研究。在Java语言规范§15.12.2.2中,我发现了一个可能与之相关的表达式——“与适用性相关”,提到“隐式类型lambda表达式”是一个例外。显然,这是一个错误的章节,但这个表达在其他地方被使用,包括关于类型推断的章节。

但老实说:我还没有真正弄清楚所有像:=Fi0这样的运算符是什么意思,为什么很难详细理解它。如果有人能澄清一下,如果这可能是奇怪行为的解释,我会很高兴。

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

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

几天前,我在官方JavaBug Database/Tracker中请求了一个条目,它刚刚被接受。由于审查我的报告的开发人员将优先级P4分配给该错误,因此可能需要一段时间才能修复。您可以在此处找到报告。

汤姆·霍丁大声喊叫——铲球,因为他提到这可能是Java本身的一个重要错误。然而,迈克·斯特罗贝尔的报告可能会比我的报告详细得多,因为他有令人印象深刻的背景知识。然而,当我写报告时,斯特罗贝尔的答案还没有出来。


共有3个答案

唐珂
2023-03-14
匿名用户

我没有找到规范的相关部分,但这里有一个部分答案。

组件类型为null肯定有一个bug。需要明确的是,这是类型令牌。键入从上方强制转换为GenericArrayType(恶心!)调用方法getGenericComponentType。API文档没有明确提及返回的null是否有效。然而,toString方法抛出了NullPointerException,因此肯定存在一个bug(至少在我使用的Java随机版本中是这样)。

我没有bugs.java.com账户,所以不能报告这个。有人应该。

让我们看看生成的类文件。

javap -private YourClass

这将生成一个包含以下内容的列表:

static <T> void test();
private static TypeToken lambda$test$0();

请注意,我们的显式test方法具有它的类型参数,但合成lambda方法没有。您可能期望这样的内容:

static <T> void test();
private static <T> TypeToken<T[]> lambda$test$0(); /*** DOES NOT HAPPEN ***/
             // ^ name copied from `test`
                          // ^^^ `Object[]` would not make sense

为什么不这样呢。大概是因为在需要类型参数的上下文中,这将是一个方法类型参数,它们是惊人的不同。lambda也有一个限制,不允许它们具有方法类型参数,显然是因为没有显式符号(有些人可能认为这似乎是一个糟糕的借口)。

结论:这里至少有一个未报告的JDK错误。反映API和语言的lambda泛型部分不符合我的口味。

丁曦
2023-03-14

作为一种解决方法,您可以将TypeToken的创建从lambda移至单独的方法,并且仍然使用lambda而不是完全声明的类:

static<T> TypeToken<T[]> createTypeToken() {
    return new TypeToken<T[]>() {};
}

Supplier<TypeToken<T[]>> sup = () -> createTypeToken();
傅茂实
2023-03-14
匿名用户

TLDR:

  • javac中存在一个错误,该错误记录了lambda嵌入内部类的错误封闭方法。因此,实际封闭方法上的类型变量无法由这些内部类解析。
  • java.lang.reflectAPI实现中可以说有两组错误:
    • 当遇到不存在的类型时,一些方法被记录为抛出异常,但它们从不这样做。相反,它们允许空引用传播。
    • 当无法解析类型时,各种Type::toString()覆盖当前抛出或传播NullPointerException

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

    通常,当您编写一个具有一个或多个泛型超类型的类时,Java编译器将发出一个签名属性,该属性包含该类超类型的完全参数化泛型签名。我以前写过这些,但简短的解释是:如果没有它们,就不可能将泛型类型作为泛型类型使用,除非您碰巧有源代码。由于类型擦除,有关类型变量的信息在编译时丢失。如果这些信息没有作为额外的元数据包含,IDE和编译器都不会知道某个类型是泛型的,因此您无法使用它。编译器也不能发出必要的运行时检查来强制类型安全。

    javac将为其签名包含类型变量或参数化类型的任何类型或方法发出泛型签名元数据,这就是为什么您能够获得匿名类型的原始泛型超类型信息。例如,此处创建的匿名类型:

    TypeToken<?> token = new TypeToken<List<? extends CharSequence>>() {};
    

    ...包含此签名

    LTypeToken<Ljava/util/List<+Ljava/lang/CharSequence;>;>;
    

    从这一点来看,java。lang.reflection可以解析关于(匿名)类的泛型超类型信息。

    但是我们已经知道,当TypeToken使用具体类型参数化时,这很好。让我们看一个更相关的示例,其中它的类型参数包括一个类型变量:

    static <F> void test() {
        TypeToken sup = new TypeToken<F[]>() {};
    }
    

    在这里,我们得到以下签名:

    LTypeToken<[TF;>;
    

    有道理,对吗?现在,让我们看看java是如何实现的。反映API能够从这些签名中提取泛型超类型信息。如果我们仔细观察类::getGenericSuperclass(),我们会发现它做的第一件事就是调用getGenericInfo()。如果我们以前没有调用过这个方法,那么就会实例化一个类存储库:

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

    如果在类本身(例如,匿名TypeToken)上找不到类型变量(例如,F

    EnclosingMethod: LambdaTest.test()V
    

    此属性的存在意味着computeEnclosingScope将为泛型方法静态生成Method odScope

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

    在本例中,为lambda主体生成的静态方法看起来像这样(如果反编译):

    private static /* synthetic */ Object lambda$test$0() {
        return new LambdaTest$1();
    }
    

    ...其中LambdaTest1美元是您的匿名类。让我们拆解它并检查我们的属性:

    Signature: LTypeToken<TW;>;
    EnclosingMethod: LambdaTest.lambda$test$0()Ljava/lang/Object;
    

    就像我们在lambda之外实例化匿名类型一样,签名包含类型变量W。但是封闭方法是指综合方法。

    合成方法lambda$test0美元()没有声明类型变量W。此外,lambda$test0美元()不包含在test()中,因此W的声明在其中不可见。您的匿名类有一个超类型,其中包含一个类型变量,您的类不知道,因为它超出了范围。

    当我们调用getGenericSuperclass()时,LambdaTest$1的作用域层次结构不包含W,因此解析器无法解析它。由于代码是如何编写的,这个未解析的类型变量会导致将null放入泛型超类型的类型参数中。

    注意,如果lambda实例化了一个不引用任何类型变量的类型(例如,TypeToken)

    (i) javac中有一个bug。Java虚拟机规范§4.7.7(“封闭方法属性”)规定:

    Java编译器有责任确保通过方法索引标识的方法确实是包含该属性的类中最接近的词汇封闭方法。(强调矿山)

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

    可以说,签名解析器/具体化器默默地允许将null类型参数传播到ParameterizedType中也是一个错误(正如@tom-hawtin-0025 line所指出的,具有像toString()抛出NPE这样的辅助效果)。

    我关于封闭方法问题的错误报告现在在线。

    (ii)java中可能存在多个bug。lang.reflect及其支持的API。

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

    我还认为,各种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>>() {};
        }
    }
    

    不幸的是,自从lambda首次引入以来,这个bug就明显存在,而且在最新的LTS版本中还没有修复,因此您可能不得不假设这个bug在修复之后很长时间仍然存在于客户机的JDK中,假设它得到了修复。

 类似资料:
  • 问题内容: 在进行一些基本的lambda练习时,一个看似完全相同的匿名内部类的输出给我的输出与lambda不同。 场景1 输出 2 和 2 。这里没有新内容。 但是当我这样做时: 场景2 输出 2 和 3 问题:两个输出不应该相同吗? 我想念什么吗? 为了完整起见: 方案3 输出 3 和 3 。这里也没有什么新鲜的。 更新:仍从1.8.0-b132获得相同的输出 更新#2:错误报告: https

  • 我正在使用Spring的转换服务,添加一个简单的转换器,将ZonedDateTime(Java 8)转换为字符串: 这很好用。但我的IDE(IntelliJ)建议用lambda表达式替换匿名内部类: 如果我这样做,那么它不再工作,我得到一个关于Spring无法确定泛型类型的错误: 表示lambda表达式的对象显然与匿名内部类的有足够的不同,以至于Spring无法再确定泛型类型。Java8如何准确地

  • 问题内容: 在此问题中用户@Holger提供了一个答案,该答案显示了匿名类的不常见用法,我并不知道。 该答案使用流,但是此问题与流无关,因为这种匿名类型构造可以在其他上下文中使用,即: 令我惊讶的是,它编译并打印了预期的输出。 注意:我很清楚,自古以来,就可以构造一个匿名内部类并按如下方式使用其成员: 但是,这不是我要问的。我的情况有所不同,因为匿名类型是通过方法链传播的。 现在,我可以想象到此功

  • 我想在tibble中应用函数,但不想显式地命名列。例如。 我得到了预期的结果: 但是,与其编写我宁愿使用但这不起作用。 所以如果我用 我得到一个 "错误:没有注册tidyselect变量" 我将感激任何帮助。

  • 问题内容: 据我了解,以下代码应已打印为输出。 但是,当我运行此代码时,它正在打印。 来自Java文档 匿名类15.9.5。: 匿名类始终是隐式最终的 为什么这段代码会这样? 问题答案: 请注意,自那时以来,该特定部分的JLS中的措词已发生重大变化。现在(JLS 11)显示: 15.9.5。匿名类声明: 匿名类永远不会是最终的(第8.1.1.2节)。 匿名类不是最终的事实与强制转换有关,尤其是强制

  • 据我所知,以下代码应该已打印为输出。 但是,当我运行此代码时,它正在打印。 来自匿名类15.9.5的Java文档: 匿名类总是隐式最终的 为什么这个代码会这样?