为什么多态性不以相同的方式对待泛型集合和普通数组?

2022-01-24 00:00:00 collections polymorphism java generics

假设 Dog 类扩展了 Animal 类:为什么不允许这种多态语句:

assume that class Dog extends class Animal: why this polymorphic statement is not allowed:

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

但是,它允许使用普通数组:

However, it's allowed with plain arrays:

Animal[] x=new Dog[3];

推荐答案

之所以会这样,是基于Java实现泛型的方式.

The reasons for this are based on how Java implements generics.

数组示例

使用数组,您可以做到这一点(数组是协变的,正如其他人所解释的)

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

最后一行编译得很好,但是如果你运行这段代码,你可能会得到一个ArrayStoreException.因为您试图将双精度数放入整数数组中(无论通过数字引用访问).

This last line would compile just fine, but if you run this code, you could get an ArrayStoreException. Because you’re trying to put a double into an integer array (regardless of being accessed through a number reference).

这意味着你可以欺骗编译器,但不能欺骗运行时类型系统.之所以如此,是因为数组就是我们所说的reifiable types.这意味着在运行时 Java 知道这个数组实际上被实例化为一个整数数组,它恰好是通过 Number[] 类型的引用访问的.

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[].

所以,如您所见,一件事是对象的实际类型,另一件事是您用来访问它的引用的类型,对吧?

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?

Java 泛型的问题

现在,Java 泛型类型的问题是类型信息被编译器丢弃,并且在运行时不可用.此过程称为 类型擦除.在 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. 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.

但这里重要的一点是,由于在运行时没有类型信息,因此无法确保我们不会造成堆污染.

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.

例如,

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 运行时允许你将任何你想要的东西放入这个列表,而它应该只包含整数,因为在创建它时,它被声明为一个整数列表.

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. 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. 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

解决方案是学习使用 Java 泛型的两个强大功能,即协变和逆变.

The solution is to learn to use two powerful features of Java generics known as covariance and contravariance.

协方差

通过协方差,您可以从结构中读取项目,但不能向其中写入任何内容.所有这些都是有效的声明.

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

您可以从 myNums 中读取:

Number n = myNums.get(0); 

因为您可以确定无论实际列表包含什么,它都可以向上转换为一个数字(毕竟任何扩展 Number 的东西都是一个数字,对吧?)

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

但是,您不能将任何内容放入协变结构中.

However, you are not allowed to put anything into a covariant structure.

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

这是不允许的,因为 Java 无法保证泛型结构中对象的实际类型.它可以是任何扩展 Number 的东西,但编译器不能确定.所以你可以读,但不能写.

This would not be allowed, because Java cannot guarantee what is the actual type of the object in the generic structure. It can be anything that extends Number, but the compiler cannot be sure. So you can read, but not write.

逆变

通过逆变,你可以做相反的事情.您可以将事物放入通用结构中,但不能从中读出.

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. As such, all Numbers are objects, and therefore this is valid.

但是,假设你会得到一个数字,你就不能安全地从这个逆变结构中读取任何内容.

However, you cannot safely read anything from this contravariant structure assuming that you will get a number.

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

如您所见,如果编译器允许您编写此行,您将在运行时收到 ClassCastException.

As you can see, if the compiler allowed you to write this line, you would get a ClassCastException at runtime.

Get/Put 原则

因此,当您只打算将泛型值从结构中取出时使用协变,当您只打算将泛型值放入结构中时使用逆变器,当您打算同时执行这两种操作时使用精确的泛型类型.

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

相关文章