Java вложенный универсальный тип
Почему нужно использовать универсальный тип Map<?, ? extends List<?>>
вместо простого Map<?, List<?>>
для следующих test()
метод?
public static void main(String[] args) {
Map<Integer, List<String>> mappy =
new HashMap<Integer, List<String>>();
test(mappy);
}
public static void test(Map<?, ? extends List<?>> m) {}
// Doesn't compile
// public static void test(Map<?, List<?>> m) {}
Отмечая, что следующее работает, и что три метода в любом случае имеют один и тот же стертый тип.
public static <E> void test(Map<?, List<E>> m) {}
2 ответа
В корне, List<List<?>>
а также List<? extends List<?>>
имеют разные аргументы типа.
На самом деле это тот случай, когда один является подтипом другого, но сначала давайте узнаем больше о том, что они имеют в виду индивидуально.
Понимание семантических различий
Вообще говоря, подстановочный знак ?
представляет некоторую "недостающую информацию". Это означает, что "когда-то здесь был аргумент типа, но мы больше не знаем, что это такое". И поскольку мы не знаем, что это такое, налагаются ограничения на то, как мы можем использовать все, что относится к этому конкретному аргументу типа.
На данный момент, давайте упростим пример с помощью List
вместо Map
,
List<List<?>>
содержит любой вид List с аргументом любого типа. Итак, т.е.List<List<?>> theAnyList = new ArrayList<List<?>>(); // we can do this theAnyList.add( new ArrayList<String>() ); theAnyList.add( new LinkedList<Integer>() ); List<?> typeInfoLost = theAnyList.get(0); // but we are prevented from doing this typeInfoLost.add( new Integer(1) );
Мы можем поставить любой
List
вtheAnyList
, но при этом мы потеряли знание об их элементах.Когда мы используем
? extends
,List
содержит некоторый конкретный подтип List, но мы больше не знаем, что это такое. Итак, т.е.List<? extends List<Float>> theNotSureList = new ArrayList<ArrayList<Float>>(); // we can still use its elements // because we know they store Float List<Float> aFloatList = theNotSureList.get(0); aFloatList.add( new Float(1.0f) ); // but we are prevented from doing this theNotSureList.add( new LinkedList<Float>() );
Больше не безопасно добавлять что-либо к
theNotSureList
потому что мы не знаем фактический тип его элементов. (Было ли это изначальноList<LinkedList<Float>>
? ИлиList<Vector<Float>>
? Мы не знаем.)Мы можем собрать их вместе и иметь
List<? extends List<?>>
, Мы не знаем, какой типList
в нем больше нет, и мы не знаем тип элемента техList
с либо. Итак, т.е.List<? extends List<?>> theReallyNotSureList; // these are fine theReallyNotSureList = theAnyList; theReallyNotSureList = theNotSureList; // but we are prevented from doing this theReallyNotSureList.add( new Vector<Float>() ); // as well as this theReallyNotSureList.get(0).add( "a String" );
Мы потеряли информацию как о
theReallyNotSureList
а также тип элементаList
внутри.(Но вы можете заметить, что мы можем присвоить ему любой вид списков, содержащих списки...)
Итак, чтобы сломать это:
// ┌ applies to the "outer" List
// ▼
List<? extends List<?>>
// ▲
// └ applies to the "inner" List
Map
работает так же, просто у него больше параметров типа:
// ┌ Map K argument
// │ ┌ Map V argument
// ▼ ▼
Map<?, ? extends List<?>>
// ▲
// └ List E argument
Зачем ? extends
является необходимым
Вы можете знать, что "конкретные" универсальные типы имеют неизменность, то есть List<Dog>
не является подтипом List<Animal>
даже если class Dog extends Animal
, Вместо этого подстановочный знак - это то, как мы имеем ковариацию, то есть List<Dog>
это подтип List<? extends Animal>
,
// Dog is a subtype of Animal
class Animal {}
class Dog extends Animal {}
// List<Dog> is a subtype of List<? extends Animal>
List<? extends Animal> a = new ArrayList<Dog>();
// all parameterized Lists are subtypes of List<?>
List<?> b = a;
Таким образом, применяя эти идеи к вложенным List
:
List<String>
это подтипList<?>
ноList<List<String>>
не является подтипомList<List<?>>
, Как показано выше, это не позволяет нам подвергать риску безопасность типов, добавляя неправильные элементы вList
,List<List<String>>
это подтипList<? extends List<?>>
, потому что ограниченный символ допускает ковариацию. То есть,? extends
позволяет тот факт, чтоList<String>
это подтипList<?>
следует рассматривать.List<? extends List<?>>
на самом деле является общим супертипом:List<? extends List<?>> ╱ ╲ List<List<?>> List<List<String>>
В обзоре
Map<Integer, List<String>>
принимает толькоList<String>
как ценность.Map<?, List<?>>
принимает любойList
как ценность.Map<Integer, List<String>>
а такжеMap<?, List<?>>
это отдельные типы, которые имеют отдельную семантику.- Одно не может быть преобразовано в другое, чтобы мы не могли вносить изменения небезопасным способом.
Map<?, ? extends List<?>>
является общим супертипом, который налагает безопасные ограничения:Map<?, ? extends List<?>> ╱ ╲ Map<?, List<?>> Map<Integer, List<String>>
Как работает универсальный метод
Используя параметр типа в методе, мы можем утверждать, что List
имеет какой-то конкретный тип.
static <E> void test(Map<?, List<E>> m) {}
Эта конкретная декларация требует, чтобы все List
в Map
имеют одинаковый тип элемента. Мы не знаем, что это за тип на самом деле, но мы можем использовать его абстрактно. Это позволяет нам выполнять "слепые" операции.
Например, этот вид объявления может быть полезен для некоторого накопления:
static <E> List<E> test(Map<?, List<E>> m) {
List<E> result = new ArrayList<E>();
for(List<E> value : m.values()) {
result.addAll(value);
}
return result;
}
Мы не можем позвонить put
на m
потому что мы не знаем, какой у него тип ключа. Тем не менее, мы можем манипулировать его ценностями, потому что мы понимаем, что все они List
с тем же типом элемента.
Просто для удовольствия
Другой вариант, который не обсуждается в этом вопросе, состоит в том, чтобы иметь ограниченный подстановочный знак и общий тип для List
:
static <E> void test(Map<?, ? extends List<E>> m) {}
Мы могли бы назвать это с чем-то вроде Map<Integer, ArrayList<String>>
, Это самая разрешающая декларация, если мы заботимся только о типе E
,
Мы также можем использовать границы для вложения параметров типа:
static <K, E, L extends List<E>> void(Map<K, L> m) {
for(K key : m.keySet()) {
L list = m.get(key);
for(E element : list) {
// ...
}
}
}
Это одновременно и уместно в отношении того, что мы можем передать, а также в отношении того, как мы можем манипулировать m
и все в этом.
Смотрите также
- "Java Generics: что такое PECS?" за разницу между
? extends
а также? super
, - JLS 4.10.2. Подтипирование среди классов и типов интерфейса и JLS 4.5.1. Введите Аргументы Параметризованных Типов для точек входа в технические детали этого ответа.
Это потому, что правила создания подклассов для дженериков немного отличаются от того, что вы можете ожидать. В частности, если у вас есть:
class A{}
class B extends A{}
затем
List<B>
не подкласс List<A>
Это подробно объясняется здесь, а здесь используется подстановочный знак (символ "?").