Як працюють дженерики з наслідуванням
Навіть якщо 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 і навіщо вони потрібні]]