Как работают дженерики с наследованием
Даже если String наследует Object, List
🟢 Junior Level
Дженерики НЕ наследуются так же, как обычные классы!
Даже если String наследует Object, List<String> НЕ наследует List<Object>.
String extends Object; // ✅ String — подкласс Object
List<String> extends List<Object>; // ❌ НЕВЕРНО!
// Пример:
List<String> strings = new ArrayList<>();
List<Object> objects = strings; // ❌ compilation error!
Почему: Если бы это было разрешено, можно было бы добавить Integer в List<String> через ссылку List<Object> — это нарушило бы безопасность типов.
// Если бы было можно:
List<String> strings = new ArrayList<>();
List<Object> objects = strings; // допустим OK
objects.add(42); // добавили Integer в List<String>!
String s = strings.get(0); // ClassCastException!
🟡 Middle Level
Invariance дженериков
Дженерики инвариантны — List<A> и List<B> не связаны, даже если A extends B.
class Animal { }
class Dog extends Animal { }
class Cat extends Animal { }
List<Dog> dogs = new ArrayList<>();
List<Animal> animals = dogs; // ❌ compilation error!
Как получить гибкость
1. Wildcards — ? extends:
List<Dog> dogs = new ArrayList<>();
List<? extends Animal> animals = dogs; // ✅ OK
// Можно читать как Animal
Animal a = animals.get(0);
// Нельзя записывать
animals.add(new Cat()); // ❌ compilation error
2. Wildcards — ? super:
List<Animal> animals = new ArrayList<>();
List<? super Dog> dogs = animals; // ✅ OK
// Можно записывать Dog
dogs.add(new Dog());
// Можно читать только Object
Object obj = dogs.get(0);
Generic классы с наследованием
public class Box<T> {
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}
// Box<Dog> НЕ наследует Box<Animal>
Box<Dog> dogBox = new Box<>();
Box<Animal> animalBox = dogBox; // ❌ error
// Но можно использовать wildcard
Box<? extends Animal> animalBox2 = dogBox; // ✅
Типичные ошибки
- Ожидание covariance:
```java
List
ints = new ArrayList<>(); List numbers = ints; // ❌ error
// ✅ Правильно List<? extends Number> numbers = ints;
2. **Generic метод без wildcard:**
```java
public void processAnimals(List<Animal> animals) {
// принимает только List<Animal>
}
List<Dog> dogs = new ArrayList<>();
processAnimals(dogs); // ❌ error
// ✅ Правильно
public void processAnimals(List<? extends Animal> animals) { }
🔴 Senior Level
Internal Implementation
Type erasure и наследование:
class Parent<T> {
private T value;
public T get() { return value; }
}
class Child extends Parent<String> {
@Override
public String get() { return super.get(); }
}
// После erasure:
class Parent {
private Object value;
public Object get() { return value; }
}
class Child extends Parent {
public String get() { return (String) super.get(); }
// Bridge method для сохранения полиморфизма:
public Object get() { return get(); }
}
Архитектурные Trade-offs
| Свойство | Массивы | Дженерики |
|---|---|---|
| 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
// ✅ С 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> НЕ наследует AnimalBox<Animal>
// Но AnimalBox<Dog> наследует Box<Dog>
3. Covariant return type:
// List<Dog> НЕ является подтипом List<Animal> — generics инвариантны!
// Этот код НЕ КОМПИЛИРУЕТСЯ.
// Правильный covariant return — с конкретными типами (не generics):
class Parent {
public Animal getAnimal() { return new Animal(); }
}
class Child extends Parent {
@Override
public Dog getAnimal() { return new Dog(); } // ✅ Dog — подтип Animal
}
// Для generics используйте wildcards:
class Parent {
public List<? extends Animal> getAnimals() { return new ArrayList<>(); }
}
Производительность
Наследование дженериков:
- Runtime: Zero overhead
- Bridge methods: +1 вызов (JIT инлайнит)
- Compile-time: сложная проверка типов
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 и Long конкретизированы
// Но UserRepository НЕ наследует Repository<Object, Object>
}
// Generic service
public class BaseService<T> {
private final Repository<T, Long> repo;
public BaseService(Repository<T, Long> repo) {
this.repo = repo;
}
}
// Специализация
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 с wildcard
public interface EventListener<T extends Event> {
void onEvent(T event);
}
// Processor принимает все 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 для гибкости
public void process(List<? extends Animal> animals) { }
// ✅ Generic класс с bound
public class AnimalBox<T extends Animal> { }
// ✅ Covariant return type
@Override
public List<Dog> getAnimals() { }
// ❌ Ожидание covariance
// ❌ Raw types для обхода ограничений
// ❌ Игнорирование invariance
🎯 Шпаргалка для интервью
Обязательно знать:
- Дженерики инвариантны:
List<Dog>НЕ наследуетList<Animal>, даже если Dog extends Animal - Причина: если бы covariance была разрешена, можно было бы добавить Cat в List
<? extends T>— covariance для чтения:List<? extends Animal> animals = dogsList<? super T>— contravariance для записи:List<? super Dog> dogs = animalList- Nested generics тоже инвариантны:
Map<String, List<Dog>>!=Map<String, List<Animal>> - Bridge methods создаются для сохранения полиморфизма при наследовании generic классов
Частые уточняющие вопросы:
- Почему дженерики инвариантны? — Type safety: covariance нарушила бы целостность типов
- Чем дженерики отличаются от массивов? — Массивы covariant (String[] extends Object[]), дженерики — нет
- **Как передать List
в метод принимающий List ?** — Использовать `List<? extends Animal>` - Что такое covariant return type? — Подкласс может вернуть более конкретный тип:
Dog getAnimal()вместоAnimal
Красные флаги (НЕ говорить):
- ❌ “List
— подтип List " — Дженерики инвариантны - ❌ “Массивы и дженерики одинаковы в наследовании” — Массивы covariant, дженерики invariant
- ❌ “Wildcards решают все проблемы” — Wildcards ограничивают read/write возможности
- ❌ “Generic класс может наследовать raw класс” — Можно, но теряется type safety
Связанные темы:
- [[11. Что такое дженерики (Generics) в Java]]
- [[13. Что такое type erasure (стирание типов)]]
- [[16. В чём разница между <? extends T> и <? super T>]]
- [[25. Что такое bridge methods и зачем они нужны]]