Несколько подстановочных знаков на общих методах делает компилятор 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 in 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 56

3 ответа:

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

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

  1. Теги Integer это Number
  2. A List<Integer> и не a List<Number>
  3. A List<Integer>и a List<? extends Number>

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

  1. A List<String> is (captureable by) a List<?>
  2. A List<List<String>> и не (captureable by) a List<List<?>>
  3. A List<List<String>>и (captureable by) a List<? extends List<?>>

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

то есть, 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>>();

  • аргумент С дженерики должны быть приняты. В случае LOLUnknowns1b the 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. добавлен комментарий о двойном 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>и a Class<Int>. это не может быть выражено с обычным параметром типа:

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

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