从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

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

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

888347 次浏览

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

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

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

你总是可以得到一个迭代器,然后步进遍历Set,一旦找到相等的元素,使用迭代器的next()方法返回你想要的结果。这与实现无关。如果实现不是随机访问(想象一个链表支持的Set),接口中的get(E element)方法将具有欺骗性,因为它必须迭代集合以找到要返回的元素,而get(E element)似乎暗示这是必要的,即Set可以直接跳转到要获取的元素。

contains()可能需要也可能不需要做同样的事情,当然,这取决于实现,但名称似乎并不会导致同样的误解。

要准确回答“为什么是否提供获取等于另一个元素的元素的操作?”这个问题,答案将是:因为集合框架的设计者不是很有前瞻性。他们没有预料到你非常合理的用例,天真地试图“建模数学集合抽象”(从javadoc),只是忘记添加有用的get()方法。

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

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

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

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

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

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

如果你只做一次获取,这将不是很好的执行,因为你将循环所有的元素,但当你在一个大的集合上执行多次检索时,你会注意到区别。

我知道,这个问题很久以前就被问过,但如果有人感兴趣,这里是我的解决方案-自定义集类支持HashMap:

http://pastebin.com/Qv6S91n9

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

尝试使用数组:

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

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

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

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

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

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

不幸的是,Java中的Default Set并不是为提供“get”操作而设计的,正如jschreiner准确解释的那样。

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

重写equals的实现,使不相等的对象“相等”,正如大卫Ogren所正确声明的那样,很容易导致维护问题。

恕我直言,使用Map作为显式替换(正如许多人建议的那样)会使代码不那么优雅。

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


我个人在用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
}

使用Java 8,你可以做到:

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

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

我在那里做过!!如果你正在使用番石榴,一个快速的方法将它转换为地图是:

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

是的,使用HashMap…但是以一种特殊的方式:我预见到试图使用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接口。

更令人满意的解决方案是< em > GettableSet < / em >,如下所示。然而,要实现这一点,你必须要么负责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()工厂方法。

最后一步是一个抽象基类(注意“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 );

原因:

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

由于这种意图/设计,如果要获得()对存储对象的引用,然后更改它,则Set的设计意图可能会受到阻碍,并可能导致意想不到的行为。

JavaDocs

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

怎样去:

现在已经引入了流,我们可以做以下事情

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

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

遵循可以是一种方法

   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]));
}
如果你想要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;
}
}

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

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

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

看起来合适的对象是来自guava的内在动机:

为其他不可变提供与String.intern()等效的行为 类型。常见的实现可以从内在动机中获得 类。< / p >

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

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

如果根据Object方法,两个对象是相等的,那么在这两个对象上调用hashCode方法必须产生相同的整数结果。

所以你的假设是

为了澄清,equals方法被重写,但它只检查其中之一 田地,不是全部。所以两个相等的Foo对象可以 有不同的值,这就是为什么我不能只使用foo。”< / p >

是错误的,你违反了合同。如果我们看Set接口的"contains"方法,我们有:

boolean contains(对象o);< br > 如果此集合包含指定的元素,则返回true。更多的 形式上,当且仅当此集合包含元素时返回true "e"使得o==null ?E ==null: o.equals(E)

为了实现您想要的效果,您可以使用Map,在其中定义键并使用定义对象如何彼此不同或相等的键存储元素。

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