Классный класс в Java?

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

public class Test {
  static class Parent {
    @Override
    public String toString() { return "parent"; }
  }

  static class ChildA extends Parent {
    @Override
    public String toString() { return "child A"; }
  }

  static class ChildB extends Parent {
    @Override
    public String toString() { return "child B"; }
  }

  public <C extends Parent> C get() {
    return (C) new ChildA();
  }

  public static void main(String[] args) {
    Test test = new Test();

    // should throw ClassCastException...
    System.out.println(test.<ChildB>get());

    // throws ClassCastException...
    System.out.println(test.<ChildB>get().toString());
  }
}

Это версия java, компиляция и запуск:

$ java -version
java version "1.7.0_17"
Java(TM) SE Runtime Environment (build 1.7.0_17-b02)
Java HotSpot(TM) 64-Bit Server VM (build 23.7-b01, mixed mode)
$ javac -Xlint:unchecked Test.java
Test.java:24: warning: [unchecked] unchecked cast
    return (C) new ChildA();
               ^
  required: C
  found:    ChildA
  where C is a type-variable:
    C extends Parent declared in method <C>get()
1 warning
$ java Test
child A
Exception in thread "main" java.lang.ClassCastException: Test$ChildA cannot be cast to Test$ChildB
  at Test.main(Test.java:30)

Ответы

Ответ 1

Тип erasure: generics - это только синтаксическая функция, которая удаляется компилятором (по соображениям совместимости) и заменяется приложением, если требуется.

Во время выполнения метод C get не знает тип C (поэтому вы не можете создать экземпляр new C()). Вызов test.<ChildB>get() на самом деле является вызовом test.get. return (C) new ChildA() преобразуется в return (Object) new ChildA(), потому что стирание неограниченного типа C равно Parent (его крайняя левая граница). Тогда никакого приведения не требуется, потому что println ожидает аргумент Object.

С другой стороны, test.<ChildB>get().toString() терпит неудачу, потому что test.<ChildB>get() передается в ChildB перед вызовом toString().

Обратите внимание, что вызов, например myPrint(test.<ChildB>get()), также завершится с ошибкой. Вывод из Parent, возвращаемый get для ввода ChildB, выполняется при вызове myPrint.

public static void myPrint(ChildB child) {
  System.out.println(child);
}

Ответ 2

Это связано с стиранием типа. Во время компиляции при компиляции

public <C extends Parent> C get() {
  return (C) new ChildA();
}

просто проверяет, что ChildA является подтипом Parent, и, следовательно, приведение не обязательно завершится неудачно. Он знает, что вы на шаткой почве, учитывая, что ChildA не может быть назначен для типа C, поэтому он выдает предупреждение без отметки, позволяя вам знать, что что-то может пойти не так. (Почему он позволяет компиляции кода, а не просто его отклонять? Выбор дизайна языка мотивирован тем, что Java-программисты должны перенести свой старый код предварительного кода с минимальным переписыванием.)

Теперь о том, почему get() не работает: не существует компонента времени выполнения для параметра типа C; после компиляции аргумент типа просто удаляется из программы и заменяется его верхней границей (Parent). Таким образом, вызов будет успешным, даже если аргумент типа несовместим с ChildA, но в первый раз, когда вы на самом деле пытаетесь использовать результат get() как ChildB, листинг (от Parent до ChildB) будет и вы получите исключение.

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

Ответ 3

Посмотрите на сгенерированный байт-код:

12  invokevirtual Test.get() : Test$Parent [30]
15  invokevirtual java.io.PrintStream.println(java.lang.Object) : void [32]
18  getstatic java.lang.System.out : java.io.PrintStream [24]
21  aload_1 [test]
22  invokevirtual Test.get() : Test$Parent [30]
25  checkcast Test$ChildB [38]
28  invokevirtual Test$ChildB.toString() : java.lang.String [40]
31  invokevirtual java.io.PrintStream.println(java.lang.String) : void [44]

Первый вызов println просто использует версию вызова Object, поэтому никакого приведения не требуется.

Ответ 4

Если проверка типа времени компиляции исключена неконтролируемым исполнением, то неясно, от чтения JLS, когда должна выполняться проверка типа времени выполнения. Я думаю, что компилятору разрешено предполагать, что типы звучат, и он может задержать проверку времени выполнения как можно позже. Это плохая новость, так как это зависит от индивидуальности каждого компилятора, поэтому поведение программы не определено.

По-видимому, компилятор преобразует первый println как

Parent tmp = test.<ChildB>get();  // ok at runtime
System.out.println(tmp);

Мы не можем поместить какую-либо ошибку в компилятор, чтобы сделать это, это совершенно законно.

Компилятор также может преобразовать код в

ChildB tmp = test.<ChildB>get();  // fail at runtime
System.out.println(tmp);

Поэтому для такой простой программы поведение среды выполнения undefined в JLS.


Поведение второго println также undefined. У компилятора нет никаких проблем, чтобы вывести, что toString() - это метод из суперкласса, поэтому ему не нужно приведение в подкласс

Parent tmp = test.<ChildB>get();  
String str = tmp.toString();
System.out.println(str);