Generic Inheritance and Subtypes

To explain Generic inheritance, subtypes and wildcards, we use following class hierarchy:

Java Generics Example Class Hierarchy

The Pet, Bulldog and Bulldog classes are as below. Cat and Persian classes are similar.


    public class Pet {  }

    public class Dog extends Pet {   }
    
    public class Bulldog extends Dog {  }

Type parameter and Inheritance

Before getting into generic subtype, let’s see how inheritance works with generic types. I know this is trivial for those who have already worked with generic types; nevertheless, let’s go through it to avoid confusion that kicks-in when we deal with generic subtypes.

Create an instance of Box<Pet> and put Pet, Dog and Bulldog, and it accepts Pet and all its subtypes.


    public class Box<T> {
        private T pet;
        
        public void put(final T pet) {
            this.pet = pet;
        }
    }

    Box<Pet> petBox = new Box<>();
    petBox.put(new Bulldog());
    petBox.put(new Dog());
    petBox.put(new Pet());

    Box<Dog> dogBox = new Box<>();
    dogBox.put(new Bulldog());
    dogBox.put(new Dog());
    dogBox.put(new Pet());          // Error
    

Substitution Principle allows us to add subtypes to a list. We can add Bulldog, Dog and Pet to Box<Pet> as all are subtypes of Pet. However, the Box<Dog> accepts only the Dog and its subtype Bulldog but not the supertype Pet.

Substitution Principle: a variable of a given type may be assigned a value of any subtype of that type, and a method with a parameter of a given type may be invoked with an argument of any subtype of that type.

As expected, inheritance in put(T t) is similar to plain types such as put(Pet pet). Now, let’s see whether same holds true with generic subtypes.

 
 

Generic Subtype and Type Compatibility

As we do with plain types, we can subtype a generic class or interface by extending or implementing it. The Collection classes from Java library is a good example of this.

Java Generics Collection

The ArrayList<E> implements List<E> which in turn extends Collection<E>. Similarly, the HashSet<E> implements Set<E> which extends Collection<E>.

We can assign an instance Bulldog or Dog to Pet variable as they are regular types i.e. non generic types.


    Pet pet = new Pet();
    pet = new Dog();
    pet = new Bulldog();

Whether same holds good with the generic types, i.e can we assign a List<E> to Collection<E>? Answer is as long as type arguments are same, the inheritance is allowed.

Java Generics Collection Subtype

    ArrayList<Dog> dogs = new ArrayList<>();
    List<Dog> dogList = dogs;
    
    HashSet<Dog> dogHashSet = new HashSet<>();
    Set<Dog> dogSet = dogHashSet;
    
    Collection<Dog> dogCollection = dogs;          
    dogCollection = dogList;  
    dogCollection = dogHashSet;  
    dogCollection = dogSet;  

In above example the type argument Dog is used in all the parameterized types and we can assign generic subtypes (ArrayList or List) to its supertype (Collection). Same way, we can assign HashSet<Dog> or Set<Dog> to Collection<Dog>.

The Substitution Principle does not work with the parameterized types with different type arguments. When type arguments are different, the rules are quite strict. Forget subtypes even the parameterized types of same type with different type arguments are not compatible with each other. Consider the following


    List<Dog> dogs = new ArrayList<>();
    List<Bulldog> bulldogs = dogs;  // not allowed, error
    List<Pet> pets = dogs;          // not allowed, error

When we try to assign List<Dog> to List<Pet> compiler throws error

  • Type mismatch: cannot convert from List<Dog> to List<Pet>

Let’s consider another example to understand its significance.

    
    List<Bulldog> bulldogs = new ArrayList<>();
    List<Dog> dogs = new ArrayList<>();
    
    dogs = bulldogs;    // error
    bulldogs = dogs;    // error
    

The List<Bulldog> is not assignable to List<Dog>: reason begin, the original intent with List<Bulldog> is that it should hold only the Bulldog and nothing else. However, when it is assigned to List<Dog> JVM has to allow it to hold both Bulldog and Dog and this messes the list. Hence, they are not compatible. Similarly, List<Dog> is not assignable to List<Bulldog>: the List<Dog> may contain Dog and its subtype Bulldog and we can’t assign it List<Bulldog> as the later can hold only the list with Bulldog but not the list with Dog and Bulldog.

We can use another way to remember this important aspect of generics. The List<Dog>, ArrayList<Dog> and Collection<Dog> can hold Dog and its subtype Bulldog. As all three can hold similar items we can assign ArrayList<Dog> to List<Dog>, and also ArrayList<Dog> or List<Dog> to Collection<Dog>. However, the List<Pet> can hold Pet, Dog and Bulldog and List<Dog> can hold Dog and Bulldog. As they can hold different types they are not similar and we can assign one to another. Same reasoning applies to the List<Pet>, ArrayList<Dog> or any other combination.

Following diagram summarizes these two rules. In the LHS hierarchy, the all type arguments are Dog and inheritance works as expected. In the RHS hierarchy, all type arguments are Bulldog and again, inheritance works as expected. But, the left side is not compatible with right side as type arguments are different.

 

Java Generics Collection Subtype

 

Same is true while calling the method that has generic types as parameters. In the following example, we can’t pass List<Bulldog> to the method as its parameter is List<Dog>. Again, List<Bulldog> is not compatible with List<Dog> even though Bulldog is subtype of Dog.

    
    public void bark(List<Dog> s) {
        ...
    }    

    List<Dog> dogs = new ArrayList<>();
    List<Bulldog> bulldog = new ArrayList<>();

    bark(dog);         
    bark(bulldog);             // Error

This is a one of the important aspects while working with generics subtypes. Let’s go through one more example to drive home the point.

    
    List<Object> objects = new ArrayList<>();
    List<String> strings = new ArrayList<>();

    objects.add(new Object());              // allowed
    objects.add(new String("hello"));       // allowed

    strings = objects;                      // error

String is subclass of Object and we can add an object or a string to List<Object>; however, there is no relationship whatsoever between the two lists - List<Object> and List<String>. That is to say, the List<Object> can hold objects of type Object as well as instances of any Java type as every class is subclass of Object, but we can’t assign list of another type to it.

 
 

Summary

  • List<Pet> can hold a Pet, Dog or Bulldog, but we can’t assign List<Bulldog> or List<Dog> to List<Pet>.

  • when type arguments are same: we can assign generic subtypes to supertype variable.

    • For example, ArrayList<Dog> is assignable to List<Dog> or Collection<Dog>. Similarly, we can assign HashSet<Dog> or Set<Dog> to Collection<Dog>.
  • when type arguments are different: generics of same type are not compatible with each other.

    • The List<Bulldog> is not subtype of List<Dog>, even though Bulldog is subtype of Dog. There is no relationship between these lists and List<Bulldog> is not assignable to List<Dog>.
  • when type arguments are different: inheritance ceases to exist.

    • The ArrayList<Bulldog> is not subtype of List<Dog>, even though Bulldog is subtype of Dog and ArrayList is subtype of List. The ArrayList<Bulldog> is not assignable to List<Dog> or Collection<Dog>. Same is true for any other combination.

We can overcome these restrictions with wildcards, and the next tutorial explains its use.