Обобщения и стирание типов

Может ли кто-нибудь объяснить мне, почему это происходит:

public class Array<E> {

    public E[] elements = (E[]) new Object[10];

    public E get(int idx) {
        return elements[idx]; // Ignore bound-checking for brevity.
    }

    public static void doSomething(Array<Integer> arr) {
        Integer good = arr.get(0);
        Integer bad1 = arr.elements[0];
        Integer bad2 = ((Integer[]) arr.elements)[0];
        Integer bad3 = (Integer) arr.elements[0];
        // `bad1', `bad2', and `bad3' all produce a 
        // runtime exception.
    }

    public static void main(String[] args) {
        Array<Integer> test = new Array<>();

        Array.doSomething(test);
    }
}

Полный пример здесь: http://pastebin.com/et7sGLGW

Я читал о стирании типов и понимаю, что проверка типов выполняется во время компиляции, а затем E просто заменяется на Object, поэтому все, что у нас есть, это public Object[] elements, но почему метод get работает успешно, а обычное приведение типов — нет? Разве тип возвращаемого значения метода get также не стирается?

Спасибо.


person d125q    schedule 15.10.2014    source источник
comment
Я не получаю исключение времени выполнения, но мне пришлось добавить дополнительный код, чтобы компилировать и создавать elements, и, вероятно, это не то же самое, что ваш код. Я думаю, нам нужно увидеть более полный пример.   -  person ajb    schedule 16.10.2014
comment
К сожалению, дженерики и массивы в Java не работают вместе.   -  person hoaz    schedule 16.10.2014
comment
@hoaz Некоторые комбинации дженериков и массивов недопустимы в Java, и компилятор отметит ошибку. Но если программа компилируется, я думаю, что программа должна работать, как и ожидалось, но спрашивающий говорит, что есть исключение во время выполнения. Тем не менее, я не мог получить один.   -  person ajb    schedule 16.10.2014
comment
Это даже лучше: Object o = arr.elements[0]; приводит к ` java.lang.ClassCastException: [Ljava.lang.Object; нельзя преобразовать в [Ljava.lang.Integer. This tells me that the cast that fails is the original Object[]` в Integer[]!   -  person    schedule 16.10.2014
comment
@ajb: вот код: pastebin.com/et7sGLGW   -  person d125q    schedule 16.10.2014


Ответы (5)


ПРИМЕЧАНИЕ. Этот ответ относится к коду по вашей ссылке pastebin. Я рекомендую вам отредактировать свой вопрос и включить весь код. Это не так долго.

Проблема в этом конкретном коде заключается в том, что вы объявили, что параметр doSomething arr имеет тип Array<Integer>. Итак, когда вы говорите

    Integer bad1 = arr.elements[0];

Поскольку arr — это Array<Integer>, т. е. параметр типа E — это Integer, компилятор предполагает, что тип elements, который был объявлен как E[], является Integer[].

Однако когда elements создается с помощью new либо в конструкторе, либо в append, где вы создаете его как temp, вы создаете его как Object[]:

elements = (E[]) new Object[(capacity = 2)];

or

E[] temp = (E[]) new Object[(capacity *= 2)];
...
elements = temp;

Это означает, что когда объект массива создается во время выполнения, его тип будет записан во время выполнения как Object[]. Приведение к E[] не влияет на это, поскольку тип объекта во время выполнения никогда не меняется. Приведение влияет только на то, как компилятор смотрит на выражение.

Поэтому в этом заявлении:

Integer bad1 = arr.elements[0];

Здесь компилятор «знает», что elements должен иметь тип Integer[], как объяснялось выше. Поскольку фактический тип elements во время выполнения равен Object[], а Object[] не может быть неявно приведен к Integer[], во время выполнения возникает ClassCastException:

Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;

([L означает массив.)

Причина, по которой этого не происходит при использовании get(), заключается в том, что код, обращающийся к массиву, находится не в том месте, где компилятор «знает», что E должно быть Integer. Следовательно, не предполагается, что elements является Integer[], и проверка не выполняется.

(Я не совсем уверен в точных механизмах, связанных с дженериками, но я думаю, что это правильно.)

person ajb    schedule 15.10.2014

Несмотря на то, что arr имеет тип Array<Integer>arr.elements имеет тип Integer[]), arr.elements на самом деле имеет тип времени выполнения Object[], поскольку фактический массив является экземпляром типа Object[].

