Множественные подстановочные знаки для общих методов делают Java-компилятор (и меня!) Очень запутанным

Сначала рассмотрим простой сценарий (см. полный источник на ideone.com):

import java.util.*;

public class TwoListsOfUnknowns {
    static void doNothing(List<?> list1, List<?> list2) { }

    public static void main(String[] args) {
        List<String> list1 = null;
        List<Integer> list2 = null;
        doNothing(list1, list2); // compiles fine!
    }
}

Две подстановочные знаки не связаны друг с другом, поэтому вы можете вызвать doNothing с помощью List<String> и a List<Integer>. Другими словами, два ? могут относиться к совершенно другим типам. Следовательно, следующее не компилируется, что следует ожидать (также на ideone.com):

import java.util.*;

public class TwoListsOfUnknowns2 {
    static void doSomethingIllegal(List<?> list1, List<?> list2) {
        list1.addAll(list2); // DOES NOT COMPILE!!!
            // The method addAll(Collection<? extends capture#1-of ?>)
            // in the type List<capture#1-of ?> is not applicable for
            // the arguments (List<capture#2-of ?>)
    }
}

До сих пор так хорошо, но здесь, где вещи начинают очень запутываться (как видно на ideone.com):

import java.util.*;

public class LOLUnknowns1 {
    static void probablyIllegal(List<List<?>> lol, List<?> list) {
        lol.add(list); // this compiles!! how come???
    }
}

Вышеприведенный код компилируется для меня в Eclipse и sun-jdk-1.6.0.17 на ideone.com, но должен ли он? Разве не возможно, что мы имеем List<List<Integer>> lol и a List<String> list, аналогичные две несвязанные ситуации с символами из TwoListsOfUnknowns?

Фактически следующая небольшая модификация в этом направлении не компилируется, что и следовало ожидать (как видно на ideone.com):

import java.util.*;

public class LOLUnknowns2 {
    static void rightfullyIllegal(
            List<List<? extends Number>> lol, List<?> list) {

        lol.add(list); // DOES NOT COMPILE! As expected!!!
            // The method add(List<? extends Number>) in the type
            // List<List<? extends Number>> is not applicable for
            // the arguments (List<capture#1-of ?>)
    }
}

Итак, похоже, что компилятор выполняет свою работу, но затем мы получаем это (как видно на ideone.com):

import java.util.*;

public class LOLUnknowns3 {
    static void probablyIllegalAgain(
            List<List<? extends Number>> lol, List<? extends Number> list) {

        lol.add(list); // compiles fine!!! how come???
    }
}

Опять же, мы можем иметь, например, a List<List<Integer>> lol и a List<Float> list, поэтому это не должно компилироваться, правильно?

На самом деле, вернемся к более простой LOLUnknowns1 (две неограниченные подстановочные знаки) и попытаемся увидеть, можем ли мы фактически вызвать probablyIllegal каким-либо образом. Сначала попробуйте "простой" случай и выберите один тип для двух подстановочных знаков (как видно на ideone.com):

import java.util.*;

public class LOLUnknowns1a {
    static void probablyIllegal(List<List<?>> lol, List<?> list) {
        lol.add(list); // this compiles!! how come???
    }

    public static void main(String[] args) {
        List<List<String>> lol = null;
        List<String> list = null;
        probablyIllegal(lol, list); // DOES NOT COMPILE!!
            // The method probablyIllegal(List<List<?>>, List<?>)
            // in the type LOLUnknowns1a is not applicable for the
            // arguments (List<List<String>>, List<String>)
    }
}

Это не имеет смысла! Здесь мы даже не пытаемся использовать два разных типа, и он не компилируется! Создание List<List<Integer>> lol и List<String> list также дает аналогичную ошибку компиляции! Фактически, из моих экспериментов единственный способ компиляции кода - это первый аргумент - явный тип null (как видно на ideone.com):

import java.util.*;

public class LOLUnknowns1b {
    static void probablyIllegal(List<List<?>> lol, List<?> list) {
        lol.add(list); // this compiles!! how come???
    }

    public static void main(String[] args) {
        List<String> list = null;
        probablyIllegal(null, list); // compiles fine!
            // throws NullPointerException at run-time
    }
}

Итак, вопросы касаются LOLUnknowns1, LOLUnknowns1a и LOLUnknowns1b:

  • Какие типы аргументов принимают probablyIllegal?
  • Должен ли lol.add(list); компилироваться вообще? Это типично?
  • Является ли это ошибкой компилятора или я неправильно понимаю правила преобразования захвата для подстановочных знаков?

Приложение A: Двойной LOL?

Если кому-то интересно, это компилируется отлично (как видно на ideone.com):

import java.util.*;

public class DoubleLOL {
    static void omg2xLOL(List<List<?>> lol1, List<List<?>> lol2) {
        // compiles just fine!!!
        lol1.addAll(lol2);
        lol2.addAll(lol1);
    }
}

Приложение B: Вложенные подстановочные знаки - что они на самом деле означают?

Дальнейшее исследование указывает на то, что, возможно, несколько подстановочных знаков не имеют ничего общего с проблемой, но скорее всего вложенные подстановочные знаки являются источником путаницы.

