从集合中获取元素


322

为什么不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


2
这篇文章已经被广泛讨论,并且已经提出了很好的答案。但是,如果您只是在寻找有序集合,则只需使用SortedSet及其基于地图的实现(例如TreeSet允许访问first())。
伊莱兰·马尔卡

3
对于您上面描述的完全相同的情况,我也缺少该方法。Objective-C(NSSet)具有这种方法。它被调用member并返回集合中的对象,该对象将“等于”与member方法的参数进行比较(当然,该对象可能是一个不同的对象,并且也具有不同的属性,该相等可能不会检查)。
Mecki'3

Answers:


118

如果元素相等,将毫无意义。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);
    }
}

234
绝对有可能获得该元素。如果您希望在元素已经添加到Set中之后更新某些元素的值该怎么办?例如,当.equals()未使用所有字段时,如指定的OP。效率较低的解决方案是删除元素,然后将其值更新后重新添加。
KyleM

14
我仍然会争论说a MapMap<Foo, Foo>在这种情况下)更适合
。– dacwe

22
@dacwe,我到达这里是因为我开始寻找一种避免这种情况的方法!同时充当键和相应值的对象正是集合应具有的全部含义。就我而言,我想通过键(字符串)从集合中获取一些复杂的对象。此String被封装(并且是唯一的)到要映射的对象。实际上,整个对象围绕所述键“旋转”。而且,调用者知道所说的字符串,但不知道对象本身。这就是为什么它要通过键检索它的原因。我现在正在使用地图,但是它仍然是奇怪的行为。
pauluss86

4
@KyleM我了解用例,但是我想强调不要触摸hashCode / equals的属性的重要性。来自Set Javadoc:“注意:如果将可变对象用作set元素,则必须格外小心。如果对象的值以影响equals比较的方式更改,而对象是对象时,则未指定set的行为。集合中的元素。” -我建议这些对象是不可变的,或者至少具有不可变的键属性。
stivlo

5
我同意您可以使用Map<Foo, Foo>它来代替,缺点是地图始终必须至少存储一个键和一个值(为了性能,它还应该存储哈希值),而集合可以只存储值(并且可能会提高性能)。因此,良好的设置实现可以同样快,Map<Foo, Foo>但占用的内存最多可减少50%。在Java的情况下,这无关紧要,因为HashSet始终在内部基于HashMap。
Mecki'3

372

为了回答精确的问题“ 为什么Set提供获取等于另一个元素的元素的操作?”,答案将是:因为集合框架的设计者并不十分前瞻。他们没有想到您的合法用例,只是天真地尝试“对数学集抽象进行建模”(来自javadoc),却忘了添加有用的get()方法。

现在,隐含的问题是“ 如何获取元素”:我认为最好的解决方案是使用a Map<E,E>而不是a Set<E>来将元素映射到自身。这样,您可以从“集合”中有效地检索元素,因为的get()方法Map将使用有效的哈希表或树算法来找到该元素。如果需要,您可以编写自己的实现Set并提供其他get()方法,并封装Map

我认为以下答案是对还是错:

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

“除了使用迭代器,您别无其他选择”:这是对集合的线性搜索,对于大型集合而言,这是完全无效的(具有讽刺意味的是,在内部将Set其组织为可以有效查询的哈希图或树)。别做!通过使用该方法,我已经看到了现实系统中的严重性能问题。在我看来,缺少该get()方法的可怕之处并不在于解决该方法有点麻烦,而是大多数程序员会在不考虑其含义的情况下使用线性搜索方法。


27
嗯 此处,问题是要覆盖等于的实现,以使非等于的对象成为“等于”。要求提供一种方法,说“给我与该对象相同的对象”,然后期望返回一个不相同的对象,这似乎很疯狂,很容易引起维护问题。正如其他人所建议的那样,使用地图可以解决所有这些问题:它使您所做的事情不言自明。很容易理解,两个不相等的对象在映射中可能具有相同的键,而具有相同的键将显示它们之间的关系。
大卫·奥格伦

