Generics & Inheritance
Generics & Inheritance
One of the most common sources of confusion when learning generics is what happens when you combine them with Java's normal class hierarchy. You already know that String extends Object. You might therefore assume that a List<String> is a subtype of List<Object>. It is not — and this lesson explains exactly why, and what the real rules are.
The Core Surprise: Invariance
In Java, generic types are invariant. That means even though String is a subtype of Object, List<String> is not a subtype of List<Object>. They are completely unrelated types as far as the compiler is concerned.
The following code does not compile:
List<String> and List<Object> are siblings, not parent and child. Neither is a subtype of the other. This is the fundamental rule of Java generics combined with inheritance.
Why Invariance Is the Right Default
The restriction exists to preserve type safety. Suppose the compiler did allow that assignment. Then consider what could happen next:
Through the objects reference you inserted an Integer into what is physically a List<String>. Reading it back as a String crashes at runtime. Invariance stops this entire chain of problems at compile time, before the program ever runs.
Java arrays are a useful contrast here: arrays are covariant, meaning String[] is a subtype of Object[]. That flexibility comes at a cost — the JVM must perform a runtime type check on every array write, and it throws ArrayStoreException when the check fails. Generics chose compile-time safety over runtime patching.
List<? extends Object> (or simply List<?>) accepts any parameterised list as a read-only source. That is covered in the Wildcards lessons; here we are focused on understanding why the base rule exists.
What Does Have a Subtype Relationship?
Two things are safe and work as expected:
- Same type argument, different container class: Because
ArrayListimplementsList, anArrayList<String>is a subtype ofList<String>. Both sides use the same type argument, so no unsafety arises. - Raw types: A raw
List(no type argument) is a supertype ofList<String>. This compiles, but you lose type safety and get unchecked-cast warnings — avoid raw types in new code.
Generic Classes Can Still Extend Other Generic Classes
You can extend or implement a generic type just like any other type. The invariance rule only prevents treating one parameterisation as another parameterisation of the same generic. When you extend with a matching or bound-compatible type parameter, everything is fine:
Implementing a Generic Interface with a Fixed Type Argument
A concrete class can implement a generic interface and fix the type argument to a specific type. The concrete class is then a subtype of that specific parameterisation:
Putting It All Together: The Mental Model
When you see a generic type like Container<T>, think of the angle-bracket part as a brand that is fixed at compile time. Two containers with different brands are entirely different types regardless of any subtype relationship between the brands themselves. The only exception is when the container class itself is in a subtype relationship (e.g. ArrayList extends AbstractList implements List) and both sides carry the same brand.
List<Object> and then being surprised that you cannot pass a List<String> to it. The fix is to use an upper-bounded wildcard: List<? extends Object> — covered in the next lessons.
Summary
- Generic types in Java are invariant:
List<String>is not a subtype ofList<Object>. - Invariance is intentional — it prevents a category of runtime
ClassCastExceptionthat covariant arrays can cause. - Normal inheritance does apply when the type argument is the same:
ArrayList<String>is a subtype ofList<String>. - Generic classes can extend or implement other generic types by forwarding or bounding the type parameter.
- When you need read-only flexibility across parameterisations, use wildcards — the subject of the next two lessons.