未经检查转换为通用 class 实现 Map<String, V>

Unchecked cast to generic class implementing 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 演员表的完整警告:

Type safety: Unchecked cast from Map<capture#3-of ?,Integer> to StringMap<Integer>

然而,StringMap class 声明指定了 Map 的第一个类型参数(即 String),map3StringMap<Integer> cast 使用与 Map 的第二个类型参数相同的类型(即 Integer)。据我了解,只要演员表不抛出 ClassCastException(而且不应该抛出 instanceof 检查),stringMap3 将是有效的 Map<String, Integer>.

这是 Java 编译器的限制吗?或者是否存在这样一种情况,如果忽略警告,使用某些参数调用 map3 或 stringMap3 的方法可能会导致意外 ClassCastException

此演员表不安全。假设您有:

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

这将引发异常。您知道自己新建了一个 StringMap<Integer> 并将其分配给 map3 并不重要。您正在做的事情被称为向下转换 Downcasting in Java 了解更多信息。

编辑:您还使所有泛型的问题变得过于复杂,如果没有任何泛型类型,您将遇到完全相同的问题。

实际上答案在 Java 语言规范中。 Section 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

行为符合规定。在 Section 5.5.2 of the Java Language Specification 中,未经检查的转换定义为:

A cast from a type S to a parameterized type T is unchecked unless at least one of the following is true:

  • S <: T

  • All of the type arguments of T are unbounded wildcards

  • T <: S and S has no subtype X other than T where the type arguments of X are not contained in the type arguments of T.

(其中 A <: B 表示:“AB 的子类型”)。

在您的第一个示例中,目标类型没有通配符(因此它们都是无界的)。在您的第二个示例中,StringMap<Integer> 实际上是 Map<String, Integer> 的子类型(并且没有第三个条件中提到的子类型 X)。

然而,在您的第三个示例中,您有一个从 Map<?, Integer>StringMap<Integer> 的转换,并且由于通配符 ?,两者都不是另一个的子类型。另外,显然,并非所有类型参数都是无限通配符,因此 none 条件适用:这是一个未经检查的异常。

如果代码中出现未经检查的转换,则需要符合规范的 Java 编译器发出警告。

像您一样,我没有看到转换无效的任何情况,因此您可以争辩说这是 Java 编译器的限制,但至少它是一个指定的限制。

当然,不可能提供 "more correct"(更正确?)的答案57=] 语言规范。然而:

  • 用实际术语给出的对观察到的行为的解释比直接向您抛出 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<>();

相同的结果