Java
Programming
Generics
Polymorphism
Subclassing

Is List<Dog> a subclass of List<Animal>? Why are Java generics not implicitly polymorphic?

Object-Oriented Design practice on Codemia

Turn requirements into classes, and defend the design, on the problems that come up in OOD rounds.

Practice OOD

In Java, generics add a way to specify, at compile-time, the types of objects that collections or other compound types can contain. This feature helps programmers avoid errors by catching type mismatches during compilation, rather than at runtime. But, despite this vastly useful system, Java’s approach to generics has some nuances that might not be immediately clear, especially concerning polymorphism.

Understanding Java Generics and Polymorphism

In Java, polymorphism generally allows a subclass instance to be treated as an instance of a superclass. That is, it allows for generalization; a Dog as a specific kind of Animal can be treated just as an Animal. However, when it comes to generic types like List<Dog> and List<Animal>, they do not exhibit the same polymorphic behavior as you might expect from non-generic object hierarchies.

Consider the below example to understand this discrepancy:

  • Animal is a superclass of Dog.
  • List<Dog> and List<Animal> are intuitively thought to hold a relationship where List<Dog> could be a subclass of List<Animal>. However, in Java, this is not the case.

Why List<Dog> is not a subclass of List<Animal>

Java implements generics through a mechanism known as type erasure. This means that the generic type information is used during compilation for type checking but is removed (erased) in the compiled bytecode. Here, List<Dog> and List<Animal> both become simply List at runtime.

Furthermore, if Java allowed List<Dog> to be treated as a List<Animal>, it would potentially lead to runtime errors, violating type safety. For example:

java
List<Dog> dogs = new ArrayList<Dog>();
List<Animal> animals = dogs;  // This is not allowed.
animals.add(new Cat());       // This would be problematic if it were allowed.

In the above code, if Java permitted List<Dog> to be a subtype of List<Animal>, adding an instance of Cat to animals would be valid at compile time. However, at runtime, it would lead to a ClassCastException when trying to treat a Cat as a Dog.

Generic Wildcards and Bounded Types

Java offers a way to work around the strict nature of generics with wildcards and bounded types, enhancing flexibility while maintaining type safety:

  • Unbounded wildcard (?) allows reading from a generic structure without knowing its exact type:
java
  List<?> someList = new ArrayList<Dog>();
  • Upper bounded wildcards (? extends) allow the structure to be read as any type that is a subclass of the specified upper bound:
java
  List<? extends Animal> animalList = new ArrayList<Dog>();
  • Lower bounded wildcards (? super) enable a structure to hold values of any type that is a superclass of the bounded type:
java
  List<? super Dog> dogList = new ArrayList<Animal>();
  dogList.add(new Dog());

Practical Implications

In practice, using Java generics requires understanding where exact type matches are necessary and where some flexibility is required. Iterating through a List<? extends Animal> allows access to elements as Animal, but prevents adding unknown types. Conversely, List<? super Dog> accepts Dog or its subclasses being added but does not allow assumptions about reading the exact type.

FeatureDescription
Type ErasureGeneric type information is stripped out during compilation, turning all generic types into their raw types.
InvarianceGeneric types are invariant, meaning List<Dog> is not a subtype nor a supertype of List<Animal>. This prevents runtime type errors (e.g., adding a Cat to a List<Dog>).
WildcardsOffer flexibility. ? extends allows reading as specific types, whereas ? super allows adding specific types.

Conclusion

While generics do not support polymorphism as directly as class hierarchies do, through understanding and using wildcards appropriately, Java generics provide robust tools for managing collections of objects in a type-safe manner. This approach ultimately allows for flexibly manipulating these collections while preserving compile-time type safety and minimizing runtime errors. This systemic balance between flexibility and type safety is crucial in large-scale software development where both robustness and efficiency are paramount.


Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Object-Oriented Design practice on Codemia

Turn requirements into classes, and defend the design, on the problems that come up in OOD rounds.

Practice OOD