Generics Oddity - я могу вставить значение Long в Map <String, String>, и он компилируется и не прерывается во время выполнения
Введите следующий код:
public static void main(String[] args) {
HashMap<String, String> hashMap = new HashMap<>();
HashMap<String, Object> dataMap = new HashMap<>();
dataMap.put("longvalue", 5L);
class TestMethodHolder {
<T> T getValue(Map<String, Object> dataMap, String value) {
return (T)dataMap.get(value);
}
}
hashMap.put("test", new TestMethodHolder().<String>getValue(dataMap, "longvalue"));
String value = hashMap.get("test"); // ClassCastException occurs HERE
System.out.println(value);
}
Мне неудивительно, что этот код компилируется, а скорее, что ClassCastException встречается в строке get, а не в строке put выше, хотя у меня есть обоснованное предположение о том, что может произойти. Поскольку общие типы стираются во время выполнения, приведение в getValue() на самом деле никогда не происходит во время выполнения и фактически является отличным от Object. Если метод будет реализован ниже следующим образом, тогда будет выполняться бросок времени выполнения, и он потерпит неудачу в строке ввода (как и ожидалось). Кто-нибудь может это подтвердить?
class TestMethodHolder {
String getValue(Map<String, Object> dataMap, String value) {
return (String)dataMap.get(value);
}
}
Является ли это известным недостатком или необычностью использования дженериков? Неправильно ли тогда использовать символ < > при вызове методов?
Изменить:
Я использую Oracle JDK 1.7_03 по умолчанию.
Еще один подразумеваемый вопрос сверху: Является ли приведение в исходном getValue STILL происходящим во время выполнения, но приведение на самом деле является объектом Object - или компилятор достаточно умен, чтобы удалить этот прилив из-за отсутствия во время выполнения? Это может объяснить разницу в том, где происходит ClassCastException, которое люди замечают при запуске.
Ответы
Ответ 1
Компилятор зависит от безопасности типа, чтобы делать предположения и делать преобразования/оптимизации. К сожалению, безопасность типа может быть подорвана без контроля. Если ваша программа содержит неверный непроверенный листинг, неясно, что должен делать компилятор. В идеале это должно сделать проверку времени выполнения в точном месте неконтролируемого приведения, в вашем примере, когда Object
отправляется на T
. Но это невозможно из-за стирания, которое не является частью системы типов.
В другом месте в вашем примере типы звучат, поэтому компилятор может предположить, что getValue()
действительно возвращает String
, нет необходимости дважды проверять. Но это также законно делать проверку, как это делает компилятор Eclipse (возможно, потому, что он присваивает возвращаемое значение локальной переменной temp String
).
Таким образом, плохая новость заключается в том, что если ваша программа содержит неправильный неконтролируемый отбор, ее поведение - undefined.... Поэтому убедитесь, что все ваши неконтролируемые отклики верны, с помощью строгих рассуждений.
Хорошей практикой является проверка всех непроверенных бросков, чтобы вы могли законно подавить непроверенное предупреждение. Например
<T> T getValue(Map<String, Object> dataMap, String value, Class<T> type)
{
Object value = dataMap.get(value);
if(value!=null && !type.isInstance(value)) // check!
throw new ClassCastException();
@SuppressWarning("unchecked")
T t = (T)value; // this is safe, because we've just checked
return t;
}
См. мой ответ на аналогичный вопрос: Классный класс в Java?
Ответ 2
Линия
return (T)dataMap.get(value);
генерирует предупреждение о немедленном броске, и, в зависимости от спецификации, наличие любого такого предупреждения делает ваш код небезопасным. ClassCastException
возникает при первом попытке назначить тип-небезопасный результат в переменную неправильного типа, потому что это первый раз, когда скомпилированный код имеет проверку типа.
Обратите внимание, что компилятор Eclipse вставляет больше проверок типов, чем указано JLS, поэтому, если вы компилируете в Eclipse, вызов hashMap.put
завершается с ошибкой CCE
. Компилятор знает, что этот вызов должен иметь два аргумента String
, и поэтому он может вставить проверки типа перед вызовом фактического метода.
Точно, как вы предполагаете, если вы замените общий T
на конкретный String
, тогда проверка типа происходит в этой точке — и не выполняется.
Ответ 3
Информация о типе стирается во время компиляции (см. Neal Gafter "Reified Generics for Java" ).
С практической точки зрения вы можете защищать свои коллекции с помощью утилиты Collections:
Class<String> type = String.class;
Map<String, String> hashMap = new HashMap<>();
Map<String, String> map = Collections.checkedMap(hashMap, type, type);
Map rawType = map; // pre-Java 1.5 code knows nothing about generics
rawType.put(1, 2); // throws ClassCastException at runtime