首页 文章

从集合中获取元素

提问于
浏览
266

为什么 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 对象实际上可以具有不同的值,'s why I can' t只使用 foo .

20 回答

  • 94
  • -1

    如果元素相等,就没有必要获得元素 . 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);
        }
    }
    
  • 10

    要回答准确的问题“ Why 不提供一个操作来获得一个等于另一个元素的元素?", the answer would be: because the designers of the collection framework were not very forward looking. They didn't anticipate your very legitimate use case, naively tried to "模型数学集抽象”(来自javadoc)并且忘记添加有用的 get() 方法 .

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

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

    "You don't need to get the element, because you already have an equal object":断言是错误的,正如您在问题中已经表明的那样 . 两个相等的对象仍然可以具有与对象相等无关的不同状态 . 目标是访问 Set 中包含的元素的这种状态,而不是用作"query"的对象的状态 .

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

  • 19

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

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

    如果你有一个相同的对象,为什么你需要一个对象?如果它只是一个键"equal",那么 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);
    
  • 331

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

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

    重写equals的实现,以便"equal"正确地说明不相等的对象"equal",很容易导致维护问题 .

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

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


    在使用Java开发客户端 - 服务器视频游戏时,我个人也有同样的需求 . 就我而言,每个客户端都有存储在服务器中的组件的副本,问题是客户端需要修改服务器的对象 .

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

    所以我创建了一个抽象类UniqueItem,它自动为其子类的每个实例提供一个随机唯一id .

    此UUID在客户端和服务器实例之间共享,因此通过简单地使用Map可以很容易地匹配它们 .

    然而,在类似的用例中直接使用Map仍然不够优雅 . 有人可能会争辩说,使用Map可能会更难以保持和处理 .

    出于这些原因,我实现了一个名为MagicSet的库,它使用了一个将“透明”映射到开发人员 .

    https://github.com/ricpacca/magicset


    与原始Java HashSet一样,MagicHashSet(它是库中提供的MagicSet的一个实现)使用支持HashMap,但它不使用元素作为键和虚拟值作为值,而是使用元素的UUID作为键和元素本身作为 Value . 与普通的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
    }
    
  • 7

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

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

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

  • 0

    使用Java 8,您可以:

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

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

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

    如果你只做一个得到这将不会很好,因为你将遍历所有元素,但在大集上执行多次检索时你会发现差异 .

  • -1

    为什么:

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

    由于这种意图/设计,如果要获得()对存储对象的引用,然后对其进行修改,则可能会阻止Set的设计意图并可能导致意外行为 .

    来自JavaDocs

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

    怎么样:

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

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

    因为Set的任何特定实现可能是也可能不是random access .

    你总是得到一个iterator并逐步执行Set,使用迭代器的 next() 方法在找到相等的元素后返回你想要的结果 . 无论实现如何,这都有效 . 如果实现不是随机访问(图片是链接列表支持的Set),则接口中的 get(E element) 方法将具有欺骗性,因为它必须迭代集合以找到要返回的元素,并且 get(E element) 似乎暗示了这一点有必要,Set可以直接跳转到要获取的元素 .

    当然,取决于实施情况, contains() 可能会也可能不会做同样的事情,但这个名称似乎不会导致同样的误解 .

  • 12

    我知道,很久以前就已经提出并回答了这个问题,但是如果有人有兴趣,这是我的解决方案 - 由HashMap支持的自定义集合类:

    http://pastebin.com/Qv6S91n9

    您可以轻松实现所有其他Set方法 .

  • 4

    去过也做过!!如果您正在使用Guava,可以快速将其转换为 Map :

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

    是的,使用 HashMap ...但是以一种特殊的方式:我试图使用 HashMap 作为伪 Set 的陷阱是 Map/Set 和"candidate"元素的"actual"元素之间可能存在的混淆,即用于测试是否为 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 "taint" candidate 以便任何使用它,除非它加入 Map 使它"anathema" . 也许你可以让 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 class:可能有人反对说这样的类本质上是 final ,这可能是不合需要的 . 实际上你总是可以添加一个虚拟的无参数 protected 构造函数:

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

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

    Final step 是一个抽象基类(对于列表为NB "element",对于一个集合为"member"),对于您的集合成员是这样的(如果可能的话 - 再次,使用包装类的范围,其中类不在您的控制之下,或者已经有一个基础类等),用于最大限度地实现隐藏:

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

    如果你想要来自HashSet的第n个元素,你可以使用下面的解决方案,这里我在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;
        }
    }
    
  • 0

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

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

    所以 HashSet 无论如何都使用 HashMap ,这意味着如果你直接使用 HashMap 并使用与键和值相同的值,你将得到你想要的效果并节省一些内存 .

  • 11

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

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

    你可以使用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);   
     }
     }
    }
    
  • 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]));
       }
    
  • -1

    尝试使用数组:

    ObjectClass[] arrayName = SetOfObjects.toArray(new ObjectClass[setOfObjects.size()]);
    

相关问题