【问题标题】:Why does Java Map<K, V> take an untyped parameter for the get and remove methods?为什么 Java Map<K, V> 为 get 和 remove 方法采用无类型参数?
【发布时间】:2010-11-24 16:35:27
【问题描述】:

我在代码中遇到了一个错误,我使用错误的键从 Java 映射中获取某些内容,我认为该映射是使用 Java 泛型进行强类型化的。查看 Map Javadocs 时,包括 get 和 remove 在内的许多方法都将 Object 作为参数而不是类型 K(对于定义为 Map 的 Map)。为什么是这样?有充分的理由还是 API 设计缺陷?

【问题讨论】:

标签: java api generics


【解决方案1】:

我认为这是为了向后兼容旧版本的 Map 界面。不幸的是,情况确实如此,但是您是对的,如果采用正确的类型会更好。

【讨论】:

  • 这些方法不能将 K 作为泛型类型,因为 K 不涵盖地图的所有有效键类型(请参阅我的答案)。
  • -1:如果强制搜索键与地图键的类型相同,不会更好 - 这是一个常见的用例,但肯定不是只有一个。我已经实现了具有复杂键的映射,并且可以采用简单的字符串或其他降低复杂度的键进行搜索。
  • 哇!我强烈不同意这个理由 - 我认为如果您想要一个可以将不同类型用作有效键的用例,那么创建限制使用的覆盖或额外方法。这样,您就有了一个强类型、清晰记录的界面,指示了预期的用途。任何使用 Object 参数的泛型都表明 IMO 设计不佳。这种设计让自己面临潜在的危险情况。
【解决方案2】:

因为如果传递给 get 方法的对象等于映射中存储的任何键,则映射将返回一个值。 Equal 并不意味着它们必须是相同的类型,而是键的和传递的对象的 equal 方法以这样一种方式实现,即不同的对象类型相互识别为相等。

当然,这同样适用于 remove 方法。


有效代码示例,如果 get 方法只允许 K 类型的参数,则会中断(不编译):

LinkedList<Number> k1 = new LinkedList<Number>();
k1.add(10);

ArrayList<Integer> k2 = new ArrayList<Integer>();
k2.add(10);

Map<LinkedList<Number>, String> map = new HashMap<LinkedList<Number>, String>();

map.put(k1, "foo");
System.out.println(map.get(k2));

【讨论】:

  • 虽然这在理论上可能是正确的,但使用 equals() 和 hashcode() 实现以这种方式创建多个类将是一个极其丑陋的黑客行为。对我来说,这些方法被定义为将对象作为参数似乎不太可能只是为了让人们可以编写这样的代码。
  • @Luke Hutterman:查看 List 接口的 equals 方法的文档。它的合同规定,如果两个列表都是列表并且它们的元素以相同的顺序相同,则无论列表的类别如何,它们都应该相等。与 Set 和 Map 接口相同
  • 即使您认为它很丑,这正是 API 文档中定义 Map 接口和 Object#equals 方法的方式。在向 Map 接口添加泛型时,必须注意不要改变接口方法的定义行为。强制 get 方法的参数实际上与泛型 K 类型的类型相同,这将是对接口定义的非向后兼容更改。
  • @newacct:在该示例中,这些值具有相同的类型 - java.util.List - 即使不是同一个类。这足以使用更严格约束的获取。你可以想出更多奇怪的场景,其中有效键不共享比 Object 更具体的类型,但我无法想象它们是我希望成为可能的场景!
  • @newacct: 但是如果你声明一个 Map,你可以使用任何类型的 List。只有当有人声明 Map 并随后尝试使用 LinkedList 调用 get() 时,您的示例(不同 List 实现之间的相等性)才会成为问题;再次,一个非常丑陋的黑客。
【解决方案3】:

这样做是为了如果类型参数是通配符,仍然可以调用这些方法。

如果您有Map&lt;?, ?&gt;,Java 将不允许您调用任何以泛型类型作为参数声明的方法。这可以防止您违反类型约束,例如,您不能使用错误的类型调用 put(key, value)

如果get() 被定义为get(K key) 而不是当前的get(Object key),它也会因为同样的规则而被排除。这将使通配符地图实际上无法使用。

理论上,这同样适用于remove(),因为删除对象也永远不会违反类型约束。

这是一个如果 get 被声明为 get(T key) 时不可能出现的代码示例:

public static <K,V> Map<K, V> intersect(Map<? extends K, ? extends V> m1, Map<? extends K, ? extends V> m2) {
    Map<K,V> result = new HashMap<K, V>();
    for (Map.Entry<? extends K, ? extends V> e1 : m1.entrySet()) {
        V value = m2.get(e1.getKey()); // this would not work in case of Map.get(K key)
        if (e1.getValue().equals(value)) {
            result.put(e1.getKey(), e1.getValue());
        }
    }
    return result;
}

e1.getKey() 返回某个未知子类型 Km1 使用的子类型)的对象,但 m2 使用可能不同的 K 子类型。如果 Map.get() 被声明为 get(K key),则不允许这种用法。

【讨论】:

  • 您可以使用通配符解决这个问题,方法是将变量推入具有自己类型参数的方法中 - 例如&lt;T&gt; void duplicateFirst(List&lt;T&gt; l) {T first = l.get(0); l.add(0, first);}。但在这种情况下,我很难理解为什么 Map, ?> 实际上并不是毫无用处。
  • Map&lt;?,?&gt; 可能相对没用,但同样的论点适用于例如Map&lt;? extends Number, String&gt;。但主要的一点是,与put&lt;K,V&gt; 不同,get() 永远不会违反在您的集合上声明的类型约束。
猜你喜欢
  • 1970-01-01
  • 2011-12-02
  • 2019-01-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-11-01
  • 2021-10-24
  • 2018-11-23
相关资源
最近更新 更多