为什么多态不会以同样的方式对待泛型集合和普通数组? [英] why polymorphism doesn't treat generic collections and plain arrays the same way?

查看:28
本文介绍了为什么多态不会以同样的方式对待泛型集合和普通数组?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

假设类 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.因为您试图将 double 放入整数数组中(无论是否通过数字引用访问).

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

这意味着你可以欺骗编译器,但你不能欺骗运行时类型系统.这是因为数组就是我们所说的可具体化的类型.这意味着在运行时 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); 

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

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.

获取/放置原则

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

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

这篇关于为什么多态不会以同样的方式对待泛型集合和普通数组?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