Как работают дженерики дженериков?

В то время как я понимаю некоторые из основных случаев обобщений, я что-то упустил в следующем примере.

У меня есть следующий класс

1 public class Test<T> {
2   public static void main(String[] args) {
3     Test<? extends Number> t = new Test<BigDecimal>();
4     List<Test<? extends Number>> l =Collections.singletonList(t);
5   }
6 }

Строка 4 дает мне ошибку

Type mismatch: cannot convert from List<Test<capture#1-of ? extends Number>> 
to List<Test<? extends Number>>`. 

Очевидно, компилятор считает, что разные ? на самом деле не равны. Хотя мое внутреннее чувство говорит мне, это правильно.

Может кто-нибудь привести пример, где я получил бы ошибку времени выполнения, если строка 4 была законной?

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

Чтобы избежать путаницы, я заменил =null в строке 3 по конкретному заданию

5 ответов

Решение

Как отметил Кенни в своем комментарии, вы можете обойти это с помощью:

List<Test<? extends Number>> l =
    Collections.<Test<? extends Number>>singletonList(t);

Это сразу говорит нам, что операция не небезопасна, она просто жертва ограниченного вывода. Если бы это было небезопасно, вышеприведенное не скомпилировалось бы.

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

Итак, что на самом деле происходит?

Ну, ошибка компиляции, которую мы получаем, показывает, что параметр типа T из Collections.singletonList выводится, чтобы быть capture<Test<? extends Number>>, Другими словами, с подстановочным знаком связаны некоторые метаданные, которые связывают его с конкретным контекстом.

  • Лучший способ думать о захвате подстановочного знака (capture<? extends Foo>) как параметр безымянного типа с теми же границами (т.е. <T extends Foo>, но без возможности ссылаться T).
  • Лучший способ "раскрыть" мощь захвата - связать его с параметром именованного типа универсального метода. Я продемонстрирую это на примере ниже. См. Учебник по Java "Захват подстановочных знаков и вспомогательные методы" (спасибо за ссылку @WChargin) для дальнейшего чтения.

Скажем, мы хотим иметь метод, который сдвигает список, оборачивая его обратно. Тогда давайте предположим, что наш список имеет неизвестный (подстановочный) тип.

public static void main(String... args) {
    List<? extends String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
    List<? extends String> cycledTwice = cycle(cycle(list));
}

public static <T> List<T> cycle(List<T> list) {
    list.add(list.remove(0));
    return list;
}

Это прекрасно работает, потому что T решено capture<? extends String>не ? extends String, Если бы мы вместо этого использовали эту неуниверсальную реализацию цикла:

public static List<? extends String> cycle(List<? extends String> list) {
    list.add(list.remove(0));
    return list;
}

Он не сможет скомпилироваться, потому что мы не сделали захват доступным, назначив его параметру типа.

Таким образом, это начинает объяснять, почему потребитель singletonList извлек бы выгоду из разрешения типа инфекции T в Test<capture<? extends Number>и, таким образом, возвращая List<Test<capture<? extends Number>>> вместо List<Test<? extends Number>>,

Но почему одно не присваивается другому?

Почему мы не можем просто назначить List<Test<capture<? extends Number>>> к List<Test<? extends Number>>?

Хорошо, если мы думаем о том, что capture<? extends Number> является эквивалентом параметра анонимного типа с верхней границей Number, тогда мы можем превратить этот вопрос в "Почему не компилируется следующий?" (это не так!):

public static <T extends Number> List<Test<? extends Number>> assign(List<Test<T>> t) {
    return t;
} 

Это имеет вескую причину не компилировать. Если бы это было так, то это было бы возможно:

//all this would be valid
List<Test<Double>> doubleTests = null;
List<Test<? extends Number>> numberTests = assign(doubleTests);

Test<Integer> integerTest = null;
numberTests.add(integerTest); //type error, now doubleTests contains a Test<Integer>

Так почему же явная работа?

Давайте вернемся к началу. Если вышесказанное небезопасно, то почему это разрешено:

List<Test<? extends Number>> l =
    Collections.<Test<? extends Number>>singletonList(t);

Чтобы это работало, это означает, что разрешено следующее:

Test<capture<? extends Number>> capturedT;
Test<? extends Number> t = capturedT;

Ну, это неверный синтаксис, так как мы не можем явно ссылаться на захват, поэтому давайте оценим его, используя ту же технику, что и выше! Давайте свяжем захват с другим вариантом "assign":

public static <T extends Number> Test<? extends Number> assign(Test<T> t) {
    return t;
} 

Это успешно компилируется. И нетрудно понять, почему это должно быть безопасно. Это самый случай использования чего-то вроде

List<? extends Number> l = new List<Double>();

Нет потенциальной ошибки во время выполнения, это просто за пределами способности компилятора определить это статически. Всякий раз, когда вы вызываете вывод типа, он автоматически генерирует новый захват <? extends Number>и два захвата не считаются эквивалентными.

Следовательно, если вы удалите логический вывод из вызова singletonList, указав <T> для этого:

List<Test<? extends Number>> l = Collections.<Test<? extends Number>>singletonList(t);

Работает нормально. Сгенерированный код ничем не отличается от того, если бы ваш вызов был законным, это всего лишь ограничение компилятора, что он не может понять это самостоятельно.

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

public static void swap(List<? extends Number> l1, List<? extends Number> l2) {
    Number num = l1.get(0);
    l1.add(0, l2.get(0));
    l2.add(0, num);
}

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

Причина в том, что компилятор не знает, что типы подстановочных знаков имеют одинаковый тип.

Он также не знает, что ваш экземпляр null, Хотя null является членом всех типов, при проверке типов компилятор учитывает только объявленные типы, а не то, что может содержать значение переменной.

Если код выполняется, он не вызовет исключения, но это только потому, что значение равно нулю. Все еще существует потенциальное несоответствие типов, и именно в этом заключается задача компилятора - запретить несоответствия типов.

Посмотрите на стирание типов. Проблема в том, что "время компиляции" является единственной возможностью, которую Java имеет для применения этих обобщений, поэтому, если она пропустит это, она не сможет определить, пытались ли вы вставить что-то недействительное. На самом деле это хорошая вещь, потому что это означает, что после компиляции программы Generics не будет терять производительность во время выполнения.

Давайте попробуем взглянуть на ваш пример по-другому (давайте использовать два типа, которые расширяют Number, но ведут себя совершенно по-разному). Рассмотрим следующую программу:

import java.math.BigDecimal;
import java.util.*;

public class q16449799<T extends Number> {
  public T val;

  public static void main(String ... args) {
    q16449799<BigDecimal> t = new q16449799<>();
    t.val = new BigDecimal(Math.PI);

    List<q16449799<BigDecimal>> l = Collections.singletonList(t);
    for(q16449799<BigDecimal> i : l) {
      System.out.println(i.val);
    }
  }
}

Это выводит (как и следовало ожидать):

3.141592653589793115997963468544185161590576171875

Теперь предположим, что представленный вами код не вызвал ошибку компилятора:

import java.math.BigDecimal;
import java.util.concurrent.atomic.AtomicLong;

public class q16449799<T extends Number> {
  public T val;

  public static void main(String ... args) {
    q16449799<BigDecimal> t = new q16449799<>();
    t.val = new BigDecimal(Math.PI);

    List<q16449799<AtomicLong>> l = Collections.singletonList(t);
    for(q16449799<AtomicLong> i : l) {
      System.out.println(i.val);
    }
  }
}

Что вы ожидаете получить на выходе? Вы не можете разумно привести BigDecimal к AtomicLong (вы могли бы создать AtomicLong из значения BigDecimal, но приведение и конструирование - это разные вещи, и Generics реализованы как сахар времени компиляции, чтобы убедиться, что приведение выполнено успешно). Что касается комментария @KennyTM, конкретный тип ищет, когда вы начинаете пример, но попробуйте скомпилировать это:

import java.math.BigDecimal;
import java.util.*;

public class q16449799<T> {
  public T val;

  public static void main(String ... args) {
    q16449799<? extends Number> t = new q16449799<BigDecimal>();

    t.val = new BigDecimal(Math.PI);

    List<q16449799<? extends Number>> l = Collections.<q16449799<? extends Number>>singletonList(t);
    for(q16449799<? extends Number> i : l) {
      System.out.println(i.val);
    }
  }
}

Это приведет к ошибке, как только вы попытаетесь установить значение t.val,

Может быть, это может объяснить проблему компилятора:

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

Этот универсальный список подстановочных знаков может содержать любые элементы, начиная с номера. Так что можно назначить ему список целых чисел. Однако теперь я мог бы добавить Double к myNums, потому что Double также расширяется от Number, что привело бы к проблеме времени выполнения. Таким образом, компилятор запрещает любой доступ на запись к myNums, и я могу использовать только методы чтения для него, потому что я знаю, что только то, что я получу, может быть приведено к Number.

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

Но, к счастью, есть хитрость, чтобы обойти эту ошибку, чтобы вы могли самостоятельно проверить, что может сломать это:

public static void main(String[] args) {

    List<? extends Number> list1 = new ArrayList<BigDecimal>();
    List<List<? extends Number>> list2 = copyHelper(list1);


}

private static <T> List<List<T>> copyHelper(List<T> list) {
    return Collections.singletonList(list);

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