在java中,万物皆对象,所有的对象都继承于Object类,Object类有两个方法equals和hashCode。equals一般用来比较两个对象的内容是否相等,而hashCode一般用来提高容器的查询效率。
public native int hashCode();
public boolean equals(Object obj) {
return (this == obj);
}
equals在没有重写的情况下和==是一样的,对于值类型,比较的是值,对于引用类型则比较的是对象的首地址。
hashCode我们一般很少直接使用,它返回的是一个int值,在HashMap中对对象进行存储时,它会调用hashCode方法来比较两个对象是否相等。查询对象的时候也会调用hashCode以提高查询效率。
一般来说equals方法比较相等,则hashCode一定相等,反过来不一定成立,因为具有相同的hashCode不一定是相同的对象。一个好的hashCode函数应该能做到为不同的对象产生不相等的hash值。
如果我们对equals方法进行重写时,一般强烈建议对hashCode方法重写,以保证相同的对象返回相同的hash值,不同的对象返回不同的hash值。因为我们在使用HashMap、HashSet的时候会使用hashCode和equals来判断存入的是否是同一个对象。如果不重写hashCode,那么会继承Object中的,它返回的是一个对象的地址,对于两个对象,这个地址是永远不会相等。如果hashCode都不相等,就不会再调用equals方法进行比较了。
当从HashSet集合中查找某个对象时,java系统首先会调用对象的hashCode()方法来获得该对象的哈希码,然后根据哈希码找到对应的存储区域,最后取得该存储区域内的每个元素与该对象进行equals方法比较。这样就不用遍历集合中的所有元素就可以得到结论,可见HashSet集合具有很好的对象检索性能。
下面我们通过几个例子,演示一下对象重写或不重写hashCode与equals能否被存入HashSet中:
public class MyObject {
public int x;
public int y;
public MyObject(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + x;
result = prime * result + y;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final MyObject myObject = (MyObject) obj;
if (x != myObject.x || y != myObject.y) {
return false;
}
return true;
}
}
测试1: MyObject类重写了父类Object中的hashCode和equals方法,如果两个MyObject对象的x y值相等的话,那么他们的hashCode的值就会相等,equals后返回true,测试代码如下:
public class Test {
public static void main(String[] args) {
HashSet<MyObject> set = new HashSet<MyObject>();
MyObject r1 = new MyObject(3, 3);
MyObject r2 = new MyObject(5, 5);
MyObject r3 = new MyObject(3, 3);
set.add(r1);
set.add(r2);
set.add(r3);
set.add(r1);
System.out.println("size:" + set.size());
}
}
我们向HashSet中存入了4个对象,打印set集合大小为size:2,为什么为2?因为我们重写了MyObject类的hashCode方法,只要MyObject对象的x,y属性值相等,那么它的hashCode值就是相等的。所以先比较hashCode的值,r1和r2对象的x y属性值不等,那么hashCode就不等,所以r2对象可以放进去。r3对象的x y属性值和r1对象的属性值相同,所以hashCode是相等的,然后再比较r1和r3的equals方法,也是相等,所以r1、r3对象时相等的,所以r3不能放进去。最后一个r1肯定也是放不进去的。
测试2:把MyObject对象的hashCode方法注释,即不重写Object对象的hashCode方法,再运行一下代码
运行结果:size:3
因为hashCode方法没有被重写,使用Object中的hashCode方法返回的是对象的地址,不同的实例对象的hashCode是不同的,所以hashset中可以存入r1,r2,r3
测试3:把MyObject对象中的equals方法注释掉,直接返回false,不注释hashCode方法,运行一下代码:
运行结果:size:3
这个结果让人比较意外,首先r1和r2对象比较hashCode不相等,那么r2放入hashset中。再来看一下r3,比较r1和r3的hashCode方法,是相等的,然后比较他们的equals方法,因为equals始终返回false,所以r1和r3也是不相等的,所以r3可以放入set。再看最后一个r1(为防混淆,我们称它为r4吧),r1和r4 hashCode相等,再比较equals返回false,所以r1和r4不相等,同理r2和r4,r3和r4也不相等,所以r4应该可以放入集合中,那为什么集合的大小是3呢?
我们有必要翻一下HashSet的源码了:
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
我们会发现HashSet是基于HashMap实现的,我们打开HashMap的put方法:
public V put(K key, V value) {
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;
}
我们主要看一下if中的判断:
if(e.hash == hash && ((k = e.key) == key || key.equals(k)))
首先是判断hashCode是否相等,不相等的话,直接跳过,相等的话,再来比较这两个对象是否相等或者这两个对象的equals方法,因为进行的是“ 或 ” 操作,所以只要有一个成立即可,那这里我们就可以解释了,其实上面的那个集合的大小是3, 因为最后的一个r1没有放进去,因为(k = e.key) == key 即 r1==r1返回true就return了,所以没有放进去了。集合的大小是3,如果我们将hashCode方法设置成始终返回false的话,这个集合就是4了。