Unchecked cast to generic class, реализующий Map <String, V>
Я пытаюсь понять, почему этот код имеет предупреждение без предупреждения. Первые два броска не имеют предупреждения, но третий делает:
class StringMap<V> extends HashMap<String, V> {
}
class StringToIntegerMap extends HashMap<String, Integer> {
}
Map<?, ?> map1 = new StringToIntegerMap();
if (map1 instanceof StringToIntegerMap) {
StringToIntegerMap stringMap1 = (StringToIntegerMap)map1; //no unchecked cast warning
}
Map<String, Integer> map2 = new StringMap<>();
if (map2 instanceof StringMap) {
StringMap<Integer> stringMap2 = (StringMap<Integer>)map2; //no unchecked cast warning
}
Map<?, Integer> map3 = new StringMap<>();
if (map3 instanceof StringMap) {
StringMap<Integer> stringMap3 = (StringMap<Integer>)map3; //unchecked cast warning
}
Это полное предупреждение для stringMap3
cast:
Тип безопасности: снят флажок с Map<capture#3-of ?,Integer>
до StringMap<Integer>
Однако объявление класса StringMap
указывает параметр первого типа Map
(т.е. String
), и оба map3
и приведение StringMap<Integer>
используют один и тот же тип для параметра второго типа Map
(т.е. Integer
). Из того, что я понимаю, до тех пор, пока актер не бросает ClassCastException
(и он не должен, так как существует проверка instanceof
), stringMap3
будет действительным Map<String, Integer>
.
Является ли это ограничением компилятора Java? Или есть сценарий, когда вызовы методов map3 или stringMap3 с определенными аргументами могут привести к неожиданному ClassCastException
, если предупреждение игнорируется?
Ответы
Ответ 1
Поведение задано. В Раздел 5.5.2 Спецификации Java Language неконтролируемый актер определяется как:
Приведение из типа S
в параметризованный тип T
не проверяется, если по крайней мере одно из следующих утверждений не выполняется:
-
S <: T
-
Все аргументы типа T
являются неограниченными подстановочными знаками
-
T <: S
и S
не имеет подтипа X
, кроме T
, где аргументы типа X
не содержатся в аргументах типа T
.
(где A <: B
означает: "A
является подтипом B
" ).
В вашем первом примере целевой тип не имеет подстановочных знаков (и, следовательно, все они неограничены). В вашем втором примере StringMap<Integer>
на самом деле является подтипом Map<String, Integer>
(и нет подтипа X
, как указано в третьем условии).
В вашем третьем примере, однако, у вас есть отличное от Map<?, Integer>
до StringMap<Integer>
, и из-за подстановочного знака ?
ни один из них не является подтипом другого. Кроме того, очевидно, что не все параметры типа являются неограниченными подстановочными знаками, поэтому ни одно из условий не применяется: это неконтролируемое исключение.
Если в коде используется непроверенный бросок, для выдачи предупреждения требуется соответствующий Java-компилятор.
Как и вы, я не вижу ни одного сценария, в котором листинг будет недействительным, поэтому вы можете утверждать, что это ограничение компилятора Java, но, по крайней мере, это определенное ограничение.
Ответ 2
Это не безопасно. Скажем, у вас есть:
Map<?, Integer> map3 = new HashMap<String,Integer>();
StringMap<Integer> stringMap3 = (StringMap<Integer>)map3;
Это приведет к исключению. Не имеет значения, что вы знаете, что создали новую версию StringMap<Integer>
и присвоили ее карте3. То, что вы делаете, известно как down-casting Downcasting в Java для получения дополнительной информации.
EDIT: Вы также слишком усложняете проблему со всеми генериками, у вас будет такая же проблема без каких-либо общих типов.
Ответ 3
Собственно, ответ находится в Спецификации языка Java.
Раздел 5.1.10 упоминает, что если вы используете подстановочный знак, то это будет новый тип захвата.
Это означает, что Map<String, Integer>
не является подклассом Map<?, Integer>
, и поэтому даже если StringMap<Integer>
присваивается типу Map<?, Integer>
, потому что Map<?, Integer>
представляет карту с некоторым типом ключа и значениями Integer, что верно для StringMap<Integer>
, литье небезопасно. Вот почему вы получаете предупреждение о непроверенной броске.
С точки зрения компилятора между присваиванием и приложением может быть что угодно, поэтому даже если map3 является экземпляром StringMap<Integer>
в операции литья, map3 имеет Map<?, Integer>
в качестве своего типа, поэтому предупреждение полностью законным.
И ответить на ваш вопрос: да, пока экземпляр map3 не будет иметь только строки в качестве ключей, и ваш код не может этого гарантировать.
Смотрите, почему нет:
Map<?, Integer> map3 = new StringMap<Integer>();
// valid operation to use null as a key. Possible NPE depending on the map implementation
// you choose as superclass of the StringMap<V>.
// if you do not cast map3, you can use only null as the key.
map3.put(null, 2);
// but after an other unchecked cast if I do not want to create an other storage, I can use
// any key type in the same map like it would be a raw type.
((Map<Double, Integer>) map3).put(Double.valueOf(0.3), 2);
// map3 is still an instance of StringMap
if (map3 instanceof StringMap) {
StringMap<Integer> stringMap3 = (StringMap<Integer>) map3; // unchecked cast warning
stringMap3.put("foo", 0);
for (String s : stringMap3.keySet()){
System.out.println(s+":"+map3.get(s));
}
}
Результат:
null:2
foo:0
Exception in thread "main" java.lang.ClassCastException: java.lang.Double cannot be cast to java.lang.String
Ответ 4
Конечно, невозможно дать ответ, который является "более правильным" (исправление?), чем тот, который объясняет наблюдаемое поведение с точки зрения спецификации языка Java. Однако:
-
Объяснение наблюдаемого поведения, данное в практическом плане, может быть легче следовать и легче запомнить, чем объяснение, которое просто бросает JLS на вас. В результате практическое объяснение часто более полезно, чем объяснение в терминах JLS.
-
JLS - это точно так, как есть, а не каким-либо другим способом, потому что он должен удовлетворять практическим ограничениям. Учитывая основополагающий выбор, сделанный языком, нередко детали JLS не могли быть иначе, чем так. Это означает, что практические причины для конкретного поведения можно считать более важными, чем JLS, поскольку они сформировали JLS, JLS не формировали их.
Итак, следует практическое объяснение того, что происходит.
Следующее:
Map<?, ?> map1 = ...;
StringToIntegerMap stringMap1 = (StringToIntegerMap)map1;
не дает никакого предупреждающего предупреждения о броске, потому что вы не используете общий тип. Это то же самое, что делать следующее:
Map map4 = ...; //gives warning "raw use of generic type"; bear with me.
StringToIntegerMap stringMap4 = (StringToIntegerMap)map4; //no unchecked warning!
Следующее:
Map<String, Integer> map2 = ...;
StringMap<Integer> stringMap2 = (StringMap<Integer>)map2;
не дает никакого предупреждающего предупреждения, потому что общие аргументы левой стороны соответствуют общим аргументам правой стороны. (Оба являются <String,Integer>
)
Следующее:
Map<?, Integer> map3 = ...;
StringMap<Integer> stringMap3 = (StringMap<Integer>)map3;
дает предупреждение без предупреждения, потому что левая сторона <String,Integer>
, но правая сторона <?,Integer>
, и вы всегда можете ожидать такое предупреждение, когда вы прикладываете подстановочный знак к определенному типу. (Или ограниченный тип для более строго ограниченного, более конкретного типа.) Обратите внимание, что "Целое число" в этом случае является красной селедой, вы получите то же самое с Map<?,?> map3 = new HashMap<>();