ClassCastException при использовании varargs и обобщений - PullRequest
5 голосов
/ 30 января 2012

Я использую Java-дженерики и переменные.

Если я использую следующий код, я получу ClassCastException, даже если я вообще не использую приведение.

Странно, но если я запускаю это на Android (dalvik), никакая трассировка стека не включается с исключением, и если я изменяю интерфейс на абстрактный класс, переменная исключения e будет пустой.

Код:

public class GenericsTest {
    public class Task<T> {
        public void doStuff(T param, Callback<T> callback) {
            // This gets called, param is String "importantStuff"

            // Working workaround:
            //T[] arr = (T[]) Array.newInstance(param.getClass(), 1);
            //arr[0] = param;
            //callback.stuffDone(arr);

            // WARNING: Type safety: A generic array of T is created for a varargs parameter
            callback.stuffDone(param);
        }
    }

    public interface Callback<T> {
        // WARNING: Type safety: Potential heap pollution via varargs parameter params
        public void stuffDone(T... params);
    }

    public void run() {
        Task<String> task = new Task<String>();
        try {
            task.doStuff("importantStuff", new Callback<String>() {
                public void stuffDone(String... params) {
                    // This never gets called
                    System.out.println(params);
                }});
        } catch (ClassCastException e) {
            // e contains "java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;"
            System.out.println(e.toString());
        }
    }

    public static void main(String[] args) {
        new GenericsTest().run();
    }
}

Если вы запустите это, вы получите ClassCastException, который Object не может быть приведен к String с трассировкой стека, указывающей на неверный номер строки.Это ошибка в Java?Я протестировал его в Java 7 и Android API 8. Я сделал обходной путь (закомментированный в doStuff -методе), но кажется глупым делать это таким образом.Если я удаляю varargs (T...), все работает нормально, но моей реальной реализации это как-то нужно.

Stacktrace от исключения:

java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;
    at GenericsTest$1.stuffDone(GenericsTest.java:1)
    at GenericsTest$Task.doStuff(GenericsTest.java:14)
    at GenericsTest.run(GenericsTest.java:26)
    at GenericsTest.main(GenericsTest.java:39)

Ответы [ 3 ]

9 голосов
/ 30 января 2012

Это ожидаемое поведение.Когда вы используете дженерики в Java, фактические типы объектов не включаются в скомпилированный байт-код (это называется стиранием типов).Все типы становятся Object, а приведения вставляются в скомпилированный код для имитации типизированного поведения.

Кроме того, varargs становятся массивами, и когда вызывается универсальный метод varargs, Java создает массив типа Object[] спараметры метода перед его вызовом.

Таким образом, ваша строка callback.stuffDone(param); компилируется как callback.stuffDone(new Object[] { param });.Однако ваша реализация обратного вызова требует массив типа String[].Java-компилятор вставил невидимое приведение в ваш код для обеспечения этой типизации, и поскольку Object[] не может быть приведено к String[], вы получаете исключение.Номер фиктивной строки, который вы видите, предположительно, потому что приведение не появляется нигде в вашем коде.

Одним из способов решения этой проблемы является полное удаление универсальных элементов из интерфейса и класса обратного вызова, заменяя все типы на Object.

0 голосов
/ 30 января 2012

Это действительно из-за стирания типа, но критическая часть здесь - это varargs.Они, как уже отмечалось, реализованы в виде таблицы.Таким образом, компилятор фактически создает Object [], чтобы упаковать ваши параметры и, следовательно, позднее привести к неверному приведению.Но тут есть хак: если вы достаточно хороши, чтобы передать таблицу как vararg, компилятор распознает ее, а не перепаковывает, и, поскольку вы сохранили ему какую-то работу, он позволит вам запустить код :-)

Попробуйте запустить после следующих модификаций:

public void doStuff(<strong>T[] param</strong>, Callback callback) {

и

task.doStuff(<strong>new String[]{"importantStuff"}</strong>, new Callback() {

0 голосов
/ 30 января 2012

grahamparks ответ правильный. Таинственный тип - это нормальное поведение. Они вставляются компилятором, чтобы гарантировать, что приложение безопасно во время выполнения перед лицом возможного неправильного использования обобщений.

Если вы играете по правилам, этот тип всегда будет успешным. Это не помогло, потому что вы проигнорировали / подавили предупреждения о небезопасном использовании дженериков. Это не разумно, особенно если вы не понимаете, что они имеют в виду, и можно ли их безопасно игнорировать.

...