为了澄清这个问题,该方法的签名是
V get(Object key)
代替
V get(K key)
我想知道为什么(同样的事情remove, containsKey, containsValue
)。
为了澄清这个问题,该方法的签名是
V get(Object key)
代替
V get(K key)
我想知道为什么(同样的事情remove, containsKey, containsValue
)。
正如其他人所提到的,原因get()
等不是通用的,因为您要检索的条目的键不必与您传入的对象的类型相同get()
;该方法的规范只要求它们相等。这源于该equals()
方法如何将对象作为参数,而不仅仅是与对象相同的类型。
equals()
尽管许多类已经定义了它的对象只能等于它自己类的对象,这可能是普遍事实,但在 Java 中有很多地方并非如此。例如,规范List.equals()
说如果两个 List 对象都是 List 并且具有相同的内容,则它们是相等的,即使它们是不同的实现List
。所以回到这个问题的例子,根据方法的规范,Map<ArrayList, Something>
我可以get()
使用 aLinkedList
作为参数调用,它应该检索作为具有相同内容的列表的键。get()
如果是通用的并且限制了它的参数类型,这是不可能的。
Google 的一位出色的 Java 编码员 Kevin Bourrillion 不久前在一篇博文中写了关于这个问题的文章(诚然是在Set
代替的上下文中Map
)。最相关的一句话:
统一地,Java 集合框架(以及 Google 集合库)的方法从不限制其参数的类型,除非有必要防止集合被破坏。
我不完全确定我是否同意它作为一个原则 - 例如,.NET 似乎需要正确的密钥类型 - 但值得遵循博客文章中的推理。(在提到 .NET 之后,值得解释的是,它在 .NET 中不是问题的部分原因是 .NET 中存在更大的问题,即差异更有限......)
合同是这样表达的:
更正式地说,如果此映射包含从键 k 到值 v 的映射,使得 (key==null ? k==null : key.equals(k) ),则此方法返回 v;否则返回null。(最多可以有一个这样的映射。)
(我的重点)
因此,成功的键查找取决于输入键对相等方法的实现。这不一定取决于 k 的类别。
这是对Postel 定律的应用, “在你所做的事情上要保守,在你从别人那里接受的事情上要自由。”
无论类型如何,都可以进行平等检查;该equals
方法在Object
类上定义并接受任何Object
作为参数。Object
因此,密钥等价和基于密钥等价的操作接受任何类型都是有意义的。
当映射返回键值时,它会通过使用 type 参数尽可能多地保存类型信息。
我认为泛型教程的这一部分解释了这种情况(我的重点):
“您需要确保通用 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 的实例时,它才会成功,但是:
原因是包含由哪些方法决定equals
,hashCode
哪些是方法,Object
并且都带有一个Object
参数。这是 Java 标准库中的一个早期设计缺陷。再加上 Java 类型系统的限制,它强制所有依赖 equals 和 hashCode 的东西取Object
.
Object.equals
在 Java 中拥有类型安全哈希表和相等性的唯一方法是避免Object.hashCode
使用泛型替代品。函数式 Java带有类型类就是为了这个目的:Hash<A>
和Equal<A>
. 提供了一个包装器,它在其构造函数HashMap<K, V>
中接受Hash<K>
and 。Equal<K>
因此,此类get
和contains
方法采用 type 的通用参数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(Object o)。
如果他们将此方法更改为 get(<K> o) ,它可能会迫使 Java 用户进行大量代码维护,以使工作代码再次编译。
他们本可以引入一个额外的方法,比如 get_checked(<K> o) 并弃用旧的 get() 方法,以便有一个更温和的过渡路径。但由于某种原因,这并没有完成。(我们现在的情况是你需要安装 findBugs 之类的工具来检查 get() 参数和地图声明的键类型 <K> 之间的类型兼容性。)
我认为与 .equals() 语义有关的论点是虚假的。(从技术上讲,它们是正确的,但我仍然认为它们是假的。如果 o1 和 o2 没有任何共同的超类,那么没有一个头脑正常的设计师会让 o1.equals(o2) 为真。)
还有一个更重要的原因,它在技术上无法完成,因为它破坏了 Map。
Java 具有多态泛型结构,例如<? extends SomeClass>
. 标记此类引用可以指向用 签名的类型<AnySubclassOfSomeClass>
。但是多态泛型使该引用成为只读的。编译器允许您仅将泛型类型用作方法的返回类型(如简单的 getter),但阻止使用泛型类型为参数的方法(如普通的 setter)。这意味着如果您编写Map<? extends KeyType, ValueType>
,编译器不允许您调用方法get(<? extends KeyType>)
,并且映射将无用。唯一的解决方案是使此方法不通用:get(Object)
.
向后兼容性,我猜。Map
(或HashMap
)仍然需要支持get(Object)
。
我看着这个,想他们为什么这样做。我认为现有的任何答案都不能解释为什么他们不能让新的通用接口只接受密钥的正确类型。实际原因是,即使他们引入了泛型,他们也没有创建新接口。Map 接口与旧的非泛型 Map 相同,它只是用作泛型和非泛型版本。这样,如果您有一个接受非泛型 Map 的方法,您可以传递它 aMap<String, Customer>
并且它仍然可以工作。同时 get 的合约接受 Object 所以新的接口也应该支持这个合约。
在我看来,他们应该添加一个新接口并在现有集合上实现,但他们决定支持兼容接口,即使这意味着 get 方法的设计更差。请注意,集合本身将与现有方法兼容,只有接口不兼容。
我们刚才正在做大的重构,我们错过了这个强类型的 get() 来检查我们没有错过一些旧类型的 get()。
但我发现编译时间检查的解决方法/丑陋的技巧:使用强类型 get、containsKey、remove... 创建 Map 接口并将其放入项目的 java.util 包中。
仅调用 get(), ... 类型错误,您将收到编译错误,其他所有编译器似乎都可以(至少在 eclipse kepler 内部)。
在检查你的构建后不要忘记删除这个接口,因为这不是你在运行时想要的。