简体   繁体   中英

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:

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<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, List<Type1> is neither a subtype nor a supertype of List<Type2>.

[..]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<E>[], new List<String>[], new E[] . All will result in generic array creation errors at compile time.[..]

Prentice Hall - Effective Java 2nd Edition

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:

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

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. More more precisely, because that is the way that the Java specification evolved * .

We cannot say what the actual thinking of the Java designers was, but consider this:

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

* Note that typing of Java arrays was specified before Java 1.0, but generic types were only added in Java 1.5. The Java language has a over-arching meta-requirement of backwards compatibility; 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. 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. ( 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. 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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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