Java泛型:无法将List<SubClass>强制转换为List<SuperClass>?

131

刚遇到了这个问题:

List<DataNode> a1 = new ArrayList<DataNode>();
List<Tree> b1 = a1;  // compile error: incompatible type

类型DataNode是树的子类型。

public class DataNode implements Tree

令我惊讶的是,这对数组也有效:

DataNode[] a2 = new DataNode[0];
Tree[] b2 = a2;   // this is okay

这看起来有点奇怪。有谁能给出一个解释吗?

10个回答

144

第二种情况中出现的是数组型变。这是我个人认为的一个坏事,因为它使得数组内部的赋值不安全——尽管在编译时没问题,但在运行时可能会失败。

在第一种情况中,想象一下如果代码确实编译通过,并接着执行以下代码:

b1.add(new SomeOtherTree());
DataNode node = a1.get(0);

你期望会发生什么?

你可以这样做:

List<DataNode> a1 = new ArrayList<DataNode>();
List<? extends Tree> b1 = a1;

因为你只能从 b1 获取东西并且它们保证与 Tree 兼容,所以这样做是正确的。你不能调用 b1.add(...),因为编译器不知道它是否安全。

请查看 Angelika Langer 的 Java 泛型 FAQ 中的此部分 以获取更多信息。


20
啊..!这个可以工作:List<? extends Tree> - SiLent SoNG
1
谢谢你的解释。你能给一个“在数组内不安全 - 尽管在编译时是正确的,但在执行时可能会失败”的例子吗?-- 不用了,我明白了。 - SiLent SoNG
1
@CDT:啊,我明白了。我建议你读一下Java泛型教程 - 你会经常看到它的 :) - Jon Skeet
@JonSkeet 如果我没有错的话,<? extends T> 表示我们可以将任何 T 子类型放入一个集合中。那么如何将两个子类 T 的列表合并为一个 T 列表呢? - Charles Follet
@CharlesFollet:抱歉,我不确定你的问题是什么。 <? extends T> 表示“它是一些未知类型,它是 T 的子类型”。 - Jon Skeet
显示剩余7条评论

36
如果你确实需要将List<DataNode>转换为List<Tree>,并且你知道这样做是安全的,那么一种丑陋的实现方法是进行双重转换: List<DataNode> a1 = new ArrayList<DataNode>(); List<Tree> b1 = (List<Tree>) (List<? extends Tree>) a1;

20
短篇解释:最初允许数组进行此操作是错误的。
长篇解释:
假设允许如下操作:
List<DataNode> a1 = new ArrayList<DataNode>();
List<Tree> b1 = a1;  // pretend this is allowed

那么我可以进行如下操作:

接着我能够:

b1.add(new TreeThatIsntADataNode()); // Hey, b1 is a List<Tree>, so this is fine

for (DataNode dn : a1) {
  // Uh-oh!  There's stuff in a1 that isn't a DataNode!!
}

现在,一个理想的解决方案应该允许你使用只读的 List 变体时获得所需的类型转换,但当使用像 List 这样的读写接口时则不允许。然而,Java 在泛型参数上不允许这种变异符号注释,(*) 即使它允许,你也不能将 List<A> 强制转换为 List<B>,除非 AB 完全相同。

(*) 也就是说,在编写类时不允许。但你可以声明变量的类型为 List<? extends Tree>,这是可以的。


10

List<DataNode>并不扩展List<Tree>,即使DataNode扩展了Tree。这是因为在您的代码之后,您可以执行,这将是一个问题,因为a1中将有一个不是DataNode的元素。

您需要使用通配符来实现类似于此的功能。

List<DataNode> a1 = new ArrayList<DataNode>();
List<? extends Tree> b1 = a1;
b1.add(new Tree()); // compiler error, instead of runtime error

另一方面,DataNode[]确实扩展了Tree[]。当时这似乎是合理的做法,但你可以尝试以下代码:
DataNode[] a2 = new DataNode[1];
Tree[] b2 = a2; // this is okay
b2[0] = new Tree(); // this will cause ArrayStoreException since b2 is actually a DataNode[] and can't store a Tree

这就是为什么在向集合添加泛型时,它们选择以稍微不同的方式来防止运行时错误的原因。

7
在设计数组时(即基本上在Java设计时),开发人员决定方差是有用的,因此允许它。然而,这个决定经常受到批评,因为它允许您进行这样的操作(假设NotADataNodeTree的另一个子类):
DataNode[] a2 = new DataNode[1];
Tree[] b2 = a2;   // this is okay
b2[0] = new NotADataNode(); //compiles fine, causes runtime error

