why polymorphism doesn't treat generic collections and plain arrays the same way?
假设类狗延伸类动物:为什么不允许使用此多态语句:
1 | List<Animal> myList = new ArrayList<Dog>(); |
但是,允许使用普通数组:
1 | Animal[] x=new Dog[3]; |
这是基于Java如何实现泛型的原因。好的。
数组示例好的。
使用数组可以做到这一点(数组是协变的,正如其他人所解释的那样)好的。
但是,如果你尝试这样做会发生什么?好的。
1 |
最后一行编译得很好,但是如果运行此代码,可以得到一个
这意味着您可以愚弄编译器,但不能愚弄运行时类型系统。这是因为数组是我们所说的可重设类型。这意味着,在运行时Java知道这个数组实际上是实例化的整数数组,它恰好通过EDCOX1(1)类型的引用访问。好的。
所以,正如你所看到的,一件事是对象的实际类型,另一件事是你用来访问它的引用的类型,对吗?好的。
Java泛型的问题好的。
现在,Java泛型类型的问题是,类型信息被编译器丢弃,并且在运行时不可用。这个过程称为类型擦除。在Java中实现类似这样的泛型是有充分理由的,但这是一个很长的故事,它与二进制代码兼容性与预先存在的代码有关。好的。
但这里重要的一点是,由于在运行时没有类型信息,因此无法确保我们没有提交堆污染。好的。
例如,好的。
1 2 3 4 5 6 | List<Integer> myInts = new ArrayList<Integer>(); myInts.add(1); myInts.add(2); List<Number> myNums = myInts; //compiler error myNums.add(3.14); //heap polution |
如果Java编译器不阻止您这样做,运行时类型系统也不能阻止您,因为在运行时没有办法确定该列表仅是整数列表。Java运行时会让你把任何你想要的东西放进这个列表中,当它只包含整数时,因为当它被创建时,它被声明为整数列表。好的。
这样,Java的设计者就确保了你不能愚弄编译器。如果你不能欺骗编译器(就像我们对待数组一样),你也不能欺骗运行时类型系统。好的。
因此,我们认为泛型类型是不可重设的。好的。
显然,这会阻碍多态性。请考虑以下示例:好的。
1 2 3 4 5 6 7 |
现在您可以这样使用它:好的。
1 2 3 4 5 6 7 |
但是,如果尝试用泛型集合实现相同的代码,则不会成功:好的。
1 2 3 4 5 6 7 | static long sum(List<Number> numbers) { long summation = 0; for(Number number : numbers) { summation += number.longValue(); } return summation; } |
如果你试图…好的。
1 2 3 4 5 6 7 | List<Integer> myInts = asList(1,2,3,4,5); List<Long> myLongs = asList(1L, 2L, 3L, 4L, 5L); List<Double> myDoubles = asList(1.0, 2.0, 3.0, 4.0, 5.0); System.out.println(sum(myInts)); //compiler error System.out.println(sum(myLongs)); //compiler error System.out.println(sum(myDoubles)); //compiler error |
解决方案是学会使用Java泛型的两个强大的特征,即协方差和逆变。好的。
协方差好的。
使用协方差,您可以从结构中读取项,但不能在其中写入任何内容。所有这些都是有效的声明。好的。
1 2 3 | List<? extends Number> myNums = new ArrayList<Integer>(); List<? extends Number> myNums = new ArrayList<Float>() List<? extends Number> myNums = new ArrayList<Double>() |
你可以从
1 |
因为您可以确保无论实际列表包含什么,都可以将其转换为一个数字(扩展数字的所有内容都是一个数字,对吗?)好的。
但是,不允许将任何内容放入协变结构中。好的。
1 | myNumst.add(45L); //compiler error |
这是不允许的,因为Java不能保证泛型结构中对象的实际类型。它可以是扩展数字的任何东西,但编译器不能确定。所以你可以读,但不能写。好的。
逆变好的。
相反,你可以做相反的事情。您可以将事物放入通用结构中,但不能从中读出。好的。
1 2 3 4 5 6 7 | List<Object> myObjs = new List<Object(); myObjs.add("Luke"); myObjs.add("Obi-wan"); List<? super Number> myNums = myObjs; myNums.add(10); myNums.add(3.14); |
在这种情况下,对象的实际性质是一个对象列表,通过反差,您可以将数字放入其中,基本上是因为所有的数字都将对象作为它们的共同祖先。因此,所有数字都是对象,因此这是有效的。好的。
但是,假设您将得到一个数字,那么您就不能安全地从这个逆向结构中读取任何内容。好的。
1 |
如您所见,如果编译器允许您编写这一行,那么您将在运行时得到一个ClassCastException。好的。
获取/输出原则好的。
因此,当您只打算将泛型值从结构中取出时,请使用协方差;当您只打算将泛型值放入结构中时,请使用协方差;当您打算同时执行这两种操作时,请使用完全相同的泛型类型。好的。
我有一个最好的例子,就是将任何类型的数字从一个列表复制到另一个列表中。它只从源中获取项目,并且只将项目放入命运中。好的。
1 2 3 4 5 | public static void copy(List<? extends Number> source, List<? super Number> destiny) { for(Number number : source) { destiny.add(number); } } |
由于协方差和反方差的力量,这种方法适用于这样的情况:好的。
1 2 3 4 5 6 | List<Integer> myInts = asList(1,2,3,4); List<Double> myDoubles = asList(3.14, 6.28); List<Object> myObjs = new ArrayList<Object>(); copy(myInts, myObjs); copy(myDoubles, myObjs); |
好啊。
Arrays differ from generic types in two important ways. First, arrays are covariant.
This scary-sounding word means simply that if Sub is a subtype of Super, then the
array type Sub[] is a subtype of Super[]. Generics, by contrast, are invariant: for
any two distinct types Type1 and Type2, Listis neither a subtype nor a
supertype of List. [..]The second major difference between arrays and generics is that arrays are
reified [JLS, 4.7]. This means that arrays know and enforce their element types at
runtime.[..]Generics, by contrast, are implemented by erasure
[JLS, 4.6]. This means that they enforce their type constraints only at compile
time and discard (or erase) their element type information at runtime. Erasure is
what allows generic types to interoperate freely with legacy code that does not use
generics (Item 23).
Because of these fundamental differences, arrays and generics do not mix
well. For example, it is illegal to create an array of a generic type, a parameterized
type, or a type parameter. None of these array creation expressions are legal: new
List[], new List [], new E[]. All will result in generic array creation
errors at compile time.[..]
普伦蒂斯霍尔-有效Java第二版
最终的答案是这样的,因为Java是这样指定的。更确切地说,因为这就是Java规范发展的方式*。
我们不能说Java设计者的实际想法是什么,但请考虑一下:
1 2 | List<Animal> myList = new ArrayList<Dog>(); myList.add(new Cat()); // compilation error |
对战
1 2 | Animal[] x = new Dog[3]; x[0] = new Cat(); // runtime error |
这里将抛出的运行时错误是
我们可以做一个例子,Java处理数组类型是错误的…因为上面的例子。
*注意Java数组的类型是在Java 1之前指定的,但是泛型类型只在Java 1.5中添加。Java语言具有向后兼容性的基本元要求;即语言扩展不应破坏旧代码。除此之外,这意味着不可能修复历史错误,例如数组类型的工作方式。(假设它被认为是一个错误…)
在泛型类型方面,类型擦除des不能解释编译错误。编译错误实际上是由于使用未擦除的泛型类型检查编译类型而发生的。
实际上,您可以通过使用取消选中的类型转换(忽略警告)来破坏编译错误,并最终导致您的
1 | List<Animal> myList = new ArrayList<Dog>(); |
是不可能的,因为在这种情况下,你可以把猫放进狗里:
1 2 3 4 5 6 7 8 9 | private void example() { List<Animal> dogs = new ArrayList<Dog>(); addCat(dogs); // oops, cat in dogs here } private void addCat(List<Animal> animals) { animals.add(new Cat()); } |
另一方面
1 | List<? extends Animal> myList = new ArrayList<Dog>(); |
可能,但在这种情况下,不能将方法与泛型参数一起使用(只接受空值):
1 2 3 4 | private void addCat(List<? extends Animal> animals) { animals.add(null); // it's ok animals.add(new Cat()); // compilation error here } |
编写集合版本以便编译的方法是:
1 | List<? extends Animal> myList = new ArrayList<Dog>(); |
您不需要使用数组的原因是由于类型擦除-非原语数组都是EDCOX1(0),Java数组不是类型化的类(像集合一样)。这种语言从来没有被设计来迎合它。
数组和泛型不混合。
这很有趣。我不能告诉你答案,但是如果你想把狗的清单放进动物的清单中,这是可行的:
1 2 | List<Animal> myList = new ArrayList<Animal>(); myList.addAll(new ArrayList<Dog>()); |
在泛型之前的日子里,编写一个可以对任意类型的数组进行排序的例程需要能够(1)以协变的方式创建只读数组,并以独立于类型的方式交换或重新排列元素,或者(2)以协变的方式创建读写数组,这些数组可以安全地读取,并且可以用t安全地写入。以前从同一数组中读取的铰链,或(3)具有数组,提供了一些与类型无关的元素比较方法。如果协变和逆变通用接口从一开始就包含在语言中,那么第一种方法可能是最好的,因为它可以避免在运行时执行类型检查的需要,以及这种类型检查可能失败的可能性。尽管如此,由于不存在这样的通用支持,因此派生类型数组除了基类型数组之外,没有其他可以明智地强制转换的内容。