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());
  1. вывод первого System.out.println() о себе:

    8
    
  2. выход второго 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 51

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), компилятор может выбрать для оптимизации выброса.

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

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

использовать целое число в MyClass в sysout, как

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

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