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


405

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

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

V get(Object key)

代替

V get(K key)

我想知道为什么(与相同remove, containsKey, containsValue)。




1
惊人。我使用Java已有20多年了,今天我意识到了这个问题。
GhostCat

Answers:


260

正如其他人所提到的,之类的原因get()不是通用的,因为您要检索的条目的键不必与您传递给的对象的类型相同get();方法的规范仅要求它们相等。这从equals()方法如何将对象作为参数(而不仅仅是与对象相同的类型)中得出。

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


28
那为什么V Get(K k)在C#中呢?

134
问题是,如果要调用m.get(linkedList),为什么不将m类型定义为Map<List,Something>?我想不出一个用例,在m.get(HappensToBeEqual)不更改Map类型以获取接口的情况下进行调用是有意义的。
Elazar Leibovich

58
哇,严重的设计缺陷。您也不会收到编译器警告,这很麻烦。我同意Elazar。如果这真的有用,我怀疑这种情况经常发生,那么getByEquals(Object key)听起来更合理……
mmm

37
这个决定似乎是根据理论上的纯正性而不是实用性做出的。对于大多数用法,开发人员宁愿看到参数受模板类型的限制,而不是使其不受限制以支持诸如newacct在其回答中提到的边缘情况。留下非模板签名会带来更多无法解决的问题。
Sam Goldberg 2013年

14
@newacct:“完全类型安全”是一种在运行时可能会意外失败的构造的强烈主张。不要将您的视图范围缩小到适用于此的哈希映射。TreeMap当您将错误类型的对象传递给get方法时,可能会失败,但有时会失败,例如,当地图碰巧为空时。甚至更糟的是,如果提供Comparator了该compare方法(具有通用签名!),则可能会使用错误类型的参数来调用该方法,而不会发出任何未经检查的警告。这坏行为。
Holger 2014年

105

Google的一位出色的Java程序员Kevin Bourrillion 在不久前的一篇博客文章中(关于Set而不是Map)就此问题进行了准确的撰写。最相关的句子:

一致地,除非有必要防止集合损坏,否则Java Collections Framework(以及Google Collections Library)的方法从不限制其参数类型。

我不能完全确定我是否同意它是一个原则-例如,.NET似乎要求正确的密钥类型就可以了-但值得遵循博客文章中的推理。(提到.NET时,值得解释的是,.NET中不存在问题的部分原因是,.NET中存在更大的问题,而方差有限。)


4
Apocalisp:事实并非如此,情况仍然一样。
凯文·布兰里恩

9
@ user102008不,帖子没有错。即使an Integer和a Double永远不能彼此相等,但询问a是否Set<? extends Number>包含值仍然是一个公平的问题new Integer(5)
凯文·布兰里恩

33
我从来没有想过查看的会员资格Set<? extends Foo>。我经常更改地图的键类型,然后感到沮丧的是,编译器无法找到需要更新代码的所有位置。我真的不相信这是正确的权衡。
Porculus 2012年

4
@EarthEngine:总是坏了。这就是重点-代码已损坏,但编译器无法捕获。
乔恩·斯基特

1
而且它仍然坏掉了,只是给我们带来了一个错误。
GhostCat

28

合同表示如下:

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

(我的重点)

因此,成功的键查找取决于输入键对等式方法的实现。那不一定取决于k的类别。


4
它也取决于hashCode()。如果没有正确实现hashCode(),那么equals()在这种情况下,很好的实现就毫无用处。
鲁道夫森,2009年

5
我想,原则上,如果重新创建整个密钥是不切实际的,那么只要您正确实现equals()和hashCode(),就可以使用轻量级代理作为密钥。
比尔·米歇尔

5
@rudolfson:据我所知,只有HashMap依赖于哈希码来找到正确的存储桶。例如,TreeMap使用二进制搜索树,并且不关心hashCode()。
罗布

4
严格来说,get()不需要采用类型的参数Object即可满足联系。假设get方法被限制为键类型K-合同仍然有效。当然,编译时间类型不是其子类的使用K现在将无法编译,但这不会使合同无效,因为合同隐式地讨论了代码编译时会发生什么。
BeeOnRope

16

这是Postel法则的应用 “对您的工作要保守,对别人接受的东西要开放。”

不论类型如何,都可以执行相等性检查。该equals方法在Object类上定义,并且可以接受任何Object参数。因此,对于密钥等效性以及基于密钥等效性的操作来说,接受任何Object类型。

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


4
那为什么V Get(K k)在C#中呢?

1
V Get(K k)在C#中,因为它也很有意义。Java和.NET方法之间的区别实际上仅是谁阻止了不匹配的东西。在C#中是编译器,在Java中是集合。大约在同时,.NET的不一致的集合类,一旦我的愤怒,但Get()Remove()只接受一个匹配的类型肯定可以防止您在无意中传递一个错误的值。
Wormbo

26
这是对Postel法则的误用。在接受别人的态度上要开放,但不要过于开放。这个愚蠢的API意味着您无法分辨“不在集合中”和“您犯了静态键入错误”之间的区别。使用get:K-> boolean可以避免成千上万的程序员时间浪费。
法官Mental

1
当然应该如此contains : K -> boolean
Mental法官2014年


13

我认为“泛型教程”的这一部分解释了这种情况(我的重点是):

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

