1

I know this question has already been asked on SO a couple of times, but I still haven't found a satisfying solution, and I'm unsure which way to go. The question is:

Why doesn't the Java library provide HashSet.get(Object o) and HashMap.getKey(Object o) methods that return the actual instance in the map providing an equal instance? Example:

// Retrieve a house with ID=10 that contains additional information like size,
// location and price.
houses.get(new House(10));

I think the best answer can be found here. So here's a mixture of answers that I'm aware of:

  • Why would you need the instance when you already have it? It doesn't make sense to try to get the same object you already have. The object has an identifier (which controls it's equality to other Foo types) plus any number of other fields that do not contribute to it's identity. I want to be able to get the object from the Set (which includes the extra fields) by constructing an 'equal' Foo object (text is taken from one of the comments). -> no answer

  • Iterate the Collection and search for the instance using equals(). This uses linear search and is extremely slow in big collections. -> bad answer

  • Use a HashMap instead of a HashSet I don't need a map and I think it's not adequate to return a map in a method like getHouses(). The getter should return a Set and not a Map.

  • Use TreeSet.ceiling - don't know

  • This hacky code below (Java 8 HashSet only) uses reflection and provides the missing functionality. I did not find something like this in other answers (no surprise). This could have been an acceptable solution if the target Java version is defined and future Java versions would finally provide such a method, now that we have default methods for interfaces. One could think of default E get(E o){stream().filter(e->e.equals(o)).findAny().orElse(null);}


// Alternative: Subclass HashSet/HashMap and provide a get()/getKey() methods
public static <T> T getFromSet(HashSet<T> set, T key) throws Exception {
    Field mapField = set.getClass().getDeclaredField("map");
    mapField.setAccessible(true);
    HashMap<T, Object> map = (HashMap) mapField.get(set);
    Method getNodeMethod = map.getClass().getDeclaredMethod("getNode",
            int.class, Object.class);
    getNodeMethod.setAccessible(true);

    return (T) ((Map.Entry) getNodeMethod.invoke(map, key.hashCode(),
            key)).getKey();
}

Here are the questions:

  • Is the best solution the use of HashMap<House, House> instead of HashSet<House>?
  • Is there another library out there that provides this functionality and supports concurrent access?
  • Do you know of a bug addressing this feature?

Similar questions on SO:

Community
  • 1
  • 1
