Несколько подстановочных знаков в универсальных методах делают Java-компилятор (и меня!) Очень запутанным - PullRequest
57 голосов
/ 23 августа 2010

Давайте сначала рассмотрим простой сценарий ( см. Полный источник на 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: Double 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 ]

69 голосов
/ 23 августа 2010

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

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

  1. Integer является Number
  2. A List<Integer> является НЕ a List<Number>
  3. A List<Integer> IS a List<? extends Number>

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

  1. A List<String> является (фиксируется) a List<?>
  2. A List<List<String>> является НЕ (фиксируется) a List<List<?>>
  3. A List<List<String>> IS (доступно для) a List<? extends List<?>>

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

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

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

Фрагменты

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

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 до G, где для 1 <= i <= n </em>:

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

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

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

2 голосов
/ 23 августа 2010
  • Без аргументов с обобщениями следует принять. В случае 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. И вложенные шаблоны.
0 голосов
/ 23 августа 2010

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

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

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<String> и a Class<Int>.это не может быть выражено параметром обычного типа:

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

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

...