interface Collection { 
  public boolean containsAll(Collection c);
  ...
}

天真地夸大它的尝试是:

interface Collection<E> { 
  public boolean containsAll(Collection<E> c);
  ...
}

尽管这肯定是类型安全的,但它不符合API的原始合同。 containsAll()方法可用于任何类型的传入集合。仅当传入集合实际上仅包含E实例时,它才会成功,但是:

  • 传入集合的静态类型可能有所不同,可能是因为调用者不知道传入的集合的确切类型,或者可能是因为它是Collection <S>,其中S是E的子类型。
  • 用其他类型的集合调用containsAll()是完全合法的。该例程应该工作,并返回false。”

2
那为什么不containsAll( Collection< ? extends E > c )呢?
法官Mental

1
@JudgeMental,虽然没有作为一个例子给出上述还必须允许containsAllCollection<S>其中S是一个超型E。如果允许的话,这是不允许的containsAll( Collection< ? extends E > c )。此外,作为在本例中明确指出,这是合法的传递不同类型的集合(有则返回值是false)。
davmac 2014年

不必让containsAll带有E的超类型的集合。我认为有必要使用静态类型检查来禁止该调用以防止错误。这是一个愚蠢的合同,我认为这是原始问题的重点。
Mental法官

6

原因是遏制是由方法决定的equalshashCode哪种方法Object都采用Object参数。这是Java标准库中的早期设计缺陷。再加上Java的类型系统中的限制,它迫使任何依赖于equals和hashCode的值接受Object

在Java中具有类型安全的哈希表和相等性的唯一方法是避免 Object.equalsObject.hashCode和使用一个通用的替代品。为此,功能Java随附类型类:Hash<A>Equal<A>HashMap<K, V>提供了for的包装,该包装采用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

4
这本身并不能阻止将“ get”的类型声明为“ V get(K key)”,因为“ Object”始终是K的祖先,因此“ key.hashCode()”仍然有效。
finnw

1
虽然它不能阻止它,但我认为它可以解释它。如果他们切换了equals方法来强制类相等,那么当这些方法的方法原型不兼容时,他们当然无法告诉人们在地图中定位对象的底层机制利用了equals()和hashmap()。
cgp 2012年

5

兼容性。

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

如果他们将这种方法更改为get(<K> o),可能会迫使对Java用户的大规模代码维护只是为了使工作代码再次编译。

他们本可以引入一种方法,例如说get_checked(<K> o)并弃用旧的get()方法,以便有一条更平缓的过渡路径。但是由于某种原因,这没有完成。(现在的情况是,您需要安装诸如findBugs之类的工具,以检查get()参数与地图的声明键类型<K>之间的类型兼容性。)

我认为与.equals()的语义有关的参数是虚假的。(从技术上讲,它们是正确的,但我仍然认为它们是虚假的。如果o1和o2没有任何共同的超类,那么在他的脑海中就不会有任何设计师可以使o1.equals(o2)为真。)


4

还有一个重要的原因,因为它破坏了Map,因此在技术上无法完成。

Java具有类似的多态通用构造<? extends SomeClass>。标记为此类引用可以指向使用签名的类型<AnySubclassOfSomeClass>。但是多态泛型使该引用变为只读。编译器仅允许您将泛型类型用作方法的返回类型(例如简单的getter),但会阻止使用泛型为参数的方法(例如普通的setter)。这意味着如果您编写Map<? extends KeyType, ValueType>,编译器将不允许您调用method get(<? extends KeyType>),并且映射将无用。唯一的解决方案是使此方法不通用:get(Object)


为什么set方法是强类型的?
圣淘沙

如果您的意思是'put':put()方法将更改map,它将不适用于<?扩展SomeClass>。如果调用它,则会出现编译异常。这样的地图将是“只读的”
Owheee

1

我想是向后兼容。Map(或HashMap)仍然需要支持get(Object)


13
但是可以使用相同的参数put(这确实限制了泛型类型)。通过使用原始类型,您可以获得向后兼容性。泛型是“选择加入”。
Thilo

我个人认为,做出此设计决定的最可能原因是向后兼容。
geekdenz's

1

我看着这个,想着为什么他们要这样做。我不认为任何现有答案都可以解释为什么它们不能仅使新的通用接口仅接受密钥的正确类型。实际原因是,即使他们引入了泛型,他们也没有创建新接口。Map接口与旧的非通用Map相同,仅用作通用和非通用版本。这样,如果您有一个接受非泛型Map的方法,则可以将其传递给a Map<String, Customer>,它仍然可以工作。同时,获取合同接受对象,因此新接口也应支持该合同。

在我看来,他们应该添加一个新接口并在现有集合上都实现,但是他们决定支持兼容接口,即使这意味着对get方法进行更糟糕的设计。请注意,集合本身将与现有方法兼容,仅接口不兼容。


0

我们现在正在进行大型重构,并且缺少此强类型的get()来检查我们是否没有丢失旧类型的某些get()。

但是我发现了一种用于编译时间检查的解决方法/难看技巧:使用强类型的get,containsKey,remove ...创建Map接口,并将其放入项目的java.util包中。

您将因为仅使用错误的类型调用get()而得到编译错误,...对于编译器而言,其他一切似乎都正常(至少在eclipse kepler内部)。

检查构建后不要忘记删除此接口,因为这不是运行时所需的。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.