Object类(为什么重写equals和hashCode方法)
一.关键字:
Object、equals()、hashCode()
二.为什么需要重写:
众所周知,Object是所有类的父类。但,我们在实际开发中自定义自己类时,往往需要重写Object中equals和hashCode方法。为什么呢?首先看看Object的API吧。
Object类中原始写法是:
publicbooleanequals(Objectobj){
return(this==obj);
}
可见,原始equals比较的是2个对象的“内存地址”。但,我们往往是需要判断的是“逻辑上的内容”是否相等,如:String、Integer、Math...等等,时而我们关心是逻辑内容上是否相等,而不关心是否指向同一对象,所以所要重写。
再者,尽管Object是一个具体的类,但是设计它主要是为了扩展。它所要的非final方法(equals hashCode toString clone和finalize)都有通用约定(general contract),因为它们被设计成要被覆盖(override)的。任何一个类,它在覆盖这些方法的时候,都有责任遵守这些通用的约定;如果不能做到这一点,其它依赖这些约定的类(例如HashMap和HashSet)就无法结合该类一起正常运行。
JDK API上重写equals约定如下:
自反性:
对于任何非空引用值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
同时,API规定“当此方法被重写时,通常有必要重写hashCode方法,以维护hashCode方法的常规协定,该协定声明相等对象必须具有相等的哈希码“所以也要重写hashCode方法。
publicnativeinthashCode();
说明是一个本地方法,它的实现是根据本地机器相关的,方法返回的是对象的地址值。时而重写hashCode一是为了遵守API约定,二是重点提高对象比较时效率。
因为,在java集合对象中比较对象是这样的,如HashSet中是不可以放入重复对象的,那么在HashSet中又是怎样判定元素是否重复的呢?让我们看看源代码(首先要知道HashSet内部实际是通过HashMap封装的):
publicbooleanadd(Object o){//HashSet的add方法
returnmap.put(o, PRESENT)==null;
}
publicObject put(Object key, Object value){//HashMap的put方法
Object k = maskNull(key);
inthash= hash(k);
inti = indexFor(hash, table.length);
for(Entry e = table[i]; e !=null; e = e.next){
if(e.hash ==hash&& eq(k, e.key)){//从这里可见先比较hashcode
Object oldValue = e.value;
e.value = value;
e.recordAccess(this);
returnoldValue;
}
}
modCount++;
addEntry(hash, k, value, i);
returnnull;
}
所以在java的集合中,判断两个对象是否相等的规则是:
1,判断两个对象的hashCode是否相等
如果不相等,认为两个对象也不相等,完毕
如果相等,转入2
2,判断两个对象用equals运算是否相等
如果不相等,认为两个对象也不相等
如果相等,认为两个对象相等
为什么是两条准则,难道用第一条不行吗?不行,因为hashCode()相等时,equals()方法也可能不等,所以必须用第2条准则进行限制,才能保证加入的为非重复元素。
三.例子:
1.首先
classStudent{
Stringname;
intage;
Student(Stringname,intage){
this.name=name;
this.age=age;
}
//没有重写equals和hashCode
}
/**
*@authorydj
*@versionApr28,20103:12:42PM
*/
publicclassOverEqualsHashcodeTest{
publicstaticvoidmain(String[]args){
Set<Student> set=newHashSet<Student>();
Studentstu1=newStudent("ydj",26);
Studentstu2=newStudent("ydj",26);
set.add(stu1);
set.add(stu2);
System.out.println("set.size():"+set.size());
}
}
结果是:2.(这个无须解释)
2.现在重写equals方法如下:
publicbooleanequals(Objectobj){
System.out.println("--------equals()-----------:"+obj);
if(obj==null){
returnfalse;
}
if(!(objinstanceofStudent)){
returnfalse;
}else{
Studentoth=(Student)obj;
returnthis.age==oth.age&&this.name==oth.name;
}
//return true;
}
结果是:2.(为什么依然是2呢?!为什么连equals方法都没调用呢)
分析:这就是为什么要重写hashCode的原因(相等对象必须具有相等的哈希码)。因为现在的hashCode依然返回各自对象的地址,就是说明此时的hashCode肯定不相等,故根本不会调用equals()。
3.重写hashCode方法如下:
publicinthashCode(){
intres=17;
res=31*res+age;
res=31*res+name.hashCode();
returnres;
}
结果是:1.
如果这样重写hashCode:
publicinthashCode(){
intres=(int)(Math.random()*100);
returnres;
}
这样的话,就等于没有重写了。
四.设计equals()和hashCode():
A.设计equals()
[1]使用instanceof操作符检查“实参是否为正确的类型”。
[2]对于类中的每一个“关键域”,检查实参中的域与当前对象中对应的域值。
[2.1]对于非float和double类型的原语类型域,使用==比较;
[2.2]对于对象引用域,递归调用equals方法;
[2.3]对于float域,使用Float.floatToIntBits(afloat)转换为int,再使用==比较;
[2.4]对于double域,使用Double.doubleToLongBits(adouble)转换为int,再使用==比较;
[2.5]对于数组域,调用Arrays.equals方法。
B.设计hashCode()
[1]把某个非零常数值,例如17,保存在int变量result中;
[2]对于对象中每一个关键域f(指equals方法中考虑的每一个域):
[2.1]boolean型,计算(f ? 0 : 1);
[2.2]byte,char,short型,计算(int);
[2.3]long型,计算(int) (f ^ (f>>>32));
[2.4]float型,计算Float.floatToIntBits(afloat);
[2.5]double型,计算Double.doubleToLongBits(adouble)得到一个long,再执行[2.3];
[2.6]对象引用,递归调用它的hashCode方法;
[2.7]数组域,对其中每个元素调用它的hashCode方法。
[3]将上面计算得到的散列码保存到int变量c,然后执行result=37*result+c;
[4]返回result。
例子:
classUnit{
privateshortashort;
privatecharachar;
privatebyteabyte;
privatebooleanabool;
privatelongalong;
privatefloatafloat;
privatedoubleadouble;
privateUnitaObject;
privateint[]ints;
privateUnit[]units;
publicbooleanequals(Objecto){
if(!(oinstanceofUnit))
returnfalse;
Unitunit =(Unit)o;
returnunit.ashort==ashort
&& unit.achar==achar
&& unit.abyte==abyte
&& unit.abool==abool
&& unit.along==along
&& Float.floatToIntBits(unit.afloat)== Float
.floatToIntBits(afloat)
&& Double.doubleToLongBits(unit.adouble)== Double
.doubleToLongBits(adouble)
&& unit.aObject.equals(aObject)&&equalsInts(unit.ints)
&&equalsUnits(unit.units);
}
privatebooleanequalsInts(int[]aints){
returnArrays.equals(ints,aints);
}
privatebooleanequalsUnits(Unit[]aUnits){
returnArrays.equals(units,aUnits);
}
publicinthashCode(){
intresult = 17;
result = 31 * result +(int)ashort;
result = 31 * result +(int)achar;
result = 31 * result +(int)abyte;
result = 31 * result +(abool? 0 : 1);
result = 31 * result +(int)(along^(along>>> 32));
result = 31 * result + Float.floatToIntBits(afloat);
longtolong = Double.doubleToLongBits(adouble);
result = 31 * result +(int)(tolong ^(tolong >>> 32));
result = 31 * result +aObject.hashCode();
result = 31 * result +intsHashCode(ints);
result = 31 * result +unitsHashCode(units);
returnresult;
}
privateintintsHashCode(int[]aints){
intresult = 17;
for(inti = 0; i <aints.length; i++)
result = 31 * result +aints[i];
returnresult;
}
privateintunitsHashCode(Unit[]aUnits){
intresult = 17;
for(inti = 0; i <aUnits.length; i++)
result = 31 * result +aUnits[i].hashCode();
returnresult;
}
}
为什么要用31这个数呢?因为它是个奇素数。如果乘以偶数,并且乘法溢出的话,信息就会丢失,因为与2相乘等价于移位运算。使用素数效果不是很明显,但是习惯上都是使用素数计算散列结果。31有个好处的特性,即用移位代替乘法,可以得到更好的性能:31*I = =(I << 5)- I。现代的VM可以自动完成这样优化。——《Effctive java SE》
五.注意:
1.equals()不相等的两个对象,却并不能证明他们的hashcode()不相等。
换句话说,equals()方法不相等的两个对象,hashcode()有可能相等
2.hashcode()不等,一定能推出equals()也不等;hashcode()相等,equals()可能相等,也可能不等。
六.参考: