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>>
    

В обзоре

  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 и все в этом.


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

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

class A{}
class B extends A{}

затем

List<B> не подкласс List<A>

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

Другие вопросы по тегам