Не проверено приведение к универсальному классу, реализующему 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 бросать:

Тип безопасности: непроверенный актерский состав от 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 если предупреждение игнорируется?

4 ответа

Решение

Поведение соответствует указанному. В Разделе 5.5.2 Спецификации языка Java непроверенное приведение определено как:

В ролях от типа 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, но, по крайней мере, это указанное ограничение.

Этот состав не является безопасным. Допустим, у вас есть:

Map<?, Integer> map3 = new HashMap<String,Integer>();
StringMap<Integer> stringMap3 = (StringMap<Integer>)map3;

Это собирается бросить исключение. Неважно, что вы знаете, что новичок StringMap<Integer> и назначил его на карту3. То, что вы делаете, известно как даункастинг в Java для получения дополнительной информации.

РЕДАКТИРОВАТЬ: Вы также чрезмерно усложняете проблему со всеми генериками, у вас будет точно такая же проблема без каких-либо универсальных типов.

Конечно, невозможно дать ответ, который является "более правильным" (более корректным?), Чем тот, который объясняет наблюдаемое поведение в терминах спецификации языка 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> и вы всегда можете ожидать такого предупреждения, когда применяете подстановочный знак к определенному типу. (Или ограниченный тип для более строго ограниченного, более конкретного типа.) Обратите внимание, что "Integer" в этом случае - красная сельдь, вы получите то же самое с Map<?,?> map3 = new HashMap<>();

На самом деле ответ находится в спецификации языка 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
Другие вопросы по тегам