import java.util.*;

public class IntoTheWild {

    public static void main(String[] args) {
        List<?> list = new ArrayList<String>(); // compiles fine!

        List<List<?>> lol = new ArrayList<List<String>>(); // DOES NOT COMPILE!!!
            // Type mismatch: cannot convert from
            // ArrayList<List<String>> to List<List<?>>
    }
}

Итак, похоже, что List<List<String>> не является List<List<?>>. Фактически, в то время как любой List<E> является List<?>, он не выглядит как List<List<E>> является List<List<?>> (как видно на ideone. ком):

import java.util.*;

public class IntoTheWild2 {
    static <E> List<?> makeItWild(List<E> list) {
        return list; // compiles fine!
    }
    static <E> List<List<?>> makeItWildLOL(List<List<E>> lol) {
        return lol;  // DOES NOT COMPILE!!!
            // Type mismatch: cannot convert from
            // List<List<E>> to List<List<?>>
    }
}

Возникает новый вопрос: то, что есть List<List<?>>?

Ответ 1

Как указано в Приложении B, это не имеет никакого отношения к нескольким подстановочным знакам, а скорее к неправильному пониманию того, что действительно означает List<List<?>>.

Позвольте сначала напомнить себе, что означает, что Java-дженерики являются инвариантными:

  • An Integer является Number
  • A List<Integer> НЕ является List<Number>
  • A List<Integer> IS a List<? extends Number>

Теперь мы просто применяем тот же аргумент к нашей ситуации с вложенным списком (подробнее см. приложение)

:
  • A List<String> (может быть захвачен) a List<?>
  • A List<List<String>> НЕ (доступен для захвата) a List<List<?>>
  • A List<List<String>> IS (доступен для захвата) a List<? extends List<?>>

С учетом этого можно объяснить все фрагменты в вопросе. Путаница возникает (ложно), полагая, что тип типа List<List<?>> может захватывать типы типа List<List<String>>, List<List<Integer>> и т.д. Это НЕ верно.

То есть a List<List<?>>:

  • НЕ является списком, элементами которого являются списки одного неизвестного типа.
    • ... это будет List<? extends List<?>>
  • Вместо этого это список, элементами которого являются списки ЛЮБОГО типа.

Отрывки

Вот фрагмент, иллюстрирующий приведенные выше пункты:

List<List<?>> lolAny = new ArrayList<List<?>>();

lolAny.add(new ArrayList<Integer>());
lolAny.add(new ArrayList<String>());

// lolAny = new ArrayList<List<String>>(); // DOES NOT COMPILE!!

List<? extends List<?>> lolSome;

lolSome = new ArrayList<List<String>>();
lolSome = new ArrayList<List<Integer>>();

Дополнительные фрагменты

Вот еще один пример с ограниченным вложенным шаблоном:

List<List<? extends Number>> lolAnyNum = new ArrayList<List<? extends Number>>();

lolAnyNum.add(new ArrayList<Integer>());
lolAnyNum.add(new ArrayList<Float>());
// lolAnyNum.add(new ArrayList<String>());     // DOES NOT COMPILE!!

// lolAnyNum = new ArrayList<List<Integer>>(); // DOES NOT COMPILE!!

List<? extends List<? extends Number>> lolSomeNum;

lolSomeNum = new ArrayList<List<Integer>>();
lolSomeNum = new ArrayList<List<Float>>();
// lolSomeNum = new ArrayList<List<String>>(); // DOES NOT COMPILE!!

Вернуться к вопросу

Чтобы вернуться к фрагментам в вопросе, следующее будет вести себя как ожидалось (как видно на ideone.com):

public class LOLUnknowns1d {
    static void nowDefinitelyIllegal(List<? extends List<?>> lol, List<?> list) {
        lol.add(list); // DOES NOT COMPILE!!!
            // The method add(capture#1-of ? extends List<?>) in the
            // type List<capture#1-of ? extends List<?>> is not 
            // applicable for the arguments (List<capture#3-of ?>)
    }
    public static void main(String[] args) {
        List<Object> list = null;
        List<List<String>> lolString = null;
        List<List<Integer>> lolInteger = null;

        // these casts are valid
        nowDefinitelyIllegal(lolString, list);
        nowDefinitelyIllegal(lolInteger, list);
    }
}

lol.add(list); является незаконным, поскольку мы можем иметь List<List<String>> lol и a List<Object> list. Фактически, если мы прокомментируем выражение о нарушении, код компилируется и что именно то, что мы имеем с первым вызовом в main.

Все методы probablyIllegal в вопросе не являются незаконными. Все они совершенно законные и типичные. В компиляторе абсолютно нет ошибок. Он делает именно то, что он должен делать.


Ссылки

Связанные вопросы


Приложение: Правила преобразования захвата

(Это было поднято в первой ревизии ответа, это достойное дополнение к инвариантному аргументу типа.)

5.1.10 Преобразование захвата

