ClassCastException при использовании varargs и generics

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

Если я использую следующий код, я получу 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)

person murgo    schedule 29.01.2012    source источник
comment
Есть ли шанс, что вы могли бы предоставить копию stacktrace. Я подозреваю, что это из-за неявного приведения, которое происходит из-за стирания типа.   -  person Matt    schedule 30.01.2012
comment
В вопрос добавлена ​​трассировка стека.   -  person murgo    schedule 30.01.2012


Ответы (3)


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

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

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

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

person grahamparks    schedule 30.01.2012
comment
Ок, подумал, что так и будет. Просто странно, что он компилируется в новый Object[] {param}, а не в новый String[] {param}, который будет работать. У компилятора есть вся информация для лучшего приведения (он все равно знает, как привести ее к String[]). Это и номер неработающей строки в трассировке стека заставили меня подумать, что это ошибка Java. Удалил varargs, оставил дженерики в моей реальной программе. - person murgo; 30.01.2012
comment
@murgo: у компилятора есть вся информация ... Нет, нет. Массив создается при вызове функции varargs, т.е. в doStuff при вызове callback.stuffDone(param);. В этом месте он не знает ни во время компиляции, ни во время выполнения, каким будет T. - person newacct; 30.01.2012

Ответ Грахампарка правильный. Загадочное приведение типов — нормальное поведение. Они вставляются компилятором, чтобы гарантировать, что приложение является типобезопасным во время выполнения перед лицом возможного неправильного использования дженериков.

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

person Stephen C    schedule 30.01.2012
comment
Были предупреждения, которые показаны в вопросе. Тем не менее, я понял, что что-то не так, потому что я думал, что буду использовать дженерики в рабочем режиме. Это действительно должно быть ошибкой компиляции, а не предупреждением. - person murgo; 30.01.2012
comment
@murgo - это предупреждения, потому что в некоторых случаях их можно безопасно игнорировать. Действительно, бывают случаи, когда лучшим решением является игнорирование/подавление предупреждения. - person Stephen C; 30.01.2012

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

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

public void doStuff(T[] param, Callback callback) {

а также

task.doStuff(new String[]{"importantStuff"}, new Callback() {

person wmz    schedule 30.01.2012