因此,在设计通用数据结构时,决定只允许显式的差异性。即使你不能做到 List<Tree> b1 = a1;,但是可以实现 List<? extends Tree> b1 = a1;

但是,如果你使用后者,尝试使用 addset 方法(或任何其他以 T 作为参数的方法)将导致编译错误。这样就不可能编译等效于上面数组问题的代码(不使用不安全转换)。


1
这解释了数组和泛型之间的设计差异。看完Jon Skeet的解释后,这正是我想问的!感谢您的回答。 - SiLent SoNG

2

简短回答: 列表a1与列表b2不是相同类型; 在a1中,您可以放置任何扩展DataNode的对象类型。因此,它可能包含除Tree之外的其他类型。


1

1

这是来自C#的答案,但我认为在这里实际上并不重要,因为原因是相同的。

“特别地,与数组类型不同,构造的引用类型不表现出“协变”转换。这意味着类型List<B>没有到List<A>的转换(无论是隐式还是显式),即使B是从A派生而来。同样,从List<B>到List<object>也不存在转换。

这样做的理由很简单:如果允许转换为List<A>,那么显然可以将类型A的值存储到列表中。但这将打破每个对象都是类型List<B>中类型B的值的不变性,否则在分配到集合类时可能会发生意外的失败。”

http://social.msdn.microsoft.com/forums/en-US/clr/thread/22e262ed-c3f8-40ed-baf3-2cbcc54a216e


1
原因有点相同 - 但Java允许以不同形式出现的差异。 - Jon Skeet

0

这是一个使用类型擦除实现泛型的经典问题。

假设你的第一个例子确实可以工作。那么你就能够执行以下操作:

List<DataNode> a1 = new ArrayList<DataNode>();
List<Tree> b1 = a1;  // suppose this works
b1.add(new Tree());

但是由于b1a1引用同一个对象,这意味着a1现在引用了一个包含DataNodeTreeList。如果您尝试获取最后一个元素,将会抛出一个异常(无法记住是哪个异常)。


2
这并不真的涉及到类型擦除。.NET泛型没有类型擦除,但它们有相同的基本问题。我想,如果没有类型擦除,你在添加时 可能 会有一个执行时异常 - 但是由于泛型的重点是在 编译 时实现类型安全,所以这里并没有太大区别。 - Jon Skeet
九年后,Scala的类型系统更加健全,不再使用特别设计的、隐式的或者简单破损的类型变量。对于初学者来说,隐藏变量可能是一件好事,但很快你就会意识到它们每天都会出现,我们需要明确地表达它们。 - Nader Ghanbari

-3

说实话,我觉得这里的泛型实现有点懒。

从语义上讲,没有理由不允许你的第一个赋值。

顺便提一下,虽然我很喜欢C++中的模板,但是泛型和我们在这里遇到的愚蠢限制是我放弃Java的主要原因。


5
你的解释是错误的,这就是为什么你被投票降低评分(不是我,我也没有费心思)。现在我们只能猜测你是因为过早放弃了Java才不理解Java泛型,还是因为不理解泛型而放弃了Java。 - Carl Smotricz
2
预计会有一些人负评您,当您不提供答案而只是陈述自己对情况的看法时。这种做法对任何人都没有帮助。 - HoLyVieR
1
@Julian:不,我已经考虑过了,我仍然认为这个限制是有道理的,因为 List<T> 表示可以添加元素的可变类型。如果您想要的话,您可以称其为一个糟糕的设计决策,但一旦做出了这个决策,通过防止添加错误类型的元素来保持类型安全是非常合理的。 - Jon Skeet
1
@JonSkeet,@BobTurbo:整个问题在于对象的可变性,这就是为什么我在我的评论中谈论它的原因:它是否应该对泛型的实现产生如此重大的影响?大多数情况下,您需要这种类型的转换来提供给第三方,该第三方将通过列表进行读取。悲伤的事实是,问题的答案是不可变的对象,而不是可变且无法读取的<?extends Fruit>,因为我们都知道这种泛型过滤器可以使代码变得多么丑陋。不是要否定你的回答,Jon,但对我来说,它忽略了真正的问题:语言缺乏不可变性。 - Julian Aubourg
1
也许集合应该像数组一样成为一等公民,但这是另一个完全不同的辩论,并且会导致运算符重载和其他“高级”主题,对于像我这样“不理解泛型”的人来说,肯定无法掌握。我坚持认为,Java泛型没有语义上的限制,设计和实现上却像小学生一样简单。他们为了感知安全性而牺牲了表现力,同时将其局限性隐藏在类似于数学的加密表达式<? extends Amateur<? extends Amateurs>>之后。 - Julian Aubourg
显示剩余15条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接