为什么Set不提供获取与另一个元素相等的元素的操作?

Set<Foo> set = ...;
...
Foo foo = new Foo(1, 2, 3);
Foo bar = set.get(foo);   // get the Foo element from the Set that equals foo

我可以问Set是否包含一个等于bar的元素,那么为什么我不能得到那个元素呢?:(

为了澄清,equals方法被重写,但它只检查其中一个字段,而不是所有字段。两个相等的Foo对象可以有不同的值,这就是为什么我不能只用Foo。


当前回答

如果你的set实际上是一个NavigableSet<Foo>(例如TreeSet),并且Foo实现Comparable<Foo>,你可以使用

Foo bar = set.floor(foo); // or .ceiling
if (foo.equals(bar)) {
    // use bar…
}

(感谢@eliran-malka的评论。)

其他回答

快速帮助方法,可以解决这种情况:

<T> T onlyItem(Collection<T> items) {
    if (items.size() != 1)
        throw new IllegalArgumentException("Collection must have single item; instead it has " + items.size());

    return items.iterator().next();
}

不幸的是,Java中的Default Set并不是为提供“get”操作而设计的,正如jschreiner所准确解释的那样。

使用迭代器找到感兴趣的元素(dacwe建议)或删除元素并更新其值重新添加元素(KyleM建议)的解决方案可能有效,但效率非常低。

重写等号的实现以使非等号对象“相等”,正如David Ogren所正确指出的那样,很容易导致维护问题。

恕我直言,使用Map作为显式替换(正如许多人建议的那样)会使代码不那么优雅。

如果目标是访问集合中包含的元素的原始实例(希望我正确理解了您的用例),这里有另一种可能的解决方案。


我个人在用Java开发客户端-服务器视频游戏时也有同样的需求。在我的例子中,每个客户机都有存储在服务器中的组件的副本,问题在于客户机何时需要修改服务器的对象。

通过互联网传递一个对象意味着客户端无论如何都有该对象的不同实例。为了将这个“复制”的实例与原始实例相匹配,我决定使用Java uuid。

因此,我创建了一个抽象类UniqueItem,它自动为其子类的每个实例提供一个随机的惟一id。

这个UUID在客户机和服务器实例之间共享,因此通过这种方式,只需使用Map就可以很容易地匹配它们。

然而,在类似的用例中直接使用Map仍然是不优雅的。有人可能会说,使用Map维护和处理可能更加复杂。

出于这些原因,我实现了一个名为MagicSet的库,它使得Map的使用对开发人员来说是“透明的”。

https://github.com/ricpacca/magicset


与原来的Java HashSet一样,MagicHashSet(库中提供的MagicSet的实现之一)使用一个支持HashMap,但是它使用元素的UUID作为键,使用元素本身作为值,而不是将元素作为键和虚拟值作为值。与普通HashSet相比,这不会导致内存使用的开销。

此外,MagicSet可以完全作为Set使用,但有一些提供额外功能的方法,如getFromId()、popFromId()、removeFromId()等。

使用它的唯一要求是您想要存储在MagicSet中的任何元素都需要扩展抽象类UniqueItem。


下面是一个代码示例,设想从MagicSet中检索一个城市的原始实例,给定该城市的另一个实例,该实例具有相同的UUID(甚至只有它的UUID)。

class City extends UniqueItem {

    // Somewhere in this class

    public void doSomething() {
        // Whatever
    }
}

public class GameMap {
    private MagicSet<City> cities;

    public GameMap(Collection<City> cities) {
        cities = new MagicHashSet<>(cities);
    }

    /*
     * cityId is the UUID of the city you want to retrieve.
     * If you have a copied instance of that city, you can simply 
     * call copiedCity.getId() and pass the return value to this method.
     */
    public void doSomethingInCity(UUID cityId) {
        City city = cities.getFromId(cityId);
        city.doSomething();
    }

    // Other methods can be called on a MagicSet too
}

要准确回答“为什么Set不提供获取等于另一个元素的元素的操作?”这个问题,答案可能是:因为集合框架的设计者不是很有前瞻性。他们没有预料到您非常合理的用例,天真地试图“建模数学集合抽象”(从javadoc),只是忘记添加有用的get()方法。

现在回到隐含的问题“如何获得元素”:我认为最好的解决方案是使用Map<E,E>而不是Set<E>来将元素映射到它们自己。通过这种方式,您可以有效地从“set”中检索元素,因为Map的get()方法将使用有效的哈希表或树算法查找元素。如果愿意,可以编写自己的Set实现,提供额外的get()方法,封装Map。

以下答案在我看来是错误的:

“您不需要获取元素,因为您已经有了一个相等的对象”:断言是错误的,正如您在问题中已经表明的那样。两个相等的对象仍然可以具有与对象相等无关的不同状态。目标是访问Set中包含的元素的状态,而不是用作“查询”的对象的状态。

"You have no other option but to use the iterator": that is a linear search over a collection which is totally inefficient for large sets (ironically, internally the Set is organized as hash map or tree that could be queried efficiently). Don't do it! I have seen severe performance problems in real-life systems by using that approach. In my opinion what is terrible about the missing get() method is not so much that it is a bit cumbersome to work around it, but that most programmers will use the linear search approach without thinking of the implications.

如果你有一个相等的对象,为什么你需要集合中的一个?如果它仅与键“相等”,则Map将是更好的选择。

不管怎样,下面的方法就可以了:

Foo getEqual(Foo sample, Set<Foo> all) {
  for (Foo one : all) {
    if (one.equals(sample)) {
      return one;
    }
  } 
  return null;
}

在Java 8中,这可以变成一行代码:

return all.stream().filter(sample::equals).findAny().orElse(null);

使用Java 8,你可以做到:

Foo foo = set.stream().filter(item->item.equals(theItemYouAreLookingFor)).findFirst().get();

但是要小心,.get()会抛出一个NoSuchElementException,或者你可以操作一个Optional项。