В сравнении с наследованием самих типов почему параметризация Java остаётся инвариантной, даже когда один аргумент типа является подтипом другого?
List<String> не является подтипом List<Object>, хотя String — подтип Object. Иначе через ссылку типа List<Object> можно было бы добавить произвольный объект в список строк, что нарушило бы типобезопасность.
Инвариантность защищает параметризованные структуры от небезопасной записи. Для контролируемого чтения применяют wildcard с верхней границей, а для записи значений определённого типа — wildcard с нижней границей.
Generics появились в Java 5, чтобы проверять типы элементов коллекций на этапе компиляции и уменьшить необходимость в явных приведениях типов. При этом Java сохранила совместимость с существующим кодом и реализовала generics через стирание типов, а не через отдельные runtime-типы для каждой параметризации.
Инвариантность стала безопасным базовым правилом: связь наследования между аргументами типов автоматически не превращается в связь наследования между параметризованными типами.
Если бы List<String> можно было присвоить переменной типа List<Object>, код, работающий через эту переменную, считал бы допустимым добавление любого Object. После этого исходный список строк содержал бы значение другого типа.
Ошибку обнаружили бы не в месте небезопасной записи, а позже — например, при извлечении элемента как String. Это нарушило бы главный контракт параметризации: значение, принятое коллекцией, соответствует её аргументу типа.
Рассмотрим минимальный пример:
List<Object> означает: список способен безопасно принять любой объект. List<String> означает: в него можно добавлять только строки. Поэтому присваивание между ними запрещено, несмотря на наследование String от Object.
List<?> означает список некоторого неизвестного типа. Из него безопасно читать значения только как Object, потому что фактический тип может быть любым. Добавлять произвольные значения нельзя; гарантированно допустимо лишь null, поскольку он совместим с любым ссылочным типом.
Для чтения элементов как некоторого базового типа используют ? extends Base: такая коллекция может быть коллекцией любого подтипа Base, но запись конкретного ненулевого значения небезопасна. Для записи объектов типа Base и его подтипов используют ? super Base: фактический тип коллекции неизвестен, но он гарантированно является Base или его супертипом.
Инвариантность не означает, что параметризованные типы вообще нельзя использовать полиморфно. Полиморфизм выражают через wildcards, параметризованные методы или общий параметр типа. Например, метод может принимать List<? extends Number>, если ему нужно только читать числа.
Это отличается от массивов: массивы в Java ковариантны, поэтому String[] можно рассматривать как Object[]. Но запись неподходящего объекта в такой массив проверяется во время выполнения и может завершиться ArrayStoreException. Generics обычно предотвращают аналогичную ошибку раньше — на этапе компиляции.
Стирание типов не отменяет инвариантность. Оно объясняет ограничения runtime-проверок и отсутствие различия между многими параметризациями во время выполнения, но правила совместимости параметризованных типов определяются компилятором.
В библиотечном API есть метод, который формирует отчёт, перебирая элементы входной коллекции и вызывая операции, доступные для Number. Клиент хочет передать List<Integer> и List<BigDecimal>.
Вариант с параметром List<Number> неудобен: он не принимает ни List<Integer>, ни List<BigDecimal>, поскольку параметризации инвариантны. Вариант с raw-типом List принимает всё, но отключает значительную часть проверки типов и может породить предупреждения или ошибки позже.
Выбранное решение — List<? extends Number>. Оно принимает коллекции конкретных числовых типов и безопасно разрешает читать из них элементы как Number. Если бы отчёт должен был добавлять значения Number в переданную коллекцию, потребовался бы другой контракт: List<? super Number>; при этом конкретный тип элементов при чтении был бы известен только как Object.
Результат — API точно выражает направление использования коллекции: чтение через extends, запись через super, без отказа от статической типобезопасности.
Дополнительный вопрос: почему List<String> нельзя передать туда, где ожидается List<Object>, даже если метод ничего не добавляет?
Ответ: совместимость типов проверяется по объявленному контракту метода, а не по тому, что конкретная реализация сейчас делает внутри. Метод с параметром List<Object> имеет право добавить любой объект, поэтому передача List<String> была бы небезопасной. Если метод фактически только читает элементы, его контракт должен отражать это через List<?> или подходящую верхнюю границу.
Дополнительный вопрос: почему List<? extends Number> нельзя использовать для добавления даже значения типа Integer?
Ответ: фактический список может быть List<Double>, List<BigDecimal> или другим списком подтипа Number. Хотя Integer сам является Number, он не подходит гарантированно для каждого возможного фактического типа. Поэтому компилятор разрешает безопасное чтение как Number, но запрещает добавление конкретного ненулевого значения.
Дополнительный вопрос: чем инвариантность параметризованных типов отличается от ковариантности массивов с точки зрения места обнаружения ошибки?
Ответ: параметризованные типы в обычном случае инвариантны, поэтому несовместимое присваивание отклоняется компилятором. Массивы ковариантны: присваивание может пройти, но JVM проверяет фактический тип массива при записи. Поэтому ошибка с массивом может проявиться во время выполнения как ArrayStoreException, тогда как аналогичная ошибка с generics обычно блокируется раньше.