Чем List<?> отличается от List<Object> по допустимым операциям записи и чтения?
List<?> означает список с неизвестным конкретным параметром типа, а List<Object> — список, параметром которого непосредственно является Object. Из List<?> безопасно читать только как Object и добавлять только null; в List<Object> можно добавлять любые ссылочные значения, но передать туда List<String> нельзя.
Generics появились в Java, чтобы проверять типобезопасность коллекций на этапе компиляции и уменьшить необходимость в явных приведениях типов. При этом параметризация коллекций остаётся инвариантной: наличие отношения String — подтип Object не делает List<String> подтипом List<Object>.
Wildcard ? решает другую задачу: позволяет описать коллекцию, конкретный параметр которой неизвестен, но сам объект всё равно можно безопасно использовать в ограниченном наборе операций.
Если заменить List<?> на List<Object>, API станет принимать только коллекции, параметризованные именно Object. Это неожиданно исключит List<String>, List<Integer> и другие типизированные списки, хотя из них безопасно читать значения как Object.
Обратная замена тоже меняет контракт. Через List<?> нельзя добавить произвольный объект: компилятор не знает, является ли фактический тип элемента String, Integer или другим типом, поэтому такая запись могла бы нарушить типобезопасность.
В List<Object> известен точный тип элемента — Object. Поэтому туда разрешено добавлять любое значение ссылочного типа, включая строки, числа, пользовательские объекты и null. При чтении результат имеет статический тип Object.
В List<?> знак вопроса обозначает некоторый фиксированный, но неизвестный тип. Пусть фактически это List<String>: добавление произвольного Integer было бы небезопасным. Поэтому компилятор разрешает добавлять только null, который совместим с любым ссылочным типом; чтение безопасно возвращает Object, поскольку любой ссылочный элемент является Object.
Пример показывает разницу:
List<?> не означает List<Object> и не является сокращением для него. Это безопасный тип для операций, не зависящих от конкретного параметра: например, можно узнать размер, очистить список или перебрать элементы как Object, но нельзя добавить произвольный объект.
Если API должен принимать список любого типа и только читать его, List<?> обычно точнее выражает намерение. Если API должен принимать и добавлять произвольные объекты, нужен именно List<Object> или другой подходящий контракт, например параметр типа либо wildcard с нижней границей.
Допустим, утилита журналирует содержимое любой коллекции. Вариант с параметром List<Object> формально кажется естественным, но вызов для List<String> не скомпилируется из-за инвариантности. Вариант с List<?> принимает списки любых параметров и позволяет безопасно преобразовать каждый элемент к строковому представлению через Object.
Можно было бы объявить метод с параметром типа, например List<T>, но это нужно, только если результат или другие аргументы должны сохранять связь с конкретным T. Для простого просмотра содержимого такая параметризация избыточна: List<?> точнее показывает, что конкретный тип элементов методу не нужен.
Выбранное решение — List<?>. Оно расширяет применимость API без небезопасных приведений и не создаёт ложного обещания, будто метод может добавлять произвольные объекты в исходную коллекцию.
Можно ли присвоить List<String> переменной типа List<?>?
Да. List<?> совместим с параметризованной коллекцией любого ссылочного типа, потому что конкретный параметр просто захватывается как неизвестный. Это не даёт права добавлять туда строки: после присваивания статический тип переменной уже не сообщает, что фактический список является List<String>.
Почему из List<?> результат чтения имеет тип Object, если фактический список может быть List<String>?
Потому что на месте использования известен только верхний безопасный тип. Компилятор не может полагаться на фактический параметр, который скрыт wildcard, поэтому выражение чтения типизируется как Object; при необходимости вызывающий код должен явно проверить или привести значение к более узкому типу.
Почему List<Object> иногда полезнее, чем List<?>?
List<Object> разрешает записывать любые ссылочные значения, поэтому подходит для потребителя, который формирует универсальную коллекцию объектов. Однако он принимает только объект, типизированный как List<Object>; коллекцию List<String> передать нельзя. List<?> шире на входе, но почти не позволяет записывать значения, поэтому выбор зависит от того, является ли API преимущественно читателем или писателем.