20
强有力的话,@ David Ogren。嗯 疯?但是在您的评论中,您使用的是“相同”和“相等”,就好像它们是同一意思。他们不。具体来说,在Java中,身份由“ ==”运算符表示,而相等性由equals()方法表示。如果它们的意思相同,那么根本就不需要equals()方法。用其他语言,这当然可以不同。例如,在Groovy中,标识是is()方法,相等性是“ ==”。好笑,不是吗?
jschreiner 2014年

15
当我本应使用等价这个词时,您对我使用“相同”这个词的批评是非常有效的。但是,在对象上定义均等以使Foo和Bar“等于”但不够“等于”,以至于他不能等效地使用它们,这将在功能以及可读性/可维护性方面造成各种问题。Set的这个问题只是潜在问题的冰山一角。例如,相等的对象必须具有相等的哈希码。因此,他将有潜在的哈希冲突。对象专门调用.get(foo)以获取foo以外的东西是否疯狂?
David Ogren

12
可能值得注意的是,例如HashSet被实现为HashMap的包装器(将键映射到伪值)。因此,显式使用HashMap代替HashSet不会导致内存使用方面的开销。
Alexey B.

4
@ user686249我觉得这已经演变成一场学术辩论。我确实承认,我可能在反对压倒一切方面有些落伍。特别是在像您这样的用途中。但是,我仍然反对调用此方法的想法get()。在您的示例中,customerSet.get(thisCustomer)会让我非常困惑。(不过,正如许多答案所建议的,地图)可以通过canonicalCustomerMap.get(此客户)使用。我也可以使用更明确命名的方法(例如NSSet上的Objective-C的成员方法)。
David Ogren

19

如果您有一个相等的对象,那么为什么需要集合中的那个?如果仅通过一个键“相等”,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);

我更喜欢这个答案,我只会避免使用两个return语句,因为这是针对OOP的,它会使Cyclomatic Complexity的值更高。
狮子座

8
@Leo感谢,但单一的出口模式是不反对OOP,是语言Fortran语言比或COBOL更现代的大多是无效的,也看到softwareengineering.stackexchange.com/questions/118703/...
阿恩蝼蛄

1
使用Map而不是Set似乎是一个更好的选择:遍历Set的元素比从Map中获取单个值要花更多的工作。(O(N)vs O(1))
Jamie Flournoy

@JamieFlournoy对,如果您必须多次检查同一集合中的不同元素,那就更好了。无需一次性使用,因为首先需要花费更多的精力来构建地图。
Arne Burmeister,

18

将集合转换为列表,然后使用get列表的方法

Set<Foo> set = ...;
List<Foo> list = new ArrayList<Foo>(set);
Foo obj = list.get(0);

37
我不明白 这将检索集合的任意对象。不是对象。
aioobe

14

不幸的是,正如jschreiner正确解释的那样,Java中的Default Set并非旨在提供“ get”操作。

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

正如David Ogren正确指出的那样,重写equals的实现,使非equal对象成为“ equal”,很容易引起维护问题。

而且,将地图用作显式替换(很多人都建议),恕我直言,这使代码不太优雅。

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


我个人在使用Java开发客户端服务器视频游戏时也有同样的需求。在我的情况下,每个客户端都具有存储在服务器中的组件的副本,问题出在客户端需要修改服务器对象时。

通过Internet传递对象意味着客户端无论如何都具有该对象的不同实例。为了使此“复制”实例与原始实例匹配,我决定使用Java UUID。

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

此UUID在客户端和服务器实例之间共享,因此,通过使用Map可以轻松匹配它们。

但是,在类似的用例中直接使用Map仍然不明智。有人可能会争辩说,使用地图可能很难维护和处理。

由于这些原因,我实现了一个名为MagicSet的库,该库使开发人员对Map的使用“透明”。

https://github.com/ricpacca/magicset


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

此外,MagicSet可以完全用作Set,但可以使用更多方法提供其他功能,例如getFromId(),popFromId(),removeFromId()等。

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


