Java Generics:无法将List <subclass>强制转换为List <superclass>?</superclass> </subclass>

时间:2010-07-14 12:39:16

标签: java generics

刚遇到这个问题:

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

类型DataNode是Tree的子类型。

public class DataNode implements Tree

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

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

这有点奇怪。谁能对此作出解释?

10 个答案:

答案 0 :(得分:112)

你在第二种情况下看到的是数组协方差。 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(...)因为编译器不知道它是否安全。

有关详细信息,请查看this section of Angelika Langer's Java Generics FAQ

答案 1 :(得分:16)

如果您必须从List<DataNode>投射到List<Tree>,并且您知道这样做是安全的,那么实现这一目标的一种丑陋方式是进行双重投射:

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

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

答案 2 :(得分:15)

简短说明:最初允许它用于阵列是错误的。

更长的解释:

假设允许这样做:

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

答案 3 :(得分:9)

即使List<DataNode>扩展List<Tree>

DataNode也不会延伸Tree。那是因为在你的代码之后你可以做b1.add(SomeTreeThatsNotADataNode),那将是一个问题,因为那时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

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

答案 4 :(得分: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作为参数的任何其他方法)将导致编译错误。这样就无法编译上述数组问题的等价物(没有不安全的强制转换)。

答案 5 :(得分:2)

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

答案 6 :(得分:1)

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

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

答案 7 :(得分:1)

这是C#的答案,但我认为这并不重要,因为原因是一样的。

“特别是,与数组类型不同,构造的引用类型不会表现出”协变“转换。这意味着类型List&lt; B&gt;没有转换(隐式或显式)到List&lt; A&gt;即使B是派生的来自A.同样,List&lt; B&gt;到List&lt; object&gt;不存在转换。

这个的基本原理很简单:如果转换为List&lt; A&gt;允许,然后显然可以将类型A的值存储到列表中。但是这会破坏List&lt; B&gt;类型列表中的每个对象的不变量。始终是B类型的值,否则在分配到集合类时可能会发生意外故障。“

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

答案 8 :(得分:0)

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

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

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

但是,由于b1a1引用同一个对象,这意味着a1现在指的是同时包含ListDataNode的{​​{1}} {1}}秒。如果你试图获得最后一个元素,你将得到一个例外(不记得是哪一个)。

答案 9 :(得分:-3)

好吧,我在这里说实话:懒惰的通用性实现。

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

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