遇到这个问题:
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
这有点奇怪。有人能解释一下吗?
如果有人有时间找到要链接的最佳问题,这个问题有100%的可能性是重复的。
在第二个例子中,我们看到的是数组协方差。这是一件糟糕的事情,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的这一部分以获取更多信息。
啊…这项工作:列表<?扩展树>
谢谢你的解释。您能举一个"在数组中不安全-它们在执行时可能失败,尽管在编译时很好。"的例子吗?--忽略它。我举了个例子。
@无声之歌:是的,基本上和我的"破碎"示例一样,但是在数组中设置一个值:)
我遇到了一个类似的情况,在经过多次搜索之后,我正要发布一个新的Q,但是遇到了这个问题。我尝试了一个独立的Java类,它具有:1。列表<新的AARYLIST < String >();X=新的ArayList.String>();ANK到Java泛型FAQ解决了这个谜!:)
List extends Tree> 真的很棘手。
@CDT:以什么方式?我不知道你的评论想说什么。
作为JavaNeBee的J.JOnSwitt,我从来没有见过这样的使用EDOCX1 1。所以这对我来说有点棘手。
@CDT:啊,我明白了。我建议你读一个Java泛型教程然后你会看到很多:
@如果我没搞错的话, extends T> 说我们可以把任何属于t子类型的东西放在一个集合中。那么,如何将子类t的两个列表合并成t的一个列表呢?
@查尔斯:对不起,我不知道你在问什么。 extends T> 的意思是"它是某种未知类型,它是t的一个亚型"。
@乔斯基特,你很明白。那么假设U extends T 和V extends T ,如果我们有一个List 和一个List ,我们可以将它们合并到List 中吗?
@查尔斯:是的,那应该是可行的……如果你很难做到这一点,请详细地问一个新问题。
如果你必须从List 到List ,并且你知道这样做是安全的,那么实现这一点的一个丑陋的方法是进行双重投射:
List a1 = new ArrayList();
List b1 = (List) (List extends Tree>) 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 extends Tree> ,这样就可以了。
虽然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被设计的时候),开发人员决定方差是有用的,所以他们允许。然而,这个决定经常受到批评,因为它允许你这样做(假设NotADataNode 是Tree 的另一个子类):
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 extends Tree> b1 = a1; 。
但是,如果使用后者,尝试使用add 或set 方法(或任何其他以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( ) ) ;
但是,由于b1 和a1 指的是同一个对象,这意味着a1 现在指的是同时持有DataNode 和Tree 的List 。如果你试图获得最后一个元素,你会得到一个例外(不记得是哪一个)。
这与类型擦除无关。.NET泛型没有类型擦除,但它们有相同的基本问题。我想,如果没有类型擦除,在添加时可能会有一个执行时异常——但是由于泛型的要点是在编译时保证类型安全,所以它在这里并没有太大的区别。
好吧,我在这里说实话:惰性的泛型实现。
没有语义上的理由不允许你的第一次做作。
顺便说一下,虽然我喜欢C++中的模板,但是泛型和我们这里的愚蠢限制是我放弃Java的主要原因。
我应该预见到投票结果的负面。
你的解释是错误的,这就是为什么你被否决的原因(不是我,我没有麻烦)。现在我们来猜测你是否理解Java泛型,因为你过早放弃Java,或者放弃Java,因为你不懂泛型。
如果你不给出答案,你只是陈述你对形势的看法,那么就应该投反对票。这在任何方面都没有帮助。
反对票是赞成"没有语义上的理由不允许你的第一次做作"。很明显,看其他答案。否决投票不是为了抨击Java,而是在技术问题上不正确。
@朱利安,也许是的-乔恩清楚地解释了为什么这种限制一点也不愚蠢。
我只是不接受通用性的实现,即苹果列表不是水果列表的"子案例"。当然,它让编译器开发人员的生活变成了一场噩梦,因为你不能在原始列表中添加除苹果以外的任何东西…这是个问题,为什么?因为在Java中,没有办法使一个对象真正不可变(是的,去链接,我可以揭穿任何不可变的实现,你也可以这样做)。我可以不断地讲述Java,它的设计是如何有缺陷的,以及每个决策是如何相互影响的。问题是,这里的大多数人都知道仿制药,而不是仿制药。
因此,简而言之,我坚持我的说法:没有语义上的理由不允许第一次做作。由于语言设计的局限性,这是被禁止的。现在,假设它是主观的,你想要的一切,如果你想一分钟,你会发现它不是。
@朱利安:不,我已经考虑过了,考虑到List 代表一个可以添加元素的可变类型,我仍然认为这种限制是有意义的。如果您愿意的话,可以将其称为糟糕的设计决策,但是一旦做出了该决策,就可以通过防止向其中添加错误类型的元素来保持类型安全性。
@朱利安:如果你想保持集合对象是可变的(而不是你有选择的),同时又想确保它实际上只包含一种特定类型的对象呢?
@Jonskeet,@bobturbo:整个问题都是关于对象的可变性,因此我为什么在自己的评论中谈论它:它会对泛型的实现方式产生那么大的影响吗?大多数情况下,你需要这种类型的演员给第三方看名单。可悲的是,问题的答案是不可变的对象,不是可变的和不可读的<?扩展水果>,因为我们都知道使用这种泛型过滤器可以获得多么难看的代码。乔恩,不要因为你的回答而放弃,但在我看来,它缺少了真正的问题:语言缺乏不可变性。
@朱利安:为了安全,你不需要什么东西是不变的。(我不是说不变不是一件好事,但这里不需要)看看c 4的方差,它只有在安全的情况下才被允许——你可以从IEnumerable 转换成IEnumerable ,但不能从IList 转换成IList 。如果调用者只想读取列表,那么它将声明一个IEnumerable 类型的参数。.NET集合并不像我希望的那样整齐地划分为读/写,但它是IMO的一种有效的替代方法。
@朱利安:而且,我认为所有这些都违背了你的说法,即"没有语义上的理由不允许你的第一次做作"——语义上的原因是List 是可变的,因此变得不安全。
@乔恩:你事实上证明了我的观点:当另一种语言有它的方法时,它是如何成为一个语义问题的?您可以在不可变性的情况下解决这个问题btw:非常繁重地克隆集合,或者更微妙地在尝试添加除原始类型参数的实例之外的任何其他内容时引发运行时异常。同样,由于爪哇的设计限制,在假装上的隐式克隆是不可行的,运行时类型参数控制也是如此(因为编译后的信息丢失了,除非上次检查后发生了更改)…
…也许集合应该是像数组一样的一流公民,但这完全是另一个争论,会导致运算符过载和其他像我这样"不懂泛型"的人肯定无法理解的"高级"主题。我赞同我的说法,Java泛型在设计和实现中是如此有限和一年级学生没有任何语义上的理由。他们为了安全感而牺牲了表现力,同时把自己的局限性隐藏在神秘的数学表达式之后。扩展业余<?扩大业余爱好者的范围。
@朱利安:对于其他的API,其他语言有其他的绕过它的方法。您不能用C(IList 中的等效API来实现这一点,原因完全相同。我认为设计仿制药以开始在各地克隆列表是一个糟糕的决定。即使执行时间检查是可用的,它们也会在很大程度上击败使用泛型的能力——编译时类型的安全性。我坚持自己的观点,在Java设计方法的其余部分中,围绕泛型转换的决策失败是完全正确的,与懒惰无关。
我认为如果你在这里定义"语义原因",它也会很有帮助。你使用这个词的方式非常多,听起来对你很重要,所以如果我们确定我们都以同样的方式理解它,这将是有用的。
乔恩:我很高兴你喜欢Java是如何设计的,我不是。语义:关于或与意义有关的,特别是在语言中的意义。看:一篮子苹果是一篮子水果,所以你应该能够写:篮子<苹果>A;篮子<水果>F=A;而不必写篮子<?延伸果实>f=a;。宣布一篮子"水果"而不是一篮子水果是什么意思?没有,这是一个解决方案,因为糟糕的设计决策和实现。当我意识到我必须把清单当我放弃Java时,扩展T>代替列表。
@乔恩,现在这段对话很快就变成了一场"谁会得到最后一个词"的比赛,我觉得这很令人沮丧,而且适得其反。我将尝试向您解释为什么Java的泛型实现和设计是愚蠢的限制,您将声明Java是这样的,这在上下文中是有意义的。为了回到我的第一条评论,我被一些人投了反对票,他们认为我对这个话题一无所知,我只是为了大声说话而已。如果他们现在看不出他们错了,那么,他们永远不会错的。
所以你只是假设任何不同意你的人都是明显错误的?虽然我没有投你反对票,但我没有理由认为那些反对你的人表达了对你的任何看法——他们不同意你在回答中的陈述。我也不同意这些说法。我非常同意Java的泛型在很多方面都是有限的,我喜欢泛化的泛型,使用它们的本原类型等等的能力。但我只是不同意你为这个案子所张贴的内容。我很高兴同意不同意,但请不要光顾那些不同意你的人。
你误会我了,乔恩。请阅读我上一篇文章的最后几句话。意思很清楚:如果他们看不出他们是错的,假设我对仿制药一无所知,他们就永远不会(见第二条评论,这条评论被意外地提高了3倍)。这种误解和你问我语义的定义(从在线词典中复制/粘贴,仅供参考)并没有那么好的味道,但我很难想象有一个20万左右的人这样的名声大打折扣。还是我应该?不过,对上一篇文章中提出的问题进行了巧妙的回避。就像我说的:令人沮丧。