简体   繁体   English

为什么多态不以同样的方式处理泛型集合和普通数组?

[英]why polymorphism doesn't treat generic collections and plain arrays the same way?

assume that class Dog extends class Animal: why this polymorphic statement is not allowed: 假设类Dog扩展类Animal:为什么不允许这种多态语句:

List<Animal> myList = new ArrayList<Dog>();

However, it's allowed with plain arrays: 但是,它允许使用普通数组:

Animal[] x=new Dog[3];

The reasons for this are based on how Java implements generics. 其原因是基于Java如何实现泛型。

An Arrays Example 一个数组示例

With arrays you can do this (arrays are covariant as others have explained) 使用数组你可以做到这一点(数组是协变的,正如其他人所解释的那样)

Integer[] myInts = {1,2,3,4};
Number[] myNumber = myInts;

But, what would happen if you try to do this? 但是,如果你试图这样做会发生什么?

Number[0] = 3.14; //attempt of heap pollution

This last line would compile just fine, but if you run this code, you could get an ArrayStoreException . 最后一行编译得很好,但是如果你运行这段代码,你可能会得到一个ArrayStoreException Because you're trying to put a double into an integer array (regardless of being accessed through a number reference). 因为您试图将double放入整数数组(无论是通过数字引用访问)。

This means that you can fool the compiler, but you cannot fool the runtime type system. 这意味着你可以欺骗编译器,但你不能欺骗运行时类型系统。 And this is so because arrays are what we call reifiable types . 这是因为数组就是我们所说的可再生类型 This means that at runtime Java knows that this array was actually instantiated as an array of integers which simply happens to be accessed through a reference of type Number[] . 这意味着在运行时Java知道这个数组实际上被实例化为一个整数数组,它恰好通过Number[]类型的引用进行访问。

So, as you can see, one thing is the actual type of the object, an another thing is the type of the reference that you use to access it, right? 因此,正如您所看到的,有一件事是对象的实际类型,另一件事是您用来访问它的引用类型,对吧?

The Problem with Java Generics Java泛型问题

Now, the problem with Java generic types is that the type information is discarded by the compiler and it is not available at run time. 现在,Java泛型类型的问题是类型信息被编译器丢弃,并且在运行时不可用。 This process is called type erasure . 此过程称为类型擦除 There are good reason for implementing generics like this in Java, but that's a long story, and it has to do with binary compatibility with pre-existing code. 有很好的理由在Java中实现这样的泛型,但这是一个很长的故事,它与二进制兼容现有的代码有关。

But the important point here is that since, at runtime there is no type information, there is no way to ensure that we are not committing heap pollution. 但重要的是,由于在运行时没有类型信息,因此无法确保我们不会造成堆污染。

For instance, 例如,

List<Integer> myInts = new ArrayList<Integer>();
myInts.add(1);
myInts.add(2);

List<Number> myNums = myInts; //compiler error
myNums.add(3.14); //heap polution

If the Java compiler does not stop you from doing this, the runtime type system cannot stop you either, because there is no way, at runtime, to determine that this list was supposed to be a list of integers only. 如果Java编译器没有阻止你这样做,那么运行时类型系统也不能阻止你,因为在运行时没有办法确定这个列表应该只是一个整数列表。 The Java runtime would let you put whatever you want into this list, when it should only contain integers, because when it was created, it was declared as a list of integers. Java运行时允许你将任何你想要的东西放入这个列表,当它只包含整数时,因为它在创建时被声明为整数列表。

As such, the designers of Java made sure that you cannot fool the compiler. 因此,Java的设计者确保您不能欺骗编译器。 If you cannot fool the compiler (as we can do with arrays) you cannot fool the runtime type system either. 如果你不能欺骗编译器(就像我们可以用数组做的那样),你也不能欺骗运行时类型系统。

As such, we say that generic types are non-reifiable . 因此,我们说泛型类型是不可恢复的

Evidently, this would hamper polymorphism. 显然,这会妨碍多态性。 Consider the following example: 请考虑以下示例:

static long sum(Number[] numbers) {
   long summation = 0;
   for(Number number : numbers) {
      summation += number.longValue();
   }
   return summation;
}

Now you could use it like this: 现在你可以像这样使用它:

Integer[] myInts = {1,2,3,4,5};
Long[] myLongs = {1L, 2L, 3L, 4L, 5L};
Double[] myDoubles = {1.0, 2.0, 3.0, 4.0, 5.0};

System.out.println(sum(myInts));
System.out.println(sum(myLongs));
System.out.println(sum(myDoubles));

But if you attempt to implement the same code with generic collections, you will not succeed: 但是,如果您尝试使用泛型集合实现相同的代码,则不会成功:

static long sum(List<Number> numbers) {
   long summation = 0;
   for(Number number : numbers) {
      summation += number.longValue();
   }
   return summation;
}

You would get compiler erros if you try to... 如果你试图...你会得到编译器错误

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