(Обратите внимание, что массивы, в отличие от дженериков, являются ковариантными и не имеют нет стирания. Object[] foo = new String[5]; является допустимым, как и String[] bar = (String[]) foo;. В то время как Integer[] baz = (Integer[]) foo; вызовет исключение ClassCastException во время выполнения.)

Таким образом, причина того, что любая ссылка на arr.elements вызывает исключение времени выполнения, заключается в том, что компилятор автоматически вставляет преобразование вниз к Integer[], чтобы привести тип и тип времени выполнения обратно в соответствие. Внутри тела doSomething arr.elements на самом деле означает (Integer[]) arr.elements с неявным приведением.

Напротив, внутри get() тип this — это просто Array<E>, поэтому тип elements — это просто E[], что невозможно проверить. Таким образом, компилятор не вставляет никаких неявных приведений.


Главный вывод заключается в том, что (E[]) new Object[10]; на самом деле неверно. new Object[10] не создает экземпляр E[]. Компилятор не увидит, что это неправильно, но вставит множество приведений, которые обнаружат, что это неправильно.

Лучше использовать Object[] elements = new Object[] и при необходимости выполнять правильные, но непроверенные приведения из Object в E, а не неправильные-и непроверенные приведения из Object[] в E[].

Вы видите, что я имею в виду?

person ruakh    schedule 15.10.2014

Во-первых, у вас есть Object[], и вы передаете его как E[], используя

E[] elements = (E[]) new Object[10];

Это виновник всех проблем.

Почему это проблема?

  1. Вы даже не можете создать такой массив. Он скомпилируется без проблем, но теоретически (и во время выполнения) это неверно. Вот пример с Integer[]:

    Integer[] stringArray = (Integer[])new Object[10];
    

    Эта строка выдает ошибку во время выполнения:

    java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;
    

    Но дженерики скрывают эту проблему до тех пор, пока вам не придется использовать данные напрямую.

  2. Давайте попробуем этот кусок кода:

    public class Array<E> {
        public E[] elements = (E[]) new Object[10];
    }
    
    public class Client {
        public static void main(String[] args) {
            Array<Integer> array = new Array<>();
            array.elements[0] = 5;
        }
    }
    

    Весь код компилируется без проблем, и инициализация array работает как положено. Но теперь мы получаем новое исключение:

    array.elements[0] = 5;
    //java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;
    

    Это означает, что внутри мы все еще работаем с Object[], но пытаемся заставить его работать как Integer[] или, более формально, как E[] (что неверно, как указано в 1.).

  3. Давайте добавим метод append (адаптированный из кода, опубликованного OP):

    public class Array<E> {
        public E[] elements = (E[]) new Object[10];
        private int size = 0;
        public void append(E element) {
            elements[size++] = element;
        }
    }
    
    public class Client {
        public static void main(String[] args) {
            Array<Integer> array = new Array<>();
            array.append(5);
            System.out.println(array.elements[0]);
        }
    }
    

    Здесь мы получим ошибку времени выполнения здесь:

    System.out.println(array.elements[0]);
    //java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;
    

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

Короче:

Вы не должны использовать E[] array напрямую. Вместо этого используйте Object[] array, как описано в ArrayList исходный код.

Больше информации:

person Luiggi Mendoza    schedule 15.10.2014

Стирание типа удаляет универсальные типы из вашего универсального класса, но при необходимости вставляет приведения типов туда, где вы используете универсальный тип. В вашем примере компилятор добавляет типы приведения, когда вы используете общий класс Array. Однако внутри общего массива вхождения E распадаются на Object. (см. комментарии и вывод javap). Ошибка, которую вы видите, — это просто компилятор, жалующийся на приведение от Object[] к Integer[], что является незаконным (обобщение или нет).

public class Array<E> {
    public E[] elements;
    @SuppressWarnings("unchecked")
    public Array() {
        this.elements = (E[])new Object[]{1,2,3};
    }
    public E get(int idx) {
        return elements[idx]; // Ignore bound-checking for brevity.                                                        
    }

