Дженерики, параметры типа и подстановочные знаки

Я пытаюсь понять java generics, и они кажутся чрезвычайно трудными для понимания. Например, это нормально...

public class Main {

    public static void main(String[] args) {
        List<?> list = null;
        method(list);
    }

    public static <T> void method(List<T> list) { }
}

... как это...

public class Main {

    public static void main(String[] args) {
        List<List<?>> list = null;
        method(list);
    }

    public static <T> void method(List<T> list) { }
}

... и это...

public class Main {

    public static void main(String[] args) {
        List<List<List<?>>> list = null;
        method(list);
    }

    public static <T> void method(List<List<T>> list) { }
}

... но это не скомпилируется:

public class Main {

    public static void main(String[] args) {
        List<List<?>> list = null;
        method(list);
    }

    public static <T> void method(List<List<T>> list) { }
}

Может кто-нибудь объяснить, что происходит на простом языке?

Ответ 1

Главное понять общие типы - это то, что они не ковариантны.

Итак, пока вы можете это сделать:

final String string = "string";
final Object object = string;

Следующие команды не будут компилироваться:

final List<String> strings = ...
final List<Object> objects = strings;

Это необходимо, чтобы избежать ситуаций, когда вы обходите общие типы:

final List<String> strings = ...
final List<Object> objects = strings;
objects.add(1);
final String string = strings.get(0); <-- oops

Итак, рассмотрим примеры один за другим

1

Ваш общий метод принимает List<T>, вы передаете List<?>; который (по существу) a List<Object>. T может быть присвоен типу Object, и компилятор счастлив.

2

Ваш общий метод тот же, вы передаете List<List<?>>. T может быть присвоен типу List<?>, и компилятор снова счастлив.

3

Это в основном то же самое, что 2 с другим уровнем вложенности. T по-прежнему является типом List<?>.

4

Вот где он выглядит немного грушевидно, и там, где приходит моя точка сверху.

Ваш общий метод принимает List<List<T>>. Вы передаете List<List<?>>. Теперь, поскольку общие типы не ковариантны, List<?> не может быть присвоен List<T>.

Фактическая ошибка компилятора (Java 8):

требуется: java.util.List<java.util.List<T>> найдено: java.util.List<java.util.List<?>> причина: не может вывести type-variable (s) T    (несоответствие аргументов, java.util.List<java.util.List<?>> не может быть преобразовано в java.util.List<java.util.List<T>>)

В основном компилятор сообщает вам, что он не может найти T для назначения из-за необходимости вывести тип List<T>, вложенный во внешний список.

Давайте рассмотрим это чуть подробнее:

List<?> является List неизвестного типа - он может быть List<Integer> или List<String>; мы можем get от него как Object, , но мы не можем add. Потому что в противном случае мы сталкиваемся с проблемой ковариации, о которой я упоминал.

List<List<?>> является List of List неизвестного типа - он может быть List<List<Integer>> или List<List<String>>. В случае 1 можно было назначить T Object и просто не разрешать операции add в списке подстановочных знаков. В случае 4 это невозможно сделать - прежде всего потому, что нет конструкции generics для предотвращения add внешнего List.

Если компилятор должен был назначить T - Object во втором случае, то возможно следующее:

final List<List<Integer>> list = ...
final List<List<?>> wildcard = list;
wildcard.add(Arrays.asList("oops"));

Таким образом, из-за ковариации невозможно присвоить a List<List<Integer>> любому другому родовому List безопасно.