在Java中转换为泛型类型不会引发ClassCastException?

时间:2010-05-04 16:45:37

标签: java generics casting

我遇到了一个奇怪的Java行为,看起来像个bug。是吗?即使对象不是K的实例,将对象转换为泛型类型(例如,ClassCastException)也不会抛出K。这是一个例子:

import java.util.*;
public final class Test {
  private static<K,V> void addToMap(Map<K,V> map, Object ... vals) {
    for(int i = 0; i < vals.length; i += 2)
      map.put((K)vals[i], (V)vals[i+1]); //Never throws ClassCastException!
  }
  public static void main(String[] args) {
    Map<String,Integer> m = new HashMap<String,Integer>();
    addToMap(m, "hello", "world"); //No exception
    System.out.println(m.get("hello")); //Prints "world", which is NOT an Integer!!
  }
}

更新:感谢cletus和Andrzej Doyle提供的有用答案。由于我只接受一个,我接受Andrzej Doyle's answer,因为它让我找到了一个我认为不是坏的解决方案。我认为这是一种在单行中初始化小地图的更好方法。

  /**
   * Creates a map with given keys/values.
   * 
   * @param keysVals Must be a list of alternating key, value, key, value, etc.
   * @throws ClassCastException if provided keys/values are not the proper class.
   * @throws IllegalArgumentException if keysVals has odd length (more keys than values).
   */
  public static<K,V> Map<K,V> build(Class<K> keyClass, Class<V> valClass, Object ... keysVals)
  {
    if(keysVals.length % 2 != 0)
      throw new IllegalArgumentException("Number of keys is greater than number of values.");

    Map<K,V> map = new HashMap<K,V>();
    for(int i = 0; i < keysVals.length; i += 2)
      map.put(keyClass.cast(keysVals[i]), valClass.cast(keysVals[i+1]));

    return map;
  }

然后你这样称呼它:

Map<String,Number> m = MapBuilder.build(String.class, Number.class, "L", 11, "W", 17, "H", 0.001);

5 个答案:

答案 0 :(得分:9)

Java泛型使用类型擦除,这意味着那些参数化类型不会在运行时保留,因此这是完全合法的:

List<String> list = new ArrayList<String>();
list.put("abcd");
List<Integer> list2 = (List<Integer>)list;
list2.add(3);

因为编译后的字节码看起来更像是这样:

List list = new ArrayList();
list.put("abcd");
List list2 = list;
list2.add(3); // auto-boxed to new Integer(3)

Java泛型只是在转换Object时的语法糖。

答案 1 :(得分:2)

正如cletus所说,擦除意味着你无法在运行时检查这一点(并且由于你的转换,你无法在编译时检查它。)

请记住,泛型是仅限编译时的功能。集合对象没有任何通用参数,只有您为该对象创建的引用。这就是为什么如果您需要从原始类型甚至Object向下转换集合,您会收到很多关于“未经检查的强制转换”的警告 - 因为编译器无法验证对象是否正确泛型类型(因为对象本身没有泛型类型)。

另外,请记住铸造意味着什么 - 它是告诉编译器的一种方式“我知道你不一定能检查类型匹配,但信任我,我知道他们这样做” 。当你覆盖类型检查(错误)然后最终导致类型不匹配时,谁会受到责备? ; - )

似乎您的问题在于缺乏异构的通用数据结构。我建议您的方法的类型签名应该更像private static<K,V> void addToMap(Map<K,V> map, List<Pair<K, V>> vals),但我不相信会让你真正得到任何东西。对列表基本上是一个映射,因此构造类型安全vals参数以调用该方法将与直接填充映射一样多。

如果您真的,那么希望保持您的课程大致原样但添加运行时类型安全性,或许以下内容会给您一些想法:

private static<K,V> void addToMap(Map<K,V> map, Object ... vals, Class<K> keyClass, Class<V> valueClass) {
  for(int i = 0; i < vals.length; i += 2) {
    if (!keyClass.isAssignableFrom(vals[i])) {
      throw new ClassCastException("wrong key type: " + vals[i].getClass());
    }
    if (!valueClass.isAssignableFrom(vals[i+1])) {
      throw new ClassCastException("wrong value type: " + vals[i+1].getClass());
    }

    map.put((K)vals[i], (V)vals[i+1]); //Never throws ClassCastException!
  }
}

答案 2 :(得分:1)

Java的泛型是使用“类型擦除”完成的,这意味着在运行时,代码不知道你有一个Map&lt; String,Integer&gt; - 它只是看到一张地图。因为你正在将这些东西转换为Objects(通过你的addToMap函数的参数列表),所以在编译时,代码“看起来正确”。它在编译时不会尝试运行这些东西。

如果您在编译时关心类型,请不要将它们称为Object。 :)使你的addToMap函数看起来像

private static<K,V> void addToMap(Map<K, V> map, K key, V value) {

如果要在地图中插入多个项目,则需要创建类似java.util的Map.Entry类,并将键/值对包装在该类的实例中。

答案 3 :(得分:1)

这是对Generics在Java中做什么和不做什么的一个相当好的解释: http://www.angelikalanger.com/GenericsFAQ/FAQSections/ParameterizedTypes.html

与C#非常不同!

我认为你试图做这样的事情?如果您要添加到地图的对的编译时安全性,那么:

addToMap(new HashMap<String, Integer>(), new Entry<String,Integer>("FOO", 2), new Entry<String, Integer>("BAR", 8));

    public static<K,V> void addToMap(Map<K,V> map, Entry<K,V>... entries) {
        for (Entry<K,V> entry: entries) {
            map.put(entry.getKey(), entry.getValue());
        }
    }

    public static class Entry<K,V> {

        private K key;
        private V value;

        public Entry(K key,V value) {
            this.key = key;
            this.value = value;
        }

        public K getKey() {
            return key;
        }

        public V getValue() {
            return value;
        }
    }

评论后编辑:

啊,那么也许你真正想要的就是用这种神秘的语法来骚扰那些刚刚转向Java的新员工。

Map<String, Integer> map = new HashMap<String,Integer>() {{
  put("Foo", 1);
  put("Bar", 2);
}};

答案 4 :(得分:0)

Java泛型仅适用于编译期间而非运行时。 问题是你实现的方式,java编译器没有机会在编译时确保类型安全。

由于你的K,V从不说它们扩展任何特定的类,在编译期间java无法知道它应该是一个整数。

如果您按以下方式更改代码

private static void addToMap(Map map,Object ... vals)

它会给你一个编译时错误