The solution is to learn to use two powerful features of Java generics known as covariance and contravariance. 解决方案是学习使用Java泛型的两个强大功能,称为协方差和逆变。

Covariance 协方差

With covariance you can read items from a structure, but you cannot write anything into it. 使用协方差,您可以从结构中读取项目,但不能在其中写入任何内容。 All these are valid declarations. 所有这些都是有效的声明。

List<? extends Number> myNums = new ArrayList<Integer>();
List<? extends Number> myNums = new ArrayList<Float>()
List<? extends Number> myNums = new ArrayList<Double>()

And you can read from myNums : 你可以从myNums

Number n = myNums.get(0); 

Because you can be sure that whatever the actual list contains, it can be upcasted to a Number (after all anything that extends Number is a Number, right?) 因为您可以确定无论实际列表包含什么,它都可以被上传到一个数字(毕竟任何扩展Number的数字都是数字,对吧?)

However, you are not allowed to put anything into a covariant structure. 但是,不允许将任何内容放入协变结构中。

myNumst.add(45L); //compiler error

This would not be allowed, because Java cannot guarantee what is the actual type of the object in the generic structure. 这是不允许的,因为Java无法保证通用结构中对象的实际类型。 It can be anything that extends Number, but the compiler cannot be sure. 它可以是扩展Number的任何东西,但编译器无法确定。 So you can read, but not write. 所以你可以阅读,但不能写。

Contravariance 逆变

With contravariance you can do the opposite. 有了逆转,你可以做相反的事情。 You can put things into a generic structure, but you cannot read out from it. 你可以把东西放到一个通用的结构中,但你不能从中读出来。

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);

In this case, the actual nature of the object is a List of Objects, and through contravariance, you can put Numbers into it, basically because all numbers have Object as their common ancestor. 在这种情况下,对象的实际性质是对象列表,并且通过逆变,您可以将Numbers放入其中,主要是因为所有数字都将Object作为它们的共同祖先。 As such, all Numbers are objects, and therefore this is valid. 因此,所有Numbers都是对象,因此这是有效的。

However, you cannot safely read anything from this contravariant structure assuming that you will get a number. 但是,假设您将获得一个数字,则无法安全地从这个逆变结构中读取任何内容。

Number myNum = myNums.get(0); //compiler-error

As you can see, if the compiler allowed you to write this line, you would get a ClassCastException at runtime. 如您所见,如果编译器允许您编写此行,则会在运行时获得ClassCastException。

Get/Put Principle 获取/放置原则

As such, use covariance when you only intend to take generic values out of a structure, use contravariance when you only intend to put generic values into a structure and use the exact generic type when you intend to do both. 因此,当您只打算从结构中获取通用值时使用协方差,当您只打算将通用值放入结构时使用逆变,并在您打算同时使用完全通用类型时使用。

The best example I have is the following that copies any kind of numbers from one list into another list. 我所拥有的最好的例子是,将任何类型的数字从一个列表复制到另一个列表中。 It only gets items from the source, and it only puts items in the destiny. 它只从源头获取物品,它只物品放入命运。

public static void copy(List<? extends Number> source, List<? super Number> destiny) {
    for(Number number : source) {
        destiny.add(number);
    }
}

Thanks to the powers of covariance and contravariance this works for a case like this: 由于协方差和逆变的力量,这适用于这样的情况:

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[]. 这个可怕的单词意味着如果Sub是Super的子类型,那么数组类型Sub []是Super []的子类型。 Generics, by contrast, are invariant: for any two distinct types Type1 and Type2, List<Type1> is neither a subtype nor a supertype of List<Type2>. 相反,泛型是不变的:对于任何两个不同类型Type1和Type2,List <Type1>既不是子类型也不是List <Type2>的超类型。

[..]The second major difference between arrays and generics is that arrays are reified [JLS, 4.7]. [..]数组和泛型之间的第二个主要区别是数组是有效的[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]. [..]相反,泛型是通过擦除实现的[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). 擦除允许泛型类型与不使用泛型的遗留代码自由互操作(第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<E>[], new List<String>[], new E[] . 这些数组创建表达式都不合法: new List <E> [],new List <String> [],new E [] All will result in generic array creation errors at compile time.[..] 所有这些都会在编译时导致通用数组创建错误。[..]

Prentice Hall - Effective Java 2nd Edition Prentice Hall - 有效的Java第2版

That's very interesting. 那非常有趣。 I can't tell you the answer, but this works if you want to put a list of Dogs into the list of Animals: 我不能告诉你答案,但是如果你想将Dogs列表放入动物列表中,这是有效的:

List<Animal> myList = new ArrayList<Animal>();
myList.addAll(new ArrayList<Dog>());

The way to code the collections version so it compiles is: 编译集合版本以便编译的方法是:

List<? extends Animal> myList = new ArrayList<Dog>();

The reason you don't need this with arrays is due to type erasure - arrays of non-primitives are all just Object[] and java arrays are not a typed class (like collections are). 你不需要使用数组的原因是由于类型擦除 - 非基元的数组都只是Object[]而java数组不是类型的类(比如集合)。 The language was never designed to cater for it. 该语言从未被设计为迎合它。

Arrays and generics don't mix. 数组和泛型不混合。

List<Animal> myList = new ArrayList<Dog>();

is not possible because in that case you could put cats into dogs: 是不可能的,因为在那种情况下你可以把猫放入狗:

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());
}

