Map.get(Object key)不是(完全)泛型的原因是什么

WMR*_*WMR 395 java generics collections map

什么是决定不具有的接口完全通用的get方法背后的原因java.util.Map<K, V>.

为了澄清这个问题,方法的签名是

V get(Object key)

代替

V get(K key)

我想知道为什么(同样的事情remove, containsKey, containsValue).

new*_*cct 258

正如其他人所提到的,之所以get()等等不是通用的,因为您要检索的条目的键不必与传入的对象的类型相同get(); 方法的规范只要求它们相等.这取决于该equals()方法如何将Object作为参数,而不仅仅是与对象相同的类型.

尽管通常equals()可以确定许多类已定义,以使其对象只能等于其自己的类的对象,但Java中有许多地方并非如此.例如,List.equals()如果两个List对象都是Lists并且具有相同的内容,则表示两个List对象相等的规范,即使它们是不同的实现List.所以回到这个问题的例子,根据方法的规范可以有一个Map<ArrayList, Something>和我get()LinkedListas参数调用,它应该检索具有相同内容的列表的键.如果get()是通用的并且限制其参数类型,则这是不可能的.

  • 问题是,如果你想调用`m.get(linkedList)`,为什么不把`m`的类型定义为`Map <List,Something>`?我想不出一个用例,在不改变`Map`类型的情况下调用`m.get(HappensToBeEqual)`来获取接口是有意义的. (133认同)
  • 哇,严重的设计缺陷.你也没有编译警告,搞砸了.我同意埃拉扎尔的观点.如果这确实很有用,我怀疑经常发生这种情况,那么getByEquals(Object key)听起来更合理...... (57认同)
  • 这个决定似乎是在理论纯度而非实用性的基础上做出的.对于大多数用法,开发人员更愿意看到受模板类型限制的参数,而不是无限制地支持边缘情况,例如他的答案中newacct提到的边缘情况.离开非模板化签名会产生比解决方案更多的问题. (35认同)
  • 那么为什么C#中的'V Get(K k)`? (27认同)
  • @newacct:"完全类型安全"是对构造的强烈要求,它可能在运行时无法预测地失败.不要将您的视图缩小到恰好与之相关的哈希映射.当您将错误类型的对象传递给`get`方法时,`TreeMap`可能会失败,但偶尔会传递,例如当地图恰好为空时.更糟糕的是,如果提供了`Comparator`,`compare`方法(具有通用签名!)可能会使用错误类型的参数调用,而不会有任何未经检查的警告.这*是*破坏的行为. (14认同)
  • @Sam Goldberg,它让我觉得它最终以这种方式结束的原因是为了保持向后兼容性.这个方法是在Generics出现之前设计的,当复古适应时,Sun竭尽全力保持旧代码的运行方式与之前相同. (7认同)
  • 如果使用"EnumMap",事情会变得更加混乱.只能为其键类型接受"枚举"的地图.无法覆盖枚举的`equals(Object o)`方法,因此如果其中一个对象是"Enum",则来自不同类型的对象可能彼此相等的参数将失败.因此,对于"EnumMap",它不会要求用于创建"EnumMap"的特定"Enum"类型.但它实现了`Map`接口,所以我们也坚持使用`get(Object o)`方法. (4认同)
  • 我使用泛型的一个重要原因是类型安全.对我来说,这绝对是安全网的一个漏洞. (3认同)
  • 注意:HashMap.get()依赖于你所说的equals,但是TreeMap.get()依赖于a.compareTo(b)== 0,而不是equals/hashCode. (3认同)
  • 使用可变列表作为"Map"键听起来不是一个好主意(因为它们与其他对象的相等性以及它们的哈希码的值可以自由改变,这可能会导致意想不到的后果)而且我不这样做我认为应该在这里提供一个例子,说明为什么`Map#get`应该使用`equals`. (3认同)
  • @newacct对于这个实现来说不是*原因*,那就是问题*.丢失编译时间检查以满足可疑的设计案例.可能的原因是......"Map#get"方法早于泛型,它必须保持断开以保持向后兼容性.虽然同时,这对于`Map#put`也不是问题,它也曾经使用`Object`作为Java 5之前的参数类型.你的参数也可以应用于它,但它是通用的现在.这个案子有什么不同?我看待它的方式,你重申合同是什么,而不是解释原因. (3认同)
  • 不同的规格. (2认同)
  • +1以Elazar的评论。PLus,如果您真的想变得如此灵活,请使用Object作为K的类型。 (2认同)
  • @ mishal153:这是完全类型安全的.类型安全意味着没有可能失败的不安全转换.这不会添加任何演员阵容. (2认同)
  • @toniedzwiedz:那么你可以拥有不可变的列表。同样的问题也适用于不可变列表的不同实现。这并不能解释为什么“Map” API 使用 equals——这有很多原因,但这是一个单独的问题。这解释了,鉴于“Map” API 使用 equals,为什么“Map.get”采用“Object”。 (2认同)

