从集合中获取元素

foo*_*bar 297 java collections equals set

为什么不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
Run Code Online (Sandbox Code Playgroud)

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

为了澄清,该equals方法被覆盖,但它只检查其中一个字段,而不是所有字段.因此,两个Foo被认为相等的对象实际上可以具有不同的值,这就是我不能使用的原因foo.

jsc*_*ner 353

为了回答准确的问题" 为什么Set提供一个操作来获得与另一个元素相等的元素?",答案是:因为集合框架的设计者不是非常具有前瞻性.他们没有预料到你的合法用例,天真地试图"模拟数学集抽象"(来自javadoc)并且忘记添加有用的get()方法.

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

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

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

"你没有别的选择,只能使用迭代器":这是对集合的线性搜索,对大型集合来说效率非常低(具有讽刺意味的是,内部Set组织为可以有效查询的哈希映射或树).不要这样做!通过使用该方法,我已经在现实系统中看到了严重的性能问题.在我看来,丢失get()方法的可怕之处并不在于解决它有点麻烦,但是大多数程序员将使用线性搜索方法而不考虑其含义.

  • MEH.覆盖equals的实现以使不相等的对象"相等"是这里的问题.询问一个方法,说"让我得到与此对象相同的对象",然后期望返回一个不相同的对象似乎很疯狂,容易造成维护问题.正如其他人所建议的那样,使用地图解决了所有这些问题:它使你所做的事情不言自明.很容易理解,两个不相等的对象可能在地图中具有相同的键,并且具有相同的键将显示它们之间的关系. (25认同)
  • 强烈的话,@大卫奥格伦.咩?疯?但是在你的评论中,你使用"相同"和"相等"这两个词,好像它们的意思相同.他们不.具体来说,在Java中,标识由"=="运算符表示,相等性由equals()方法表示.如果它们意味着相同的东西,则根本不需要equals()方法.在其他语言中,这当然可以是不同的.例如,在Groovy中,identity是is()方法,而相等是"==".好笑,不是吗? (19认同)
  • 当我应该使用等价的词时,你批评我使用相同的词是非常有效的.但是在一个对象上定义等于使得Foo和Bar"相等"但是并不"等于"他等同地使用它们会产生各种功能和可读性/可维护性的问题.这个问题与Set只是潜在问题的冰山一角.例如,相等的对象必须具有相同的哈希码.所以他将会有潜在的哈希冲突.反对调用.get(foo)来获取除foo以外的东西是不是很疯狂? (15认同)
  • 值得注意的是,例如,HashSet被实现为围绕HashMap的包装器(将键映射到虚拟值).因此,显式地使用HashMap而不是HashSet不会导致内存使用的开销. (12认同)
  • @ user686249我觉得这已经转变为一场学术辩论.我确实承认我可能过分反对压倒平等.特别是像你这样的用途.但是,我仍然反对调用这个方法`get()`的想法.在您的示例中,我会对customerSet.get(thisCustomer)感到困惑.(然而,许多答案所建议的地图)对于canonicalCustomerMap.get(此客户)来说会很好.我也可以使用更明确命名的方法(例如NSSet上的Objective-C的成员方法). (4认同)
  • @DavidOgren它很有意义.我可以给你具体的例子.WeakHashSet(遗憾地不存在)来创建弱对象的缓存.然后你有一些东西可以创建一些东西,比如getClass().getMethods().这将创建许多您不想要的Method实例的相同副本,但是您希望拥有一个,即您存储在集合中的那个.因此,set.get(object)会有意义. (2认同)
  • @DavidOgren:这只是另一个用例,您的“等于”断言太宽松了,不成立:我有不可变的对象,并且希望避免重复项的激增。这些对象已经存储在 Set 中(实际上作为 Map 的键,但问题是相同的),因此在其他地方重用 Set 中的对象而不是从数据库值新创建的对象会很有帮助。 (2认同)

dac*_*cwe 105

如果元素相等,就没有必要获得元素.A Map更适合这个用例.


如果您仍想查找元素,则除了使用迭代器之外没有其他选择:

public static void main(String[] args) {

    Set<Foo> set = new HashSet<Foo>();
    set.add(new Foo("Hello"));

    for (Iterator<Foo> it = set.iterator(); it.hasNext(); ) {
        Foo f = it.next();
        if (f.equals(new Foo("Hello")))
            System.out.println("foo found");
    }
}

