Reputation: 31885
I read interface Map's JavaDoc and it says this:
Many methods in Collections Framework interfaces are defined in terms of the equals method. For example, the specification for the
containsKey(Object key)
method says: "returns true if and only if this map contains a mapping for a keyk
such that(key==null ? k==null : key.equals(k))
." This specification should not be construed to imply that invokingMap.containsKey
with a non-null
argumentkey
will causekey.equals(k)
to be invoked for any keyk
. Implementations are free to implement optimizations whereby theequals
invocation is avoided, for example, by first comparing the hash codes of the two keys. (TheObject.hashCode()
specification guarantees that two objects with unequal hash codes cannot be equal.)
My understanding is when containsKey()
is invoked, both hashCode() and equals() wil be called, therefore I wrote my own code to test it.
HappyDay class will be stored as a key in HashMap, I override hashCode() and equals() methods, and add System.out.println("invoking hashCode()" + this.happyHour);
and System.out.println("invoking equals()");
to check whether the method is invoked or not.
public class HappyDay {
private final int happyHour;
public HappyDay(int hh) {
this.happyHour = hh;
}
public int getHappyHour() {
return this.happyHour;
}
@Override
public boolean equals(Object o) {
System.out.println("invoking equals()");
if (o == null) {return false;}
if (o == this) {return true;}
//this is an easy overridden, if happy hour equal, objects will be equal.
if (o instanceof HappyDay) {
HappyDay other = (HappyDay) o;
int otherHappyHour = other.getHappyHour();
if (this.happyHour == otherHappyHour) {
return true;
}
}
return false;
}
@Override
public int hashCode() {
System.out.println("invoking hashCode()" + this.happyHour);
int hash = 7;
hash = hash + this.happyHour;
return hash;
}
}
public class Main {
public static void main(String[] args) {
Map<HappyDay,String> hm = new HashMap<>();
HappyDay hd1 = new HappyDay(1);
HappyDay hd2 = new HappyDay(2);
hm.put(hd1, "hd1");
hm.put(hd2, "hd2");
if(hm.containsKey(hd2)){
System.out.println("found");
}else{
System.out.println("not exist");
}
}
}
The Main class is to put two HappyDay instances into HashMap, after insertion(put() method), I invoke hm.containsKey(hd2)
, as I quoted from JavaDoc, it should call hashCode() first and then call equals(), but the output is
invoking hashCode()1 //call put()
invoking hashCode()2 //call put()
invoking hashCode()2 //call containsKey()
found
I am expecting there's another output line should be invoking equals()
, can anyone explain this for me why equals() is not called?
Upvotes: 4
Views: 3174
Reputation: 129497
Hash map first checks key equality via ==
; only if that fails does it carry on to check with equals
.
Now, you put hd2
into the map as a key, then you are checking containsKey
using the same object as an argument, so the ==
test passes and equals
is never called.
Checking if a map contains a given key comes down to checking if getEntry(key)
returns null
. Let's look at the source:
360 final Entry<K,V> getEntry(Object key) {
361 int hash = (key == null) ? 0 : hash(key.hashCode());
362 for (Entry<K,V> e = table[indexFor(hash, table.length)];
363 e != null;
364 e = e.next) {
365 Object k;
366 if (e.hash == hash &&
367 ((k = e.key) == key || (key != null && key.equals(k))))
368 return e;
369 }
370 return null;
371 }
On line 367, we can see that an ==
test is performed before a equals
test. The short-circuiting of ||
completely skips the equals
test if ==
passes, which is what is happening here.
This is probably implemented as it is to skip potentially expensive equals
methods (e.g. String#equals
which has to check each character of the given string). The equals
contract also states that o1.equals(o2)
should be true if o1 == o2
, so this is a valid optimization.
Let's do a sanity check by modifying your code slightly:
if (hm.containsKey(new HappyDay(2))) {
System.out.println("found");
} else {
System.out.println("not exist");
}
Now the output is:
invoking hashCode()1 invoking hashCode()2 invoking hashCode()2 invoking equals() found
Notice that equals
is invoked. This makes sense because we are invoking containsKey
with a new but equal object, so the ==
test returns false and the equals
test is executed.
Upvotes: 7
Reputation: 178253
The source code for HashMap
tells what's going on.
The containsKey
method calls getEntry
:
360 final Entry<K,V> More ...getEntry(Object key) {
361 int hash = (key == null) ? 0 : hash(key.hashCode());
362 for (Entry<K,V> e = table[indexFor(hash, table.length)];
363 e != null;
364 e = e.next) {
365 Object k;
366 if (e.hash == hash &&
367 ((k = e.key) == key || (key != null && key.equals(k))))
368 return e;
369 }
370 return null;
371 }
If the hashes match, then the key
s are compared with ==
first. Since you pass in the original hd2
object, the ==
operator returns true
(it's the same object), and the short-circuiting ||
operator never evaluates the right side, and equals
isn't called.
If you were to make another HappyDay
, also passing 2
, then ==
would yield false
and equals
would be called.
As for why ==
is there first, I would have to guess that it's on average more efficient to do a fast ==
first, and only fall back on a potentially more costly call to equals
if they aren't the same object.
Upvotes: 5