首先来说说两者的区别,接着给出得出这些区别的原因,最后从HashMap和HashSet实现的角度谈谈这两个集合类对hashCode和equals的使用,其实说白了,个人认为这两个区别也只是在HashMap和HashSet中体现的比较明显点;
两者的关系:
(1):两个对象如果equals,那么他们的hashCode也相等
(2):两个对象如果hashCode相等,但他们不一定equals
(3):两个对象hashCode值不等,他们一定不equals
(4):两个对象不equals,他们的hashCode值不一定不等
也就是说我们在判断两个对象等不等的时候,首先判断两者的hashCode值等不等,不等的话两个对象直接就不等了,相等的话再去看equals,这点我们可以从HashMap的使用中体现出来;
首先我们通过实例来具体看下这两者的区别:
首先来印证结论(2):
public class Test {
@Override
public int hashCode() {
System.out.println("执行了hashCode方法");
return 1;
}
@Override
public boolean equals(Object obj) {
System.out.println("执行了equals方法");
return false;
}
public static void main(String[] args) {
HashMap<Test, String> map = new HashMap<>();
Test test1 = new Test();
Test test2 = new Test();
map.put(test1, "test1");
map.put(test2, "test2");
System.out.println("map的长度: "+map.size());
}
}
在此之前我们有必要看看HashMap的put方法实现原理了,查看源码如下:
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
put方法的实现过程是首先调用hash(key)计算出当前key对应的hash值,在hash(key)方法中会执行当前key对象的hashCode方法,在计算出hash值之后,会调用indexFor算出当前hash值对应要存储到的数组的下标位置,因为HashMap是数组加链表实现的,数组部分就是通过我们key的hash值来对应位置的,而链表部分存储的是hash值相同的所有键值对,indexFor的目的就是找到当前hash值对应的数组下标,有了这个下标之后你会看到一个for循环,他会遍历当前数组下标里面的链表,通过调用key的equals方法来判断当前链表中是否有与将要插入的键值对的key值相同的元素,没有的话,会将当前当前键值对插入到这个链表的表头,如果有的话,则会替换掉原先已经存在的值;
可以看到,put方法首先调用的是hashCode,随后在找到对应hash值在数组中的存储位置之后才会执行equals方法找链表中有没有将要插入的键值对的key值的,为了方便,我们可以这样理解,hashCode找对应hash值在数组中的位置,equals找当前key值在该数组位置相应链表中位置的,直观点就是下面这幅图;
在测试中我们将Test类作为了HashMap的key值,随后调用了HashMap的put方法,接下来我们看看输出结果:
执行了hashCode方法
执行了hashCode方法
执行了equals方法
map的长度: 2
第一行的输出是调用map.put(test1, "test1");执行的,第二行的输出是调用map.put(test2, "test2");执行的,因为上面测试中我们让Test类的hashCode方法的返回值都是1,那么此时所有put进map的key的hash值都是相同的,此时在
执行map.put(test2, "test2");的时候就需要调用equals方法来查看当前hash值对应的数组位置的链表中有没有与当前key equals的键值对存在,因为我们在Test类中令equals的返回值是false,所以肯定就不存在了,因此map的长度为2;
我们修改上面的测试代码,将Test类中的equals方法的返回值修改为true,也就是修改成如下代码:
public class Test {
@Override
public int hashCode() {
System.out.println("执行了hashCode方法");
return 1;
}
@Override
public boolean equals(Object obj) {
System.out.println("执行了equals方法");
return true;
}
public static void main(String[] args) {
HashMap<Test, String> map = new HashMap<>();
Test test1 = new Test();
Test test2 = new Test();
map.put(test1, "test1");
map.put(test2, "test2");
System.out.println("map的长度: "+map.size());
}
}
查看输出:
执行了hashCode方法
执行了hashCode方法
执行了equals方法
map的长度: 1
不同于上面的测试,这里的map大小变成了1,原因在于我们将equals返回值设置成了true,因为两次put操作hashCode方法返回值是相同的,所以他会执行equals查看当前hash值对应的数组位置处的链表中是否存在于当前key equals的键值对,因为equals始终返回true,那么就是存在了,根据上面对put源码的分析,当前最新的值将替换掉原先的值,不信你可以试试输出key值等于test1或者test2的值,结果都将是"test2";
如果我们把测试代码修改成下面这样:
public class Test {
public static int count = 0;
@Override
public int hashCode() {
System.out.println("执行了hashCode方法");
count++;
return count;
}
@Override
public boolean equals(Object obj) {
System.out.println("执行了equals方法");
return true;
}
public static void main(String[] args) {
HashMap<Test, String> map = new HashMap<>();
Test test1 = new Test();
Test test2 = new Test();
map.put(test1, "test1");
map.put(test2, "test2");
System.out.println("map的长度: "+map.size());
}
}
查看输出:
执行了hashCode方法
执行了hashCode方法
map的长度: 2
你会发现根本连equals方法都不会执行,原因在于我们让Test的hashCode方法每次都返回的值都是不一样的,这样两次put方法执行之后key值对应的hash值根本就不相等,也就是他们根本就是存储在数组中的不同下标处了,不会存储到同一下标处当然不用equals方法来查看到底是存储到下标对应处链表的哪个位置了;
从上面的三个测试可以看出来,HashMap在put时候首先调用的是hashCode方法,如果发现当前hash值对应的数组位置处的链表为空的话是不会执行equals的,也就是hashCode方法先于equals方法执行;