Вложенный универсальный тип 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) {}

person Louis Bliss    schedule 02.04.2014    source источник
comment
Какое именно сообщение об ошибке компиляции вы получаете?   -  person Mik378    schedule 02.04.2014
comment
Метод test(Map<?,List<?>>) в типе Main неприменим для аргументов (Map<Integer,List<String>>)   -  person Louis Bliss    schedule 02.04.2014
comment
Не совсем ответ, но вы можете использовать Map<?, List<?>>. Затем вам нужно будет использовать метод с Map<Integer, List<?>> mappy = new HashMap<>(); в качестве аргумента.   -  person Balder    schedule 02.04.2014
comment
Действительно, я могу, но тогда я теряю проверку шрифта на том, что я помещаю в свою карту! Я мог бы, например, написать mappy.put(123, new ArrayList<Integer>()) без ошибок, что нежелательно.   -  person Louis Bliss    schedule 02.04.2014


Ответы (2)


По сути, List<List<?>> и List<? extends List<?>> имеют разные аргументы типа.

На самом деле один является подтипом другого, но сначала давайте узнаем больше о том, что они означают по отдельности.

Понимание семантических различий

Вообще говоря, подстановочный знак ? представляет некоторую «отсутствующую информацию». Это означает "здесь когда-то был аргумент типа, но мы больше не знаем, что это такое". И поскольку мы не знаем, что это такое, накладываются ограничения на то, как мы можем использовать все, что относится к этому конкретному аргументу типа.

На данный момент давайте упростим пример, используя List вместо Map.

  • 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

Возможно, вы знаете, что "concrete" универсальные типы обладают инвариантностью< /em>, то есть 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>>
    

Рассматриваемый

  1. Map<Integer, List<String>> принимает только List<String> в качестве значения.
  2. Map<?, List<?>> принимает любое List в качестве значения.
  3. Map<Integer, List<String>> и Map<?, List<?>> — это разные типы с отдельной семантикой.
  4. Одно нельзя преобразовать в другое, чтобы мы не могли вносить небезопасные модификации.
  5. 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 и всем, что в нем.


Смотрите также

person Radiodef    schedule 02.04.2014
comment
Я понял, что List<List<?>> не то же самое, что List<? extends List<?>>, для меня это ясно. Мой вопрос больше о том, почему в моем случае я не могу использовать первое, когда я могу использовать второе или даже List<List<E>>. Редактировать: для общего метода, который берет Map‹?, List‹E››, это немного отличается, но в основном похоже. Извините? - person Louis Bliss; 02.04.2014
comment
Луи, ? extends относится к V карты. Ваша карта параметризовала список как V. Это означает, что он может хранить в нем только List<String>. Карта, которая занимает List<?>, принимает любой список. - person Radiodef; 02.04.2014
comment
Хорошо, понял. Это на самом деле логично. - person Louis Bliss; 02.04.2014
comment
Вы, сэр, гений. Спасибо за такое хорошее объяснение! - person Maurice Müller; 02.06.2016

Это связано с тем, что правила создания подклассов для дженериков немного отличаются от того, что вы можете ожидать. В частности, если у вас есть:

class A{}
class B extends A{}

тогда

List<B> не является подклассом List<A>

Это подробно объясняется здесь, а также использование подстановочного знака ( символ "?") объясняется здесь.

person Bartek Maraszek    schedule 02.04.2014