Java Generics: недопонимание подстановочных знаков
Читая онлайн-учебник по Java, я ничего не понимаю о захвате с подстановочными знаками
Например:
import java.util.List;
public class WildcardError {
void foo(List<?> i) {
i.set(0, i.get(0));
}
}
почему компилятор не может сохранить присваивание в безопасности?
Он знает, что, выполняя, например, метод со списком целых чисел, он получает из i.get значение целого числа. Поэтому попытайтесь установить значение Integer с индексом 0 в тот же список Integer (i).
Итак, что же не так? Зачем писать помощник Wildcard?
Ответы
Ответ 1
почему компилятор не может сохранить присвоение безопасным?
Компилятор ничего не знает о типе элементов в List<?> i
, по определению ?
. Подстановочный знак не означает "любой тип"; это означает "какой-то неизвестный тип".
Он знает, что, выполняя, например, метод с целым списком, он получает от i.get значение Integer.
Это правда, но, как я сказал выше: компилятор может знать только - время компиляции, помните - что i.get(0)
возвращает Object
, что является верхней границей ?
, Но нет гарантии, что ?
находится во время выполнения Object
, поэтому компилятор не знает, что i.set(0, i.get(0))
является безопасным вызовом. Это как написано:
List<Foo> fooz = /* init */;
Object foo = fooz.get(0);
fooz.set(0, foo); // won't compile because foo is an object, not a Foo
Дополнительная информация:
Ответ 2
почему компилятор не может сохранить присвоение безопасным? Он знает, что, выполняя, например, метод с целым списком, он получает от i.get значение Integer. Поэтому он пытается установить значение Integer в индексе 0 в тот же список Integer (i).
По-другому, почему компилятор не знает, что два использования подстановочного типа List<?>
в
i.set(0, i.get(0));
относятся к одному и тому же фактическому типу?
Ну, это потребует от компилятора знать, что i
содержит один и тот же экземпляр для обеих оценок выражения. Поскольку i
даже не является окончательным, компилятор должен был бы проверить, возможно ли было бы i
между оценкой двух выражений. Такой анализ прост для локальных переменных (кто знает, будет ли вызываемый метод обновлять конкретное поле определенного объекта?). Это довольно сложная задача в компиляторе, редко проявляющая преимущества. Я полагаю, что разработчики языка программирования Java все упрощали, указав, что разные виды использования одного и того же типа подстановочных знаков имеют разные захваты.
Ответ 3
Я также затрудняюсь понять этот вопрос; разделение одной команды на две команды помогло мне.
Ниже код - это то, что на самом деле происходит в фоновом режиме, когда исходный метод проверяется и компилируется, компилятор создает свою локальную переменную: результат вызова i.get(0)
помещается в регистр в стек локальной переменной.
И это - для понимания этой проблемы - так же, как создание локальной переменной, которая для удобства я дал имя element
.
import java.util.List;
public class WildcardError {
void foo(List<?> i) {
Object element = i.get(0); // command 1
i.set(0, element); // command 2
}
}
Когда команда 1 проверяется, она может устанавливать только тип element
в Object
(- > понятие верхнего уровня, см. ответ Matt), поскольку он не может использовать ?
как тип переменной; ?
используется только для указания того, что общий тип неизвестен.
Переменные типы могут быть только реальными типами или универсальными типами, но поскольку вы не используете общий тип в этом методе, например, <T>
, это принудительный, чтобы использовать реальный тип. Это форсирование выполняется из-за следующих строк в спецификации java (jls8, 18.2.1):
Формула ограничения формы <Выражение → T > уменьшается следующим образом:
[...]
- Если выражение представляет собой выражение создания экземпляра класса или выражение вызова метода, ограничение сводится к связанному множеству B3, который будет использоваться для определения типа вызова выражения при таргетинге на T, как определено в §18.5.2. (Для выражения создания экземпляра класса соответствующий "метод", используемый для вывода, определен в п. 15.9.3).
Ответ 4
Решение будет,
import java.util.List;
public class WildcardError {
private void fooHelper(List<T> i){
i.set(0, i.get(0));
}
public void foo(List<?> i){
fooHelper(i);
}
}
здесь fooHelper будет захватывать тип T подстановочного знака? (так как имя подстановочного знака).
Ответ 5
По принципу Get-Put:
Если у вас есть подстановочный знак расширения, как в List<? extends Something>
, то:
1А. Вы можете получить из структуры, используя Something
или его ссылку superclass
.
void foo(List<? extends Number> nums) {
Number number = nums.get(0);
Object number = nums.get(0); // superclass reference also works.
}
1B. Вы не можете ничего добавить к структуре (кроме null
).
void foo(List<? extends Number> nums) {
nums.add(1); Compile error
nums.add(1L); Compile error
nums.add(null); // only null is allowed.
}
Точно так же, если у вас есть супер подстановочный знак, как в List<? super Something>
, то:
2А. Вы можете добавить к структуре, которая является Something
или его subclass
.
Например:
void foo(List<? super Number> nums) {
nums.add(1); // Integer is a subclass of Number
nums.add(1L); // Long is a subclass of Number
nums.add("str"); // Compile error: String is not subclass of Number
}
2A. Вы не можете получить из структуры (кроме как через ссылку на объект).
Например:
void foo(List<? super Integer> nums) {
Integer num = nums.get(0); // Compile error
Number num = nums.get(0); // Compile error
Object num = nums.get(0); // Only get via Object reference is allowed.
}
Возвращаясь к вопросу OP, List<?> i
является лишь кратким представлением для List<? extends Object> i
. А поскольку это подстановочный знак extends
, операция set
завершается неудачей.
Последний оставшийся фрагмент - ПОЧЕМУ операция не удалась? Или почему принцип Get-Put на первом месте? - Это связано с безопасностью типов, как ответил Джон Скит здесь.
Ответ 6
Я полагаю, что ваше неправильное понимание ограничения происходит от замены ?
на any type
, Object
или чего-то подобного в вашем уме. Но это предположение неверно, ?
фактически означает unknown type
. Итак, в следующей строке
fooz.set(0, foo);
вы пытаетесь присвоить переменную некоторого типа переменной неизвестного типа (поскольку сигнатура функции похожа на void set(int, ?)
), что никогда не может быть возможным, независимо от типа foo
. В вашем случае тип foo - Object
, и вы не можете присвоить его переменной неизвестного типа, которая на самом деле может быть Foo
, Bar
или любым другим.