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 и получил первый вывод, но исключение на втором.
Ответы
Ответ 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));
.