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), компилятор может выбрать для оптимизации выброса.компилятор может даже иметь приведение в одном случае, а не в другом, возможно, потому, что он только оптимизирует приведение в некоторых простых случаях, но не утруждает себя выполнением анализа в более сложных случаях. Нам не нужно останавливаться на том, почему конкретный компилятор решил скомпилировать в ту или иную форму тот или иной оператор, потому что оба допустимы, и вы не должны полагаться на него, чтобы работать так или иначе.