问题
Why doesn't Set
provide an operation to get an element that equals another element?
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
I can ask whether the Set
contains an element equal to bar
, so why can't I get that element? :(
To clarify, the equals
method is overridden, but it only checks one of the fields, not all. So two Foo
objects that are considered equal can actually have different values, that's why I can't just use foo
.
回答1:
There would be no point of getting the element if it is equal. A Map
is better suited for this usecase.
If you still want to find the element you have no other option but to use the iterator:
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:
To answer the precise question "Why doesn't Set
provide an operation to get an element that equals another element?", 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 "model the mathematical set abstraction" (from the javadoc) and simply forgot to add the useful get()
method.
Now to the implied question "how do you get the element then": I think the best solution is to use a Map<E,E>
instead of a Set<E>
, to map the elements to themselves. In that way, you can efficiently retrieve an element from the "set", because the get() method of the Map
will find the element using an efficient hash table or tree algorithm. If you wanted, you could write your own implementation of Set
that offers the additional get()
method, encapsulating the Map
.
The following answers are in my opinion bad or wrong:
"You don't need to get the element, because you already have an equal object": the assertion is wrong, as you already showed in the question. Two objects that are equal still can have different state that is not relevant to the object equality. The goal is to get access to this state of the element contained in the Set
, not the state of the object used as a "query".
"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.
回答3:
Convert set to list, and then use get
method of list
Set<Foo> set = ...;
List<Foo> list = new ArrayList<Foo>(set);
Foo obj = list.get(0);
回答4:
If you have an equal object, why do you need the one from the set? If it is "equal" only by a key, an Map
would be a better choice.
Anyway, the following will do it:
Foo getEqual(Foo sample, Set<Foo> all) {
for (Foo one : all) {
if (one.equals(sample)) {
return one;
}
}
return null;
}
With Java 8 this can become a one liner:
return all.stream().filter(sample::equals).findAny().orElse(null);
回答5:
Default Set in Java is, unfortunately, not designed to provide a "get" operation, as jschreiner accurately explained.
The solutions of using an iterator to find the element of interest (suggested by dacwe) or to remove the element and re-add it with its values updated (suggested by KyleM), could work, but can be very inefficient.
Overriding the implementation of equals so that non-equal objects are "equal", as stated correctly by David Ogren, can easily cause maintenance problems.
And using a Map as an explicit replacement (as suggested by many), imho, makes the code less elegant.
If the goal is to get access to the original instance of the element contained in the set (hope I understood correctly your use case), here is another possible solution.
I personally had your same need while developing a client-server videogame with Java. In my case, each client had copies of the components stored in the server and the problem was whenever a client needed to modify an object of the server.
Passing an object through the internet meant that the client had different instances of that object anyway. In order to match this "copied" instance with the original one, I decided to use Java UUIDs.
So I created an abstract class UniqueItem, which automatically gives a random unique id to each instance of its subclasses.
This UUID is shared between the client and the server instance, so this way it could be easy to match them by simply using a Map.
However directly using a Map in a similar usecase was still inelegant. Someone might argue that using an Map might be more complicated to mantain and handle.
For these reasons I implemented a library called MagicSet, that makes the usage of an Map "transparent" to the developer.
https://github.com/ricpacca/magicset
Like the original Java HashSet, a MagicHashSet (which is one of the implementations of MagicSet provided in the library) uses a backing HashMap, but instead of having elements as keys and a dummy value as values, it uses the UUID of the element as key and the element itself as value. This does not cause overhead in the memory use compared to a normal HashSet.
Moreover, a MagicSet can be used exactly as a Set, but with some more methods providing additional functionalities, like getFromId(), popFromId(), removeFromId(), etc.
The only requirement to use it is that any element that you want to store in a MagicSet needs to extend the abstract class UniqueItem.
Here is a code example, imagining to retrieve the original instance of a city from a MagicSet, given another instance of that city with the same UUID (or even just its 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
}
回答6:
If your set is in fact a NavigableSet<Foo>
(such as a TreeSet
), and Foo implements Comparable<Foo>
, you can use
Foo bar = set.floor(foo); // or .ceiling
if (foo.equals(bar)) {
// use bar…
}
(Thanks to @eliran-malka’s comment for the hint.)
回答7:
With Java 8 you can do:
Foo foo = set.stream().filter(item->item.equals(theItemYouAreLookingFor)).findFirst().get();
But be careful, .get() throws a NoSuchElementException, or you can manipulate a Optional item.
回答8:
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);
If you only do one get this will not be very performing because you will loop over all your elements but when performing multiple retrieves on a big set you will notice the difference.
回答9:
Why:
It seems that Set plays a useful role in providing a means of comparison. It is designed not to store duplicate elements.
Because of this intention/design, if one were to get() a reference to the stored object, then mutate it, it is possible that the design intentions of Set could be thwarted and could cause unexpected behavior.
From the JavaDocs
Great care must be exercised if mutable objects are used as set elements. The behavior of a set is not specified if the value of an object is changed in a manner that affects equals comparisons while the object is an element in the set.
How:
Now that Streams have been introduced one can do the following
mySet.stream()
.filter(object -> object.property.equals(myProperty))
.findFirst().get();
回答10:
You better use the Java HashMap object for that purpose http://download.oracle.com/javase/1,5.0/docs/api/java/util/HashMap.html
回答11:
I know, this has been asked and answered long ago, however if anyone is interested, here is my solution - custom set class backed by HashMap:
http://pastebin.com/Qv6S91n9
You can easily implement all other Set methods.
回答12:
Been there done that!! If you are using Guava a quick way to convert it to a map is:
Map<Integer,Foo> map = Maps.uniqueIndex(fooSet, Foo::getKey);
回答13:
you can use Iterator class
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);
}
}
}
回答14:
If you want nth Element from HashSet, you can go with below solution, here i have added object of ModelClass in HashSet.
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:
If you look at the first few lines of the implementation of java.util.HashSet
you will see:
public class HashSet<E>
....
private transient HashMap<E,Object> map;
So HashSet
uses HashMap
interally anyway, which means that if you just use a HashMap
directly and use the same value as the key and the value you will get the effect you want and save yourself some memory.
回答16:
it looks like the proper object to use is the Interner from guava :
Provides equivalent behavior to String.intern() for other immutable types. Common implementations are available from the Interners class.
It also has a few very interesting levers, like concurrencyLevel, or the type of references used (it might be worth noting that it doesn't offer a SoftInterner which I could see as more useful than a WeakInterner).
回答17:
What about using the Arrays class ?
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);
}
}
output:
items one,two
回答18:
Because any particular implementation of Set may or may not be random access.
You can always get an iterator and step through the Set, using the iterators' next()
method to return the result you want once you find the equal element. This works regardless of the implementation. If the implementation is NOT random access (picture a linked-list backed Set), a get(E element)
method in the interface would be deceptive, since it would have to iterate the collection to find the element to return, and a get(E element)
would seem to imply this would be necessary, that the Set could jump directly to the element to get.
contains()
may or may not have to do the same thing, of course, depending on the implementation, but the name doesn't seem to lend itself to the same sort of misunderstandings.
回答19:
Yes, use HashMap
... but in a specialised way: the trap I foresee in trying to use a HashMap
as a pseudo-Set
is the possible confusion between "actual" elements of the Map/Set
, and "candidate" elements, i.e. elements used to test whether an equal
element is already present. This is far from foolproof, but nudges you away from the trap:
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);
}
}
Then do this:
SelfMappingHashMap<SomeClass> selfMap = new SelfMappingHashMap<SomeClass>();
...
SomeClass candidate = new SomeClass();
if( selfMap.contains( candidate ) ){
SomeClass realThing = selfMap.tryToGetRealFromCandidate( candidate );
...
realThing.useInSomeWay()...
}
But... you now want the candidate
to self-destruct in some way unless the programmer actually immediately puts it in the Map/Set
... you'd want contains
to "taint" the candidate
so that any use of it unless it joins the Map
makes it "anathema". Perhaps you could make SomeClass
implement a new Taintable
interface.
A more satisfactory solution is a GettableSet, as below. However, for this to work you have either to be in charge of the design of SomeClass
in order to make all constructors non-visible (or... able and willing to design and use a wrapper class for it):
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
}
Implementation:
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 ...
}
Your NoVisibleConstructor
classes then look like this:
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 one technical issue with such a NoVisibleConstructor
class: it may be objected that such a class is inherently final
, which may be undesirable. Actually you could always add a dummy parameterless protected
constructor:
protected SomeClass(){
throw new UnsupportedOperationException();
}
... which would at least let a subclass compile. You'd then have to think about whether you need to include another getOrCreate()
factory method in the subclass.
Final step is an abstract base class (NB "element" for a list, "member" for a set) like this for your set members (when possible - again, scope for using a wrapper class where the class is not under your control, or already has a base class, etc.), for maximum implementation-hiding:
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 );
}
... usage is fairly obvious (inside your SomeClass
's static
factory method):
SomeClass setMember = new SomeClass( param1, param2 ).addOrGetExisting( set );
回答20:
Quick helper method that might address this situation:
<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();
}
回答21:
Following can be an approach
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]));
}
回答22:
Try using an array:
ObjectClass[] arrayName = SetOfObjects.toArray(new ObjectClass[setOfObjects.size()]);
来源:https://stackoverflow.com/questions/7283338/getting-an-element-from-a-set