В API метод должен перемещать элементы между двумя коллекциями согласованного типа. Почему два независимых wildcard не выражают это требование, а общий параметр типа выражает?
Два независимых wildcard обозначают два неизвестных типа, причём эти типы могут быть разными. Общий параметр типа связывает параметры метода одной переменной типа: компилятор проверяет, что обе коллекции согласованы с одним и тем же T.
Поэтому List<?> подходит для независимого чтения, но не позволяет безопасно перенести элемент из одной коллекции в другую. Для операции, где значение из одного параметра передаётся во второй, нужен общий параметр типа.
Generics появились в Java для статической проверки типов и повторного использования алгоритмов без явных приведений. До параметризации коллекции хранили значения как Object, поэтому ошибки типов обнаруживались преимущественно во время выполнения.
При этом Java сохранила совместимость с существующим байткодом благодаря стиранию типов. Связи между параметрами типа проверяются компилятором, но сами параметры обычно не участвуют в обычных runtime-проверках.
Представим операцию перемещения элемента: значение извлекается из первой коллекции и добавляется во вторую. Если параметры описаны как List<?>, компилятор знает лишь, что каждая коллекция содержит некоторый неизвестный тип, но не знает, что эти типы совпадают.
Разрешение добавления элемента из одной такой коллекции в другую могло бы привести к нарушению типобезопасности. Например, первая коллекция может фактически содержать String, а вторая — Integer, хотя обе подходят под List<?>.
В записи с общим параметром типа обе коллекции используют одну переменную T: List<T>. Значит, извлечённое значение имеет тип T, и второй параметр гарантированно принимает значение этого же типа.
Здесь T — не конкретный класс, известный заранее, а единое обозначение типа, выбранное для конкретного вызова. Для двух списков String компилятор подставит T как String; для двух списков Integer — как Integer.
У двух wildcard захват происходит независимо. В выражении с двумя List<?> компилятор фактически рассматривает их как List<CAP1> и List<CAP2>, где CAP1 и CAP2 — разные неизвестные типы. Поэтому значение типа CAP1 нельзя передать туда, где требуется CAP2.
Это не означает, что wildcard всегда хуже. List<?> лучше описывает API, которому нужно работать с одной коллекцией без зависимости от её конкретного параметра типа, например проверять размер или читать значения как Object. Общий T следует выбирать, когда между параметрами, результатом или несколькими операциями существует типовая связь.
Важно отличать согласованность типа от идентичности runtime-класса. Параметр типа проверяет связь на этапе компиляции, но из-за стирания типов не гарантирует, что фактические объекты во время выполнения имеют один и тот же класс.
В библиотеке реализуют перенос элементов между источником и приёмником. Вариант с двумя параметрами List<?> принимает максимально широкий набор списков, но не позволяет безопасно добавить прочитанный элемент во второй список. Разработчики могут попытаться добавить приведение к Object или к конкретному типу, что либо не решает проблему типобезопасности, либо создаёт риск ClassCastException.
Вариант с двумя List<T> корректно выражает связь и не требует приведений. Его компромисс — он не принимает произвольные комбинации списков с разными параметризациями, даже если между их типами есть наследование; это следствие инвариантности List.
Если операция действительно должна работать с иерархией типов, API можно спроектировать с разными ролями wildcard: источник читать через ? extends T, а приёмник заполнять через ? super T. Но это уже другая связь: общий T сохраняет согласованный тип операции, а wildcard расширяет допустимые границы конкретных коллекций.
List<T> автоматически означает, что два аргумента имеют один и тот же фактический тип во время выполнения?Нет. Это утверждение действует на уровне статической проверки конкретного вызова. После компиляции параметризация обычно стирается, поэтому JVM не сравнивает T у двух аргументов как отдельное runtime-значение.
Кроме того, компилятор может вывести для T общий подходящий тип, если контекст это допускает. Поэтому общий параметр выражает необходимую типовую связь, но не является гарантией идентичности классов объектов во время выполнения.
T на List<? extends Number> для максимальной гибкости?Такая замена позволяет читать элементы как Number, но не связывает две коллекции одним подтипом. Одна коллекция может быть List<Integer>, другая — List<Double>; обе подходят под List<? extends Number>.
Из первой коллекции нельзя безопасно добавить значение во вторую, потому что неизвестно, какой именно подтип Number она принимает. Общий T сохраняет эту связь, а ? extends Number только задаёт верхнюю границу чтения.
Integer и список Number в метод с двумя List<T>?Обычная параметризация List инвариантна: List<Integer> не является подтипом List<Number>. Поэтому компилятор не может просто выбрать T = Number и принять оба аргумента.
Это ограничение защищает типобезопасность: если бы List<Integer> считался List<Number>, в него можно было бы добавить Double. Для более гибкого API обычно разделяют тип элемента и границы контейнеров, например используют ? extends T для источника и ? super T для приёмника.