为什么java.util.Set<V>接口没有提供get(Object o)方法?

49

我知道根据.equals(),Set只允许一个对象的实例存在,并且如果您已经拥有相等的对象,您不应该需要从Set中获取对象,但我仍然想要一个.get()方法,它可以返回Set中与给定参数相等的对象实例(或null)。

有关其设计原因的任何想法/理论?

通常,我必须通过使用Map并使键和值相同或类似的方法来绕过此问题。

编辑:到目前为止,我认为人们不理解我的问题。 我想要的是已经在集合中的确切对象实例,而不是.equals()返回true的可能不同的对象实例。

至于为什么我希望出现这种行为,通常.equals()不能考虑对象的所有属性。 我想提供一些虚拟查找对象,并获取Set中的实际对象实例。


我真的很想这样做,但是使用Map的键。就像,我希望Map.getKey(K k)返回k',其中k.equals(k')。我能用你的hack来做到这一点吗?还是我必须制作一个Pair<K,V>,并将我的Map<K,V>更改为Map<K,Pair<K,V>>? - Jayen
与大多数其他集合类型不同,集合通常不是检索特定元素,而是测试一个值是否属于该集合。 - GClaramunt
24个回答

30
虽然纯度论点确实使得方法get(Object)看起来不太可信,但底层意图并未无关紧要。
有各种类和接口族,它们稍微重新定义了equals(Object)。我们只需看看集合接口便知道这一点。例如,ArrayList和LinkedList可以相等,它们各自的内容仅需要相同且顺序也相同即可。
因此,在集合中查找匹配元素有非常好的理由。或许更清晰地表明意图的方法是拥有像下面这样的一个方法:
public interface Collection<E> extends ... {
  ...
  public E findMatch(Object o) throws UnsupportedOperationException;
  ...
}
请注意,这个API的价值不仅限于Set内部。
至于问题本身,我没有任何理论可以解释为什么会省略这样的操作。我只能说,最小生成集合的论点并不成立,因为集合API中定义的许多操作都是出于方便和效率的考虑。

7
总的来说,我们没有一个很好的理由不拥有具备这种功能的收藏品。 - Johnathon Sanders
4
许多“为什么XX不做YY”的问题的答案是“因为有人认为不应该这样做”。 实际上,几乎所有此类问题的真正答案都是如此。一个更好的问题可能是“设计XX的人是否有任何理由不让它做YY”。我喜欢你找到了隐含的问题,并回答说“在这种情况下,有理由让设计XX的人做YY,尽管Java的XX恰巧没有这样做”。 - supercat
添加此类功能的一个反对观点是,人们不应该“认可”不纯粹的.equals并使API难以理解对于没有沿着这些思路思考的人。为不太纯粹的.equals定义添加支持会使这些定义更可能出现。显然,当您有一个很好的原因时,这种情况令人不安。 - Zachary Vance
1
在许多语言中,尤其是Java:a.equals(b)!=(a==b),因此一个好的合法示例将是字符串缓存。由于引用相等性和对象相等性不同,您将拥有本质上相同的多个字符串副本。集合将是存储和查找对象的最佳方式 - 允许您检索先前的引用并保存额外的内存。引用替换是我知道的主要示例,它不会破坏equals的意图。对于那些认为所有Set实际上都是映射的人,请看Trove的Set,它比Map实现更节省内存。 - NightDweller
@NightDweller:如果使用更复杂的嵌套不可变类型,节省的效果可能会更加显著。对于包含相同项的大型不可变集合的引用在语义上可能是等价的,但这并不意味着将所有对相同集合的引用替换为其中一个的引用不能提供数量级的加速。请注意,对于这种目的,弱哈希集将是最佳选择,但WeakHashMap不适用,因为它持有值的强引用。 - supercat
为了支持这种观点,其他一些集合API也提供了获取对象的方法。例如,Cocoa的 NSSet 类包含了 -containsObject: 方法以测试一个对象是否被包含在其中并返回一个布尔值,以及 -member: 方法来获取与给定对象相等的对象。OCaml标准库的 Set 包含了 mem 方法用于测试成员资格和 find 方法来获取相等的元素。 - newacct

