📋 Section 20 · Question #24

How do Generics work with inheritance

In Java Generics, the core architectural principle governing inheritance is Invariance: the inheritance relationship between type arguments does not transfer to the parameterize...

In Java Generics, the core architectural principle governing inheritance is Invariance: the inheritance relationship between type arguments does not transfer to the parameterized types that wrap them. Understanding how generics interact with inheritance requires navigating two distinct axes of polymorphism and utilizing wildcards for use-site variance.


🟢 Junior Level

The Invariance Principle

Even though Dog is a direct subtype of Animal, List<Dog> is NOT a subtype of List<Animal>:

class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}

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

// ❌ Compilation Error: Incompatible types!
List<Animal> animals = dogs; 

Why Invariance is Strictly Enforced

If the Java compiler allowed assigning List<Dog> to List<Animal>, type safety would be catastrophically compromised:

// Hypothetical scenario IF generics were covariant:
List<Dog> dogs = new ArrayList<>();
List<Animal> animals = dogs; // Suppose this compiled...

// Legal for List<Animal>: Inserting a Cat into an animal list
animals.add(new Cat()); 

// Memory corruption: dogs and animals point to the exact same heap collection!
Dog myDog = dogs.get(0); // 💥 ClassCastException: Cat cannot be cast to Dog!

By enforcing strict invariance, the compiler catches type mismatches at build time, eliminating runtime casting crashes.


🟡 Middle Level

The Two Axes of Generic Inheritance

To master generic polymorphism, separate the Container Type Axis from the Type Argument Axis:

  1. The Container Axis (Standard OOP Inheritance): When the generic type argument is identical, standard inheritance applies:
    // ✅ ArrayList<String> is a subtype of List<String>
    List<String> list = new ArrayList<String>();
    
    // ✅ List<String> is a subtype of Collection<String>
    Collection<String> collection = list;
    
  2. The Type Argument Axis (Strict Invariance): When container types match but arguments differ across a hierarchy, the types are completely unrelated:
    // ❌ List<Dog> is UNRELATED to List<Animal>
    List<Animal> animalList = new ArrayList<Dog>(); // Compilation error!
    

Enabling Variance via Wildcards

To bridge the gap between unrelated type arguments, Java provides Use-Site Variance using bounded wildcards:

  1. Covariance (? extends T): Preserves hierarchy direction for reading data (Producer).
    // ✅ List<? extends Animal> accepts List<Dog>, List<Cat>, List<Animal>
    List<? extends Animal> animals = new ArrayList<Dog>();
    Animal a = animals.get(0); // Safe read: returns Animal
    // animals.add(new Dog()); // ❌ Compilation Error: Writing is prohibited!
    
  2. Contravariance (? super T): Inverts hierarchy direction for writing data (Consumer).
    // ✅ List<? super Dog> accepts List<Dog>, List<Animal>, List<Object>
    List<? super Dog> dogConsumers = new ArrayList<Animal>();
    dogConsumers.add(new Dog()); // ✅ Safe write: Dog fits into Animal or Object
    

🔴 Senior Level

Array Covariance vs. Generic Invariance

Java 1.0 (1996) lacked generics. To support utility algorithms like Arrays.sort(Object[]), arrays were made covariant:

String[] strings = new String[5];
Object[] objects = strings; // Compiles cleanly due to array covariance

// Explodes AT RUNTIME with ArrayStoreException!
objects[0] = Integer.valueOf(42); 

The JVM must perform an expensive component-type check (checkcast) on every array write. Learning from this design flaw, the architects of Java 5 made generics strictly invariant to enforce absolute compile-time safety.

Three Strategies for Subclassing Generic Classes

When a class extends a generic class or implements a generic interface, it must choose one of three architectural patterns:

1. Concrete Specialization (Pins Type Parameter)

public class StringRepository implements Repository<String, Long> {
    // T is pinned to String, ID to Long.
    // javac synthesizes synthetic Bridge Methods in bytecode!
}

2. Generic Pass-Through (Preserves Generics)

public class AuditableRepository<T, ID> implements Repository<T, ID> {
    // Subclass remains generic
}

3. Bound Narrowing (Constrains Parameter)