Пусть G называет объявление общего типа с n формальными параметрами A 1... A n с соответствующими границами U 1... U псуб > . Существует преобразование захвата из G < T 1... T n > к G < S 1... S n > , где, для 1 <= я <= n:

  • Если T i является аргументом типа подстановки формы ?, то...
  • Если T i является аргументом типа подстановки формы ? extends B i, тогда...
  • Если T i является аргументом типа подстановки формы ? super B i, тогда...
  • В противном случае S i= T i.

Преобразование захвата не применяется рекурсивно.

Этот раздел может ввести в заблуждение, особенно в отношении нерекурсивного применения преобразования захвата (здесь CC), но ключ заключается в том, что не все ? могут CC; это зависит от того, где он появляется. В правиле 4 нет рекурсивного приложения, но когда применяются правила 2 или 3, соответствующий B i сам может быть результатом CC.

Проделайте несколько простых примеров:

  • List<?> может CC List<String>
    • ? может CC по правилу 1
  • List<? extends Number> может CC List<Integer>
    • ? может CC по правилу 2
    • При применении правила 2 B i просто Number
  • List<? extends Number> не может СС List<String>
    • ? может CC по правилу 2, но ошибка времени компиляции возникает из-за несовместимых типов

Теперь попробуйте некоторое вложение:

  • List<List<?>> не может СС List<List<String>>
    • Правило 4 применяется, и CC не является рекурсивным, поэтому ? НЕ может СС
  • List<? extends List<?>> может CC List<List<String>>
    • Первая ? может CC по правилу 2
    • При применении правила 2 B i теперь имеет значение List<?>, которое может CC List<String>
    • Оба ? могут CC
  • List<? extends List<? extends Number>> может CC List<List<Integer>>
    • Первая ? может CC по правилу 2
    • При применении правила 2 B i теперь является List<? extends Number>, который может CC List<Integer>
    • Оба ? могут CC
  • List<? extends List<? extends Number>> не может СС List<List<Integer>>
    • Первая ? может CC по правилу 2
    • При применении правила 2 B i теперь является List<? extends Number>, который может CC, но дает ошибку времени компиляции при применении к List<Integer>
    • Оба ? могут CC

Чтобы еще раз пояснить, почему некоторые ? могут CC и другие не могут, рассмотрите следующее правило: вы НЕ можете напрямую создавать шаблон подстановки. То есть следующее дает ошибку времени компиляции:

    // WildSnippet1
    new HashMap<?,?>();         // DOES NOT COMPILE!!!
    new HashMap<List<?>, ?>();  // DOES NOT COMPILE!!!
    new HashMap<?, Set<?>>();   // DOES NOT COMPILE!!!

Однако следующие компилируются просто:

    // WildSnippet2
    new HashMap<List<?>,Set<?>>();            // compiles fine!
    new HashMap<Map<?,?>, Map<?,Map<?,?>>>(); // compiles fine!

Причина компиляции WildSnippet2 заключается в том, что, как объяснялось выше, ни один из ? не может CC. В WildSnippet1 либо K, либо V (или оба) HashMap<K,V> могут СС, что делает прямое инстанцирование через new незаконным.

Ответ 2

  • Нет аргументов с дженериками. В случае LOLUnknowns1b null принимается так, как будто первый аргумент был введен как List. Например, это компилируется:

    List lol = null;
    List<String> list = null;
    probablyIllegal(lol, list);
    
  • IMHO lol.add(list); не должен компилироваться, но поскольку lol.add() нужен аргумент типа List<?>, а как только список List<?> работает. Странный пример, который заставляет меня думать об этой теории:

    static void probablyIllegalAgain(List<List<? extends Number>> lol, List<? extends Integer> list) {
        lol.add(list); // compiles fine!!! how come???
    }
    

    lol.add() нужен аргумент типа List<? extends Number>, и список набирается как List<? extends Integer>, он вписывается. Он не будет работать, если он не соответствует. То же самое для двойного LOL и других вложенных подстановок, если первый захват совпадает с вторым, все в порядке (и не может быть).

  • Опять же, я не уверен, но это действительно похоже на ошибку.

  • Я рад быть не единственным, кто постоянно использует переменные lol.

Ресурсы:
http://www.angelikalanger.com, FAQ о дженериках

РЕДАКТИРОВАТЬ:

  • Добавлен комментарий к Double Lol
  • И вложенные подстановочные знаки.

Ответ 3

не эксперт, но я думаю, что могу это понять.

измените свой пример на нечто эквивалентное, но с более отличительными типами:

static void probablyIllegal(List<Class<?>> x, Class<?> y) {
    x.add(y); // this compiles!! how come???
}

чтобы изменить список на [], чтобы более освещать:

static void probablyIllegal(Class<?>[] x, Class<?> y) {
    x.add(y); // this compiles!! how come???
}

теперь, x является not массивом некоторого типа класса. это массив любого типа класса. он может содержать Class<String> и a Class<Int>. это не может быть выражено с помощью обычного параметра типа:

static<T> void probablyIllegal(Class<T>[] x  //homogeneous! not the same!

Class<?> является супер-типом Class<T> для любого T. Если мы считаем, что тип представляет собой набор объектов, set Class<?> является объединением всех наборов Class<T> для всех T. (не включает ли он его, я не знаю...)