这是一个代码示例,假设给定城市的另一个实例具有相同的UUID(甚至只是其UUID),则可以从MagicSet检索城市的原始实例。

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
}

11

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

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

(感谢@ eliran-malka的提示。)


5
如果我不介意任何人在阅读我的代码时最初以为我完全疯了,那将是一个不错的解决方案。
亚当

10

使用Java 8,您可以执行以下操作:

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

但是要小心,.get()会引发NoSuchElementException,或者您可以操纵Optional项。


5
item->item.equals(theItemYouAreLookingFor)可以缩短为theItemYouAreLookingFor::equals
Henno Vermeulen

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);

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


5

为什么:

Set似乎在提供比较手段方面发挥了有用的作用。它被设计为不存储重复的元素。

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

JavaDocs

如果将可变对象用作集合元素,则必须格外小心。如果在对象是集合中的一个元素的情况下,如果对象的值以影响相等比较的方式更改,则不会指定集合的​​行为。

怎么样:

现在已经介绍了Streams,可以执行以下操作

mySet.stream()
.filter(object -> object.property.equals(myProperty))
.findFirst().get();

2

那么使用Arrays类呢?

import java.util.Arrays;
import java.util.List;
import java.util.HashSet;
import java.util.Arrays;

public class MyClass {
    public static void main(String args[]) {
        Set mySet = new HashSet();
        mySet.add("one");
        mySet.add("two");
        List list = Arrays.asList(mySet.toArray());
        Object o0 = list.get(0);
        Object o1 = list.get(1);
        System.out.println("items " + o0+","+o1);
    }
}

输出:
项目一,二




1

去过也做过!!如果您使用的是番石榴,将其转换为地图的快速方法是:

Map<Integer,Foo> map = Maps.uniqueIndex(fooSet, Foo::getKey);

1

您可以使用Iterator类

import java.util.Iterator;
import java.util.HashSet;

public class MyClass {
 public static void main(String[ ] args) {
 HashSet<String> animals = new HashSet<String>();
animals.add("fox");
animals.add("cat");
animals.add("dog");
animals.add("rabbit");

Iterator<String> it = animals.iterator();
while(it.hasNext()) {
  String value = it.next();
  System.out.println(value);   
 }
 }
}

1

如果您想从HashSet中获取第n个Element,则可以采用以下解决方案,在这里我在HashSet中添加了ModelClass对象。

ModelClass m1 = null;
int nth=scanner.nextInt();
for(int index=0;index<hashset1.size();index++){
    m1 = (ModelClass) itr.next();
    if(nth == index) {
        System.out.println(m1);
        break;
    }
}

1

如果看一下实现的前几行,java.util.HashSet您将看到:

public class HashSet<E>
    ....
    private transient HashMap<E,Object> map;

因此无论如何都可以相互HashSet使用HashMap,这意味着,如果您HashMap直接使用Direct 并使用与键和值相同的值,则您将获得想要的效果并节省一些内存。


1

看起来合适的对象是guava 的Interner

为其他不可变类型提供与String.intern()等效的行为。通用的实现可从Interners 类中获得。

它还有一些非常有趣的杠杆,例如concurrencyLevel或使用的引用类型(可能值得注意的是,它没有提供SoftInterner,我认为它比WeakInterner更有用)。


0

因为Set的任何特定实现都可能是随机访问,也可能不是。

找到迭代器next()的相等元素后,始终可以使用迭代器的方法来获得所需的结果,并获得迭代器并逐步遍历Set 。无论实现如何,此方法均有效。如果实现不是随机访问(图片为链接列表支持的Set),get(E element)则接口中的方法将具有欺骗性,因为它必须迭代集合以找到要返回的元素,并且a get(E element)似乎暗示这将是Set可以直接跳转到要获取的元素。

contains() 当然,根据实现的不同,可能不一定要做相同的事情,但是这个名称似乎并没有引起同样的误解。