15
问题是:Set 不是用来“获取”对象的,而是用来添加和测试是否存在。 我理解你的需求,我曾遇到过类似的情况,最终使用了一个将相同对象作为键和值的映射表。
编辑:只是澄清一下:http://en.wikipedia.org/wiki/Set_(abstract_data_type)

你的这个定义来源在哪里?我没有在任何地方的javadocs中找到过。此外,set的英文定义是:“一组相似性质、设计或功能的数字、组合或事物”。那么,谁说它只用于添加和测试存在呢?仅仅因为我们没有公开一个get方法,并不意味着将来不能改变。 - Johnathon Sanders
与大多数其他集合类型不同,Set类型通常不是检索集合中的特定元素,而是测试一个值是否在集合内。http://en.wikipedia.org/wiki/Set_(abstract_data_type) 仅仅因为计算机科学术语与英语相同,并不意味着它们具有相同的含义 :) - GClaramunt
1
非常正确关于英文定义的说法,那是我试图寻找正式定义的尝试。谢谢你提供的来源链接,我现在受教了! :) - Johnathon Sanders

6

我在几年前的Java论坛上也有同样的问题。他们告诉我Set接口是已经定义好的,不能更改,因为这会破坏Set接口当前的实现。然后,他们开始说一些胡言乱语,就像你在这里看到的:“Set不需要get方法”,并开始向我灌输Map必须始终用于从Set中获取元素的观念。

如果您仅将Set用于数学运算,例如交集或并集,则可能只需要contains()方法。但是,在集合中定义Set是用于存储数据的。我使用关系数据模型解释了需要在Set中使用get()方法。

接下来,一个SQL表就像一个类。列定义属性(Java中称为字段),记录表示类的实例。因此,对象是字段的向量。其中一些字段是主键。它们定义对象的唯一性。这就是Java中contains()方法的作用:

class Element {

