Поясните асимметрию совместимости wildcard параметризаций: почему коллекция Integer совместима с ожиданием ...

Поясните асимметрию совместимости wildcard-параметризаций: почему коллекция Integer совместима с ожиданием коллекции неизвестного подтипа Number, но коллекция с неизвестным подтипом Number не совместима с ожиданием коллекции Integer?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

List<Integer> можно передать туда, где ожидается List<? extends Number>, потому что Integer является допустимым неизвестным подтипом Number. Обратное присваивание запрещено: тип ? extends Number может обозначать не только Integer, но и, например, Double, поэтому Java не может гарантировать безопасность операций как с коллекцией Integer.

Исторический контекст

Обобщения появились в Java для статической проверки типов коллекций без постоянных приведений и риска скрытых ошибок во время выполнения. Однако параметризация Java инвариантна: List<Integer> не является подтипом List<Number>, даже если Integer является подтипом Number.

Wildcards добавили use-site-вариантность: вызывающий код может описать не конкретный параметр типа, а допустимое семейство параметризаций. Верхняя граница ? extends Number выражает возможность безопасно читать значения как Number, сохраняя запрет на потенциально небезопасную запись.

Постановка проблемы

Нужно принять коллекцию конкретного числового типа, например Integer, через API, которому достаточно читать элементы как Number. Простая параметризация List<Number> слишком узкая: она не принимает List<Integer> из-за инвариантности.

Но если API получает List<? extends Number>, внутри нельзя считать, что фактический тип элементов — именно Integer. Неверное расширение допустимой совместимости привело бы к возможности записать значение, которое не соответствует реальному типу коллекции.

Подробное решение

Верхняя граница wildcard читается как «существует некоторый неизвестный тип X, причём X является подтипом Number». Для List<Integer> таким X будет Integer, для List<Double> — Double. Поэтому обе коллекции совместимы с List<? extends Number> и из них безопасно извлекать значения типа Number.

Обратное присваивание требует доказать, что неизвестный X является именно Integer или его подтипом. Такой гарантии нет: исходная коллекция может фактически иметь параметр Double. Поэтому компилятор отклоняет присваивание.

import java.util.List; class Demo { static void read(List<? extends Number> values) { Number value = values.get(0); } static void assign(List<Integer> values) { List<? extends Number> numbers = values; // List<Integer> integers = numbers; // ошибка компиляции } }

Выражение List<? extends Number> является более общим описанием, чем List<Integer>, но это не обычное наследование параметризованных типов. Внутри метода чтение безопасно как чтение Number, а добавление произвольного Number запрещено, поскольку фактический X неизвестен.

Для записи используется другой контракт: List<? super Integer>. Он принимает коллекции Integer и его супертипов, потому что любую такую коллекцию безопасно пополнить Integer. Таким образом, выбор wildcard определяется требуемой направленностью использования: extends расширяет множество источников для чтения, а super — множество приёмников для записи.

Ситуация из практики

Сервис рассчитывает статистику по числовым значениям и не изменяет входную коллекцию. Вариант с List<Number> пришлось бы отклонить: вызывающий код не смог бы передать список Integer или Double без копирования.

Вариант с необобщённым List формально принимает любые списки, но отключает значительную часть проверки типов и может привести к ClassCastException или загрязнению кучи. Вариант с List<? extends Number> точно описывает контракт: сервис принимает любой список числового типа и читает значения через общий тип Number.

Выбранное решение сохраняет типобезопасность и не требует копирования данных. Цена — невозможность добавлять в такой список значения, поскольку API намеренно не знает его фактический параметр типа.

Что кандидаты часто упускают

  1. Почему List<Number> нельзя считать эквивалентом List<? extends Number>?

List<Number> означает коллекцию, в которую разрешено добавлять любой объект Number: Integer, Double или другой подтип. Поэтому она не может быть представлена списком Integer, который обязан принимать только Integer. List<? extends Number> означает источник с неизвестным конкретным типом, поэтому чтение разрешено, а безопасная запись произвольного Number — нет.

  1. Может ли List<? extends Integer> принимать список Number?

Нет. Верхняя граница означает, что фактический параметр должен быть Integer или его подтипом. Number является супертипом Integer и не удовлетворяет этому ограничению; кроме того, список Number может содержать Double, что делает чтение как Integer небезопасным.

  1. Почему wildcard не превращает параметризацию в полноценную ковариантность?

Wildcard предоставляет совместимость только в конкретном месте использования, а не меняет отношение подтипов между самими типами List<A> и List<B>. List<Integer> по-прежнему не становится подтипом List<Number>; она лишь может быть передана в параметр, объявленный как List<? extends Number>. Это позволяет выразить безопасное чтение, не разрешая небезопасные операции записи.