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

Давайте сначала рассмотрим простой сценарий ( см. Полный исходный код на ideone.com ):

import java.util.*;

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

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

Два подстановочных символа не связаны между собой, поэтому вы можете вызвать Ничего не делать со списком List и списком List . Другими словами, два ? могут относиться к совершенно разным типам. Следовательно, следующее не компилируется, чего и следовало ожидать ( также на 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)
            // in the type List is not applicable for
            // the arguments (List)
    }
}

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

import java.util.*;

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

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

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

import java.util.*;

public class LOLUnknowns2 {
    static void rightfullyIllegal(
            List> lol, List list) {

        lol.add(list); // DOES NOT COMPILE! As expected!!!
            // The method add(List) in the type
            // List> is not applicable for
            // the arguments (List)
    }
}

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

import java.util.*;

public class LOLUnknowns3 {
    static void probablyIllegalAgain(
            List> lol, List list) {

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

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

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

import java.util.*;

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

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

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

import java.util.*;

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

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

Итак, вопросы касательно LOLUnknowns1 , LOLUnknowns1a и LOLUnknowns1b :

  • Какие типы аргументов принимает , вероятно, Illegal ?
  • Если lol.add (список); компилировать вообще? Это типично?
  • Это ошибка компилятора, или я неправильно понимаю правила преобразования захвата для подстановочных знаков?

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

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

import java.util.*;

public class DoubleLOL {
    static void omg2xLOL(List> lol1, 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(); // compiles fine!

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

Таким образом, похоже, что List > не является List > . Фактически, хотя любой List является List > , не похоже, что какой-либо List > является Список > ( на ideone.com ):

import java.util.*;

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

Тогда возникает новый вопрос: что такое List > ?

58
задан polygenelubricants 23 August 2010 в 00:39
поделиться

3 ответа

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

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

  1. Integer - это Number
  2. A List is ] НЕ a Список
  3. A Список IS a Список

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

  1. A List is (захватывается ) a List
  2. A List > is НЕ (захватывается) a List >
  3. A List > IS (захватывается) a List >

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

То есть 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 > lol и список List list . Фактически, если мы закомментируем некорректное утверждение, код компилируется, и это именно то, что мы имеем при первом вызове в main .

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


Ссылки

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


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

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

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 B i , тогда…
  3. Если T i является аргументом типа подстановочного знака формы ? super B i , затем…
  4. В противном случае S i = T i .

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

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

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

  • List can CC List
    • The ? can CC by rule 1
  • List can CC List
    • The ? может CC по правилу 2
    • При применении правила 2, B i это просто Число
  • Список can NOT CC List
    • ? может CC по правилу 2, но ошибка времени компиляции возникает из-за несовместимых типов

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

  • List > может НЕ CC List >
    • Применяется правило 4, и CC не является рекурсивным, поэтому ? может НЕ CC
  • Список > can CC List >
    • Первый ? может CC по правилу 2
    • При применении правила 2, B i теперь является List , который может CC List
    • Оба ? могут CC
  • List > can CC List >
    • Первый ? может CC по правилу 2
    • При применении правила 2, B i теперь является списком , который может CC List
    • Оба ? могут CC
  • List > can NOT CC List >
    • Первый ? может CC по правилу 2
    • При применении правила 2 , B i теперь является списком , который может CC, но дает ошибку времени компиляции при применении к List
    • Both ? can 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 can CC, что делает прямое создание через новое незаконно.

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

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

     static void вероятноIllegalAgain (List > lol, List  list) {
    lol.add (список); // компилируется нормально !!! почему???
    }
    

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

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

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

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

РЕДАКТИРОВАНИЯ:

  1. Добавлен комментарий о двойном Lol
  2. и вложенных подстановочных знаках.
2
ответ дан 7 November 2019 в 05:35
поделиться

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

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

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

давайте изменим List на [], чтобы быть более понятным:

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

теперь x - это , а не массив из ] некоторый тип класса. это массив любого типа класса. он может содержать Class и a Class . это не может быть выражено с помощью обычного параметра типа:

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

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

0
ответ дан 7 November 2019 в 05:35
поделиться
Другие вопросы по тегам:

Похожие вопросы: