Java泛型:Java Generics:无法将List 强制转换为List

Java Generics: Cannot cast List<SubClass> to List<SuperClass>?

本问题已经有最佳答案,请猛点这里访问。

遇到这个问题:

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

其中类型datanode是树的子类型。

1
public class DataNode implements Tree

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

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

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


在第二个例子中,我们看到的是数组协方差。这是一件糟糕的事情,IMO会使数组中的赋值不安全——尽管在编译时很好,但在执行时它们可能会失败。

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

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

你希望会发生什么?

您可以这样做:

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

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

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


如果你必须从ListList,并且你知道这样做是安全的,那么实现这一点的一个丑陋的方法是进行双重投射:

List a1 = new ArrayList();

List b1 = (List) (List) a1;


简短的解释:最初允许它用于数组是一个错误。

更长的解释:

假设允许这样做:

1
2
List<DataNode> a1 = new ArrayList<DataNode>();
List<Tree> b1 = a1;  // pretend this is allowed

那我就不能继续:

1
2
3
4
5
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不允许对泛型参数进行这种类型的方差表示(*),但即使是这样,如果EDCOX1引用4和EDCOX1 5相同,则不能将EDCOX1的2个Ed投给EDCOX1×0。

(*)也就是说,在编写类时不允许这样做。您可以声明变量的类型为List,这样就可以了。


虽然DataNode扩展了Tree,但List并不扩展List。这是因为在你的代码之后,你可以做b1.add(sometrethatsnotadatanode),这将是一个问题,因为a1将有一个元素,它也不是一个数据节点。

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

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

另一方面,DataNode[]确实扩展了Tree[]。当时,这似乎是合乎逻辑的事情,但你可以这样做:

1
2
3
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

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


当阵列被设计时(也就是说,当Java被设计的时候),开发人员决定方差是有用的,所以他们允许。然而,这个决定经常受到批评,因为它允许你这样做(假设NotADataNodeTree的另一个子类):

1
2
3
DataNode[] a2 = new DataNode[1];
Tree[] b2 = a2;   // this is okay
b2[0] = new NotADataNode(); //compiles fine, causes runtime error

因此,在设计泛型时,决定了泛型数据结构应该只允许显式的差异。也就是说,你不能做List b1 = a1;,但你可以做List b1 = a1;

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


简短回答:列表A1与列表B2的类型不同;在A1中,可以放置任何ObjectType wich扩展数据节点。所以它可能包含除树之外的其他类型。


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

特别是,与数组类型不同,构造的引用类型不显示"协变"转换。这意味着类型列表没有到列表的转换(隐式或显式),即使b是从a派生的。同样,从列表到列表的转换也不存在。

其基本原理很简单:如果允许转换到list中,那么显然可以将a类型的值存储到list中。但这会破坏类型列表中的每个对象始终是类型B的值的不变量,否则在分配到集合类时可能会发生意外的失败。"

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


datanode可能是树的子类型,但list datanode不是list tree的子类型。

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


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

假设您的第一个示例确实有效。然后您可以执行以下操作:

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

但是,由于b1a1指的是同一个对象,这意味着a1现在指的是同时持有DataNodeTreeList。如果你试图获得最后一个元素,你会得到一个例外(不记得是哪一个)。


好吧,我在这里说实话:惰性的泛型实现。

没有语义上的理由不允许你的第一次做作。

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