How do Generics work with inheritance
Even though String inherits from Object, List
π’ 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
- 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 fromList<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 ofAnimal
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]]