Java的generics:不能强制List <SubClass>列表<SuperClass>?
刚刚遇到这个问题:
List<DataNode> a1 = new ArrayList<DataNode>(); List<Tree> b1 = a1; // compile error: incompatible type
typesDataNode是Tree的子types。
public class DataNode implements Tree
令我惊讶的是,这对arrays起作用:
DataNode[] a2 = new DataNode[0]; Tree[] b2 = a2; // this is okay
这有点奇怪 任何人都可以对此作出解释吗?
你在第二种情况下看到的是arrays协方差 。 这是一个坏的事情,国际海事组织,使arrays内的任务不安全 – 他们可以在执行时间,尽pipe在编译时罚款失败。
在第一种情况下,假设代码编译完成,然后是:
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 Generics FAQ的这一部分 。
简单的解释:原来允许数组是一个错误。
更长的解释:
假设这是允许的:
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!! }
现在,一个理想的解决scheme将允许使用List
为只读的变体时所需的types,但在使用可读写的接口(如List
)时会禁止该types。 Java不允许在generics参数(*)上使用这种方差符号,但即使它不能将List<A>
转换为List<B>
除非A
和B
是相同的。
(*)也就是说,写作课时不允许。 你可以声明你的variables的types为List<? extends Tree>
List<? extends Tree>
,这很好。
即使DataNode
扩展了Tree
List<DataNode>
也不会扩展List<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
这就是为什么当他们将generics添加到集合时,他们select稍微不同的方式来防止运行时错误。
如果您必须从List<DataNode>
为List<Tree>
,并且您知道这样做是安全的,那么实现这一目的的一个难看的方法是做一个双重投射:
List<DataNode> a1 = new ArrayList<DataNode>();
List<Tree> b1 = (List<Tree>) (List<? extends Tree>) a1;
当数组被devise的时候(也就是在devisejava的时候),开发人员决定这些变化是有用的,所以他们允许它。 然而,这个决定经常被批评,因为它允许你这样做(假设NotADataNode
是Tree
另一个子类):
DataNode[] a2 = new DataNode[1]; Tree[] b2 = a2; // this is okay b2[0] = new NotADataNode(); //compiles fine, causes runtime error
所以当devisegenerics时,决定了generics数据结构应该只允许显式差异。 即你不能做List<Tree> b1 = a1;
,但你可以做List<? extends Tree> b1 = a1;
List<? extends Tree> b1 = a1;
。
但是,如果你使用后者,试图使用add
或set
方法(或其他任何以T
作为参数的方法)将导致编译错误。 这样就不可能使上面的数组问题相当于编译(没有不安全的转换)。
简短回答:列表a1与列表b2不是一样的types; 在a1中,你可以把任何对象types扩展到DataNode。 所以它可能包含除Tree以外的其他types。
这是来自C#的答案,但我认为这并不重要,原因是一样的。
“特别是,与数组types不同,构造的引用types不会显示”协变“转换,这意味着即使B是从A派生的,typesList <B>也没有转换(隐式或显式)到List <A>。同样,从List <B>到List <object>也不存在任何转换。
理由很简单:如果允许转换到List <A>,那么显然可以将Atypes的值存储到列表中。 但是,这将打破List <B>types的列表中的每个对象始终是typesB的值的不variables,否则在分配到集合类时可能会发生意外的故障。
http://social.msdn.microsoft.com/forums/en-US/clr/thread/22e262ed-c3f8-40ed-baf3-2cbcc54a216e
DataNode可能是Tree的子types,但List DataNode不是List Tree的子types。
https://docs.oracle.com/javase/tutorial/extra/generics/subtype.html
genericstypes擦除是一个典型的问题。
假设你的第一个例子确实有效。 您将可以执行以下操作:
List<DataNode> a1 = new ArrayList<DataNode>(); List<Tree> b1 = a1; // suppose this works b1.add(new Tree());
但是由于b1
和a1
引用的是同一个对象,这意味着a1
现在引用一个包含DataNode
和Tree
的List
。 如果你试图获得最后一个元素,你会得到一个exception(不记得哪一个)。
那么,我会在这里诚实:懒惰的通用性实现。
没有任何语义上的原因不允许你的第一个作为。
顺便说一句,虽然我很喜欢C ++的模板,但是generics,以及我们在这里的那种愚蠢的限制,是我放弃Java的主要原因。