Почему при выводе типов массивов нет java-типов?
Я играл с дженериками и обнаружил, что, к моему удивлению, следующий код компилируется:
class A {}
class B extends A {}
class Generic<T> {
private T instance;
public Generic(T instance) {
this.instance = instance;
}
public T get(){ return instance; }
}
public class Main {
public static void main(String[] args) {
fArray(new B[1], new Generic<A>(new A())); // <-- No error here
}
public static <T> void fArray(T[] a, Generic<? extends T> b) {
a[0] = b.get();
}
}
Я ожидал бы, что T
будет выведен на B
. A
не распространяется B
. Так почему компилятор не жалуется на это?
T
, как представляется, выводится на Object
, так как я могу передать a Generic<Object>
.
Кроме того, при фактическом запуске кода он выдает ArrayStoreException
в строке a[0] = b.get();
.
Я не использую сырые общие типы. Я чувствую, что этого исключения можно было избежать с ошибкой времени компиляции или, по крайней мере, предупреждением, если T
было фактически выведено как B
.
При дальнейшем тестировании с эквивалентом List<...>
:
public static void main(String[] args) {
fList(new ArrayList<B>(), new Generic<A>(new A())); // <-- Error, as expected
}
public static <T> void fList(List<T> a, Generic<? extends T> b) {
a.add(b.get());
}
Это приводит к ошибкам:
The method fList(List<T>, Generic<? extends T>) in the type Main is not applicable for the arguments (ArrayList<B>, Generic<A>)
Как и более общий случай:
public static <T> void fList(List<? extends T> a, Generic<? extends T> b) {
a.add(b.get()); // <-- Error here
}
Компилятор правильно распознает, что первый ?
может быть ниже иерархии наследования, чем второй ?
.
например. Если первый ?
был B
, а второй ?
был A
, тогда это не безопасно для типа.
Так почему же первый пример не создает аналогичную ошибку компилятора? Это просто надзор? Или существует техническое ограничение?
Единственный способ, которым я мог создать ошибку, - это явно указать тип:
Main.<B>fArray(new B[1], new Generic<A>(new A())); // <-- Not applicable
Я действительно ничего не нашел в своих исследованиях, кроме этой статьи с 2005 года (до дженериков), в котором говорится о опасности ковариации массива.
Ковариация массивов, похоже, намекает на объяснение, но я не могу думать об этом.
Текущий jdk равен 1.8.0.0_91
Ответы
Ответ 1
Рассмотрим следующий пример:
class A {}
class B extends A {}
class Generic<T> {
private T instance;
public Generic(T instance) {
this.instance = instance;
}
public T get(){ return instance; }
}
public class Main {
public static void main(String[] args) {
fArray(new B[1], new Generic<A>(new A())); // <-- No error here
}
public static <T> void fArray(T[] a, Generic<? extends T> b) {
List<T> list = new ArrayList<>();
list.add(a[0]);
list.add(b.get());
System.out.println(list);
}
}
Как вы можете видеть, сигнатуры, используемые для вывода параметров типа, идентичны, единственное, что отличается тем, что fArray()
только считывает элементы массива вместо их записи, делая вывод T -> A
совершенно оправданным во время выполнения.
И нет возможности компилятору рассказать, для чего будет использоваться ссылка на массив в ходе реализации метода.
Ответ 2
Я ожидал бы, что T будет выведено на B. A не расширяет B. Так почему компилятор не жалуется на это?
T
не считается B
, предполагается, что он равен A
. Поскольку B
extends A
, B[]
является подтипом A[]
, и поэтому вызов метода правильный.
В отличие от generics, тип элемента массива доступен во время выполнения (они обновляются). Поэтому, когда вы пытаетесь сделать
a[0] = b.get();
среда выполнения знает, что A
на самом деле является массивом B
и не может содержать A
.
Проблема в том, что Java расширяется динамически. Массивы существуют с первой версии Java, а generics - только в Java 1.5. В общем, Oracle пытается сделать новые версии Java обратно совместимыми, и поэтому ошибки, сделанные в более ранних версиях (например, ковариация массива), не исправлены в более новых версиях.