为什么多态性不以同样的方式处理通用集合和平面数组? [英] why polymorphism doesn't treat generic collections and plain arrays the same way?

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

问题描述

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

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

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

  Animal [] x = new Dog [3]; 


解决方案

这样做的原因是基于Java如何实现泛型



使用数组可以做到这一点(数组是covariant as others are explained)

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

但是,如果您尝试这样做会发生什么?

  Number [0] = 3.14; //尝试堆污染

这最后一行将编译正常,但如果运行此代码,你可以得到一个 ArrayStoreException 。因为你试图把一个double放入整数数组(不管是否通过数字引用访问)。



这意味着你可以欺骗编译器,但是你不能欺骗运行时类型系统。这是因为数组是我们所称的 reifiable类型。这意味着在运行时Java知道这个数组实际上被实例化为一个整数数组,它只是简单地通过 Number [] 的引用访问。



所以,你可以看到,一个东西是对象的实际类型,另一个东西是你用来访问它的引用类型,对吗?



Java泛型的问题



现在,Java泛型类型的问题是类型信息被编译器丢弃,并且在运行时不可用。此过程称为类型擦除。有很好的理由在Java中实现这样的泛型,但这是一个很长的故事,它与二进制兼容性与现有的代码。



但重要的点这里是,因为在运行时没有类型信息,没有办法确保我们不提交堆污染。



例如,

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

List< Number> myNums = myInts; //编译器错误
myNums.add(3.14); //堆溢出

如果Java编译器不阻止这样做,运行时类型系统不能停止你,因为没有办法,在运行时,确定这个列表应该是一个整数列表。 Java运行时会让你把任何你想要的进入这个列表,当它应该只包含整数,因为当它被创建,它被声明为一个整数列表。



因此,Java的设计师确保你不能欺骗编译器。如果你不能欺骗编译器(就像我们可以做的数组一样),你不能欺骗运行时类型系统。



因此,我们说泛型类型是不可重复



显然,这将阻碍多态性。考虑下面的例子:

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

现在你可以这样使用:

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

但是如果你试图用通用集合实现相同的代码,你不会成功:

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



如果您尝试...则会得到编译器错误

  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)); //编译器错误
System.out.println(sum(myLongs)); //编译器错误
System.out.println(sum(myDoubles)); //编译器错误

解决方案是学习使用Java泛型的两个强大的功能称为协方差



协方差可以从结构中读取项目,但你不能写任何东西。所有这些都是有效的声明。

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

您可以从

  Number n = myNums.get(0); 

因为你可以确定,无论实际列表包含什么,它可以被转换为一个数字)



但是,你不能将任何东西放入协变结构中。

  myNumst.add(45L); //编译器错误

这是不允许的,因为Java不能保证什么是实际类型对象在通用结构中。它可以是任何扩展Number,但编译器不能确定。


$ b

违反



可以做到相反。

  List< Object>可以把一些东西放到一个通用结构中, myObjs = new List< Object(); 
myObjs.add(Luke);
myObjs.add(Obi-wan);

List< ;?超级号> myNums = myObjs;
myNums.add(10);
myNums.add(3.14);

在这种情况下,对象的实际性质是一个对象列表,通过逆向可以把数字放进去,基本上因为所有的数字都有Object作为它们的共同祖先。因此,所有的数字都是对象,因此这是有效的。



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

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



如你所见,如果编译器允许你写这行,



因此,在运行时会产生ClassCastException。 ,当你只打算从一个结构中取出通用值时,使用协方差,当你只打算将通用值放入结构中时,使用逆变,并且当你打算同时使用确切的通用类型时。



我最好的例子是下面的,从一个列表中的任何种类的数字复制到另一个列表。

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

由于协方差和逆变的能力,这样的情况:

  List< Integer> myInts = asList(1,2,3,4); 
List< Integer> myDoubles = asList(3.14,6.28);
List< Object> myObjs = new ArrayList< Object>();

copy(myInts,myObjs);
copy(myDoubles,myObjs);


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

解决方案

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

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. Because you’re trying to put a double into an integer array (regardless of being accessed through a number reference).

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?

The Problem with Java Generics

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.

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

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

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

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:

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

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.

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

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

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<Integer> myDoubles = asList(3.14, 6.28);
List<Object> myObjs = new ArrayList<Object>();

copy(myInts, myObjs);
copy(myDoubles, myObjs);

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

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