Question 24 Β· Section 20

How do Generics work with inheritance

Even though String inherits from Object, List does NOT inherit from List.

Language versions: English Russian Ukrainian

🟒 Junior Level

Generics are NOT inherited the same way as regular classes!

Even though String inherits from Object, List<String> does NOT inherit from List<Object>.

String extends Object;               // βœ… String is a subclass of Object
List<String> extends List<Object>;   // ❌ WRONG!

// Example:
List<String> strings = new ArrayList<>();
List<Object> objects = strings;  // ❌ compilation error!

Why: If this were allowed, you could add an Integer to a List<String> through a List<Object> reference β€” this would break type safety.

// If it were allowed:
List<String> strings = new ArrayList<>();
List<Object> objects = strings;  // suppose OK
objects.add(42);  // added Integer to List<String>!
String s = strings.get(0);  // ClassCastException!

🟑 Middle Level

Invariance of generics

Generics are invariant β€” List<A> and List<B> are unrelated, even if A extends B.

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

List<Dog> dogs = new ArrayList<>();
List<Animal> animals = dogs;  // ❌ compilation error!

How to get flexibility

1. Wildcards β€” ? extends:

List<Dog> dogs = new ArrayList<>();
List<? extends Animal> animals = dogs;  // βœ… OK

// Can read as Animal
Animal a = animals.get(0);

// Cannot write
animals.add(new Cat());  // ❌ compilation error

2. Wildcards β€” ? super:

List<Animal> animals = new ArrayList<>();
List<? super Dog> dogs = animals;  // βœ… OK

// Can write Dog
dogs.add(new Dog());

// Can only read Object
Object obj = dogs.get(0);

Generic classes with inheritance

public class Box<T> {
    private T value;
    public void set(T value) { this.value = value; }
    public T get() { return value; }
}

// Box<Dog> does NOT inherit from Box<Animal>
Box<Dog> dogBox = new Box<>();
Box<Animal> animalBox = dogBox;  // ❌ error

// But you can use wildcard
Box<? extends Animal> animalBox2 = dogBox;  // βœ…

Common mistakes

  1. Expecting covariance: ```java List ints = new ArrayList<>(); List numbers = ints; // ❌ error

// βœ… Correct List<? extends Number> numbers = ints;


2. **Generic method without wildcard:**
```java
public void processAnimals(List<Animal> animals) {
    // accepts only List<Animal>
}

List<Dog> dogs = new ArrayList<>();
processAnimals(dogs);  // ❌ error

// βœ… Correct
public void processAnimals(List<? extends Animal> animals) { }

πŸ”΄ Senior Level

Internal Implementation

Type erasure and inheritance:

class Parent<T> {
    private T value;
    public T get() { return value; }
}

class Child extends Parent<String> {
    @Override
    public String get() { return super.get(); }
}

// After erasure:
class Parent {
    private Object value;
    public Object get() { return value; }
}

class Child extends Parent {
    public String get() { return (String) super.get(); }

    // Bridge method to preserve polymorphism:
    public Object get() { return get(); }
}

Architectural Trade-offs

Property Arrays Generics
Covariance βœ… Covariant ❌ Invariant
Runtime check βœ… ArrayStoreException ❌ Compile-time only
Type safety Runtime Compile-time
Flexibility High (dangerous) Low (safe)

Edge Cases

1. Nested generics:

Map<String, List<Dog>> dogMap = new HashMap<>();
Map<String, List<Animal>> animalMap = dogMap;  // ❌ error

// βœ… With wildcard
Map<String, ? extends List<? extends Animal>> animalMap = dogMap;

2. Generic subclass:

public class AnimalBox<T extends Animal> extends Box<T> {
    public void addAnimal(T animal) {
        set(animal);
    }
}

// AnimalBox<Dog> does NOT inherit AnimalBox<Animal>
// But AnimalBox<Dog> inherits Box<Dog>

