Why Java generics do not support sub-typing

adminguy's picture
Posted November 25th, 2014 by adminguy

If you have programmed in Java for a while, you will know and will also have experienced first hand, that  up-casting in Java is automatic. What this means is - we can provide a sub-type wherever a super-type is expected. Let us look at a simple code snippet to understand it better. In the example shown below, the print method expects an object of type List, but we give it an ArrayList object when we invoke it from the main method. This is not a problem because ArrayList will automatically be up-cast to a  List when it is received by the print method.

 
public class AutomaticUpcast {
   
    public static void main(String args[]) {
        // Note: You should always declare a variable as the appropriate
        // super-type, which in this case is List. We have declared
        // someInts as an ArrayList only to avoid any confusion in relation
        // to the type of someInts
        ArrayList<Number> someInts = new ArrayList<Number>();
        print(someInts); // up-casting will happen automatically
    }
 
    public static void print(List<Number> ints) {
        // print all the Integers
    }
}
Code Example 1
 
However, this concept does not extend itself to generics. In Code Example 1, we saw how it is possible to provide an ArrayList of Number objects when a method requires a List of Number objects, because ArrayList<Number> is automatically up-cast to List<Number>. But, will the same magic work if we were to provide ArrayList<Integer> objects ? Will Integer also be up-cast to Number ? Let's modify the code in Code Example 1, to invoke the print method with an ArrayList of Integer objects, and see what happens.
 
public class AutomaticUpcast {
  
    public static void main(String args[]) {
        List<Integer> someInts = new ArrayList<Integer>();
        print(someInts); //ouch!!! the compiler doesn't like this
    }
  
    public static void print(List<Number> ints) {
        // print all the Integers
    }
}
Code Example 2
 
Unfortunately this code does not work. The compiler disciplined my little adventure, with this error message:
 
Type mismatch: cannot convert from ArrayList<Integer> to ArrayList<Number>
 
So what went wrong ? If up-casting worked from ArrayList to List, why did it fail from Integer to Number ? After all Integer is a sub-type of Number.
 
It's really important to understand why Code Example 2 did not work. It will save you a lot of pain if you understand the reason well. But first let's take a step back. Remember that generics were introduced in Java to provide type safety ? Unfortunately type safety will be broken if we consider ArrayList<Integer> to be a sub-type of ArrayList<Number>, because of which ArrayList<Integer> and ArrayList<Number> are considered to be different types. This is really important;  ArrayList<Integer> is a sub-type of List<Integer>, but ArrayList<Integer> is not a sub-type of List<Number>, neither is it a sub-type of ArrayList<Number>.This is so important that I am going to repeat it once again. Even though Integer is a sub-type of NumberArrayList<Integer> is not a sub-type of ArrayList<Number>. They are totally different types. They are two ends of the spectrum, just like programmers and humility. You see they are very very different.
 
Now you could repeat this a hundred times, and it will perhaps help you remember and avoid compiler errors in the future, but the truth of the matter is that this concept is somehow not very intuitive. Why are they considered to be different types? Why don't the natural rules of up-casting work with type parameters ? Do generics belong to the realm of quantum physics where natural laws as we know them don't apply? 
 
I really don't know much about quantum physics, but I know about Eclipse and I know how to code, so I'll do what I do best and show yet another code example to understand why the rules of sub-typing don't work with generic type parameters.
 
public class AutomaticUpcast {
   
    public static void main(String args[]) {
        ArrayList<Integer> someInts = new ArrayList<Integer>();
        //By now we know this will not compile, but see below to understand why
        print(someInts);
    }
  
    public static void print(List<Number> ints) {
        // print all the Integers
        ints.add( new Double("1.46" )); //Ouch!!! Did we just break something ???
    }
}
Code Example 3
 
Thankfully the code in Code Example 3 will not compile, but do you see what could happen if sub-typing were allowed with generic type parameters. We could pass a ArrayList<Integer> object to the print method, which would accept it as a ListNumber > object and happily insert a Double object in it, breaking type safety of the original ArrayList<Integer> object, because it was up-cast to a List<Number> object. And why should this not be allowed? Because Integer and Double are siblings, therefore ArrayList<Integer> should never hold a Double object.