【问题标题】:Generics Oddity - I can insert a Long value into a Map<String, String> and it compiles and doesn't fail at runtimeGenerics Oddity - 我可以将 Long 值插入 Map<String, String> 并且它可以编译并且在运行时不会失败
【发布时间】:2013-05-17 14:04:49
【问题描述】:

给出以下代码:

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 的转换。如果该方法将按如下方式实现,则运行时转换将发生并且它将在 put 行上失败(如预期的那样)。谁能证实这一点?

class TestMethodHolder {
        String getValue(Map<String, Object> dataMap, String value) {
            return (String)dataMap.get(value);
        }
    }

这是使用泛型的已知缺陷或奇怪之处吗?那么在调用方法时使用 表示法是不好的做法吗?

编辑: 我使用的是默认的 Oracle JDK 1.7_03。

上面的另一个隐含问题:原始 getValue 中的转换是否仍然在运行时发生,但转换实际上是针对 Object 的 - 或者编译器是否足够聪明,可以完全避免在运行时发生这种转换?这可能解释了人们在运行 ClassCastException 时注意到的发生位置的差异。

【问题讨论】:

  • 它编译并运行,因为类型擦除会在运行时使其成为Map&lt;Object, Object&gt;。当然,您会得到一个ClassCastException,因为您将Long 视为String,这是错误的。
  • @LuiggiMendoza 我认为 OP 知道这一点 - 请参阅他的“有根据的猜测”部分。
  • 您在return (T)dataMap.get(value); 中有一个未经检查的从 Object 到 T 的强制转换。你在这里推翻了编译器。
  • @ZiyaoWei 我猜这个例子不是最正确的,因为OP使用Object并且任何对象引用都可以作为Object。此外,在运行时,代码将作为hashMap.put("test", someObject) 执行,这里没有强类型,因为类型擦除会将其视为HashMap&lt;Object, Object&gt;,在尝试将其检索为String 时显然会失败,因为它的真实类型是Long.
  • 我当然不会称其为“缺陷”......显然这应该被认为是不安全的。

标签: java generics


【解决方案1】:

线

return (T)dataMap.get(value);

会生成一个未经检查的强制转换警告,并且根据规范,任何此类警告的存在都会使您的代码类型不安全。 ClassCastException 在您第一次尝试将类型不安全的结果分配给错误类型的变量时出现,因为这是编译后的代码第一次进行类型检查。

请注意,Eclipse 的编译器插入的类型检查比 JLS 要求的要多,因此,如果您在 Eclipse 中编译,hashMap.put 调用将失败并显示CCE。编译器知道此调用必须有两个 String 参数,因此可以在实际方法调用之前插入类型检查。

正如您所猜测的那样,如果您将通用的 T 替换为特定的 String,则类型检查会在该点进行,但会失败。

【讨论】:

  • 抱歉,误读了 OP。以为他们是在暗示 CCE 出现在你引用的那一行(在T 的演员表上),这与它是未经检查的演员表相反,这实际上会令人惊讶。
  • @MarkPeters 我会改进它以涵盖这两种行为。
  • @Marko Topolnik +1 关于 eclipse 编译器的很好的解释。
【解决方案2】:

编译器依赖于类型安全来做出假设并进行转换/优化。不幸的是,类型安全可以通过未经检查的强制转换来破坏。如果你的程序包含不正确的未经检查的强制转换,那么编译器应该做什么就不清楚了。理想情况下,它应该在未检查强制转换的确切点进行运行时检查,在您的示例中,当Object 被强制转换为T 时。但这是不可能的,因为擦除不完全是类型系统的一部分。

在您的示例中的其他任何地方,类型都是正确的,因此编译器可以假定 getValue() 确实返回 String,没有必要仔细检查。但是进行检查也是合法的,就像 Eclipse 编译器所做的那样(可能是因为它将返回值分配给了 String 本地临时变量)。

所以坏消息是,如果你的程序包含不正确的未经检查的强制转换,它的行为是未定义的......所以通过严格的推理确保你所有的未经检查的强制转换都是正确的。

一个好的做法是检查所有未检查的强制转换,以便您可以合法地抑制未检查的警告。例如

        <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;
        }

查看我对类似问题的回答:Lazy class cast in Java?

【讨论】:

  • 在实际代码中,您可以使用更短的解决方案:Object value = dataMap.get(name); return type.cast(value);
【解决方案3】:

此类型信息在编译期间被删除(请参阅Neal Gafter's "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

【讨论】:

    猜你喜欢
    • 2010-12-28
    • 1970-01-01
    • 2014-05-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-05-19
    • 1970-01-01
    相关资源
    最近更新 更多