从集合中获取元素

时间:2011-09-02 12:22:55

标签: java collections set equals

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

24 个答案:

答案 0 :(得分:332)

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

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

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

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

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

答案 1 :(得分:94)

如果元素相等,那么获取元素是没有意义的。 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);
    }
}

答案 2 :(得分:21)

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

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

答案 3 :(得分:12)

如果你有一个相同的物体,为什么你需要一套?如果它只是一个键“相等”,那么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);

答案 4 :(得分:11)

遗憾的是,Java中的默认设置并非旨在提供&#34; get&#34;操作,jschreiner准确解释。

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

David Ogren所述,覆盖equals的实现以使不相等的对象等于&#34;,很容易导致维护问题。

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

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

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

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

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

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

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

由于这些原因,我实现了一个名为MagicSet的库,它使得Map&#34;透明&#34;致开发者。

https://github.com/ricpacca/magicset

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

答案 5 :(得分:10)

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

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

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

答案 6 :(得分:7)

使用Java 8,您可以:

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

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

答案 7 :(得分:4)

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

如果你只做了一次,那么这将不会很好,因为你会遍历所有的元素,但是当你在一个大集合上执行多次检索时,你会注意到差异。

答案 8 :(得分:2)

为什么:

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

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

来自JavaDocs

  

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

如何:

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

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

答案 9 :(得分:1)

答案 10 :(得分:1)

如何使用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);
    }
}

输出:
项目一,二

答案 11 :(得分:0)

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

http://pastebin.com/Qv6S91n9

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

答案 12 :(得分:0)

去过那里!!如果您正在使用Guava,可以快速将其转换为地图:

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

答案 13 :(得分:0)

是的,使用HashMap ...但是以一种特殊的方式:我预见到试图使用HashMap作为伪 - Set的陷阱是&#之间可能存在的混淆34;实际&#34; Map/Set和&#34;候选人&#34;的元素元素,即用于测试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 ......你想要{{1} to to&#34; taint&#34; contains以便任何使用它,除非它加入candidate使它成为&#34;诅咒&#34;。也许你可以让Map实现一个新的SomeClass接口。

更令人满意的解决方案是 GettableSet ,如下所示。但是,要使其工作,您要么负责Taintable的设计,以使所有构造函数不可见(或者......能够并且愿意为它设计和使用包装类):

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

PS这样一个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 } } 类的一个技术问题:可能会反对这样一个类本身就是NoVisibleConstructor,这可能是不可取的。实际上你总是可以添加一个伪参数final构造函数:

protected

...至少会让子类编译。然后,您必须考虑是否需要在子类中包含另一个protected SomeClass(){ throw new UnsupportedOperationException(); } 工厂方法。

最后一步是一个抽象基类(NB&#34;元素&#34;对于列表,&#34;成员&#34;对于一个集合)对于你的集合成员来说是这样的(在可能的情况下 - 再次,使用包装类的范围,其中类不在您的控制之下,或者已经具有基类等),以实现最大的实现隐藏:

getOrCreate()

...使用情况相当明显(在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 ); } SomeClass工厂方法中):

static

答案 14 :(得分:0)

如果你想要来自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;
    }
}

答案 15 :(得分:0)

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

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

所以HashSet无论如何都会使用HashMap,这意味着如果您直接使用HashMap并使用相同的值作为键和值,您将获得所需的效果并节省一些记忆。

答案 16 :(得分:0)

看起来合适的对象是番石榴中的Interner

  

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

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

答案 17 :(得分:0)

因为Set的任何特定实现可能是random access,也可能不是。{/ p>

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

当然,

get(E element)可能会或可能不会做同样的事情,具体取决于实施,但这个名称似乎不会导致同样的误解。

答案 18 :(得分:0)

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

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

所以您的假设:

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

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

布尔包含(对象o);
如果此集合包含指定的元素,则返回true。更多 形式上,当且仅当此集合包含一个元素时,才返回true “ e”使得o == null? e == null:o.equals(e)

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

答案 19 :(得分:0)

如果您有NavigableSet(例如TreeSet),可以执行以下操作:

public static <E> E get(NavigableSet<E> set, E key) {
    return set.tailSet(key, true).floor(key);
}

HashSet及其后代(例如LinkedHashSet)的事情有些棘手:

import java.util.*;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class Test {
    private static final Field mapField;
    private static final Method hashMethod;
    private static final Method getNodeMethod;
    private static final Field keyField;
    static {
        try {
            mapField = HashSet.class.getDeclaredField("map");
            mapField.setAccessible(true);
            hashMethod = HashMap.class.getDeclaredMethod("hash", Object.class);
            hashMethod.setAccessible(true);
            getNodeMethod = HashMap.class.getDeclaredMethod("getNode",
                    Integer.TYPE, Object.class);
            getNodeMethod.setAccessible(true);
            keyField = Class.forName("java.util.HashMap$Node").getDeclaredField("key");
            keyField.setAccessible(true);
        } catch (ReflectiveOperationException e) {
            throw new RuntimeException(e);
        }
    }

    public static <E> E get(HashSet<E> set, E key) {
        try {
            Object map = mapField.get(set);
            Object hash = hashMethod.invoke(null, key);
            Object node = getNodeMethod.invoke(map, hash, key);
            if (node == null)
                return null;
            @SuppressWarnings("unchecked")
            E result = (E)keyField.get(node);
            return result;
        } catch (ReflectiveOperationException e) {
            throw new RuntimeException(e);
        }
    }

    public static <E> E get(NavigableSet<E> set, E key) {
        return set.tailSet(key, true).floor(key);
    }

    public static void main(String[] args) {
        HashSet<Integer> s = new HashSet<>();
//      HashSet<Integer> s = new LinkedHashSet<>();
//      TreeSet<Integer> s = new TreeSet<>();
        for (int i = 0; i < 100_000; i++)
            s.add(i);
        Integer key = java.awt.event.KeyEvent.VK_FIND;
        Integer hidden = get(s, key);
        System.out.println(key);
        System.out.println(hidden);
        System.out.println(key.equals(hidden));
        System.out.println(key == hidden);
    }
}

答案 20 :(得分:-1)

尝试使用数组:

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

答案 21 :(得分:-1)

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

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

答案 22 :(得分:-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);   
 }
 }
}

答案 23 :(得分:-1)

以下可以是一种方法

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