Jon*_*eet 105

一个真棒的Java编码器在谷歌,凯文Bourrillion,写了一篇关于正是这个问题在博客文章(当然在的背景下,前一段时间Set,而不是Map).最相关的句子:

统一地,Java Collections Framework(以及Google Collections Library)的方法从不限制其参数的类型,除非必须防止集合被破坏.

我并不完全确定我同意它作为一个原则 - 例如,.NET似乎很好,需要正确的密钥类型 - 但值得遵循博客文章中的推理.(提到.NET之后,值得解释的是,在.NET中不存在问题的一部分原因是.NET中存在更大限制的方差问题......)

  • 我从来没有想过检查`Set <?的会员资格?扩展Foo>`.我经常更改地图的密钥类型,然后对编译器找不到代码需要更新的所有地方感到沮丧.我真的不相信这是正确的权衡. (32认同)
  • @ user102008不,帖子没错.即使一个"整数"和一个"双重"永远不能相等,但问一个"集合<?"仍然是一个公平的问题.extends Number>`包含值`new Integer(5)`. (9认同)
  • Apocalisp:事实并非如此,情况仍然相同. (4认同)
  • @EarthEngine:它总是被打破.这就是重点 - 代码被破坏了,但编译器无法捕获它. (4认同)

Bri*_*new 29

合同表达如下:

更正式地说,如果此映射包含从键k到值v的映射,使得(key == null?k == null: key.equals(k)),则此方法返回v; 否则返回null.(最多可以有一个这样的映射.)

(我的重点)

因此,成功的键查找取决于输入键的相等方法的实现.这不一定取决于k的类别.

  • 我想,原则上,如果正确实现equals()和hashCode(),这将允许您为密钥使用轻量级代理,如果重新创建整个密钥是不切实际的. (5认同)
  • @rudolfson:据我所知,只有HashMap依赖于哈希码才能找到正确的桶.例如,TreeMap使用二叉搜索树,而不关心hashCode(). (5认同)
  • 它还依赖于`hashCode()`.如果没有正确实现hashCode(),在这种情况下,一个很好实现的`equals()`就没用了. (4认同)
  • 严格地说,`get()`不需要采用类型为`Object`的参数来满足联系.想象一下,get方法仅限于键类型"K" - 合同仍然有效.当然,编译时类型不是`K`的子类的用法现在无法编译,但这不会使合同无效,因为合同隐含​​地讨论了代码编译时会发生什么. (4认同)

eri*_*son 17

这是Postel定律的一个应用, "在你所做的事情上要保守,在你接受别人的事情上保持自由."

无论何种类型,都可以进行平等检查; 该equals方法在Object类上定义,并接受any Object作为参数.因此,关键等价和基于键等价的操作接受任何Object类型都是有意义的.

当映射返回键值时,通过使用type参数,它可以保留尽可能多的类型信息.

  • 这是对Postel定律的错误应用.你从别人那里接受自由,但不要过于自由.这种愚蠢的API意味着你无法区分"不在集合中"和"你犯了一个静态输入错误".使用get:K - > boolean可以防止数千个丢失的程序员小时数. (24认同)
  • [Postel错了](https://tools.ietf.org/html/draft-thomson-postel-was-wrong-00). (4认同)
  • 那么为什么C#中的'V Get(K k)`? (3认同)
  • 在 C# 中它是“V Get(K k)”,因为它也有意义。Java 和 .NET 方法之间的区别实际上只是谁阻止了不匹配的内容。在 C# 中是编译器,在 Java 中是集合。我偶尔会对 .NET 不一致的集合类感到愤怒,但是仅接受匹配类型的“Get()”和“Remove()”肯定可以防止您意外地传递错误的值。 (2认同)

