Решил сегодня пройтись по Generics с прицелом на вопросы, которые любят задавать на технических интервью. Вроде бы тема базовая, но когда доходит до ? extends и ? super, многие начинают плавать



1. Почему List<Cat> — это не List<Animal>?
Есть иерархия:
class Animal {}
class Cat extends Animal {}
class Dog extends Animal {}

Казалось бы, раз Cat — это Animal, то список котов можно передать туда, где ждут список животных. Но Java не даёт:

List<Cat> cats = new ArrayList<>();
List<Animal> animals = cats; // Ошибка компиляции!

Почему? Представь, что это бы сработало. Тогда мы бы могли сделать:
animals.add(new Dog()); // Добавили собаку в список котов
Cat myCat = cats.get(0); // А тут нас ждёт сюрприз — это собака!

Компилятор не может гарантировать типобезопасность, поэтому Generics в Java инвариантны. List<Cat> и List<Animal> — совершенно разные типы, и точка.

И тут мы приходим к PECS, который часто стопорит на собеседовании:

2. Как читать данные из списка наследников? 

Допустим, нам нужен метод, который печатает имена всех животных из списка. Мы хотим, чтобы он принимал и List<Cat>, и List<Dog>.

Наивная попытка:

void printNames(List<Animal> animals) {  // не сработает для List<Cat>
    for (Animal a : animals) System.out.println(a);
}

Метод не примет список котов. Исправляем с помощью wildcard:

void printNames(List<? extends Animal> animals) {
    for (Animal a : animals) {          // читать можно — тип Animal гарантирован
        System.out.println(a);
    }
}

Теперь можно вызвать:

List<Cat> cats = List.of(new Cat(), new Cat());
printNames(cats); // Ок
List<Dog> dogs = List.of(new Dog());
printNames(dogs); // Ок

Важно: внутрь такого списка мы не можем ничего добавить (кроме null):

animals.add(new Cat()); // Ошибка компиляции!

Потому что компилятор не знает, какой именно подтип Animal лежит внутри. Вдруг это List<Dog>? Поэтому ? extends — это Producer: он только отдаёт данные (читает).

3. Как записывать данные?

Допустим есть задача: мы хотим метод, который добавляет котов в переданную коллекцию. При этом коллекция может быть не только List<Cat>, но и List<Animal>, и даже List<Object>.
Пишем:

void addCats(List<? super Cat> list) {
    list.add(new Cat());               // можно добавлять Cat и его наследников
    list.add(new BengalCat());         // тоже ок, если есть такой класс
}

Вызываем:

List<Animal> animals = new ArrayList<>();
addCats(animals); // ок
List<Object> objects = new ArrayList<>();
addCats(objects); // тоже ок
List<Cat> cats = new ArrayList<>();
addCats(cats);    // и даже так можно

А вот прочитать из такого списка что-то конкретное, кроме Object, уже нельзя:
Cat c = list.get(0); // Ошибка! Вдруг это List<Object>?

Можно только:
Object obj = list.get(0); // но это бесполезно, если мы ждали Cat

Поэтому ? super — это Consumer: он принимает данные (записывает).

Тут мы получаем золотое правило PECS (Producer Extends, Consumer Super)

Запомнить легко:
·         Если метод читает из структуры — используй extends.
·         Если метод пишет в структуру — используй super.

Пример из реальной жизни — метод Collections.copy(dest, src):

public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    // dest — Consumer (мы туда пишем) → super
    // src — Producer (мы оттуда читаем) → extends
}

Всё встаёт на свои места.

Generics — это не просто синтаксический сахар, а целая система, которая помогает отлавливать ошибки на этапе написания кода. А умение объяснить extends/super, PECS и рассказать про type erasure — это тот самый уровень, который выделяет разработчика на собеседовании.
Просмотры: 88
Открыть в MAX Сообщить о неточности

Репосты публикации

Публикация канала

Сообщить о неточности

Опишите, что нужно исправить. Если есть источник, добавьте ссылку.

Вы покидаете datool.ru

Ссылка ведёт на внешний сайт. datool.ru не отвечает за его содержимое — проверьте адрес, прежде чем вводить пароли или платёжные данные.

Переход через 10 с

Перейти