Как работает наследование с дженериками (обобщенными типами) в Java?

Ответ

Наследование с дженериками в Java не является интуитивным. Хотя класс Integer наследуется от Number, контейнер ArrayList<Integer> НЕ является подтипом ArrayList<Number>. Это обеспечивает безопасность типов во время компиляции.

Проблема:

List<Number> numbers = new ArrayList<Integer>(); // Ошибка компиляции!
// Если бы это было разрешено, можно было бы добавить Double в список Integer:
numbers.add(3.14); // Нарушение типобезопасности!

Решение: Wildcards (подстановочные знаки) Для работы с иерархиями в дженериках используются ? extends (ковариантность) и ? super (контравариантность).

  1. ? extends T (Producer - читает): Можно безопасно читать элементы как T.

    List<? extends Number> numbers = new ArrayList<Integer>(); // OK
    Number num = numbers.get(0); // Чтение разрешено
    // numbers.add(new Integer(10)); // Ошибка компиляции - запись запрещена
  2. ? super T (Consumer - пишет): Можно безопасно передавать объекты типа T (или его подтипов) в коллекцию.

    List<? super Integer> list = new ArrayList<Number>(); // OK
    // Integer можно добавить, т.к. он является подтипом любого "super" типа (Number, Object)
    list.add(42); // Запись разрешена
    // Integer value = list.get(0); // Ошибка компиляции - чтение как Integer небезопасно
    Object obj = list.get(0); // Чтение как Object разрешено

Правило PECS (Producer-Extends, Consumer-Super):

  • Используй ? extends T, если параметр является источником (producer) данных (например, возвращает T).
  • Используй ? super T, если параметр является приемником (consumer) данных (например, принимает T).

Ответ 18+ 🔞

А, ну это классика, блядь! Привет, студент-страдалец! Сел разбираться с дженериками в Java? Ну, держись, щас я тебе на пальцах объясню, как тут всё ебнуто, но логично.

Смотри, вроде бы всё просто: Integer — это Number, да? Как сын — отец. Ну логично же, что ArrayList<Integer> — это тоже типа ArrayList<Number>? А вот нихуя!

List<Number> numbers = new ArrayList<Integer>(); // Компилятор: "Иди нахуй, мудила!"

И он прав, ёпта! Потому что если бы это прокатило, начался бы пиздец. Ты бы в этот, с позволения сказать, список Integer'ов спокойно запихнул Double — и всё, приехали. Runtime ошибка, всё падает, ай-ай-ай. Компилятор этого не любит, он за безопасность, блядь, типобезопасность.

Так вот, чтобы не быть совсем уж мудаком и всё-таки работать с этими семейными связями, придумали дикие карты, wildcards. Это вот эти знаки вопроса ?, от которых глаза на лоб лезут.

Их два основных фокус-покуса:

1. ? extends Твой_Тип (Читатель, Производитель) Представь, что у тебя есть мешок. Ты точно знаешь, что в нём лежат какие-то наследники Number (может, Integer, может, Double). Ты из этого мешка можешь доставать и быть уверен, что это будет Number. А вот засунуть туда нихуя нельзя! Потому что если мешок для Integer, а ты сунешь Double — опять пиздец.

List<? extends Number> numbers = new ArrayList<Integer>(); // Теперь ок!
Number num = numbers.get(0); // Достать — пожалуйста, отец родной.
// numbers.add(new Integer(10)); // А вот сунуть — НИЗЯ! Компилятор бьёт по рукам.

2. ? super Твой_Тип (Писатель, Потребитель) А вот обратная история. У тебя есть яма (List). Ты знаешь, что эта яма принимает как минимум Integer и всё, что выше по иерархии (Number, Object). В такую яму ты смело можешь бросать свои Integer (и даже его наследников, если бы они были). А вот достать оттуда конкретный Integer — опасно, хуй знает, что там на самом деле лежит. Максимум — Object.

List<? super Integer> list = new ArrayList<Number>(); // Окей, яма готова.
list.add(42); // Бросай Integer, не стесняйся! Всё схватит.
// Integer value = list.get(0); // А вот так — НИЗЯ! Чё достанешь — хз.
Object obj = list.get(0); // Только так, с прищуром, как Object.

А чтобы не запоминать эту хуйню, умные дядьки придумали мнемонику PECS (Producer-Extends, Consumer-Super). Запомни раз и нахуй:

  • Если параметр ПРОИЗВОДИТ (producer) тебе значения (T), используй extends. Читать можно.
  • Если параметр ПОТРЕБЛЯЕТ (consumer) твои значения (T), используй super. Писать можно.

Вот и вся магия, блядь. Сначала мозг ебётся, а потом привыкаешь. Главное — не пытайся протолкнуть Double в список для Integer, а то комплятор тебе такую пиздюлину впишет, что мало не покажется.