2
get()方法可以做的任何事情已经由contains()方法完成。您必须先获取包含的对象并调用其.equals()方法,才能实现contains()。尽管在某些实现中会很慢,但API设计人员似乎对将get()添加到java.util.List毫无疑虑。
Bryan Rink

我不认为这是真的。两个对象可以通过等号相等,但通过==可以不相等。如果您有对象A,并且包含对象B的集合S和A.equals(B)但A!= B,并且想要获取对B的引用,则可以调用S.get(A)来获取对B的引用B,假设您有一个具有List的get方法的语义的get方法,这与检查S.contains(A)(它会那样)是一个不同的用例。这甚至不是稀有的收藏用例。
汤姆·特雷桑斯基

0

是的,使用HashMap...,但是以一种特殊的方式:我预见到试图将a HashMap用作伪- 的陷阱Set是该“​​实际”元素之间可能的混淆。Map/Set和“候选”元素(即用于测试是否equal元素已经存在。这远非万无一失,但会使您远离陷阱:

class SelfMappingHashMap<V> extends HashMap<V, V>{
    @Override
    public String toString(){
        // otherwise you get lots of "... object1=object1, object2=object2..." stuff
        return keySet().toString();
    }

    @Override
    public V get( Object key ){
        throw new UnsupportedOperationException( "use tryToGetRealFromCandidate()");
    }

    @Override
    public V put( V key, V value ){
       // thorny issue here: if you were indavertently to `put`
       // a "candidate instance" with the element already in the `Map/Set`: 
       // these will obviously be considered equivalent 
       assert key.equals( value );
       return super.put( key, value );
    }

    public V tryToGetRealFromCandidate( V key ){
        return super.get(key);
    }
}

然后执行以下操作:

SelfMappingHashMap<SomeClass> selfMap = new SelfMappingHashMap<SomeClass>();
...
SomeClass candidate = new SomeClass();
if( selfMap.contains( candidate ) ){
    SomeClass realThing = selfMap.tryToGetRealFromCandidate( candidate );
    ...
    realThing.useInSomeWay()...
}

但是...您现在希望candidate以某种方式自我毁灭,除非程序员实际上立即将其放入Map/Set... 中,否则您希望contains“污染”该物,candidate以便对其进行任何使用,除非它加入Map使它成为“麻醉剂”。 ”。也许您可以SomeClass实现一个新的Taintable接口。

一个更令人满意的解决方案是GettableSet,如下所示。但是,SomeClass要使此工作正常进行,您必须负责设计,以使所有构造函数都不可见(或...能够并愿意为其设计和使用包装器类):

public interface NoVisibleConstructor {
    // again, this is a "nudge" technique, in the sense that there is no known method of 
    // making an interface enforce "no visible constructor" in its implementing classes 
    // - of course when Java finally implements full multiple inheritance some reflection 
    // technique might be used...
    NoVisibleConstructor addOrGetExisting( GettableSet<? extends NoVisibleConstructor> gettableSet );
};

public interface GettableSet<V extends NoVisibleConstructor> extends Set<V> {
    V getGenuineFromImpostor( V impostor ); // see below for naming
}

实现方式:

public class GettableHashSet<V extends NoVisibleConstructor> implements GettableSet<V> {
    private Map<V, V> map = new HashMap<V, V>();

    @Override
    public V getGenuineFromImpostor(V impostor ) {
        return map.get( impostor );
    }

    @Override
    public int size() {
        return map.size();
    }

    @Override
    public boolean contains(Object o) {
        return map.containsKey( o );
    }

    @Override
    public boolean add(V e) {
        assert e != null;
        V result = map.put( e,  e );
        return result != null;
    }

    @Override
    public boolean remove(Object o) {
        V result = map.remove( o );
        return result != null;
    }

    @Override
    public boolean addAll(Collection<? extends V> c) {
        // for example:
        throw new UnsupportedOperationException();
    }

    @Override
    public void clear() {
        map.clear();
    }

    // implement the other methods from Set ...
}

您的 NoVisibleConstructor课程如下所示:

class SomeClass implements NoVisibleConstructor {

    private SomeClass( Object param1, Object param2 ){
        // ...
    }

    static SomeClass getOrCreate( GettableSet<SomeClass> gettableSet, Object param1, Object param2 ) {
        SomeClass candidate = new SomeClass( param1, param2 );
        if (gettableSet.contains(candidate)) {
            // obviously this then means that the candidate "fails" (or is revealed
            // to be an "impostor" if you will).  Return the existing element:
            return gettableSet.getGenuineFromImpostor(candidate);
        }
        gettableSet.add( candidate );
        return candidate;
    }

    @Override
    public NoVisibleConstructor addOrGetExisting( GettableSet<? extends NoVisibleConstructor> gettableSet ){
       // more elegant implementation-hiding: see below
    }
}

PS这种NoVisibleConstructor类的一个技术问题:可能会反对这种类是固有的final,这可能是不希望的。实际上,您总是可以添加一个无参数的虚拟对象protected构造函数:

protected SomeClass(){
    throw new UnsupportedOperationException();
}

...这至少会让子类编译。然后,您必须考虑是否需要getOrCreate()在子类中包括另一个工厂方法。

最后一步是为您的集合成员使用这样的抽象基类(对于列表而言,NB为“元素”,对于集合为“成员”)(如果可能的话,同样,使用包装类的范围,该类不受您的控制)或已经具有基类等),以实现最大程度的实现隐藏:

