Несколько подстановочных знаков в универсальных методах делают 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> и 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 и 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???
    }
}

Опять же, мы можем иметь, например, List<List<Integer>> lol и 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.com):

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

3 ответа

Решение

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

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

  1. Integer это Number
  2. List<Integer> НЕ является List<Number>
  3. List<Integer> Это List<? extends Number>

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

  1. List<String> является (захватывается) List<?>
  2. List<List<String>> НЕ (захватывается) List<List<?>>
  3. List<List<String>> IS (захватывается) List<? extends List<?>>

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

Это 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 и List<Object> list, На самом деле, если мы закомментируем ошибочное утверждение, код скомпилируется, и это именно то, что мы имеем при первом вызове в main,

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


Рекомендации

Смежные вопросы


Приложение: Правила захвата конвертации

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

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

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

  1. Если T i является аргументом подстановочного типа в форме ? затем …
  2. Если T i является аргументом подстановочного типа в форме ? extends Тогда я…
  3. Если T i является аргументом подстановочного типа в форме ? super Тогда я…
  4. В противном случае 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> не могу CC List<String>
    • ? может CC по правилу 2, но ошибка времени компиляции происходит из-за несовместимых типов

Теперь давайте попробуем вложить:

  • List<List<?>> не могу CC List<List<String>>
    • Применяется правило 4, а CC не является рекурсивным, поэтому ? не могу CC
  • List<? extends List<?>> может CC List<List<String>>
    • Первый ? может CC по правилу 2
    • При применении правила 2 B i теперь List<?>, который может УК List<String>
    • И то и другое ? может CC
  • List<? extends List<? extends Number>> может CC List<List<Integer>>
    • Первый ? может CC по правилу 2
    • При применении правила 2 B i теперь List<? extends Number>, который может УК List<Integer>
    • И то и другое ? может CC
  • List<? extends List<? extends Number>> не могу CC 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> может CC, который делает прямую реализацию через new незаконным.

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

    List lol = null;
    List<String> list = null;
    probablyIllegal(lol, list);
    
  • по моему мнению 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 о дженериках

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

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

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

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

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 не является массивом некоторого типа класса. это массив любого типа класса. он может содержать Class<String> и Class<Int>, это нельзя выразить обычным параметром типа:

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

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

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