Generic Inheritance and Subtypes
To explain Generic inheritance, subtypes and wildcards, we use following 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.
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.
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.
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 assignList<Bulldog>
orList<Dog>
toList<Pet>
.when type arguments are same: we can assign generic subtypes to supertype variable.
- For example,
ArrayList<Dog>
is assignable toList<Dog>
orCollection<Dog>
. Similarly, we can assignHashSet<Dog>
orSet<Dog>
toCollection<Dog>
.
- For example,
when type arguments are different: generics of same type are not compatible with each other.
- The
List<Bulldog>
is not subtype ofList<Dog>
, even though Bulldog is subtype of Dog. There is no relationship between these lists andList<Bulldog>
is not assignable toList<Dog>
.
- The
when type arguments are different: inheritance ceases to exist.
- The
ArrayList<Bulldog>
is not subtype ofList<Dog>
, even though Bulldog is subtype of Dog and ArrayList is subtype of List. TheArrayList<Bulldog>
is not assignable toList<Dog>
orCollection<Dog>
. Same is true for any other combination.
- The
We can overcome these restrictions with wildcards, and the next tutorial explains its use.