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 и получил первый выход, но исключение во втором.
4 ответа:
поведение, которое показывает 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
-> не приведен кLong
происходит.new MyClass<Long>().n.toString()
не удалось бы с исключением, потому чтоtoString()
пытаются получить доступ через статический типLong
. Поэтому приведение n к типуLong
происходит то, что невозможно(Integer
не может быть приведен кLong
).то же самое происходит при выполнении 2-ое заявление:
System.out.println(new MyClass<Long>().n.getClass());
The
getClass
способ (объявленное вObject
) типаLong
пытаются получить доступ через статический типLong
. Поэтому приведение n к типаLong
происходит, что дает исключение приведения.поведение JShell:
я попытался воспроизвести полученное исключение для первого оператора вывода на JShell-Java 9 early access 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 - не исключение.
это происходит из-за стирания Java.
С
Integer
выходитNumber
, компилятор принимает приведение кN
. Во время выполнения, так какN
заменить наNumber
(из-за стирания), нет никаких проблем, чтобы сохранитьInteger
внутриn
.аргумент метода
System.out.println
типаObject
так что нет никаких проблем, чтобы печатать стоимостьюn
.однако, при вызове метода
n
проверка типа добавляется компилятор для обеспечения правильного метода будет вызван. Следовательно, в результатеClassCastException
.
как исключение, так и отсутствие исключения являются допустимыми поведениями. В основном, это сводится к тому, как компилятор стирает операторы, будь то что-то вроде этого без приведений:
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
), компилятор может выбрать для оптимизации выброса.компилятор может даже иметь приведение в одном случае, а не в другом, возможно, потому, что он только оптимизирует приведение в некоторых простых случаях, но не утруждает себя выполнением анализа в более сложных случаях. Нам не нужно останавливаться на том, почему конкретный компилятор решил скомпилировать в ту или иную форму тот или иной оператор, потому что оба допустимы, и вы не должны полагаться на него, чтобы работать так или иначе.