В API метода нужно только читать значения типа T из коллекции и принимать коллекции T и его подтипов. Какую параметризацию параметра выбрать?
Следует выбрать верхнюю границу wildcard: ? extends T, например List<? extends T>. Такая параметризация принимает коллекцию T или любого его подтипа и безопасно позволяет читать элементы как T.
Добавлять в такую коллекцию значения типа T нельзя, кроме null: фактический тип коллекции неизвестен и может быть более узким подтипом T.
Generics появились в Java, чтобы проверять совместимость типов во время компиляции, уменьшить количество явных приведений и сделать универсальные структуры данных безопаснее. При этом Java сохранила совместимость с кодом, использовавшим коллекции без параметров типов.
Механизм wildcard позволяет описывать не один конкретный параметр типа, а допустимый диапазон типов. Это особенно важно для API, которое должно принимать разные специализации одной обобщённой структуры.
Параметр List<T> принимает только список с точно такой параметризацией. Например, список Integer нельзя передать туда, где ожидается List<Number>, хотя Integer является подтипом Number: обобщённые типы в Java инвариантны.
Параметр List<?> принимает список любого типа, но при чтении его элементы имеют тип Object. Поэтому клиентский код теряет возможность безопасно работать с ними как с T без дополнительных проверок или приведений.
Запись List<? extends T> означает: существует некоторый неизвестный тип, являющийся T или его подтипом. Например, List<Integer> и List<Double> совместимы с List<? extends Number>.
Компилятор разрешает читать элементы как Number, потому что любой возможный элемент такой коллекции гарантированно является Number. Но добавление Number запрещено: если фактический список имеет тип List<Integer>, добавление произвольного Double нарушило бы его типобезопасность.
Это проявление принципа PECS: Producer Extends, Consumer Super. Коллекцию, из которой метод получает значения, обычно описывают через extends; коллекцию, в которую метод записывает значения типа T, — через super.
? extends T не делает коллекцию неизменяемой. Она может оставаться изменяемой для владельца, но через ссылку с такой параметризацией нельзя безопасно добавлять туда значения T. Если методу нужно сохранить точный тип параметра для возвращаемого значения или нескольких аргументов, вместо wildcard может подойти именованный параметр типа T.
Сервис форматирования чисел должен вычислять статистику для списков Integer, Long и BigDecimal, рассматривая каждый элемент как Number.
Вариант с List<Number> прост для чтения, но отклоняет списки List<Integer> и List<Long> из-за инвариантности generics. Вариант с необобщённым List принимает всё, однако отключает значительную часть проверок типов и может привести к ошибкам во время выполнения.
Вариант с List<?> универсальнее, но внутри метода значения доступны только как Object, поэтому придётся использовать небезопасные или лишние проверки. Выбранный вариант List<? extends Number> точно выражает контракт: метод только читает числовые значения, принимает все нужные специализации и сохраняет статическую типобезопасность.
Почему в коллекцию с ? extends T нельзя добавить значение самого типа T?
Потому что неизвестный тип внутри wildcard может быть конкретным подтипом T. Для List<? extends Number> это может быть List<Integer>, поэтому добавление Double или даже произвольного Number небезопасно. Компилятор запрещает такую операцию, поскольку не может доказать, что значение соответствует фактическому типу элементов.
Исключение — null: он совместим с любым ссылочным типом, поэтому его добавление не нарушает типовую безопасность. Это не означает, что добавление null всегда полезно с точки зрения бизнес-логики.
Чем ? extends T отличается от ? super T с точки зрения чтения и записи?
? extends T описывает производителя: из него можно получать значения как T, но нельзя добавлять обычные значения. ? super T описывает потребителя: в него можно безопасно добавлять T, однако при чтении гарантированно доступен только Object.
Например, источник чисел обычно имеет вид List<? extends Number>, а метод, складывающий Integer в переданную коллекцию, может принимать List<? super Integer>. Нижняя граница позволяет передать List<Integer>, List<Number> или List<Object>.
Когда вместо wildcard стоит использовать именованный параметр типа?
Wildcard подходит, когда метод использует тип только локально и его достаточно ограничить границей. Если тип нужно связать между несколькими параметрами или вернуть тот же конкретный тип, нужен именованный параметр, например T с верхней границей.
Это различие влияет на выразительность контракта. List<? extends T> говорит, что конкретный тип элемента не важен методу, а именованный T позволяет сохранить связь между аргументами и результатом. Выбор зависит не от предпочтения синтаксиса, а от того, требуется ли API знать и передавать дальше точную типовую идентичность.