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


person blurredd    schedule 23.11.2015    source источник


Ответы (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, но, по крайней мере, это заданное ограничение.

person Hoopje    schedule 02.12.2015
comment
К такому выводу я пришел после прочтения ответа @pifta и различных разделов JLS. Я предполагаю, что JLS можно было бы обновить, чтобы добавить лучшие правила приведения между двумя параметризованными типами, где соответствующие общие классы используют разное количество параметров типа, но я не знаю, насколько это возможно или побочные эффекты. Этот вопрос кажется связанным, если я не ошибаюсь: Приведение к общим подтипам универсальный класс. - person blurredd; 03.12.2015

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

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

Это вызовет исключение. Неважно, что вы знаете, что создали StringMap<Integer> и назначили его на карту3. То, что вы делаете, известно как нисходящее приведение.

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

person Charles Durham    schedule 23.11.2015
comment
Точно. По сути, вы пытаетесь преобразовать Map‹?, Integer› в HashMap‹String, Integer›, поэтому ? в строку... - person JayC667; 23.11.2015
comment
Использование дженериков занимает центральное место в этом вопросе. Под безопасным я имел в виду, можно ли вызвать метод (или набор методов) с определенными аргументами для map3 или stringMap3, который вызовет неожиданное исключение ClassCastException, если предупреждение будет проигнорировано. Я не спрашивал, может ли первоначальное приведение к StringMap‹Integer› вызвать ClassCastException. Я обновил вопрос. - person blurredd; 23.11.2015

На самом деле ответ находится в Спецификации языка Java. В Раздел 5.1.10 упоминается, что если вы используете подстановочный знак, то это будет новый тип захвата.

Это означает, что Map<String, Integer> не является подклассом Map<?, Integer>, и, следовательно, даже если StringMap<Integer> можно присвоить типу Map<?, Integer>, поскольку Map<?, 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
person pifta    schedule 30.11.2015
comment
О, и, кстати, таким же образом можно злоупотреблять и первой версией, для которой нужен другой непроверенный бросок. И пока вы не сделаете это непроверенное приведение, вы можете использовать только метод put карты с нулевым значением в качестве ключа и нулевым значением, что в некотором смысле безопасно, потому что если реализация карты поддерживает нулевые ключи и нулевые значения, то ваши операции чтения должны учитывать возможный нулевой ключ и нулевое значение на карте. - person pifta; 30.11.2015
comment
Я не имел в виду, что будут другие непроверенные приведения, кроме StringMap<Integer>. Вы можете потенциально испортить любую карту — или любой универсальный объект, если на то пошло — с правильным непроверенным приведением, поэтому ваш пример не отвечает на вопрос, может ли приведение от Map<?, Integer> к StringMap<Integer> когда-либо допустить загрязнение кучи, в отличие, скажем, от приведения от Map<?, Integer> к Map<Boolean, Integer> . Вы можете создать сценарий, в котором карта, назначенная map3, заполняется в другом месте или использует другую реализацию карты, если это помогает. - person blurredd; 02.12.2015
comment
Хорошо, после вашего комментария я нашел тот же ответ, который дал Хупье всего несколько минут назад, раньше я пропустил эту часть jls. В любом случае, это непроверенный бросок, основанный на 5.5.2. Однако я по-прежнему считаю, что использование такого рода приведения не является хорошей практикой. - person pifta; 03.12.2015

Конечно, невозможно дать «более правильный» (correctier?) ответ, чем тот, который объясняет наблюдаемое поведение с точки зрения Спецификации языка 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<>();

person Mike Nakis    schedule 04.12.2015
comment
Но бросок на самом деле не от Map<?,Integer> до Map<String,Integer> (что в большинстве случаев соответствует адресу), а от Map<?,Integer> до StringMap<Integer>. Это делает вопрос гораздо более интересным, потому что если объект является StringMap (проверка, которую можно выполнить во время выполнения), то он должен быть Map<String,...>. Так что, интуитивно, актерский состав вовсе не непроверенный. - person Hoopje; 05.12.2015