Question

This questions is prompted by strange HashMap.put() behaviour

I think I understand why Map<K,V>.put takes a K but Map<K,V>.get takes an Object, it seems not doing so will break too much existing code.

Now we get into a very error-prone scenario:

java.util.HashMap<Long, String> m = new java.util.HashMap<Long, String>();
m.put(5L,"Five"); // compiler barfs on m.put(5, "Five")
m.contains(5); // no complains from compiler, but returns false

Couldn't this have been solved by returning true if the Long value was withing int range and the values are equal?

Was it helpful?

Solution

Here is the source from Long.java

public boolean equals(Object obj) {
    if (obj instanceof Long) {
        return value == ((Long)obj).longValue();
    }
    return false;
}

I.e. it needs to be a Long type to be equal. I think the key difference between:

long l = 42L
int i = 42;
l == i

and your example above is that with primitives an implicit widening of the int value can occur, however with object types there are no rules for implicitly converting from Integer to a Long.

Also check out Java Puzzlers, it has a lot of examples similar to this.

OTHER TIPS

Generally speaking, although it is not strictly expressed in the contract for equals(), objects should not consider themselves equal to another object that is not of the exact same class (even if it is a subclass). Consider the symmetric property - if a.equals(b) is true, then b.equals(a) must also be true.

Let's have two objects, foo of class Super, and bar of class Sub, which extends Super. Now consider the implementation of equals() in Super, specifically when it's called as foo.equals(bar). Foo only knows that bar is strongly typed as an Object, so to get an accurate comparison it needs to check it's an instance of Super and if not return false. It is, so this part is fine. It now compares all the instance fields, etc. (or whatever the actual comparison implementation is), and finds them equal. So far, so good.

However, by the contract it can only return true if it know that bar.equals(foo) is going to return true as well. Since bar can be any subclass of Super, it's not clear whether the equals() method is going to be overridden (and if probably will be). Thus to be sure that your implementation is correct, you need to write it symmetrically and ensure that the two objects are the same class.

More fundamentally, objects of different classes cannot really be considered equal - since in this case, only one of them can be inserted into a HashSet<Sub>, for example.

Yes, but it all comes down to the comparing algorithm and how far to take the conversions. For example, what do you want to happen when you try m.Contains("5")? Or if you pass it an array with 5 as the first element? Simply speaking, it appears to be wired up "if the types are different, the keys are different".

Then take a collection with an object as the key. What do you want to happen if you put a 5L, then try to get 5, "5", ...? What if you put a 5L and a 5 and a "5" and you want to check for a 5F?

Since it's a generic collection (or templated, or whatever you wish to call it), it would have to check and do some special comparing for certain value types. If K is int then check if the object passed is long, short, float, double, ..., then convert and compare. If K is float then check if the object passed is ...

You get the point.

Another implementation could have been to throw an exception if the types didn't match, however, and I often wish it did.

Your question seems reasonable on its face, but it would be a violation of the general conventions for equals(), if not its contract, to return true for two different types.

Part of the Java language design was for Objects to never implicitly convert to other types, unlike C++. This was part of making Java a small, simple language. A reasonable portion of C++'s complexity comes from implicit conversions and their interactions with other features.

Also, Java has a sharp and visible dichotomy between primitives and objects. This is different from other languages where this difference is hidden under the covers as an optimization. This means that you can't expect Long and Integer to act like long and int.

Library code can be written to hide these differences, but that can actually do harm by making the programming environment less consistent.

So you code should be....

java.util.HashMap<Long, String> m = new java.util.HashMap<Long, String>();
m.put(5L, "Five"); // compiler barfs on m.put(5, "Five")
System.out.println(m.containsKey(5L)); // true

You are forgetting that java is autoboxing your code, so the above code would be equivelenet to

java.util.HashMap<Long, String> m = new java.util.HashMap<Long, String>();
m.put(new Long(5L), "Five"); // compiler barfs on m.put(5, "Five")
System.out.println(m.containsKey(new Long(5))); // true
System.out.println(m.containsKey(new Long(5L))); // true

So a part of your problem is the autoboxing. The other part is that you have different types as other posters have stated.

The other answers adequately explain why it fails, but none of them address how to write code that is less error prone around this issue. Having to remember to add type-casts (no compiler help), suffix primitives with L and so forth is just not acceptable IMHO.

I highly recommend using the GNU trove library of collections when you have primitives (and in many other cases). For example, there is a TLongLongHashMap that stores things interally as primitive longs. As a result, you never end up with boxing/unboxing, and never end up with unexpected behaviors:

TLongLongHashMap map = new TLongLongHashMap();
map.put(1L, 45L);
map.containsKey(1); // returns true, 1 gets promoted to long from int by compiler
int x = map.get(1); // Helpful compiler error. x is not a long
int x = (int)map.get(1); // OK. cast reassures compiler that you know
long x = map.get(1); // Better.

and so on. There is no need to get the type right, and the compiler gives you an error (that you can correct or override) if you do something silly (try to store a long in an int).

The rules of auto-casting mean that comparisons work properly as well:

if(map.get(1) == 45) // 1 promoted to long, 45 promoted to long...all is well

As a bonus, the memory overhead and runtime performance is much better.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top