3. Covariant return type:

// List<Dog> is NOT a subtype of List<Animal> β€” generics are invariant!
// This code does NOT COMPILE.

// Correct covariant return β€” with concrete types (not generics):
class Parent {
    public Animal getAnimal() { return new Animal(); }
}
class Child extends Parent {
    @Override
    public Dog getAnimal() { return new Dog(); } // βœ… Dog is a subtype of Animal
}

// For generics, use wildcards:
class Parent {
    public List<? extends Animal> getAnimals() { return new ArrayList<>(); }
}

Performance

Generic inheritance:
- Runtime: Zero overhead
- Bridge methods: +1 call (JIT inlines)
- Compile-time: complex type checking

Production Experience

Repository pattern:

public interface Repository<T, ID> {
    Optional<T> findById(ID id);
    List<T> findAll();
}

public interface UserRepository extends Repository<User, Long> {
    // User and Long are concretized
    // But UserRepository does NOT inherit Repository<Object, Object>
}

// Generic service
public class BaseService<T> {
    private final Repository<T, Long> repo;

    public BaseService(Repository<T, Long> repo) {
        this.repo = repo;
    }
}

// Specialization
public class UserService extends BaseService<User> {
    public UserService(UserRepository repo) {
        super(repo);  // βœ… UserRepository extends Repository<User, Long>
    }
}

Event hierarchy:

public abstract class Event { }
public class UserCreatedEvent extends Event { }
public class UserDeletedEvent extends Event { }

// Listener with wildcard
public interface EventListener<T extends Event> {
    void onEvent(T event);
}

// Processor accepts all Events
public class EventProcessor {
    private final List<EventListener<? extends Event>> listeners = new ArrayList<>();

    public <T extends Event> void register(EventListener<T> listener) {
        listeners.add(listener);
    }

    public <T extends Event> void publish(T event) {
        for (EventListener<? extends Event> listener : listeners) {
            // dispatch
        }
    }
}

Best Practices

// βœ… Wildcard for flexibility
public void process(List<? extends Animal> animals) { }

// βœ… Generic class with bound
public class AnimalBox<T extends Animal> { }

// βœ… Covariant return type
@Override
public List<Dog> getAnimals() { }

// ❌ Expecting covariance
// ❌ Raw types to bypass restrictions
// ❌ Ignoring invariance

🎯 Interview Cheat Sheet

Must know:

  • Generics are invariant: List<Dog> does NOT inherit from List<Animal>, even if Dog extends Animal
  • Reason: if covariance were allowed, you could add a Cat to a List
  • <? extends T> β€” covariance for reading: List<? extends Animal> animals = dogsList
  • <? super T> β€” contravariance for writing: List<? super Dog> dogs = animalList
  • Nested generics are also invariant: Map<String, List<Dog>> != Map<String, List<Animal>>
  • Bridge methods are created to preserve polymorphism when inheriting generic classes

Frequent follow-up questions:

  • Why are generics invariant? β€” Type safety: covariance would break type integrity
  • How do generics differ from arrays? β€” Arrays are covariant (String[] extends Object[]), generics are not
  • **How to pass List to a method accepting List?** β€” Use `List<? extends Animal>`
  • What is covariant return type? β€” A subclass can return a more specific type: Dog getAnimal() instead of Animal

Red flags (DO NOT say):

  • ❌ β€œList is a subtype of List" β€” Generics are invariant
  • ❌ β€œArrays and generics are the same in inheritance” β€” Arrays are covariant, generics are invariant
  • ❌ β€œWildcards solve all problems” β€” Wildcards limit read/write capabilities
  • ❌ β€œA generic class can inherit a raw class” β€” You can, but type safety is lost

Related topics:

  • [[11. What are Generics in Java]]
  • [[13. What is type erasure]]
  • [[16. What is the difference between <? extends T> and <? super T>]]
  • [[25. What are bridge methods and why are they needed]]