Is List<Dog> a subclass of List<Animal>? Why are Java generics not implicitly polymorphic?
Solution 1
No, a List<Dog>
is not a List<Animal>
. Consider what you can do with a List<Animal>
- you can add any animal to it... including a cat. Now, can you logically add a cat to a litter of puppies? Absolutely not.
// Illegal code - because otherwise life would be Bad
List<Dog> dogs = new ArrayList<Dog>(); // ArrayList implements List
List<Animal> animals = dogs; // Awooga awooga
animals.add(new Cat());
Dog dog = dogs.get(0); // This should be safe, right?
Suddenly you have a very confused cat.
Now, you can't add a Cat
to a List<? extends Animal>
because you don't know it's a List<Cat>
. You can retrieve a value and know that it will be an Animal
, but you can't add arbitrary animals. The reverse is true for List<? super Animal>
- in that case you can add an Animal
to it safely, but you don't know anything about what might be retrieved from it, because it could be a List<Object>
.
Solution 2
What you are looking for is called covariant type parameters. This means that if one type of object can be substituted for another in a method (for instance, Animal
can be replaced with Dog
), the same applies to expressions using those objects (so List<Animal>
could be replaced with List<Dog>
). The problem is that covariance is not safe for mutable lists in general. Suppose you have a List<Dog>
, and it is being used as a List<Animal>
. What happens when you try to add a Cat to this List<Animal>
which is really a List<Dog>
? Automatically allowing type parameters to be covariant breaks the type system.
It would be useful to add syntax to allow type parameters to be specified as covariant, which avoids the ? extends Foo
in method declarations, but that does add additional complexity.
Solution 3
The reason a List<Dog>
is not a List<Animal>
, is that, for example, you can insert a Cat
into a List<Animal>
, but not into a List<Dog>
... you can use wildcards to make generics more extensible where possible; for example, reading from a List<Dog>
is the similar to reading from a List<Animal>
-- but not writing.
The Generics in the Java Language and the Section on Generics from the Java Tutorials have a very good, in-depth explanation as to why some things are or are not polymorphic or permitted with generics.
Solution 4
A point I think should be added to what other answers mention is that while
List<Dog>
isn't-aList<Animal>
in Java
it is also true that
A list of dogs is-a list of animals in English (under a reasonable interpretation)
The way the OP's intuition works - which is completely valid of course - is the latter sentence. However, if we apply this intuition we get a language that is not Java-esque in its type system: Suppose our language does allow adding a cat to our list of dogs. What would that mean? It would mean that the list ceases to be a list of dogs, and remains merely a list of animals. And a list of mammals, and a list of quadrapeds.
To put it another way: A List<Dog>
in Java does not mean "a list of dogs" in English, it means "a list of dogs and nothing other than dogs".
More generally, OP's intuition lends itself towards a language in which operations on objects can change their type, or rather, an object's type(s) is a (dynamic) function of its value.
Solution 5
I would say the whole point of Generics is that it doesn't allow that. Consider the situation with arrays, which do allow that type of covariance:
Object[] objects = new String[10];
objects[0] = Boolean.FALSE;
That code compiles fine, but throws a runtime error (java.lang.ArrayStoreException: java.lang.Boolean
in the second line). It is not typesafe. The point of Generics is to add the compile time type safety, otherwise you could just stick with a plain class without generics.
Now there are times where you need to be more flexible and that is what the ? super Class
and ? extends Class
are for. The former is when you need to insert into a type Collection
(for example), and the latter is for when you need to read from it, in a type safe manner. But the only way to do both at the same time is to have a specific type.
froadie
Updated on July 12, 2022Comments
-
froadie almost 2 years
I'm a bit confused about how Java generics handle inheritance / polymorphism.
Assume the following hierarchy -
Animal (Parent)
Dog - Cat (Children)
So suppose I have a method
doSomething(List<Animal> animals)
. By all the rules of inheritance and polymorphism, I would assume that aList<Dog>
is aList<Animal>
and aList<Cat>
is aList<Animal>
- and so either one could be passed to this method. Not so. If I want to achieve this behavior, I have to explicitly tell the method to accept a list of any subclass of Animal by sayingdoSomething(List<? extends Animal> animals)
.I understand that this is Java's behavior. My question is why? Why is polymorphism generally implicit, but when it comes to generics it must be specified?