5

Я использую java generics и varargs.ClassCastException при использовании varargs и generics

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

есть ли шанс, что вы могли бы предоставить копию StackTrace. Мое подозрение в том, что это из-за неявного приведения, которое происходит из-за стирания типа. – Matt

+0

Добавлена ​​трассировка стека на вопрос. – murgo

ответ

9

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

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

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

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

+0

Хорошо, понял, что это будет так. Странно, что он скомпилирован в новый Object [] {param}, а не новый String [] {param}, который будет работать. У компилятора есть вся информация, чтобы сделать лучший листинг (он знает, чтобы передать его String [] в любом случае). Это и номер ломаной строки в трассировке стека заставили меня подумать, что это ошибка Java. Удалены varargs, сохранены дженерики в моей реальной программе. – murgo

+0

@murgo: «У компилятора есть вся информация ...» Нет, это не так. Массив создается при вызове функции varargs, т. Е. В 'doStuff', когда вы вызываете' callback.stuffDone (param); '. В этом месте он не знает ни времени компиляции, ни времени выполнения, что Т будет. – newacct

0

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

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

+0

Были предупреждения, которые указаны в вопросе. Тем не менее, подумал, что что-то было подозрительно, потому что я думал, что буду использовать дженерики в «рабочем режиме». Это должно быть скорее ошибкой компиляции, чем предупреждением. – murgo

+1

@murgo - это предупреждения, потому что есть случаи, когда можно их игнорировать. Действительно, бывают случаи, когда лучшее решение включает в себя игнорирование/подавление предупреждения. –

0

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

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

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

и

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

+0

ваше прокомментированное обходное решение в основном то же самое, не заметили вначале ... – wmz