Java 泛型: 不能将 List < SubClass > 转换为 List < SuperClass > ?

只要遇到这个问题:

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

其中 DataNode 类型是 Tree 的子类型。

public class DataNode implements Tree

令我惊讶的是,这适用于 array:

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

这有点奇怪,有人能解释一下吗?

73376 次浏览

好吧,这里我要诚实地说: 懒惰的泛型实现。

没有任何语义上的理由不允许你的第一次装腔作势。

顺便说一句,尽管我很喜欢 C + + 中的模板,但是泛型,加上我们这里的愚蠢的限制,是我放弃 Java 的主要原因。

你在第二个例子中看到的是 数组协方差数组协方差。这是一件坏事 IMO,它使得数组中的赋值不安全——它们可能在执行时失败,尽管在编译时没有问题。

在第一种情况下,假设代码 是的是编译的,后面跟着:

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

你觉得会发生什么?

你可以这样做:

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

因为你只能从 b1取东西,而且它们保证与 Tree兼容。不能精确地调用 b1.add(...),因为编译器不知道它是否安全。

Have a look at 这是 Angelika Langer 的 Java 泛型常见问题解答的一部分 for more information.

DataNode 可能是 Tree 的子类型,但 List DataNode 不是 List Tree 的子类型。

Https://docs.oracle.com/javase/tutorial/extra/generics/subtype.html

List<DataNode> does not extend List<Tree> even though DataNode extends Tree. That's because after your code you could do b1.add(SomeTreeThatsNotADataNode), and that would be a problem since then a1 would have an element that is not a DataNode in it as well.

您需要使用通配符来实现类似的操作

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

这就是为什么当他们将泛型添加到集合中时,为了防止运行时错误,他们选择了稍微不同的方法。

It is the answer from C#, but I think it doesn't actually matter here, as the reason is the same.

”特别是,与数组类型不同,构造的引用类型不显示“协变”转换。这意味着类型 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

当数组被设计时(比如 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作为参数的方法)将导致编译错误。这样就不可能编译上述数组问题的等价物(没有不安全的强制转换)。

简短的解释: 最初为 Array 允许它是一个错误。

更长的解释是:

假设这是允许的:

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>类型,这很好。

简短的回答: 列表 a1与列表 b2的类型不同; 在 a1中,你可以放置任何扩展 DataNode 的对象类型 wichs,所以它可能包含树以外的其他类型。

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

假设您的第一个示例确实有效,那么您将能够执行以下操作:

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

但是由于 b1a1引用同一个对象,这意味着 a1现在引用一个同时拥有 DataNodeTreeList。如果尝试获取最后一个元素,将会得到一个异常(记不住是哪个)。

如果你确实需要从 List<DataNode>List<Tree>进行转换,而且你知道这样做是安全的,那么实现这一点的一个丑陋的方法就是进行双转换:

List<DataNode> a1 = new ArrayList<DataNode>();

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