Yar*_*ena 12

我认为Generics Tutorial的这一部分解释了这种情况(我的重点):

"您需要确保通用API不会过度限制;它必须继续支持API的原始合同.再次考虑java.util.Collection中的一些示例.预通用API如下所示:

interface Collection { 
  public boolean containsAll(Collection c);
  ...
}
Run Code Online (Sandbox Code Playgroud)

一种天真的尝试,它是:

interface Collection<E> { 
  public boolean containsAll(Collection<E> c);
  ...
}
Run Code Online (Sandbox Code Playgroud)

虽然这当然是类型安全的,但它不符合API的原始合同. containsAll()方法适用于任何类型的传入集合.只有当传入的集合实际上只包含E的实例时,它才会成功,但是:

  • 传入集合的静态类型可能不同,可能是因为调用者不知道传入的集合的精确类型,或者可能是因为它是Collection <S>,其中S是E的子类型.
  • 使用不同类型的集合调用containsAll()是完全合法的.例程应该有效,返回错误."

  • 为什么不`containsAll(Collection <?extends E> c)`,那么? (2认同)

Apo*_*isp 6

原因是遏制是由方法决定的equals,hashCode哪些是方法Object,两者都是Object参数.这是Java标准库中的早期设计缺陷.再加上Java类型系统的局限性,它会强制依赖于equals和hashCode的任何东西Object.

有在Java的类型安全哈希表和平等的唯一办法是避开Object.equalsObject.hashCode和使用一个通用的替代品.功能Java为此提供了类型类:Hash<A>Equal<A>.一个包装的HashMap<K, V>规定,采用Hash<K>Equal<K>在其构造.因此,这个类getcontains方法采用类型的泛型参数K.

例:

HashMap<String, Integer> h =
  new HashMap<String, Integer>(Equal.stringEqual, Hash.stringHash);

h.add("one", 1);

h.get("one"); // All good

h.get(Integer.valueOf(1)); // Compiler error
Run Code Online (Sandbox Code Playgroud)

  • 这本身并不妨碍'get'的类型被声明为"V get(K key)",因为'Object'始终是K的祖先,所以"key.hashCode()"仍然有效. (3认同)

小智 5

还有一个更重要的原因,技术上做不到,因为它破坏了Map。

Java 有像<? extends SomeClass>. 标记的此类引用可以指向带有签名的类型<AnySubclassOfSomeClass>。但是多态泛型使该引用成为readonly。编译器只允许您使用泛型类型作为方法的返回类型(如简单的 getter),但会阻止使用泛型类型为参数的方法(如普通的 setter)。这意味着如果你写Map<? extends KeyType, ValueType>,编译器不允许你调用 method get(<? extends KeyType>),那么映射就没有用了。唯一的解决方案是使此方法不通用:get(Object).


Erw*_*out 5

兼容性。

在泛型可用之前,只有 get(Object o)。

如果他们将此方法更改为 get(<K> o) 它可能会迫使 Java 用户进行大量代码维护,只是为了再次编译工作代码。

他们本可以引入一个额外的方法,比如 get_checked(<K> o) 并弃用旧的 get() 方法,所以有一个更温和的过渡路径。但由于某种原因,这并没有完成。(我们现在的情况是你需要安装 findBugs 之类的工具来检查 get() 参数和映射的声明键类型 <K> 之间的类型兼容性。)

我认为与 .equals() 语义相关的论点是虚假的。(从技术上讲,它们是正确的,但我仍然认为它们是假的。如果 o1 和 o2 没有任何共同的超类,那么头脑正常的设计师不会让 o1.equals(o2) 为真。)

  • 但是 put(Object key, Object value) 方法也已更改为 put(K key, V value),没有问题! (2认同)