steffen
  • 16,138
  • 4
  • 42
  • 81
  • 2
    IMHO, thinking in this statement "The object has an identifier (which controls it's equality to other Foo types) plus any number of other fields that do not contribute to it's identity" -- I THINK what you really need here is a Map with that identifier as a key and the object as a value. – Leo Aug 04 '14 at 11:43
  • It seems to me this question is `Why doesn't HashSet implement Map`. If you want a key-value pair relationship, that is a `Map`. If you want a `HashMap`, use it. – John B Aug 04 '14 at 11:44
  • "I know this question has already been asked on SO a couple of times" which means this is a duplicate, and should be closed as such. – Raedwald Aug 04 '14 at 11:49
  • "Is there another library out there" off topic request/recommendation for a third-party library – Raedwald Aug 04 '14 at 11:50
  • "Here are the questions": multiple questions=too broad. – Raedwald Aug 04 '14 at 11:51
  • 3
    Has it occurred to you that it isn't the world that's gone completely mad but you designed your system wrong? If two entities are equal, it means that they're not only interchangeable, they are in fact indistinguishable from each other. That's what we mean by saying two things are equal. If in your design that isn't the case, it means that you're abusing the `equals()` method. – biziclop Aug 04 '14 at 11:58
  • Remember that `HashSet` is backed by a `HashMap`, so you just might as well go for the map. – Kayaman Aug 04 '14 at 12:01
  • @Leo @JohnB I know what you mean, but still it's seems wrong to return a `Map` in methods like `getHouses()`. The caller expects a `Set` of houses and shouldn't write `getHouses().values()` to get it. – steffen Aug 04 '14 at 12:22
  • 1
    @biziclop [`equals() == true` => these instances are not indistinguishable from each other] this is not true for many classes in `java.*`. I'd rather say, two objects are equal if they refer to the same real-world instance. – steffen Aug 04 '14 at 12:32
  • @Kayaman I know (see the reflection code in my question). – steffen Aug 04 '14 at 12:33
  • Don't return a `Map` or a `Set`. Abstract it away by providing methods like `getHouseById(id)` and `getHouseBySomethingElse(value)` or even a `getSimilarHouses(House h)`. You're getting stuck on `Map` vs. `Set` instead of looking at the big picture. – Kayaman Aug 04 '14 at 12:38
  • @Kayaman That's maybe the way to go: Back the `Set` with a `Map` and provide methods with appropriate names. Still code would be easier if these methods would exist. Also read http://stackoverflow.com/a/18380755/1296402 ... – steffen Aug 04 '14 at 12:48
  • @steffen Which classes? – biziclop Aug 04 '14 at 15:05
  • 1
    @biziclop Here's an example: `HttpCookie c1 = new HttpCookie("name", "value"); c1.setComment("c1"); HttpCookie c2 = new HttpCookie("name", "value"); c2.setComment("c2"); System.out.printf("c1.comment=%s, c2.comment=%s, c1.equals(c2)=%b\n", c1.getComment(), c2.getComment(), c1.equals(c2));` – steffen Aug 04 '14 at 15:50
  • @steffen The Servlet API is a great example of a poorly designed API. Having said that, cookie comments are optional and disposable, you shouldn't be expecting them to be of any particular value. Two cookies with the same name and value should be considered identical. In your example, house size, location and price are neither optional nor disposable. – biziclop Aug 04 '14 at 16:06
  • 1
    @biziclop I was using the House to create an example that shows the *principle, nothing more* of retrieving elements from a `Set`. The data I'm dealing with is completely different, both their identifying and their optional values are defined in RFCs. As I said: Two instances should be equal when they refer to the same real-world object. ...so now I'm curious: Would *you* include the comment in `equals()`? You contradicted yourself: The 2 Cookies should be considered equal while being distiguishable from each other. – steffen Aug 04 '14 at 16:45
  • @steffen But they should be considered indistinguishable by the code. – biziclop Aug 04 '14 at 21:49

2 Answers2

2

The reason this behaviour hasn't been catered for is that creating a House instance with invalid data just to obtain one with valid data is really poor design.

Composition is the correct solution here:

/** immutable class containing all the fields defining identity */
public final class HouseIdentifier {
    private final String id;
}

public class House {
    private final HouseIdentifier id;
    /** all the mutable, ephemeral properties of the house should go here */
    private int size;
    private Person owner;
}

If you design your class hierarchy like this, then all you need for your lookups is a simple and straightforward Map<HouseIdentifier, House>.

biziclop
  • 48,926
  • 12
  • 77
  • 104
  • The good thing about this is that you make it explicit whether you're dealing with something that identifies your instance or whether it is the "real" instance containing "all the data". That's a good advantage. – steffen Aug 04 '14 at 16:49
0

Map doesn't have a getKey(Object o) because it's not a bidirectional map. It only maps keys to values, not the other way around.

Set doesn't have get(Object o) because that's the job for a Map.

Mapping a House object to another House object is just bad design on your part. You want to get a House by an address or a number or similar, so you have one or more maps that give you those mappings (or more likely, a database). Your question makes sense only to you, because you're thinking "in the wrong way".

Your "wrong way of thinking" is evidenced by your statement

I don't need a map and I think it's not adequate to return a map in a method like getHouses(). The getter should return a Set and not a Map.

I have never heard that a getter can't return a Map. Although I would probably name it getHouseMap(). You're creating a huge problem out of a trivial little issue. This is the job for a database anyways, so your dataset must be quite small.

Kayaman
  • 72,141
  • 5
  • 83
  • 121