Питання 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 і навіщо вони потрібні]]