    public static void doSomething(Array<Integer> arr) {
        Integer good = arr.get(0);                             // produces (Integer) arr.get(0)                            
        Integer good1 = (Integer) ((Object[])arr.elements)[0]; // no implicit cast                                         
        Integer bad1 = arr.elements[0];                        // produces ((Integer[])arr.elements)[0]                    
        Integer bad2 = ((Integer[]) arr.elements)[0];          // produces ((Integer[])((Integer[])arr.elements))[0]       
        Integer bad3 = (Integer) arr.elements[0];              // produces ((Integer[])arr.elements)[0]                    
        // `bad1', `bad2', and `bad3' all produce a                                                                        
        // runtime exception.                                                                                              
    }

    public static void main(String[] args) throws Exception{
        doSomething(new Array<Integer>());
    }
}

Вывод javap -cp . -c Массив

> public static void
> doSomething(Array<java.lang.Integer>);
>     Code:
>        0: aload_0       
>        1: iconst_0      
>        2: invokevirtual #6                  // Method get:(I)Ljava/lang/Object;
>        5: checkcast     #7                  // class java/lang/Integer
>        8: astore_1      
>        9: aload_0       
>       10: getfield      #5                  // Field elements:[Ljava/lang/Object;
>       13: checkcast     #4                  // class "[Ljava/lang/Object;"
>       16: iconst_0      
>       17: aaload        
>       18: checkcast     #7                  // class java/lang/Integer
>       21: astore_2      
>       22: aload_0       
>       23: getfield      #5                  // Field elements:[Ljava/lang/Object;
>       26: checkcast     #8                  // class "[Ljava/lang/Integer;"
>       29: iconst_0      
>       30: aaload        
>       31: astore_3      
>       32: aload_0       
>       33: getfield      #5                  // Field elements:[Ljava/lang/Object;
>       36: checkcast     #8                  // class "[Ljava/lang/Integer;"
>       39: checkcast     #8                  // class "[Ljava/lang/Integer;"
>       42: iconst_0      
>       43: aaload        
>       44: astore        4
>       46: aload_0       
>       47: getfield      #5                  // Field elements:[Ljava/lang/Object;
>       50: checkcast     #8                  // class "[Ljava/lang/Integer;"
>       53: iconst_0      
>       54: aaload        
>       55: astore        5
>       57: return
person revau.lt    schedule 15.10.2014

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

public class Array {

    public Object[] elements = new Object[10];

    public Object get(int idx) {
        return elements[idx]; // Ignore bound-checking for brevity.
    }

    public static void doSomething(Array arr) {
        Integer good = (Integer)arr.get(0);
        Integer bad1 = ((Integer[])arr.elements)[0];
        Integer bad2 = ((Integer[]) arr.elements)[0];
        Integer bad3 = (Integer) ((Integer[])arr.elements)[0];
        // `bad1', `bad2', and `bad3' all produce a 
        // runtime exception.
    }

    public static void main(String[] args) {
        Array test = new Array();

        Array.doSomething(test);
    }
}

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

Вы можете спросить, почему приведения происходят, когда они происходят. Почему arr.elements заменено на Integer[]? Это потому, что после стирания типа arr.elements имеет тип Object[]; но в методе мы используем его и ожидаем, что он будет Integer[], поэтому приведение необходимо, когда оно выходит из общей области и входит в метод, где у нас есть определенный тип, замененный на T.

Это приведение не обязательно всегда выполняется, если arr.elements сразу передается или присваивается чему-то, что ожидает тип Object[] или Object, то приведение не выполняется, потому что оно не нужно. Но в остальных случаях делается слепок.

Возможно, это не будет стиранием кода неправильного типа:

Integer bad1 = (Integer)arr.elements[0];

И компилятор, по-видимому, мог бы скомпилировать это так, если бы был действительно умным и хотел бы этого. Но для этого компилятору нужно будет добавить дополнительное сложное правило о том, следует ли приводить объекты, выходящие из дженериков, чтобы выяснить, может ли он отложить это до тех пор, пока не будет получен доступ к массиву. Кроме того, это не принесет пользы с точки зрения компилятора, потому что в любом случае все равно будет одно приведение. Таким образом, компилятор не делает этого таким образом.

person newacct    schedule 17.10.2014