On the other hand 另一方面

List<? extends Animal> myList = new ArrayList<Dog>();

is possible, but in that case you can't use methods with generic paramteres (only null is accepted): 是可能的,但在这种情况下,你不能使用泛型参数的方法(只接受null):

private void addCat(List<? extends Animal> animals) {
    animals.add(null);      // it's ok
    animals.add(new Cat()); // compilation error here
}

The ultimate answer is it is that way because Java was specified that way. 最终的答案就是这样,因为Java是这样指定的。 More more precisely, because that is the way that the Java specification evolved * . 更确切地说,因为这是Java规范演变的方式*

We cannot say what the actual thinking of the Java designers was, but consider this: 我们不能说Java设计者的实际想法是什么,但考虑一下:

List<Animal> myList = new ArrayList<Dog>();
myList.add(new Cat());   // compilation error

versus

Animal[] x = new Dog[3];
x[0] = new Cat();        // runtime error

The runtime error that will be thrown here is ArrayStoreException . 将在此处抛出的运行时错误是ArrayStoreException This could potentially be thrown on any assignment to any array of non-primitives. 这可能会在任何非基元数组的任何赋值上抛出。

One could make a case that Java's handling of array types is wrong ... because of examples like the above one. 人们可以说Java对数组类型的处理是错误的...因为上面的例子。

* Note that typing of Java arrays was specified before Java 1.0, but generic types were only added in Java 1.5. *请注意,在Java 1.0之前指定了Java数组的类型,但是只在Java 1.5中添加了泛型类型。 The Java language has a over-arching meta-requirement of backwards compatibility; Java语言具有向后兼容性的总体元要求; ie language extensions should not break old code. 即语言扩展不应该破坏旧代码。 Among other things, that means that it is not possible to fix historical mistakes, such as the way that array typing works. 除此之外,这意味着无法修复历史错误,例如数组键入的工作方式。 (Assuming it is accepted that was a mistake ...) (假设接受这一个错误......)


On the generic type side, type erasure des not explain the compilation error. 在泛型类型方面,键入erasure des不解释编译错误。 The compilation error is actually occuring because of the compile type checking using the non-erased generic types. 由于使用非擦除泛型类型进行编译类型检查,实际上发生了编译错误。

And in fact, you can subvert the compilation error by using an uncheck typecast (ignore the warning) and end up in a situation where your ArrayList<Dog> actually contains Cat objects at runtime. 实际上,您可以通过使用取消选中类型转换(忽略警告)来破坏编译错误,最终导致ArrayList<Dog>在运行时实际包含Cat对象。 ( That is a consequence of type erasure!) But beware, that your subversion of compilation errors using an unchecked conversion is liable to lead to runtime errors in unexpected places ... if you get it wrong. 是类型擦除的结果!)但要注意,使用未经检查的转换对编译错误的颠覆可能导致意外地点的运行时错误......如果你弄错了。 That's why it is a bad idea. 这就是为什么这是一个坏主意。

In the days before generics, writing a routine which could sort arrays of arbitrary type would have required either being able to (1) create read-only arrays in covariant fashion and swap or rearrange elements in type-independent fashion, or (2) create read-write arrays in covariant fashion that can be read safely, and can be written safely with things that were previously read from the same array, or (3) have arrays provide some type-independent means of comparing elements. 在泛型之前的几天,编写一个可以对任意类型的数组进行排序的例程将要求能够(1)以协变方式创建只读数组,并以独立于类型的方式交换或重新排列元素,或者(2)创建可以安全读取的协变方式的读写数组,可以使用先前从同一数组中读取的内容安全地编写,或者(3)使数组提供一些与类型无关的元素比较方法。 If covariant and contravariant generic interfaces had been included in the language from the start, the first approach might have been the best, since it would have avoided the need to perform type-checking at run time as well as the possibility that such type-checks could fail. 如果从一开始就在语言中包含了协变和逆变通用接口,那么第一种方法可能是最好的,因为它可以避免在运行时执行类型检查的需要以及这种类型检查的可能性可能会失败。 Nonetheless, since such generic support didn't exist, there wasn't anything a derived-type array could sensibly be cast to other than an array of a base type. 尽管如此,由于不存在这样的通用支持,因此除了基类型数组之外,没有任何派生类型数组可以合理地转换为其他类型。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM