Я играл с дженериками и обнаружил, что, к моему удивлению, компилируется следующий код:

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, поскольку я также могу передать 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 г. (ранее generics), в котором говорится об опасностях ковариантности массивов.

Ковариация массивов, кажется, намекает на объяснение, но я не могу его придумать.


Текущий jdk - 1.8.0.0_91

12
Jorn Vernee 4 Май 2016 в 13:03

2 ответа

Лучший ответ

Рассмотрим этот пример:

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 совершенно оправданным во время выполнения.

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

5
biziclop 4 Май 2016 в 14:30

Я ожидал, что T будет выведен из B. A не расширяет B. Так почему же компилятор не жалуется на это?

T не выводится как B, он выводится как A. Поскольку B расширяет A, B[] является подтипом A[], и поэтому вызов метода правильный.

В отличие от универсальных типов, тип элемента массива доступен во время выполнения (они переопределены ). Итак, когда вы пытаетесь сделать

a[0] = b.get();

Среда выполнения знает, что a на самом деле является массивом B и не может содержать A.

Проблема здесь в том, что Java динамически расширяется. Массивы существуют с первой версии Java, в то время как дженерики были добавлены только в Java 1.5. В общем, Oracle пытается сделать новые версии Java обратно совместимыми, поэтому ошибки, сделанные в более ранних версиях (например, ковариация массива), не исправляются в новых версиях.

2
Hoopje 4 Май 2016 в 10:45