地图的原因是什么?get(对象键)不是(完全)通用的

决定不使用完全泛型的get方法的原因是什么

.在java.util.Map<K, V>接口中

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

V get(Object key)

而不是

V get(K key)

我想知道为什么(remove, containsKey, containsValue也是如此)。

61646 次浏览

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

合同的表述如下:

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

(我的重点)

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

谷歌的一名出色的Java程序员Kevin Bourrillion不久前在博客中写了这个问题(诚然是在Set而不是Map的上下文中)。最相关的句子:

统一为Java的方法 Collections框架(和谷歌. Collections框架) 收藏图书馆也是)从不 限制其参数的类型 除非是为了预防

我不完全确定我是否同意这一原则——例如,. net似乎需要正确的键类型就可以了——但值得遵循博客文章中的推理。(在提到。net之后,值得解释的是,为什么它在。net中不是问题的部分原因是。net中存在更有限方差的更大的问题…)

原因是容器是由equalshashCode决定的,它们是Object上的方法,并且都带有Object参数。这是Java标准库的早期设计缺陷。加上Java类型系统中的限制,它强制依赖于equals和hashCode的任何东西都采用Object

在Java中拥有类型安全哈希表和相等性的唯一方法是避免Object.equalsObject.hashCode,并使用通用的替代品。Object.hashCode0的类型类就是为了这个目的:Object.hashCode1和Object.hashCode2。提供了Object.hashCode3的包装器,在其构造函数中接受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

正如其他人所提到的,get()等不是泛型的原因是,你正在检索的项的键不一定与你传递给get()的对象的类型相同;该方法的规范只要求它们相等。这源于equals()方法如何接受Object作为参数,而不仅仅是与对象相同的类型。

虽然很多类都定义了equals(),使得它的对象只能等于它自己类的对象,但在Java中有很多地方不是这样的。例如,List.equals()的规范说,如果两个List对象都是List并且具有相同的内容,则它们是相等的,即使它们是List的不同实现。所以回到这个问题中的例子,根据方法的规范,可以有一个Map<ArrayList, Something>,我可以用LinkedList作为参数调用get(),它应该检索具有相同内容的列表的键。如果get()是泛型的并且限制了它的参数类型,这是不可能的。

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

"你需要确保通用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是a
  • <李>它是完美的 使用containsAll()调用是合法的 不同类型的集合。的 例程应该工作,返回false。”李< / >

这是__abc“在你做的事情上要保守,在你接受别人的东西上要自由”的应用。

无论类型如何,都可以执行相等性检查;equals方法定义在Object类上,并接受任何Object作为参数。因此,键等价性和基于键等价性的操作接受任何Object类型是有意义的。

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

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

Java具有像<? extends SomeClass>这样的多态泛型结构。有标记的此类引用可以指向带有<AnySubclassOfSomeClass>符号的类型。但多态泛型使该引用只读的。编译器允许你只使用泛型类型作为方法的返回类型(就像简单的getter),但是阻止使用泛型类型为参数的方法(就像普通的setter)。 这意味着如果你写了Map<? extends KeyType, ValueType>,编译器不允许你调用方法get(<? extends KeyType>),并且映射将是无用的。唯一的解决方案是使这个方法不是泛型的:get(Object).

我看着这个,想着他们为什么要这样做。我认为现有的任何答案都不能解释为什么他们不能让新的泛型接口只接受键的正确类型。真正的原因是,即使他们引入了泛型,他们也没有创建一个新的接口。Map接口是相同的旧的非泛型Map,它只是作为泛型和非泛型版本。这样,如果你有一个接受非泛型Map的方法,你可以给它传递Map<String, Customer>,它仍然可以工作。同时,get的契约接受Object,所以新的接口也应该支持这个契约。

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

我们正在做大的重构,我们错过了这个强类型的get(),以检查我们是否错过了旧类型的get()。

但是我发现了编译时间检查的变通/丑陋的技巧:创建Map接口,强类型get, containsKey, remove…然后放到java中。您的项目的Util包。

你只调用get()就会得到编译错误,…对于错误的类型,其他的编译器似乎都可以(至少在eclipse kepler内部)。

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

兼容性。

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

如果他们改变了这个方法来获得(<K> o),这可能会迫使java用户进行大量的代码维护,只是为了让工作代码重新编译。

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

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