为什么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
。
如果元素相等,就没有必要获得元素。 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);
}
}
为了这个目的,你最好使用Java HashMap对象http://download.oracle.com/javase/1,5.0/docs/api/java/util/HashMap.html
因为Set的任何特定实现可能是也可能不是random access。
你总是可以获得一个iterator并逐步执行Set,使用迭代器的next()
方法在找到相等元素后返回所需的结果。无论实现如何,这都有效。如果实现不是随机访问(图片是链接列表支持的Set),接口中的get(E element)
方法将具有欺骗性,因为它必须迭代集合以找到要返回的元素,并且get(E element)
似乎暗示这有必要,Set可以直接跳转到要获取的元素。
当然,contains()
可能会或可能不会做同样的事情,具体取决于实施情况,但这个名称似乎并不适用于同样的误解。
我知道,很久以前就已经问过并回答了这个问题,但是如果有人有兴趣,这是我的解决方案 - 由HashMap支持的自定义集合类:
您可以轻松实现所有其他Set方法。
去过也做过!!如果您正在使用Guava,可以快速将其转换为地图:
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
界面。
一个更令人满意的解决方案是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 );
}
...用法相当明显(在你的SomeClass
的static
工厂方法内):
SomeClass setMember = new SomeClass( param1, param2 ).addOrGetExisting( set );
你可以使用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);
}
}
}
如果你想要来自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
并使用相同的值作为键和值,你将得到你想要的效果,并为自己节省一些记忆。
可以解决这种情况的快速帮助方法:
<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
中包含的元素的这种状态,而不是用作“查询”的对象的状态。
“你没有其他选择,只能使用迭代器”:这是对集合的线性搜索,对于大集合来说效率非常低(具有讽刺意味的是,在内部,Set
被组织为可以有效查询的哈希映射或树)。不要这样做!通过使用该方法,我已经在现实系统中看到了严重的性能问题。在我看来,缺少get()
方法的可怕之处并不在于解决它有点麻烦,但大多数程序员将使用线性搜索方法而不考虑其含义。
以下可以是一种方法
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]));
}
尝试使用数组:
ObjectClass[] arrayName = SetOfObjects.toArray(new ObjectClass[setOfObjects.size()]);
将set转换为list,然后使用get
列表方法
Set<Foo> set = ...;
List<Foo> list = new ArrayList<Foo>(set);
Foo obj = list.get(0);
如果你有一个相同的对象,为什么你需要一个对象?如果它只是一个键“相等”,那么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);
遗憾的是,Java中的默认设置不是为了提供“获取”操作而设计的,正如jschreiner准确解释的那样。
使用迭代器查找感兴趣的元素(由dacwe建议)或删除元素并重新添加其更新值(由KyleM建议)的解决方案可能有效,但效率可能非常低。
如David Ogren正确陈述的那样,重写equals的实现以使不相等的对象“相等”,很容易导致维护问题。
使用Map作为显式替换(如许多人的建议),imho使代码不那么优雅。
如果目标是访问集合中包含的元素的原始实例(希望我正确理解您的用例),这是另一种可能的解决方案。
在使用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
}
如果你的集合实际上是NavigableSet<Foo>
(例如TreeSet
)和Foo implements Comparable<Foo>
,你可以使用
Foo bar = set.floor(foo); // or .ceiling
if (foo.equals(bar)) {
// use bar…
}
(感谢@ eliran-malka对提示的评论。)
使用Java 8,您可以:
Foo foo = set.stream().filter(item->item.equals(theItemYouAreLookingFor)).findFirst().get();
但要小心,.get()会抛出NoSuchElementException,或者您可以操作Optional项。
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);
如果你只做一个得到这将不会很好,因为你将遍历所有元素,但在大集上执行多次检索时你会发现差异。
为什么:
似乎Set在提供比较手段方面发挥了有用的作用。它旨在不存储重复元素。
由于这种意图/设计,如果要获取()对存储对象的引用,然后对其进行修改,则可能会阻止Set的设计意图并可能导致意外行为。
来自JavaDocs
如果将可变对象用作集合元素,则必须非常小心。如果在对象是集合中的元素的同时以影响等于比较的方式更改对象的值,则不指定集合的行为。
怎么样:
现在已经介绍了Streams,可以执行以下操作
mySet.stream()
.filter(object -> object.property.equals(myProperty))
.findFirst().get();