Java Generics кастует странно

Я использую Java 8.

Я недавно сталкивался с этим:

public class Test {
    public static void main(String[] args) {
        String ss = "" + (Test.<Integer>abc(2));
        System.out.println(Test.<Integer>abc(2));
    }
    public static <T> T abc(T a) {
        String s = "adsa";
        return (T) s;
    }
}

Это не создает исключение java.lang.ClassCastException. Это почему?

Я всегда думал, что + и System.out.println вызывает toString. Но когда я пытаюсь это сделать, возникает исключение, как и ожидалось.

String sss = (Test.<Integer>abc(2)).toString();

Ответ 1

Он не ClassCastException потому что вся информация об общем типе ClassCastException из скомпилированного кода (процесс, называемый стиранием типа). По сути, любой тип параметра заменяется на Object. Вот почему первая версия работает. Это также, почему код компилируется вообще. Если вы попросите компилятор предупредить о непроверенных или небезопасных операциях с -Xlint:unchecked, вы получите предупреждение о непроверенном приведении в операторе return abc().

С этим утверждением:

String sss = (Test.<Integer>abc(2)).toString();

история немного другая. В то время как параметр типа T заменяется на Object, вызывающий код преобразуется в байтовый код, который явно приводит результат к Integer. Это как если бы код был написан методом с подписью static Object abc(Object) а оператор был написан:

String sss = ((Integer) Test.abc(Integer.valueOf(2))).toString();

То есть, не только преобразование внутри abc() исчезает из-за стирания типа, но и новое приведение вставляется компилятором в вызывающий код. Это приведение генерирует ClassCastException во время выполнения, потому что объект, возвращаемый из abc() является String, а не Integer.

Обратите внимание, что утверждение

String ss = "" + (Test.<Integer>abc(2));

не требует приведения, потому что компилятор просто передает объект, возвращенный abc() в операцию конкатенации строк для объектов. (Детали того, как это делается, зависят от компилятора Java, но это либо вызов метода добавления StringBuilder либо, StringBuilder с Java 9, вызов метода, созданного StringConcatFactory.) Детали здесь не имеют значения; Дело в том, что компилятор достаточно умен, чтобы признать, что приведение не требуется.

Ответ 2

Обобщения исчезают во время выполнения, поэтому приведение к T на самом деле является просто приведением к Object (от которого компилятор фактически просто избавится), поэтому исключений приведения к классу нет.

abc - это просто метод, который принимает объект и возвращает объект. StringBuilder.append(Object) - это метод, который вызывается, как видно из байт-кода:

...  
16: invokestatic  #7   // Method abc:(Ljava/lang/Object;)Ljava/lang/Object;
19: invokevirtual #8   // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
...

Когда вы делаете

String sss = (Test.<Integer>abc(2)).toString();

Тогда байт-код

...
 4: invokestatic  #3   // Method abc:(Ljava/lang/Object;)Ljava/lang/Object;
 7: checkcast     #4   // class java/lang/Integer
10: invokevirtual #5   // Method java/lang/Integer.toString:()Ljava/lang/String;
...

Ваш код завершается ошибкой при операции checkcast, которой раньше не было.