为什么通用类型信息在第二次调用后丢失?

Mos*_*ani 1 java generics

我正在用 Java 泛型做一些实验,遇到了一些奇怪的事情,希望你能帮我弄清楚!(此代码不会仅用于玩泛型)

public abstract class A {
    abstract static class Builder<T extends Builder> {
        T obj;

        public T builder(){
            return obj;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

现在假设我们定义了 B 类,它以两种不同的形式实现了 A 类。一次使用泛型,另一次不使用泛型。

public class B extends A {
    public static class Builder extends A.Builder<Builder> {

    }
}
Run Code Online (Sandbox Code Playgroud)

现在,如果我使用上面的代码,如下所示:

new B.Builder().builder().builder()
Run Code Online (Sandbox Code Playgroud)

两种builder()方法的返回类型都是,B.Builder但如果我这样定义 B:

public class B extends A {
    public static class Builder<T> extends A.Builder<Builder> {
        
    }
}
Run Code Online (Sandbox Code Playgroud)

并像new B.Builder<String>().builder().builder()第一个builder()方法返回类型一样使用它,B.Builder但第二个builder()返回类型是A.Builder() 我读到 Java 中使用类型擦除实现的泛型,但这将如何导致这种效果?

rzw*_*oot 5

(注意:这之前被标记为重复,但链接到问题的答案并没有以任何方式回答这里发生的事情)。

问题是生的具有传染性

在 Java 中,“原始”类型是具有泛型的类型,但您不指定它们。例如,List x;是“原始类型”。你也可以有一个原始调用:new ArrayList()是原始的。

当你有一个原始类型时,与它的所有交互本身也被标记为原始类型,即使不需要 - 例如因为你锁定了它。B.Builder一旦你深入研究原始类型,你已经“锁定”了 A.Builder 的 T 的事实就会消失,这就是为什么你会在这里观察到奇怪的行为。这有复杂的向后兼容性原因,如今这些原因与 20 年前引入的泛型完全无关。唯一相关的教训是:[A] 从不使用任何原始内容,而 [B] 使用原始内容会导致编译器警告,请注意这些。你没有,在这里。首先摆脱原始警告,只有当您有任何剩余问题时,才四处询问:)

那么,这里发生了什么?

您的构建器类已参数化。它有一个<T>. 这是一个非常糟糕的名字;它是一个子类,A.Builder它也有一个名为 的类型变量T,但这两个类型变量是完全不相关的——它们具有相同的名称,这不会使它们成为相同的类型变量。因此,你所拥有的是一团混乱。我强烈建议您使用不同的字母,比方说U,以确保您永远不会在A.Builder's <T>(被锁定为B.Builder)和B.Builder's之间混淆<T>。我将做这个答案的其余部分。

T充满当你写的extends A.Builder<Builder>; 具体来说,你用 填充它Builder,它指的是B.Builder这是一个原始类型。哎呀。这就是问题所在。你永远B.Builder不应该在没有立即写入<>并选择性地在这些括号内填充一些东西的情况下进行写作。在这种情况下,您需要做的就是写extends A.Builder<Builder<T>>,尽管我会这样做A.Builder<B.Builder<T>>,只是为了清楚起见。

那为什么不起作用呢?

当您调用 时builder(),该方法被指定返回一个T(即 A.Builder 的T),它被设置为B.Builder,这是一个原始类型。然后你再次调用builder()这个原始类型,当你弄乱原始类型时,你总是只得到擦除的签名,即使JVM 拥有它需要的所有信息,它可以准确地知道任何给定的类型变量必然是什么。为什么?Spec 是这样说的(并且有充分的理由,但深入研究这些原因会使这个答案变得更长、更长。现在可以说,在泛型引入 20 年后,它已经无关紧要了)。

builder()方法的签名是public T builder(){}。(那就是A.Builder's <T>)。但是,我们在原始类型土地上,所以它T被擦除到它的擦除类型,这等于 的下限<T>。这T被声明为:<T extends A.Builder>因此,您正在调用的构建器方法的擦除版本是public A.Builder builder() {}这就是您得到的。是的,任何类型的实例都会B.Builder<WhateverGoesHereDoesNotMatter>将 T 锁定为存在B.Builder,但原始类型并不关心这一点,您会得到擦除的类型。时期。这是A.Builder。

没有选项可以告诉 java 不要这样做。目前还没有计划改变这一点。我将这种不断变化的可能性评为无穷小。

这让我们回到了建议:永远不要使用原始类型。它们会导致像这样的疯狂难题。

一旦你离开它们,它就会“修复”自己,通过制作那个extends A.Builder<B.Builder<T>>.

  • 因为表达式“new B.Builder&lt;String&gt;()”的类型是“B.Builder&lt;String&gt;”,而它本身并不是原始的。然后该 + `.builder()` 的表达式是原始的,因此调用该原始类型的方法会导致获得擦除的签名。 (2认同)