Как работают дженерики дженериков?
В то время как я понимаю некоторые из угловых случаев генериков, я пропускаю что-то в следующем примере.
У меня есть следующий класс
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 была законной?
EDIT:
Чтобы избежать путаницы, я заменил =null
в строке 3 на конкретное назначение
Ответы
Ответ 1
Как заметил Кенни в своем комментарии, вы можете обойти это:
List<Test<? extends Number>> l =
Collections.<Test<? extends Number>>singletonList(t);
Это немедленно говорит нам, что операция не небезопасная, она просто жертва ограниченного вывода. Если бы это было небезопасно, вышеупомянутое не собиралось.
Поскольку использование явных параметров типа в обобщенном методе, как указано выше, необходимо когда-либо использовать как подсказку, мы можем предположить, что здесь требуется техническое ограничение механизма вывода. В самом деле, компилятор Java 8 в настоящее время планируется добавить несколько улучшений для ввода-вывода типов. Я не уверен, будет ли разрешен ваш конкретный случай.
Итак, что на самом деле происходит?
Ну, ошибка компиляции, которую мы получаем, показывает, что параметр типа T
of 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;
Ну, это недопустимый синтаксис, так как мы не можем напрямую ссылаться на захват, поэтому давайте оценим его, используя ту же технику, что и выше! Пусть привязка захвата к другому варианту "присваивать":
public static <T extends Number> Test<? extends Number> assign(Test<T> t) {
return t;
}
Это успешно компилируется. И нетрудно понять, почему это должно быть безопасно. Это очень практичный пример чего-то вроде
List<? extends Number> l = new List<Double>();
Ответ 2
Не существует потенциальной ошибки времени выполнения, она просто вне возможности компилятора статически определять это. Всякий раз, когда вы вызываете вывод типа, он автоматически генерирует новый захват <? 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);
}
Да, спецификация языка и компилятор, возможно, могут быть более сложными, чтобы рассказать о вашем примере помимо этого, но это не так, и это достаточно просто, чтобы обойти.
Ответ 3
Причина в том, что компилятор не знает, что ваши подстановочные типы одного типа.
Он также не знает, что ваш экземпляр null
. Хотя null
является членом всех типов, компилятор рассматривает только объявленные типы, а не то, что может содержать значение переменной при проверке типов.
Если код выполнен, это не вызовет исключения, но это только потому, что значение равно null. По-прежнему существует несоответствие потенциального типа и что задание компилятора - запретить несоответствие типов.
Ответ 4
Посмотрите тип стирания. Проблема заключается в том, что "время компиляции" - единственная возможность, которую Java должна обеспечивать соблюдение этих дженериков, поэтому, если бы она позволяла этому через нее не была в состоянии сказать, пытались ли вы вставить что-то недействительное. Это на самом деле хорошо, потому что это означает, что после компиляции программы Generics не подвергаются какому-либо снижению производительности во время выполнения.
Попробуем и посмотрим на ваш пример другим способом (пусть используют два типа, которые расширяют число, но ведут себя по-разному). Рассмотрим следующую программу:
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
.
Ответ 5
Возможно, это может объяснить проблему компилятора:
List<? extends Number> myNums = new ArrayList<Integer>();
Этот список подстановочных символов может содержать любые элементы, простирающиеся от числа. Так что его ОК, чтобы назначить ему список Integer. Однако теперь я мог бы добавить Double to 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);
}