泛型的泛型如何工作?

2022-09-02 01:25:19

虽然我确实了解泛型的一些极端情况,但我在下面的示例中遗漏了一些东西。

我有以下课程

1 public class Test<T> {
2   public static void main(String[] args) {
3     Test<? extends Number> t = new Test<BigDecimal>();
4     List<Test<? extends Number>> l =Collections.singletonList(t);
5   }
6 }

第 4 行给我错误

Type mismatch: cannot convert from List<Test<capture#1-of ? extends Number>> 
to List<Test<? extends Number>>`. 

显然,编译器认为不同之处并不真正相等。虽然我的直觉告诉我,这是正确的。?

任何人都可以举个例子,如果第4行是合法的,我会得到一个运行时错误?

编辑:

为了避免混淆,我将第 3 行中的 替换为具体任务=null


答案 1

正如肯尼在他的评论中指出的那样,你可以通过以下方式解决这个问题:

List<Test<? extends Number>> l =
    Collections.<Test<? extends Number>>singletonList(t);

这立即告诉我们,操作并非不安全,它只是有限推断的受害者。如果它不安全,上述内容将无法编译。

由于在上述泛型方法中使用显式类型参数只是为了充当提示,因此我们可以推测,这里需要它是推理引擎的技术限制。事实上,Java 8编译器目前计划对类型推断进行许多改进。我不确定您的具体案例是否会得到解决。

那么,到底发生了什么呢?

好吧,我们得到的编译错误表明,的类型参数被推断为 。换句话说,通配符具有一些与之关联的元数据,这些元数据将其链接到特定上下文。TCollections.singletonListcapture<Test<? extends Number>>

  • 将通配符 () 的捕获视为相同边界的未命名类型参数(即,但无法引用 )的最佳方法是。capture<? extends Foo><T extends Foo>T
  • “释放”捕获功能的最佳方法是将其绑定到泛型方法的命名类型参数。我将在下面的示例中对此进行演示。有关进一步阅读,请参阅 Java 教程“通配符捕获和帮助程序方法”(感谢@WChargin参考)。

假设我们想要一个移动列表的方法,将列表包装到后面。然后,让我们假设我们的列表具有未知(通配符)类型。

public static void main(String... args) {
    List<? extends String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
    List<? extends String> cycledTwice = cycle(cycle(list));
}

public static <T> List<T> cycle(List<T> list) {
    list.add(list.remove(0));
    return list;
}

这工作正常,因为 解析为 ,而不是 。如果我们改用这个循环的非通用实现:Tcapture<? extends String>? extends String

public static List<? extends String> cycle(List<? extends String> list) {
    list.add(list.remove(0));
    return list;
}

它将无法编译,因为我们没有通过将捕获分配给类型参数来使其可访问。

因此,这开始解释为什么 的使用者会从类型推断器解析为 中受益,从而返回 a 而不是 a。singletonListTTest<capture<? extends Number>List<Test<capture<? extends Number>>>List<Test<? extends Number>>

但是,为什么一个不能分配给另一个呢?

为什么我们不能只将 a 分配给 ?List<Test<capture<? extends Number>>>List<Test<? extends Number>>

好吧,如果我们考虑一个事实,它等效于一个上限为 的匿名类型参数,那么我们可以把这个问题变成“为什么下面的代码不编译?(它没有!capture<? extends Number>Number

public static <T extends Number> List<Test<? extends Number>> assign(List<Test<T>> t) {
    return t;
} 

这是不编译的一个很好的理由。如果是这样,那么这将是可能的:

//all this would be valid
List<Test<Double>> doubleTests = null;
List<Test<? extends Number>> numberTests = assign(doubleTests);

Test<Integer> integerTest = null;
numberTests.add(integerTest); //type error, now doubleTests contains a Test<Integer>

那么,为什么显式工作有效呢?

让我们回到开头。如果以上是不安全的,那么为什么这是允许的:

List<Test<? extends Number>> l =
    Collections.<Test<? extends Number>>singletonList(t);

为此,这意味着允许以下内容:

Test<capture<? extends Number>> capturedT;
Test<? extends Number> t = capturedT;

好吧,这不是有效的语法,因为我们不能显式引用捕获,所以让我们使用与上面相同的技术来评估它!让我们将捕获绑定到“赋值”的不同变体:

public static <T extends Number> Test<? extends Number> assign(Test<T> t) {
    return t;
} 

这将成功编译。不难看出为什么它应该是安全的。这是类似的东西的用例

List<? extends Number> l = new List<Double>();

答案 2

没有潜在的运行时错误,它只是超出了编译器静态确定这一点的能力。每当引起类型推断时,它都会自动生成 一个新的捕获,并且两个捕获不被视为等效。<? extends Number>

因此,如果您通过指定单例列表从调用中移除推理:<T>

List<Test<? extends Number>> l = Collections.<Test<? extends Number>>singletonList(t);

它工作正常。生成的代码与调用合法没有什么不同,它只是编译器的一个限制,它无法自己弄清楚。

推理创建捕获且捕获不兼容的规则阻止了本教程示例在运行时编译然后爆炸:

public static void swap(List<? extends Number> l1, List<? extends Number> l2) {
    Number num = l1.get(0);
    l1.add(0, l2.get(0));
    l2.add(0, num);
}

是的,语言规范和编译器可能会变得更加复杂,以区分您的示例,但事实并非如此,而且它非常简单。


推荐