Несогласованное исключение ClassCastException для необработанных типов

При выполнении приведенного ниже кода код выполняется без каких-либо ошибок, но для переменной типа List<Integer> тип возвращаемого значения метода get() должен быть Integer, но при выполнении этого кода, когда я вызываю x.get(0), возвращается строка, тогда как это должен генерировать исключение.

public static void main(String[] args)
      {
            ArrayList xa = new ArrayList();
            xa.addAll(Arrays.asList("ASDASD", "B"));
            List<Integer> x = xa;
            System.out.println(x.get(0));
      }

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

public static void main(String[] args)
      {
            ArrayList xa = new ArrayList();
            xa.addAll(Arrays.asList("ASDASD", "B"));
            List<Integer> x = xa;
            System.out.println(x.get(0).getClass());
      }

Почему java выполняет преобразование типа при получении типа класса объекта?


person Aman J    schedule 21.06.2017    source источник


Ответы (2)


Компилятор должен вставлять инструкции проверки типов на уровне байтового кода, где это необходимо, поэтому при присвоении Object, например. Object o = x.get(0); или System.out.println(x.get(0));, может не требовать этого, вызов метода для выражения x.get(0) требует этого.

Причина кроется в правилах совместимости бинарных файлов . Проще говоря, не имеет значения, был ли вызванный метод унаследован или явно объявлен типом получателя, формальный тип выражения x.get(0) равен Integer, и вы вызываете для него метод getClass(), следовательно, вызов будет закодирован как вызов метода с именем getClass с сигнатурой () → java.lang.Class в классе получателя java.lang.Integer. Тот факт, что этот метод был унаследован от java.lang.Object и что он был объявлен final во время компиляции, не отражается в скомпилированном классе.

Таким образом, теоретически во время выполнения метод можно было бы удалить из java.lang.Object и добавить новый метод java.lang.Class getClass() в java.lang.Integer без нарушения совместимости с этим конкретным кодом. Хотя мы знаем, что этого никогда не произойдет, компилятор просто следует формальным правилам, чтобы не вводить в код предположения о наследовании.

Поскольку вызов будет скомпилирован как вызов, нацеленный на java.lang.Integer, перед инструкцией вызова необходимо выполнить приведение типа, что приведет к сбою в сценарии загрязнения кучи.

Обратите внимание, что если вы измените код на

System.out.println(((Object)x.get(0)).getClass());

вы сделаете явное предположение, что метод был объявлен в java.lang.Object. Расширение до java.lang.Object не будет генерировать никаких дополнительных инструкций байт-кода, все, что делает этот код, — это изменяет тип получателя вызова метода на java.lang.Object, устраняя необходимость в приведении типа.

Здесь есть интересное отклонение от правил: компилятор делает кодирование вызова как вызов java.lang.Object на уровне байт-кода, если метод является одним из известных final методов, объявленных в java.lang.Object. Это может быть связано с тем, что эти конкретные методы указаны в JLS, и их кодирование в этой форме позволяет JVM быстро идентифицировать эти специальные методы. Но комбинация инструкции checkcast и инструкции invokevirtual по-прежнему демонстрирует такое же совместимое поведение.

person Holger    schedule 21.06.2017
comment
разве checkcast не добавлено из-за стирания типов, а не из-за бинарной совместимости? - person Eugene; 22.06.2017
comment
@Eugene: это комбинация стирания типа, поэтому ссылочным типом является Object, и бинарная совместимость, поэтому зачем нужен Integer, даже если мы могли бы вместо этого вызвать Object#getClass(). Поскольку вопрос фокусируется на различии между двумя случаями, оба из которых подлежат стиранию типа, релевантным различием между ними является проблема двоичной совместимости, которая применима только ко второму случаю. - person Holger; 22.06.2017

Это из-за PrintStream#println:

public void println(Object x) {
    String s = String.valueOf(x);
    ...

Посмотрите, как он преобразует все, что вы ему даете, в String, но сначала присваивая его Object (что работает, потому что Integer является Object). Измените свой первый код на:

    ArrayList xa = new ArrayList();
    xa.addAll(Arrays.asList("ASDASD", "B"));
    List<Integer> x = xa;
    Integer i = x.get(0);
    System.out.println(i);

и вы получите тот же провал.

ИЗМЕНИТЬ

Да, Дидье прав в своем комментарии; Таким образом, немного подумав об обновлении.

Это может быть даже упрощено, чтобы понять, почему компилятор вставляет дополнительные checkcast #5 // class java/lang/Integer:

 ArrayList<Integer> l = new ArrayList<>();
 l.get(0).getClass();

Во время выполнения нет типа Integer, просто Object; который будет компилироваться, среди прочего, в:

  10: invokevirtual #4 // Method java/util/ArrayList.get:(I)Ljava/lang/Object;
  13: checkcast     #5 // class java/lang/Integer
  16: invokevirtual #6 // Method java/lang/Object.getClass:()Ljava/lang/Class;

Обратите внимание на checkcast, чтобы проверить, что тип, который мы получаем из этого List, на самом деле является Integer. List::get — это общий метод, и этот общий параметр во время выполнения будет Object; для поддержания правильного List<Integer> во время выполнения требуется checkcast.

person Eugene    schedule 21.06.2017
comment
Это на самом деле не объясняет, почему компилятор генерирует приведение при вызове getClass() - person Didier L; 21.06.2017
comment
@DidierL хорошо, тип ссылки List<Integer>; мне кажется довольно логичным - person Eugene; 21.06.2017
comment
Ну, я бы ожидал приведения при назначении переменной Integer (как в вашем примере), при вызове метода, принадлежащего Integer, или при передаче его методу, который принимает Integer в качестве аргумента. Без объяснения Хольгера я бы не ожидал приведения при вызове метода, объявленного в Object, например getClass(). - person Didier L; 21.06.2017
comment
Интересным моментом в вашем выводе дизассемблирования является то, что инструкция checkcast не потребуется для выполнения. Это снова заставило меня задуматься, поэтому я только что проверил, что применение формальных правил по-прежнему выполняется для любого другого класса, вызов будет закодирован с точным типом получателя, что делает необходимым checkcast. Таким образом, специальная обработка метода java.lang.Object в инструкции вызова не должна влиять на другие инструкции, что наиболее примечательно, не предназначена для исключения checkcast - person Holger; 22.06.2017
comment
@Holger, это так интригует! на самом деле вы можете просто выбросить checkcast здесь; это выглядит как неудачный сценарий для меня ... - person Eugene; 22.06.2017
comment
@Holger также, этот Integer i = 12;i.getClass(); тоже будет компилироваться в Object.getClass; что опять же совсем не очевидно. - person Eugene; 22.06.2017
comment
Как уже было сказано, похоже, это свойство всех вызовов этих final методов, объявленных в java.lang.Object, и только их. Например, если вы объявляете enum Foo { BAR }, то Foo.BAR.getDeclaringClass() компилируется как Foo.getDeclaringClass(), а не java.lang.Enum.getDeclaringClass(), тогда как Foo.BAR.getClass() компилируется как java.lang.Object.getClass:(). - person Holger; 22.06.2017