Классный класс в 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);