public class NumericBox<T extends Number> extends Box<T> {
    // T is constrained from Object to Number
}

Covariant Method Return Types with Generics

Java 5 allows overridden methods in subclasses to return a subtype of the parent method’s return type (Covariant Return Types). However, this rule is strictly governed by generic invariance:

class SuperService {
    public List<Animal> getAnimals() { return new ArrayList<>(); }
    public List<? extends Animal> getFlexibleAnimals() { return new ArrayList<>(); }
}

class SubService extends SuperService {
    // ❌ COMPILATION ERROR: List<Dog> is NOT a subtype of List<Animal>!
    @Override
    public List<Dog> getAnimals() { return new ArrayList<>(); } 

    // ✅ VALID: List<Dog> IS a valid subtype of List<? extends Animal>!
    @Override
    public List<Dog> getFlexibleAnimals() { return new ArrayList<>(); } 
}

Architecture Rule: If a base service returns a wildcard collection (List<? extends Animal>), subclasses acquire the legal right to override the method and return concrete specialized collections (List<Dog>).


4 Tricky Questions

1. Can a class implement both Comparable<Parent> and Comparable<Child> simultaneously?

Answer: No, this is a compilation error (JLS §8.1.5).

class Parent implements Comparable<Parent> { ... }

// ❌ Compilation Error:
// 'Comparable' cannot be inherited with different type arguments: 'Parent' and 'Child'
class Child extends Parent implements Comparable<Child> { ... }

Due to Type Erasure, both interfaces erase to the raw interface Comparable with bytecode method descriptor int compareTo(Object o). A class cannot implement the same interface twice with conflicting type arguments because the compiler cannot synthesize two bridge methods with the identical JVM signature.

2. Is List<String>[] a subtype of Object[]?

Answer: Yes. Because Java arrays are covariant and List<String> is a reference type (which is an Object), List<String>[] is technically a subtype of Object[].

While direct creation via new List<String>[10] is banned by the compiler (Generic Array Creation error), if an array reference is obtained via raw types or wildcards ((List<String>[]) new List<?>[10]), assigning it to Object[] is legally permitted by the compiler, creating an immediate vector for heap pollution.

3. Why is the declaration class MyException<T> extends Exception strictly prohibited?

Answer: Because exception handling is evaluated exclusively by the JVM at runtime during stack unwinding.

If generic exceptions were permitted:

try { ... } 
catch (MyException<String> e) { ... } 
catch (MyException<Integer> e) { ... }

Under type erasure, both catch blocks would erase to catch (MyException e). The JVM cannot differentiate between MyException<String> and MyException<Integer> in the compiled exception table. Consequently, JLS §8.1.2 explicitly forbids generic subclasses of java.lang.Throwable.

4. Why does Collections.copy(List<? super T> dest, List<? extends T> src) declare its bounds this way?

Answer: It is the classic realization of the PECS principle (Producer Extends, Consumer Super):

  • src acts as a Producer: the method only reads elements of type $T$ from it, so covariance ? extends T is safe (allowing callers to pass a List<Dog> into a copy operation typed to Animal).
  • dest acts as a Consumer: the method only writes elements of type $T$ into it, so contravariance ? super T is safe (allowing callers to write dogs into a List<Animal> or List<Object>).

🎯 Interview Cheat Sheet

  • Core Axiom: Generics are invariant. If Child extends Parent, List<Child> is not a List<Parent>.
  • Why Invariance Exists: Prevents heap pollution and runtime ClassCastException caused by writing incompatible subtypes into shared collections.
  • Two Axes:
    • Container axis: Polymorphic (ArrayList<T> is a List<T>).
    • Argument axis: Invariant (List<Parent> and List<Child> are unrelated).
  • Wildcard Bridge: Use ? extends Parent for reading/covariance and ? super Child for writing/contravariance.
  • Array Flaw: Arrays are covariant (String[] <: Object[]), requiring runtime ArrayStoreException checks. Generics eliminate this via compile-time invariance.
  • Overriding Trick: Base methods returning List<? extends Animal> allow subclasses to covariantly return List<Dog>.
  • Prohibitions: Cannot implement the same generic interface with different type arguments; cannot create generic exceptions (class Ex<T> extends Exception).