equals和HashCode深入理解以及Hash算法原理
1.深入理解equals():
提醒:Object类中的equals方法和“==”是一样的,没有区别,即俩个对象的比较是比较他们的栈内存中存储的内存地址。而String类,Integer类等等一些类,是重写了equals方法,才使得equals和“==不同”,他们比较的是值是不是相等。所以,当自己创建类时,自动继承了Object的equals方法,要想实现不同的等于比较,必须重写equals方法。
我们看下面这个例子:
packagecn.galc.test;
publicclassTestEquals{
publicstaticvoidmain(String[]args){
/
这里使用构造方法Cat()在堆内存里面new出了两只猫,
这两只猫的color,weight,height都是一样的,
但c1和c2却永远不会相等,这是因为c1和c2分别为堆内存里面两只猫的引用对象,
里面装着可以找到这两只猫的地址,但由于两只猫在堆内存里面存储在两个不同的空间里面,
所以c1和c2分别装着不同的地址,因此c1和c2永远不会相等。
/
Catc1=newCat(1,1,1);
Catc2=newCat(1,1,1);
System.out.println("c1==c2的结果是:"+(c1==c2));//false
System.out.println("c1.equals(c2)的结果是:"+c1.equals(c2));//false
}
}
classCat{
intcolor,weight,height;
publicCat(intcolor,intweight,intheight){
this.color=color;
this.weight=weight;
this.height=height;
}
}
画出内存分析图分析c1和c2比较的结果,当执行Catc1=newCat(1,1,1);Catc2=newCat(1,1,1);之后内存之中布局如下图:
由此我们看出,当我们new一个对象时,将在内存里加载一份它自己的内存,而不是共用!对于static修饰的变量和方法则保存在方法区中,只加载一次,不会再多copy一份内存。
所以我们在判断俩个对象逻辑上是否相等,即对象的内容是否相等不能直接使用继承于Object类的equals()方法,我们必须得重写equals()方法,改变这个方法默认的实现。下面在Cat类里面重写这个继承下来的equals()方法:
classCat{
intcolor,weight,height;
publicCat(intcolor,intweight,intheight){
this.color=color;
this.weight=weight;
this.height=height;
}
/
这里是重写相等从Object类继承下来的equals()方法,改变这个方法默认的实现,
通过我们自己定义的实现来判断决定两个对象在逻辑上是否相等。
这里我们定义如果两只猫的color,weight,height都相同,
那么我们就认为这两只猫在逻辑上是一模一样的,即这两只猫是“相等”的。
/
publicbooleanequals(Objectobj){
if(obj==null){
returnfalse;
}
else{
/
instanceof是对象运算符。
对象运算符用来测定一个对象是否属于某个指定类或指定的子类的实例。
如果左边的对象是右边的类创建的对象,则运算结果为true,否则为false。
/
if(objinstanceofCat){
Catc=(Cat)obj;
if(c.color==this.color&&c.weight==this.weight&&c.height==this.height){
returntrue;
}
}
}
returnfalse;
}
}
设计思路很简单:先判断比较对象是否为null—>判断比较对象是否为要比较类的实例—–>比较俩个成员变量是否完全相等。
//另外一种常用重写方法
@Override
publicbooleanequals(Objectobj){
if(this==obj)returntrue;
if(obj==null)returnfalse;
if(getClass()!=obj.getClass())returnfalse;
Peopleother=(People)obj;
if(age!=other.age)returnfalse;
if(firstName==null){
if(other.firstName!=null)returnfalse;
}elseif(!firstName.equals(other.firstName))returnfalse;
if(lastName==null){
if(other.lastName!=null)returnfalse;
}elseif(!lastName.equals(other.lastName))returnfalse;
returntrue;
}
这样通过在类中重写equals()方法,我们可以比较在同一个类下不同对象是否相等了。
2.Hash算法原理以及HashCode深入理解
Java中的Collection有两类,一类是List,一类是Set。List内的元素是有序的,元素可以重复。Set元素无序,但元素不可重复。要想保证元素不重复,两个元素是否重复应该依据什么来判断呢?用Object.equals方法。但若每增加一个元素就检查一次,那么当元素很多时,后添加到集合中的元素比较的次数就非常多了。也就是说若集合中已有1000个元素,那么第1001个元素加入集合时,它就要调用1000次equals方法。这显然会大大降低效率。于是Java采用了哈希表的原理。
当Set接收一个元素时根据该对象的内存地址算出hashCode,看它属于哪一个区间,再这个区间里调用equeals方法。【特别注意】这里需要注意的是:当俩个对象的hashCode值相同的时候,Hashset会将对象保存在同一个位置,但是他们equals返回false,所以实际上这个位置采用链式结构来保存多个对象。
上面方法确实提高了效率。但一个面临问题:若两个对象equals相等,但不在一个区间,因为hashCode的值在重写之前是对内存地址计算得出,所以根本没有机会进行比较,会被认为是不同的对象。所以Java对于eqauls方法和hashCode方法是这样规定的:
1如果两个对象相同,那么它们的hashCode值一定要相同。也告诉我们重写equals方法,一定要重写hashCode方法,也就是说hashCode值要和类中的成员变量挂上钩,对象相同–>成员变量相同—->hashCode值一定相同。
2如果两个对象的hashCode相同,它们并不一定相同,这里的对象相同指的是用eqauls方法比较。
下面来看一下一个具体的例子:RectObject对象:
packagecom.weijia.demo;
publicclassRectObject{
publicintx;
publicinty;
publicRectObject(intx,inty){
this.x=x;
this.y=y;
}
@Override
publicinthashCode(){
finalintprime=31;
intresult=1;
result=primeresult+x;
result=primeresult+y;
returnresult;
}
@Override
publicbooleanequals(Objectobj){
if(this==obj)
returntrue;
if(obj==null)
returnfalse;
if(getClass()!=obj.getClass())
returnfalse;
finalRectObjectother=(RectObject)obj;
if(x!=other.x){
returnfalse;
}
if(y!=other.y){
returnfalse;
}
returntrue;
}
}
我们重写了父类Object中的hashCode和equals方法,看到hashCode和equals方法中,如果两个RectObject对象的x,y值相等的话他们的hashCode值是相等的,同时equals返回的是true;
下面是测试代码:
packagecom.weijia.demo;
importjava.util.HashSet;
publicclassDemo{
publicstaticvoidmain(String[]args){
HashSetset=newHashSet();
RectObjectr1=newRectObject(3,3);
RectObjectr2=newRectObject(5,5);
RectObjectr3=newRectObject(3,3);
set.add(r1);
set.add(r2);
set.add(r3);
set.add(r1);
System.out.println("size:"+set.size());
}
}
我们向HashSet中存入到了四个对象,打印set集合的大小,结果是多少呢?运行结果:size:2
为什么会是2呢?这个很简单了吧,因为我们重写了RectObject类的hashCode方法,只要RectObject对象的x,y属性值相等那么他的hashCode值也是相等的,所以先比较hashCode的值,r1和r2对象的x,y属性值不等,所以他们的hashCode不相同的,所以r2对象可以放进去,但是r3对象的x,y属性值和r1对象的属性值相同的,所以hashCode是相等的,这时候在比较r1和r3的equals方法,因为他么两的x,y值是相等的,所以r1,r3对象是相等的,所以r3不能放进去了,同样最后再添加一个r1也是没有没有添加进去的,所以set集合中只有一个r1和r2这两个对象
下面我们把RectObject对象中的hashCode方法注释,即不重写Object对象中的hashCode方法,在运行一下代码:
运行结果:size:3
这个结果也是很简单的,首先判断r1对象和r2对象的hashCode,因为Object中的hashCode方法返回的是对象本地内存地址的换算结果,不同的实例对象的hashCode是不相同的,同样因为r3和r1的hashCode也是不相等的,但是r1==r1的,所以最后set集合中只有r1,r2,r3这三个对象,所以大小是3
下面我们把RectObject对象中的equals方法中的内容注释,直接返回false,不注释hashCode方法,运行一下代码:
运行结果:size:3这个结果就有点意外了,我们来分析一下:
首先r1和r2的对象比较hashCode,不相等,所以r2放进set中,再来看一下r3,比较r1和r3的hashCode方法,是相等的,然后比较他们两的equals方法,因为equals方法始终返回false,所以r1和r3也是不相等的,r3和r2就不用说了,他们两的hashCode是不相等的,所以r3放进set中,再看r4,比较r1和r4发现hashCode是相等的,在比较equals方法,因为equals返回false,所以r1和r4不相等,同一r2和r4也是不相等的,r3和r4也是不相等的,所以r4可以放到set集合中,那么结果应该是size:4,那为什么会是3呢?
这时候我们就需要查看HashSet的源码了,下面是HashSet中的add方法的源码:
/
Addsthespecifiedelementtothissetifitisnotalreadypresent.
Moreformally,addsthespecifiedelementetothissetif
thissetcontainsnoelemente2suchthat
(e==null?e2==null:e.eqwww.shanxiwang.netuals(e2)).
Ifthissetalreadycontainstheelement,thecallleavestheset
unchangedandreturnsfalse.
@parameelementtobeaddedtothisset
@returntrueifthissetdidnotalreadycontainthespecified
element
/
publicbooleanadd(Ee){
returnmap.put(e,PRESENT)==null;
}
这里我们可以看到其实HashSet是基于HashMap实现的,我们在点击HashMap的put方法,源码如下:
/
Associatesthespecifiedvaluewiththespecifiedkeyinthismap.
Ifthemappreviouslycontainedamappingforthekey,theold
valueisreplaced.
@paramkeykeywithwhichthespecifiedvalueistobeassociated
@paramvaluevaluetobeassociatedwiththespecifiedkey
@returnthepreviousvalueassociatedwithkey,or
nulliftherewasnomappingforkey.
(Anullreturncanalsoindicatethatthemap
previouslyassociatednullwithkey.)
/
publicVput(Kkey,Vvalue){
if(key==null)
returnputForNullKey(value);
inthash=hash(key);
inti=indexFor(hash,table.length);
for(Entrye=table[i];e!=null;e=e.next){
Objecwww.sm136.comtk;
if(e.hash==hash&&((k=e.key)==key||key.equals(k))){
VoldValue=e.value;
e.value=value;
e.recordAccess(this);
returnoldValue;
}
}
modCount++;
addEntry(hash,key,value,i);
returnnull;
}
我们主要来看一下if的判断条件,
首先是判断hashCode是否相等,不相等的话,直接跳过,相等的话,然后再来比较这两个对象是否相等或者这两个对象的equals方法,因为是进行的或操作,所以只要有一个成立即可,那这里我们就可以解释了,其实上面的那个集合的大小是3,因为最后的一个r1没有放进去,以为r1==r1返回true的,所以没有放进去了。所以集合的大小是3,如果我们将hashCode方法设置成始终返回false的话,这个集合就是4了。
最后我们在来看一下hashCode造成的内存泄露的问题:看一下代码:
packagecom.weijia.demo;
importjava.util.HashSet;
publicclassDemo{
publicstaticvoidmain(String[]args){
HashSetset=newHashSet();
RectObjectr1=newRectObject(3,3);
RectObjectr2=newRectObject(5,5);
RectObjectr3=newRectObject(3,3);
set.add(r1);
set.add(r2);
set.add(r3);
r3.y=7;
System.out.println("删除前的大小size:"+set.size());
set.remove(r3);
System.out.println("删除后的大小size:"+set.size());
}
}
运行结果:
删除前的大小size:3
删除后的大小size:3
擦,发现一个问题了,而且是个大问题呀,我们调用了remove删除r3对象,以为删除了r3,但事实上并没有删除,这就叫做内存泄露,就是不用的对象但是他还在内存中。所以我们多次这样操作之后,内存就爆了。看一下remove的源码:
/
Removesthespecifiedelementfromthissetifitispresent.
Moreformally,removesanelementesuchthat
(o==null?e==null:o.equals(e)),
ifthissetcontainssuchanelement.Returnstrueif
thissetcontainedtheelement(orequivalently,ifthisset
changedasaresultofthecall).(Thissetwillnotcontainthe
elementoncethecallreturns.)
@paramoobjecttoberemovedfromthisset,ifpresent
@returntrueifthesetcontainedthespecifiedelement
/
publicbooleanremove(Objecto){
returnmap.remove(o)==PRESENT;
}
然后再看一下remove方法的源码:
/
Removesthemappingforthespecifiedkeyfromthismapifpresent.
@paramkeykeywhosemappingistoberemovedfromthemap
@returnthepreviousvalueassociatedwithkey,or
nulliftherewasnomappingforkey.
(Anullreturncanalsoindicatethatthemap
previouslyassociatednullwithkey.)
/
publicVremove(Objectkey){
Entrye=removeEntryForKey(key);
return(e==null?null:e.value);
}
在看一下removeEntryForKey方法源码:
/
Removesandreturnstheentryassociatedwiththespecifiedkey
intheHashMap.ReturnsnulliftheHashMapcontainsnomapping
forthiskey.
/
finalEntryremoveEntryForKey(Objectkey){
inthash=(key==null)?0:hash(key);
inti=indexFor(hash,table.length);
Entryprev=table[i];
Entrye=prev;
while(e!=null){
Entrynext=e.next;
Objectk;
if(e.hash==hash&&
((k=e.key)==key||(key!=null&&key.equals(k)))){
modCount++;
size--;
if(prev==e)
table[i]=next;
else
prev.next=next;
e.recordRemoval(this);
returne;
}
prev=e;
e=next;
}
returne;
}
我们看到,在调用remove方法的时候,会先使用对象的hashCode值去找到这个对象,然后进行删除,这种问题就是因为我们在修改了r3对象的y属性的值,又因为RectObject对象的hashCode方法中有y值参与运算,所以r3对象的hashCode就发生改变了,所以remove方法中并没有找到r3了,所以删除失败。即r3的hashCode变了,但是他存储的位置没有更新,仍然在原来的位置上,所以当我们用他的新的hashCode去找肯定是找不到了。
|
|