Нет ClassCastException при отливке на общий тип, отличный от фактического класса
У меня есть код, который выглядит примерно так (часть отрицательного теста метода get
):
import java.util.*;
public class Test {
Map<String, Object> map = new HashMap<>();
public static void main (String ... args) {
Test test = new Test();
test.put("test", "value"); // Store a String
System.out.println("Test: " + test.get("test", Double.class)); // Retrieve it as a Double
}
public <T> T get(String key, Class<T> clazz) {
return (T) map.get(key);
}
public void put(String key, Object value) {
map.put(key, value);
}
}
Я ожидал, что он запустит ClassCastException
, но успешно завершит печать:
Test: value
Почему он не бросает?
Ответы
Ответ 1
Поучительно рассмотреть, как выглядит класс после удаления параметров типа (стирание стилей):
public class Test {
Map map = new HashMap();
public static void main (String ... args) {
Test test = new Test();
test.put("test", "value");
System.out.println("Test: " + test.get("test", Double.class));
}
public Object get(String key, Class clazz) {
return map.get(key);
}
public void put(String key, Object value) {
map.put(key, value);
}
}
Это компилирует и производит тот же результат, который вы видите.
Сложная часть - это строка:
System.out.println("Test: " + test.get("test", Double.class));
Если вы это сделали:
Double foo = test.get("test", Double.class);
то после стирания типа компилятор должен был бы добавить cast (потому что после стирания типа test.get()
возвращает Object
):
Double foo = (Double)test.get("test", Double.class);
Таким же образом, компилятор также мог бы добавить литье в вышеприведенной строке, например:
System.out.println("Test: " + (Double)test.get("test", Double.class));
Однако он не вставляет актерский состав, потому что для его компиляции и правильной работы не требуется приведение, поскольку конкатенация строк (+
) работает на всех объектах одинаково; он должен знать только тип Object
, а не определенный подкласс. Поэтому компилятор может опустить ненужный приведение, и в этом случае он делает.
Ответ 2
Это потому, что вы выполняете общий тип T
, который стирается во время выполнения, как и все генерируемые Java. Итак, что на самом деле происходит во время выполнения, является то, что вы выполняете Object
, а не Double
.
Обратите внимание, что, например, если T
был определен как <T extends Number>
, вы должны были бы перейти к Number
(но все же не к Double
).
Если вы хотите выполнить некоторую проверку типа времени выполнения, вам нужно использовать фактический параметр clazz
(который доступен во время выполнения), а не общий тип T
(который стирается). Например, вы можете сделать что-то вроде:
public <T> T get(String key, Class<T> clazz) {
return clazz.cast(map.get(key));
}
Ответ 3
Я обнаружил разницу в байтовом коде, когда вызывается метод для возвращаемого "Double" и когда не вызывается метод.
Например, если вы должны были вызвать doubleValue()
(или даже getClass()
) в возвращаемом "Двойном", тогда произойдет ClassCastException
. Используя javap -c Test
, я получаю следующий байт-код:
34: ldc #15 // class java/lang/Double
36: invokevirtual #16 // Method get (Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/Object;
39: checkcast #15 // class java/lang/Double
42: invokevirtual #17 // Method java/lang/Double.doubleValue:()D
45: invokevirtual #18 // Method java/lang/StringBuilder.append:(D)Ljava/lang/StringBuilder;
Операция checkcast
должна вызывать ClassCastException
. Кроме того, в неявном StringBuilder
, append(double)
было бы вызвано.
Без вызова doubleValue()
(или getClass()
):
34: ldc #15 // class java/lang/Double
36: invokevirtual #16 // Method get:(Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/Object;
39: invokevirtual #17 // Method java/lang/StringBuilder.append (Ljava/lang/Object;)Ljava/lang/StringBuilder;
Нет операции checkcast
, а append(Object)
вызывается в неявном StringBuilder
, потому что после стирания типа T
просто Object
в любом случае.
Ответ 4
Вы не получаете ClassCastException
, потому что контекст, в котором вы возвращаете значение с карты, не требует, чтобы компилятор выполнял проверку в этот момент, поскольку он эквивалентен присвоению значения переменной типа Object
(см. ответ rgettman).
Вызов:
test.get("test", Double.class);
является частью операции конкатенации строк с помощью оператора +
. Объект, возвращаемый с вашей карты, просто обрабатывается, как будто это Object
. Чтобы отобразить возвращенный "объект" как String
, требуется вызов метода toString()
, и поскольку это метод в Object
, не требуется листинг.
Если вы вызываете вызов test.get("test", Double.class);
вне контекста конкатенации строк, вы увидите, что он не работает i.e.
Это не скомпилируется:
// can't assign a Double to a variable of type String...
String val = test.get("test", Double.class);
Но это делает:
String val = test.get("test", Double.class).toString();
Другими словами, ваш код:
System.out.println("Test: " + test.get("test", Double.class));
эквивалентно:
Object obj = test.get("test", Double.class);
System.out.println("Test: " + obj);
или
Object obj = test.get("test", Double.class);
String value = obj.toString();
System.out.println("Test: " + value);
Ответ 5
Похоже, что Java не может обработать приведение с использованием типа inferred, однако, если вы используете метод Class.cast
, вызов get генерирует исключение как ожидалось:
public <T> T get(String key, Class<T> clazz) {
return clazz.cast(map.get(key)) ;
}
К сожалению, я не могу объяснить это более подробно.
Изменить: вам может быть интересно это Oracle doc.