Подтвердить что ты не робот

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) {}
4b9b3361

Ответ 1

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

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

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

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

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

  • A List<List<?>> содержит любой вид List с любым аргументом типа. Итак, i.e.:

    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, но мы не знаем, что это такое. Итак, i.e.:

    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>>? Или a List<Vector<Float>>? Мы не знаем.)

  • Мы можем объединить их и иметь List<? extends List<?>>. Мы не знаем, какой тип List он имеет в нем больше, и мы не знаем тип элемента этих List. Итак, i.e.:

    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 и все в нем.


См. также

Ответ 2

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

class A{}
class B extends A{}

затем

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

Здесь подробно объясняется и объясняется использование шаблона (символ "?" ) здесь.