Подтвердить что ты не робот

Java generics: почему этот выход возможен?

У меня есть этот класс:

class MyClass<N extends Number> {
    N n = (N) (new Integer(8));
}

И я хочу получить эти выходы:

System.out.println(new MyClass<Long>().n);
System.out.println(new MyClass<Long>().n.getClass());
  • Вывод первого оператора System.out.println():

    8
    
  • Вывод второго оператора System.out.println():

    java.lang.ClassCastException: java.lang.Integer (in module: java.base)
        cannot be cast to java.lang.Long (in module: java.base)
    

Почему я получаю первый вывод? Разве нет отливки? Почему я получаю исключение во втором выходе?

PS: Я использую Java 9; Я попробовал это с помощью JShell, и я получил исключение на обоих выходах. Затем я попробовал его с IntelliJ IDE и получил первый вывод, но исключение на втором.

4b9b3361

Ответ 1

Поведение, которое показывает IntelliJ, мне понятно:

У вас установлен флажок в MyClass. Это означает, что new Integer(8) не сразу отображается на Long, а на стирание Number (которое работает), когда эта строка выполняется: N n =(N)(new Integer(8));

Теперь посмотрим на выходные операторы:

System.out.println(new MyClass<Long>().n);

сводится к String.valueOf(new MyClass<Long>().n)((Object)new MyClass<Long>().n).toString(), который отлично работает, поскольку к n обращается через Object, а также к способу toString() доступен через статический тип Object → no cast to Long, new MyClass<Long>().n.toString() завершится с ошибкой, потому что toString() пытается получить доступ через статический тип Long. Поэтому возникает отливка n для типа Long, которая невозможна (Integer нельзя отнести к Long).

То же самое происходит при выполнении второго оператора:

System.out.println(new MyClass<Long>().n.getClass()); 

Метод getClass (объявленный в Object) типа Long пытается получить доступ через статический тип Long. Поэтому возникает приведение n к типу Long, которое дает исключение cast.

поведение JShell:

Я попытался воспроизвести полученное исключение для первого вывода в JShell - раннем доступе Java 9 Build 151:

jshell> class MyClass<N extends Number> {
   ...>     N n = (N) (new Integer(8));
   ...> }
|  Warning:
|  unchecked cast
|    required: N
|    found:    java.lang.Integer
|      N n = (N) (new Integer(8));
|                ^--------------^
|  created class MyClass

jshell> System.out.println(new MyClass<Long>().n);
8

jshell> System.out.println(new MyClass<Long>().n.getClass());
|  java.lang.ClassCastException thrown: java.base/java.lang.Integer cannot be cast to java.base/java.lang.Long
|        at (#4:1)

Но похоже, что JShell дает те же результаты, что и IntelliJ. System.out.println(new MyClass<Long>().n); выходы 8 - исключение.

Ответ 2

Это происходит из-за стирания Java.

Так как Integer extends Number, компилятор принимает приведение к N. Во время выполнения, поскольку N заменяется на Number (из-за стирания), нет никакой проблемы для хранения Integer внутри N.

Аргумент метода System.out.println имеет тип Object, поэтому нет необходимости печатать значение N.

Однако при вызове метода на N компилятор добавляет проверку типа, чтобы гарантировать, что будет вызван правильный метод. Отсюда вытекает a ClassCastException.

Ответ 3

Оба исключения и исключение - допустимое поведение. В основном, это сводится к тому, как компилятор стирает операторы, будь то что-то вроде этого без приведения:

System.out.println(new MyClass().n);
System.out.println(new MyClass().n.getClass());

или к чему-то подобному с приведениями:

System.out.println((Long)new MyClass().n);
System.out.println(((Long)new MyClass().n).getClass());

или один для одного оператора, а другой для другого. Обе версии являются допустимым кодом Java, который будет компилироваться. Вопрос заключается в том, допустимо ли компилятору компилировать одну версию, или другую, или и то, и другое.

Допустимо вставлять приведение здесь, потому что обычно это происходит, когда вы берете что-то из общего контекста, где тип является переменной типа, и возвращаете его в контекст, где переменная типа принимает определенный тип. Например, вы можете назначить new MyClass<Long>().n в переменную типа Long без каких-либо бросков или передать new MyClass<Long>().n в место, которое ожидает Long без каких-либо бросков, оба случая которых, очевидно, потребуют от компилятора вставить бросать. Компилятор может просто принять решение о том, чтобы всегда вставлять приведение, если у вас есть new MyClass<Long>().n, и это не так, так как выражение должно иметь тип Long.

С другой стороны, также допускается, чтобы в этих двух операторах не было акта, потому что в обоих случаях выражение используется в контексте, где может использоваться любой Object, поэтому для его создания не требуется никакого перевода скомпилировать и быть безопасным по типу. Кроме того, в обоих утверждениях приведение или отливка не повлияет на поведение, если значение действительно было Long. В первом выражении он передается в версию .println(), которая принимает Object, и нет более конкретной перегрузки println, которая принимает Long или Number или что-то в этом роде, поэтому такая же перегрузка будет выбираться независимо от того, считается ли аргумент Long или Object. Для второго оператора .getClass() предоставляется Object, поэтому он доступен независимо от того, находится ли вещь слева от Long или Object. Поскольку стертый код действителен как с литом, так и без него, и поведение будет одинаковым с литым и без него (при условии, что вещь действительно является Long), компилятор может выбрать оптимизацию выдачи.

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

Ответ 4

Это происходит потому, что вы уже определили n как объект integer, чтобы он не отдавал его длинным

либо используйте Integer в MyClass в sysout, например

System.out.println(new MyClass<Integer>().n);

или определите n как: N n =(N)(new Long(8));.