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


当前回答

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

其他回答

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

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

要准确回答“为什么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.

如果你有一个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);
    }
}

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

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

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

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

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

HashSet在内部使用HashMap,这意味着如果你直接使用HashMap并使用与键相同的值,你会得到你想要的效果,并节省一些内存。