为什么要复写equals()的同时最好复写hashcode()

我们都知道所有的类都从Object继承了equals()和hashcode(),先来看看equals(),和hashcode()在Object中的实现:

public boolean equals(Object obj) {
return (this == obj);
}

public native int hashCode();

可以知道equals比较的实际是引用的地址,hashcode实际是一个本地方法,可以理解为返回的是
一个具体的地址。在实际使用中如果我们要使用向HashSet这类不能重复添加的集合时就必须重写equals和hashcode方法,如果你不使用集合的话,自己定义一个一般的类一般情况下是不需要重写的。为什么要重写hashcode和equals呢?hashset的内部实际是一个HashMap我们先看一下在向HashMap中添加一个元素时实际发生了什么

public V put(K key, V value) {//在此映射中关联指定值与指定键。如果该映射以前包含了一个该键的映射关系,则旧值被替换。
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
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;
}
void recordAccess(HashMap<K,V> m) {
}

可以发现它首先调用key的hashcode()得到一个hash值,再调用indexFor(HashMap的底层也是用一个数组保存key和value的,有兴趣可以去看看)得到key的索引。下面将该索引对应的值赋给一个Entry(保存着key和value),e.hash == hash && ((k = e.key) == key || key.equals(k))将Entry中的key和要放入的比较,调用equals和要放入的元素比较。如果为真,用vlaue替换oldvalue,接下来记录这个值(实际重新new了一个HashMap)返oldvalue.如果为假,将HashMap长度+1,返回null。
上面分析put的过程发现,put方法内部实际上调用了对象的hashcode方法和equals方法。这就是为什么在重写equals的同时最好复写hashcode方法,如果重写equals而不重写hashcode方法会发送什么呢?在举例之前我们看看在java中,String和Integer这些预定义的包装类中已经帮我们重写了equals和hashcode:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = count;
if (n == anotherString.count) {
char v1[] = value;
char v2[] = anotherString.value;
int i = offset;
int j = anotherString.offset;
while (n-- != 0) {
if (v1[i++] != v2[j++])
return false;
}
return true;
}
}
return false;
}
public int hashCode() {
int h = hash;
if (h == 0 && count > 0) {
int off = offset;
char val[] = value;
int len = count;

for (int i = 0; i < len; i++) {
h = 31*h + val[off++];
}
hash = h;
}
return h;
}

可以发现String的equals实际比较的是对象的内容而不是对象引用的地址,hashcode方法是根据字符串的长度来生成hash值的。所以在实际使用的有的初学者可能会发现这样的问题:

A a = new A("hello");
A b = new A("hello");
a.equals(b);//false
String a = new String("hello");
String b = new String("hello");
a.equals(b);//true,A没有重写equals,而String帮我们重写了

回到上面的问题:假如重写equals而不重写hashcode方法会发什么?重写之后有什么变化
为了输出方便,我们这里使用HashSet(不重复的散列集)

public class EqualsAndHashcode {

public static void main(String[] args) {
HashSet h = new HashSet();
A a1 = new A(1);//不作任何重写默认使用Object的
A a2 = new A(1);// output:false false 2018699554,1311053135
System.out.println(a1 == a2);
System.out.println(a1.equals(a2));
System.out.println(a1.hashCode() + "," + a2.hashCode());
// 只复写euqals
B b1 = new B(1);//output:true,118352462,1550089733
B b2 = new B(1);// false,118352462,865113938
B b3 = new B(2);//[cn.dx.cellection.B@5c647e05
//,cn.dx.cellection.B@33909752,
//cn.dx.cellection.B@70dea4e
System.out.println(b1.equals(b2) + "," + b1.hashCode() + ","
+ b2.hashCode());
System.out.println(b1.equals(b3) + "," + b1.hashCode() + ","
+ b3.hashCode());
h.add(b1);
h.add(b2);
h.add(b3);
System.out.println(h);
// 同时重写code,和equals
C c1 = new C(1);
C c2 = new C(1);
C c3 = new C(2);
System.out.println(c1.equals(c2) + "," + c1.hashCode() + ","
+ c2.hashCode());
System.out.println(c1.equals(c3) + "," + c1.hashCode() + ","
+ c3.hashCode());
h = new HashSet();
h.add(c1);
h.add(c2);
h.add(c3);
System.out.println(h);
// true,2,2
// false,2,4
//[cn.dx.cellection.C@2, cn.dx.cellection.C@4]
}

}

class A {
private int a;

A(int i) {
this.a = i;
}

}

class B {
private int a;

B(int i) {
this.a = i;
}

@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (null == obj)
return false;
if (obj instanceof B) {
B other = (B) obj;
if (this.a == other.a)
return true;
}
return false;
}

}
class C{
private int a;
C(int i){
this.a = i;
}
@Override
public boolean equals(Object obj) {
if(this == obj)
return true;
if (null== obj)
return false;
if(obj instanceof C){
C other = (C)obj;
if(this.a == other.a)
return true;
}
return false;
}
@Override
public int hashCode() {
// TODO Auto-generated method stub
return a*2<<10%10;
}


}
/**
false
false
2018699554,1311053135
true,118352462,1550089733
false,118352462,865113938
[cn.dx.cellection.B@5c647e05, cn.dx.cellection.B@33909752, cn.dx.cellection.B@70dea4e]
true,2,2
false,2,4
[cn.dx.cellection.C@2, cn.dx.cellection.C@4]

*/

从上面可以发现,如果不作任何复写默认使用Object的,只复写equals方法向hashset添加时都添加进去了,都复写不会添加重复的元素。所以,如果两个对象equals返回true,hashcode也应该是相同的,不然相同的元素也被添加进去了,违背了我们使用集合的初衷。
反之:两个元素equals返回true,hashcode不一定相等,同时都重写才相等。
hashcode相等的两个元素equals一定为true,即充分不必要。
复写equal方法应该满足:
自反性:对于任何非空引用值 x,x.equals(x) 都应返回 true。
对称性:对于任何非空引用值 x 和 y,当且仅当 y.equals(x) 返回 true 时,x.equals(y) 才应返回 true。
传递性:对于任何非空引用值 x、y 和 z,如果 x.equals(y) 返回 true,并且 y.equals(z) 返回 true,那么 x.equals(z) 应返回 true。
一致性:对于任何非空引用值 x 和 y,多次调用 x.equals(y) 始终返回 true 或始终返回 false,前提是对象上 equals 比较中所用的信息没有被修改。
对于任何非空引用值 x,x.equals(null) 都应返回 false。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值