static class Foo {
    String string;
    Foo(String string) {
        this.string = string;
    }
    @Override
    public int hashCode() { 
        return string.hashCode(); 
    }
    @Override
    public boolean equals(Object obj) {
        return string.equals(((Foo) obj).string);
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 获得元素绝对是有意义的.如果您希望在已将某些元素的值添加到集合后更新它,该怎么办?例如,当.equals()不使用所有字段时,如OP指定的那样.一种效率较低的解决方案是删除元素并重新添加元素并更新其值. (217认同)
  • @dacwe,我来到这里是因为我开始寻找避免这种情况的方法!同时作为键和对应值起作用的对象正是集合应该是什么.在我的情况下,我想通过键(String)从集合中获取一些复杂的对象.此String被封装(并且唯一)到要映射的对象.实际上,整个对象围绕着所述关键词"旋转".此外,调用者知道所述字符串,但不知道对象本身; 这正是它想要通过密钥检索它的原因.我当然正在使用Map,但它仍然是奇怪的行为. (21认同)
  • 我仍然认为`Map`更适合(在这种情况下`Map <Foo,Foo>`). (14认同)
  • 我同意你可以使用`Map <Foo,Foo>`作为替代品,缺点是地图总是必须至少存储一个键和一个值(并且为了性能它也应该存储哈希),而一个集合可以离开只是存储价值(也许哈希表现).因此,一个好的集合实现可以同样快速地"Map <Foo,Foo>",但使用的内存减少了50%.在Java的情况下,无关紧要,因为HashSet无论如何都在内部基于HashMap. (5认同)
  • @KyleM我理解用例,但我想强调不接触属于hashCode/equals的属性的重要性.来自Set Javadoc:"注意:如果将可变对象用作set元素,则必须非常小心.如果对象的值以影响等于比较的方式更改,则对象的行为未指定集合中的元素." - 我建议这些对象是不可变的,或者至少具有不可变的键属性. (4认同)

To *_*Kra 21

将set转换为list,然后使用getlist的方法

Set<Foo> set = ...;
List<Foo> list = new ArrayList<Foo>(set);
Foo obj = list.get(0);
Run Code Online (Sandbox Code Playgroud)

  • 我不懂.这将检索该集合的*任意*对象.不是*对象. (30认同)
  • 为什么这会得到这么多人的点赞?在这个答案中,您将集合转换为列表并检索第一个对象,而不是“foo” (3认同)

Arn*_*ter 17

如果你有一个相同的对象,为什么你需要一个对象?如果它只是一个键"相等",那Map将是一个更好的选择.

无论如何,以下将做到:

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

使用Java 8,这可以成为一个单行:

return all.stream().filter(sample::equals).findAny().orElse(null);
Run Code Online (Sandbox Code Playgroud)

  • @Leo谢谢,但是单一退出范例不是针对OOP而且对于比Fortran或COBOL更现代的语言大多无效,另请参阅http://softwareengineering.stackexchange.com/questions/118703/where-did-the-notion-of - 酮返回仅-来从# (8认同)

ric*_*cca 14

遗憾的是,Java中的默认设置并非旨在提供"获取"操作,正如jschreiner准确解释的那样.

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

正如David Ogren所说的那样,重写equals的实现以使不相等的对象"相等" 很容易导致维护问题.

使用Map作为显式替换(如许多人的建议),imho使代码不那么优雅.

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


在使用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
}
Run Code Online (Sandbox Code Playgroud)


Jes*_*ick 11

如果您的集合实际上是NavigableSet<Foo>(例如a TreeSet)Foo implements Comparable<Foo>,那么您可以使用

Foo bar = set.floor(foo); // or .ceiling
if (foo.equals(bar)) {
    // use bar…
}
Run Code Online (Sandbox Code Playgroud)

(感谢@ eliran-malka对提示的评论.)

  • 如果我不介意任何人在最初认为我已经完全疯了的情况下阅读我的代码,这将是一个很好的解决方案. (4认同)

clo*_*her 10

使用Java 8,您可以:

Foo foo = set.stream().filter(item->item.equals(theItemYouAreLookingFor)).findFirst().get();
Run Code Online (Sandbox Code Playgroud)

但要小心,.get()会抛出NoSuchElementException,或者您可以操作Optional项.

  • `item-> item.equals(theItemYouAreLookingFor)`可以缩写为`theItemYouAreLookingFor :: equals` (4认同)

小智 7

为什么:

似乎 Set 在提供比较方法方面发挥了有用的作用。它旨在不存储重复元素。

由于这种意图/设计,如果要 get() 对存储对象的引用,然后对其进行变异,则 Set 的设计意图可能会受阻并可能导致意外行为。

来自JavaDocs

如果将可变对象用作集合元素,则必须非常小心。如果对象的值以影响等于比较的方式更改,而对象是集合中的元素,则不会指定集合的​​行为。

如何:

现在已经引入了 Streams,可以执行以下操作

mySet.stream()
.filter(object -> object.property.equals(myProperty))
.findFirst().get();
Run Code Online (Sandbox Code Playgroud)


Xym*_*mon 5

Object objectToGet = ...
Map<Object, Object> map = new HashMap<Object, Object>(set.size());
for (Object o : set) {
    map.put(o, o);
}
Object objectFromSet = map.get(objectToGet);
Run Code Online (Sandbox Code Playgroud)

如果只执行一次操作,这将不会很好地执行,因为您将循环遍历所有元素,但是当对一个大集合执行多次检索时,您会注意到其中的区别。