        public int hashCode() {return sumOfKeyFields()}
        public boolean equals(Object e) {keyField1.equals(e) && keyField2.equals(e) && ..}

我不了解数据库内部。但是,您在定义表时只需一次指定关键字段。您只需使用@primary注释关键字段。添加记录到表时,无需第二次指定键。您不会将键与数据分开,就像映射一样。 SQL表是集合。它们不是映射。然而,它们提供了get()方法 ,除了维护唯一性和包含性检查之外。
在《计算机程序设计艺术》中,介绍搜索时,D.Knuth 也说了同样的话:

本章大部分内容都致力于研究一个非常简单的搜索问题:如何找到已经存储了给定标识的数据。

你看,数据是带有标识存储的。不是标识指向数据,而是数据带有标识。他接着说:

例如,在数值应用程序中,我们可能想要找到f(x),给定x和f值的表;在非数值应用程序中,我们可能想要找到给定俄语单词的英语翻译。

看起来他开始讲述映射。但是,

一般来说,我们假设已经存储了一组N个记录,并且问题是定位适当的记录。通常,我们要求N个键不同,以便每个键唯一地标识其记录。所有记录的集合称为“表”或“文件”,其中单词“表”通常用于指示小文件,“文件”通常用于指示大表。一个大文件或一组文件通常被称为“数据库”。搜索算法使用所谓的参数K进行演示,问题是找到具有K作为其键的记录。尽管搜索的目标是找到与K关联的记录中存储的信息,但本章中的算法通常忽略除键本身之外的所有内容。在实践中,一旦我们定位了K,就可以找到相关数据;例如,如果K出现在位置TABLE + i,则相关数据(或指向它的指针)可能位于位置TABLE + i + 1。
即搜索定位记录的关键字段,而不应将关键字映射到数据中。它们都位于同一记录中,作为Java对象的字段。也就是说,搜索算法检查记录的关键字段,就像在集合中一样,而不是在映射中检查某个远程关键字。
我们有N个要排序的项目;我们称它们为“记录”,整个包含N个记录的集合称为“文件”。每个记录Rj都有一个关键字Kj,它控制着排序过程。通常还存在除关键字以外的其他数据,“卫星信息”对排序没有影响,除了必须作为每个记录的一部分进行携带。
我认为在讨论排序时无需在额外的“关键字集”中复制关键字。
……[“计算机程序设计艺术”,第6章,引言]

实体集是特定实体类型的所有实体的集合或组合[http://wiki.answers.com/Q/What_is_entity_and_entity_set_in_dbms]。单个类的对象共享其类属性。同样,DB中的记录也是如此。它们共享列属性。

集合的一个特殊情况是类范围,它是属于该类的所有对象的集合。类范围允许将类视为关系

... ["数据库系统概念"第6版]

基本上,类描述了所有实例共有的属性。关系型DB中的表也是如此。“您将拥有的最简单的映射是单个属性到单个列的属性映射。” 这就是我所说的情况。

我在证明对象和DB记录之间的类比(同构)时非常冗长,因为有些愚蠢的人不接受它(以证明他们的Set必须没有get方法)

你在回放中看到一些人不理解这个,认为使用 Set 和 get 是多余的?这是因为他们滥用了 map,在 set 的位置使用它,导致了冗余。他们调用 put(obj.getKey(), obj) 存储两个键:原始键作为对象的一部分和副本在 map 的键集中。这种重复就是冗余。它还涉及代码的更多膨胀,并浪费运行时消耗的内存。我不知道数据库内部如何,但良好设计和数据库规范化的原则表明这种重复是一个坏主意 - 必须只有一个真相来源。冗余意味着可能发生不一致:关键映射到具有不同键的对象。不一致是冗余的一种表现形式。Edgar F. Codd 提出了 DB 规范化 只是为了摆脱冗余及其引起的不一致性。老师们对规范化非常明确:规范化永远不会生成两个具有一对一关系的表。没有理论上的理由将单个实体分别放置在两个表的单个记录中的某些字段和另一个表的单个记录中的其他字段中
因此,我们有四个理由,说明为什么在set中使用map实现get是不好的:
  1. 当我们有一组唯一对象时,使用map是不必要的
  2. map在运行时存储中引入了冗余
  3. map在DB(在Collections中)中引入了代码膨胀
  4. 使用map违反了数据存储规范化
即使您不了解记录集概念和数据规范化,也可以像我们、org.eclipse.KeyedHashSet和C++ STL设计者一样,自己发现这种数据结构和算法。
我因指出这些想法而被禁止在Sun论坛上发言。偏见是反对理性的唯一论据,这个世界被偏见主导。他们不想看到概念以及事物如何不同/改进。他们只看到实际世界,无法想象Java Collections的设计可能存在缺陷并且可以改进。向这样的人提醒理性的东西是危险的。他们教给你他们的盲目,并惩罚你如果你不服从。
2013年12月添加:SICP还说,DB是具有键控记录的集合,而不是映射 一个典型的数据管理系统花费大量时间来访问或修改记录中的数据,因此需要一种高效的方法来访问记录。这是通过识别每个记录中的一部分作为识别键来完成的。现在我们将数据库表示为一组记录。

我有时会感觉到一种倾向,这种倾向并不是Java维护者所特有的,即对任何改进持敌视态度,如果采用这些改进,很快就能证明由于没有早些采用它们而浪费了数百万小时的编程时间。实施变革所能实现的节约越大,对它的敌视就越大。Java至少最终允许接口声明默认方法,这是.NET目前还没有的功能,尽管在Java中这种能力比在.NET中更具问题性。 - supercat

2

如果你已经从集合中“获取”了这个东西,那么你就不需要再使用get()方法获取它了,对吧?;-)

我认为你使用Map的方法是正确的。听起来你正在尝试通过equals()方法将对象“规范化”,而我一直都是使用像你建议的Map来实现这一点。


1
我同意:对于Set来说,返回不同的对象是没有意义的,因为它们已经是equal()的,这意味着它们可以互换使用。从语义上讲,你可能希望有一个从对象到其规范化实例的映射(例如,对于字符串来说是"interned")。唯一不寻常的地方在于键和值是相同的对象。 - Joachim Sauer
直到现在我都同意这个说法。但是现在我不再同意了:想象一个有字段A、B和C的XYZ类。XYZ的相等性仅基于A和B。因此,在集合中,我可能会有所有字段都设置的XYZ实例。假设我有另一个XYZ实例,其中只设置了A和B。需要使用get(Object)从设置了A、B和C的集合中获取该实例。 - Boss Man

1

我理解你可能会有两个相等的对象,但它们不是同一个实例。

比如:

Integer a = new Integer(3);
Integer b = new Integer(3);

在这种情况下,a.equals(b)是成立的,因为它们引用相同的内在值,但a!= b,因为它们是两个不同的对象。

还有其他Set的实现,例如IdentitySet,它在项目之间进行了不同的比较。

然而,我认为您正在尝试将不同的哲学应用于Java。如果您的对象相等(a.equals(b)),尽管a和b具有不同的状态或含义,那么这里肯定有问题。您可能需要将该类拆分为两个或多个语义类,这些类实现了一个公共接口-或者重新考虑.equals和.hashCode。

如果您有Joshua Bloch的Effective Java,请查看名为“重写equals时遵守通用合同”和“最小化可变性”的章节。


假设代码将从文件中读取大量字符串,其中许多字符串将包含相同的字符。将对从磁盘读取的字符串的引用替换为对存储在映射中的字符串的引用可以大大减少所需的存储量,并且还可以加快它们之间的比较速度。拥有X和Y作为不同的具有相同内容的5,000个字符的字符串的引用可能在语义上等同于将它们视为相同的字符串,但在后一种情况下,X.Equals(Y)将更快。 - supercat

1

我认为,鉴于某些Set实现,你唯一的解决方案是迭代其元素以找到一个equals()相等的元素--然后你就有了与Set中匹配的实际对象。

K target = ...;
Set<K> set = ...;
for (K element : set) {
  if (target.equals(element)) {
    return element;
  }
}

1
如果你把它看作是数学集合,你可以推导出一种寻找对象的方法。
将该集合与仅包含你想查找的对象的对象集进行交集运算。 如果交集不为空,则集合中仅剩下你要查找的那个对象。
public <T> T findInSet(T findMe, Set<T> inHere){
   inHere.retainAll(Arrays.asList(findMe));
   if(!inHere.isEmpty){
       return inHere.iterator().next();
   }
   return null;
}

这不是最有效地使用内存的方法,但它在功能和数学上是正确的。


1

我不确定您是在寻找Sets如此行为的解释,还是对其提出的问题的简单解决方案。其他答案处理了前者,因此这里提供一个后者的建议。

您可以遍历Set的元素,并使用equals()方法测试每个元素是否相等。它易于实现且几乎没有错误。显然,如果您不确定元素是否在集合中,请先使用contains()方法进行检查。

与例如HashSet的contains()方法相比,这并不高效,后者会“查找”存储的元素,但不会返回它。如果您的集合可能包含许多元素,甚至可能需要使用"更重"的解决方法,例如您提到的映射实现。但是,如果这对您很重要(我确实看到有这种能力的好处),那么它可能是值得的。


2
当然可以遍历所有的元素,但是寻找对象实例的复杂度会变成线性,这非常糟糕,尤其是如果后备集是 HashSet 的情况下。你说得对,我要的是解释 Sets 为什么会有这种行为,而不是一个解决方法。 - GreenieMeanie

1

只需使用Map解决方案... TreeSet和HashSet也可以,因为它们由TreeMap和HashMap支持,所以这样做没有任何惩罚(实际上应该是最小的收益)。

您还可以扩展您喜欢的Set以添加get()方法。

[]]


0
Object fromSet = set.tailSet(obj).first();

if (! obj.equals(fromSet)) fromSet = null;

这就是你要找的功能。我不知道为什么Java把它隐藏起来了。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接