java - Map.get(Object key)不是(完全)通用的原因是什么?





generics collections (10)


合同表示如下:

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

(我的重点)

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

决定在java.util.Map<K, V>的接口中没有完全通用的get方法的原因是什么?

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

V get(Object key)

代替

V get(K key)

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




我正在看这个,并思考他们为什么这样做。 我不认为任何现有的答案解释了为什么他们不能只让新的通用接口只接受适当的密钥类型。 实际的原因是,即使他们引入了泛型,他们也没有创建一个新的接口。 Map接口与旧的非泛型地图只是泛型和非泛型版本。 这样,如果你有一个接受非通用Map的方法,你可以传递一个Map<String, Customer> ,它仍然可以工作。 同时get的合同接受Object,所以新的接口也应该支持这个合同。

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




正如其他人提到的那样, get()等原因不是泛型的,因为您正在检索的条目的键不必与您传递给get()的对象的类型相同。 该方法的规范只要求它们相等。 这来自equals()方法如何将Object作为参数,而不仅仅是与对象相同的类型。

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




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




兼容性。

在泛型可用之前,刚刚获得(Object o)。

如果他们改变这个方法来得到(<K> o),它可能会迫使大量的代码维护到Java用户,只是为了再次编译工作代码。

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

有关.equals()的语义的争论是假的,我想。 (从技术上讲,他们是正确的,但我仍然认为他们是假的。如果o1和o2没有任何共同的超类,任何设计师都不会使他的正确思想使o1.equals(o2)成立。)




我们刚刚进行了大量的重构,并且我们遗漏了这个强类型的get()来检查我们是否错过了旧类型的get()。

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

你会得到编译错误只是为了调用get(),...类型错误,其他编译器似乎可以编译器(至少在eclipse kepler里面)。

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




还有一个更重要的原因,它不能在技术上完成,因为它破坏了Map。

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




原因是遏制是由equalshashCode决定的,它们是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



Google的一位出色的Java编码人员Kevin Bourrillion刚才在一篇博客文章中写到了这个问题(当然,在Set而不是Map )。 最相关的句子:

统一地,Java集合框架(以及Google集合库)的方法不会限制其参数的类型,除非有必要防止集合被破坏。

我不完全相信我同意它作为一个原则 - 例如,.NET似乎很好,需要正确的键类型,但值得在博客文章中推理。 (在提到.NET的时候,值得解释的一点是它在.NET中不存在问题的部分原因是.NET中存在更大限度的问题:更有限的方差...)




这是一个有意的决定,而不是一个遗漏(正如其他地方已经提出的那样)。虽然起初看起来显而易见的是,人们希望在缺省方法上支持synchronized修饰符,但事实证明这样做会很危险,所以被禁止。

同步方法是一种方法的缩写,其行为就好像整个主体被包含在synchronized块中,其锁对象是接收者。 将这种语义扩展到默认方法似乎也是明智的; 毕竟,它们也是带有接收器的实例方法。 (请注意, synchronized方法完全是句法优化;它们不是必需的,它们只比相应的synchronized块更紧凑。有一个合理的理由可以说,这首先是一个过早的语法优化,同步的方法会导致比他们解决的问题更多的问题,但那艘船很久以前就航行了。)

那么,他们为什么危险? 同步是关于锁定的。 锁定是关于协调对可变状态的共享访问。 每个对象应该有一个同步策略,确定哪些锁保护哪些状态变量。 (请参阅Java并发实践 ,第2.4节。)

许多对象用作Java监视器模式Java Monitor Pattern ,JCiP 4.1)的同步策略,其中对象的状态由其固有的锁保护。 这种模式没有什么神奇或特殊之处,但它很方便,并且在方法上使用synchronized关键字隐含地假定了这种模式。

它是拥有状态的类来确定对象的同步策略。 但是接口并不拥有它们混入其中的对象的状态。因此,在接口中使用同步方法会假设一个特定的同步策略,但是您没有合理的假设基础,所以很可能是这种情况使用同步不会提供任何额外的线程安全性(您可能会在错误的锁定上同步)。 这会给你错误的自信感,认为你已经完成了有关线程安全的一些事情,并且没有错误消息告诉你,你正在假设错误的同步策略。

始终如一地维护单个源文件的同步策略已经非常困难; 更难以确保子类正确地遵守其超类定义的同步策略。 试图在这种松散耦合的类(一个接口和实现它的可能的多个类)之间这样做几乎是不可能的,并且极易出错。

鉴于所有这些反对意见,将会有什么争论? 看起来他们主要是为了让界面更像特质。 虽然这是一个可以理解的愿望,但默认方法的设计中心是界面演进,而不是“特质 - ”。 如果能够始终如一地实现这两项目标,我们努力做到这一点,但如果两者发生冲突,我们不得不选择主要设计目标。







java generics collections map