Вопрос 24 · Раздел 20

Как работают дженерики с наследованием

Даже если String наследует Object, List НЕ наследует List.

Версии по языкам: English Russian Ukrainian

🟢 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;  // ✅

Типичные ошибки

  1. Ожидание 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 и зачем они нужны]]