public abstract class AbstractSetMember implements NoVisibleConstructor {
    @Override
    public NoVisibleConstructor
            addOrGetExisting(GettableSet<? extends NoVisibleConstructor> gettableSet) {
        AbstractSetMember member = this;
        @SuppressWarnings("unchecked") // unavoidable!
        GettableSet<AbstractSetMembers> set = (GettableSet<AbstractSetMember>) gettableSet;
        if (gettableSet.contains( member )) {
            member = set.getGenuineFromImpostor( member );
            cleanUpAfterFindingGenuine( set );
        } else {
            addNewToSet( set );
        }
        return member;
    }

    abstract public void addNewToSet(GettableSet<? extends AbstractSetMember> gettableSet );
    abstract public void cleanUpAfterFindingGenuine(GettableSet<? extends AbstractSetMember> gettableSet );
}

...用法相当明显(在您SomeClassstatic工厂方法中):

SomeClass setMember = new SomeClass( param1, param2 ).addOrGetExisting( set );

0

哈希码的约定清楚地表明:

“如果根据Object方法,两个对象相等,则在两个对象中的每个对象上调用hashCode方法必须产生相同的整数结果。”

所以你的假设:

“为澄清起见,equals方法被覆盖,但它只检查一个字段,而不是全部。因此,两个被认为相等的Foo对象实际上可以具有不同的值,这就是为什么我不能只使用foo。”

是错的,您正在违约。如果我们看一下Set接口的“ contains”方法,就会发现:

boolean contains(Object o);
如果此集合包含指定的元素,则返回true。更正式地讲,当且仅当此集合包含元素“ e”使得o == null时,才返回true。e == null:o.equals(e)

要完成所需的操作,可以使用Map,在其中定义键,并将元素与键一起存储,该键定义对象之间如何不同或相等。


-2

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

<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();
}

8
奇怪的是,这个答案获得了如此多的好评,因为它没有回答问题,甚至没有试图以任何方式解决它。
戴维·康拉德

-2

以下可能是一种方法

   SharedPreferences se_get = getSharedPreferences("points",MODE_PRIVATE);
   Set<String> main = se_get.getStringSet("mydata",null);
   for(int jk = 0 ; jk < main.size();jk++)
   {
      Log.i("data",String.valueOf(main.toArray()[jk]));
   }

-2

尝试使用数组:

ObjectClass[] arrayName = SetOfObjects.toArray(new ObjectClass[setOfObjects.size()]);
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.