Простой код с массивом в обобщённом классе и непонятное ClassCastException

Рейтинг: 4Ответов: 2Опубликовано: 13.04.2015
class Super {}

class Sub extends Super {}

class GenericArrayHolder<T extends Super>
{
    T[] array;

    @SuppressWarnings("unchecked")
    GenericArrayHolder(int n)
    {
        array = (T[]) new Super[n];
    }

    void set(int i, T t)
    {
        array[i] = t;
    }
}

public class Test
{
    public static void main(String... args)
    {
        GenericArrayHolder<Sub> h = new GenericArrayHolder<>(10);
        h.set(3, new Sub());
        h.array[3] = new Sub(); // ClassCastException
    }
}

Собственно вопрос в том, почему 3 строчка в main генерирует исключение? Особенно меня удивляет, что это происходит не смотря на то, что 2 строчка работает нормально.

Ответы

▲ 3Принят

Кажется, я понял. Дело таки в type erasure.

Смотрите, вот эксперимент. Уберём new Sub(), запишем просто null. http://ideone.com/zXc7kG

Получим ошибку: Exception in thread "main" java.lang.ClassCastException: [LSuper; cannot be cast to [LSub;.

Документация говорит, что [LSuper; означает массив элементов типа Super, a [LSub; — массив элементов типа Sub.

Как работает type erasure? На время компиляции T заменяется на Super, и всё, что выдаёт наружу T, обкладывается рантайм-проверками. То есть код h.array на самом деле превращается в (Sub[])h.array.

В вашем случае array на самом деле типа Super[], каст из-за type erasure не обнаруживает, что тип-то не тот! Ошибка возникает лишь при доступе.

Что делать? Создайте несущий массив правильного типа:

array = (Т[])Array.newInstance(cl, 10);

Для этого вам понадобится класс:

GenericArrayHolder(int n, Class<T> cl)
{
    array = (Т[])Array.newInstance(cl, 10);
}

Более прямого пути с type erasure, кажется, нет.

▲ 2

Это привет от type erasure.

В коде дженериков на самом деле хранится код для базового типа, и везде расставляются преобразования типов. В вашем случае код:

array = (T[]) new Super[n];

на самом деле выполняется так:

array = (Super[]) new Super[n];

потому что T в GenericArrayHolder ограничен "снизу" этим типом.

Когда вы выполняете метод у дженерикового типа, то все касты корректно расставляются, в результате код:

array[i] = t;

выполняется так:

array[i] = (Super)t;

Когда же вы обращаетесь к массиву напрямую, то компилятор считает, что массив надо преобразовать к типу Sub[]. Сделать он этого не может, отсюда исключение.

Похожий вопрос был рассмотрен на английском Stack Overflow: Generic array throws ClassCastException when referenced directly (it doesn't when calling through helper method). Там в похожем коде исключение выбрасывается даже при обращении к полю массива length.