Подтвердить что ты не робот

Нет 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

Почему он не бросает?

4b9b3361

Ответ 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.