一龙巨、前言
前段時間使用list.remove(obj)的時候重寫了obj的equals方法嘀韧,因為list的remove是以equals來判斷標(biāo)準(zhǔn)的揪阶。但是,今天被公司的代碼掃描工具提示未重寫hashCode方法2锹蕖轨功!之前準(zhǔn)備面試時也多少看過,但是沒有細細研究過這個hashCode和equals到底背后是什么個關(guān)系容达,趁此機會古涧,總結(jié)一波。
本文章所用到的自定義測試對象類Stu:
public class Stu {
private String name;
private int age;
Stu(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
二花盐、equals的具體作用
首先要說的是equals是Object的方法羡滑,所以只能用于對象間菇爪,基本類型之間比較用“==”,反則他們的封裝類型可以用equals柒昏。
public static void main(String[] args) {
Stu s1 = new Stu("張三", 18);
Stu s2 = new Stu("張三", 18);
System.out.println("stu:" + s1.equals(s2));
Integer i1 = new Integer(18);
Integer i2 = new Integer(18);
System.out.println("Integer:" + i1.equals(i2));
String str1 = "張三";
String str2 = "張三";
System.out.println("String:" + str1.equals(str2));
}
很簡單凳宙,可以得到下面的結(jié)果:
stu:false
Integer:true
String:true
通過idea工具可以看到各自的equals實現(xiàn)代碼:
Stu
public boolean equals(Object obj) {
return (this == obj);
}
Integer
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
String
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String) anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
Stu因為沒有重寫equals方法,所以直接使用的父類Object的equals方法职祷,后面Integer和String都各自實現(xiàn)了自己的equals方法氏涩,所以Integer(基本類型)的equals實際上都是用的自己的實際值比較,String則是逐個char比較相等于否有梆。
三是尖、hashCode的具體作用
hashcode方法返回該對象的哈希碼值。支持該方法是為哈希表提供一些優(yōu)點泥耀,例如饺汹,java.util.Hashtable 提供的哈希表。
hashCode 的常規(guī)協(xié)定是:
在 Java 應(yīng)用程序執(zhí)行期間爆袍,在同一對象上多次調(diào)用 hashCode 方法時首繁,必須一致地返回相同的整數(shù),前提是對象上 equals 比較中所用的信息沒有被修改陨囊。從某一應(yīng)用程序的一次執(zhí)行到同一應(yīng)用程序的另一次執(zhí)行弦疮,該整數(shù)無需保持一致。以下情況不 是必需的:如果根據(jù) equals(java.lang.Object) 方法蜘醋,兩個對象不相等胁塞,那么在兩個對象中的任一對象上調(diào)用 hashCode 方法必定會生成不同的整數(shù)結(jié)果。但是压语,程序員應(yīng)該知道啸罢,為不相等的對象生成不同整數(shù)結(jié)果可以提高哈希表的性能。
實際上胎食,由 Object 類定義的 hashCode 方法確實會針對不同的對象返回不同的整數(shù)扰才。(這一般是通過將該對象的內(nèi)部地址轉(zhuǎn)換成一個整數(shù)來實現(xiàn)的,但是 JavaTM 編程語言不需要這種實現(xiàn)技巧厕怜。)
當(dāng)equals方法被重寫時衩匣,通常有必要重寫 hashCode 方法,以維護 hashCode 方法的常規(guī)協(xié)定粥航,該協(xié)定聲明相等對象必須具有相等的哈希碼琅捏。
上面是引用的官方文檔上面的一段話,我們需要他說人話:
- 對象equals方法參與運算的自身屬性attr不能被修改递雀,并且同一個對象的hashCode值任何時候的返回值都應(yīng)該相等柄延;
- hashCode不等的兩個對象equals一定不相等,但是hashCode相等的兩個對象equals不一定相等缀程;
- 根據(jù)規(guī)定搜吧,重寫對象的equals方法必須重寫hashCode方法市俊,盡管不寫也能通過編譯;
這里引用網(wǎng)上一個很容易理解的例子:
hashcode是用來查找的滤奈,如果你學(xué)過數(shù)據(jù)結(jié)構(gòu)就應(yīng)該知道秕衙,在查找和排序這一章有
例如內(nèi)存中有這樣的位置
0 1 2 3 4 5 6 7
而我有個類,這個類有個字段叫id,我要把這個類存放在以上8個位置之一僵刮,如果不用hashcode而任意存放,那么當(dāng)查找時就需要到這八個位置里挨個去找鹦牛,或者用二分法一類的算法搞糕。
但如果用hashCode那就會使效率提高很多。
我們這個類中有個字段叫id,那么我們就定義我們的hashCode為id%8曼追,然后把我們的類存放在取得得余數(shù)那個位置窍仰。比如我們的ID為9,9除8的余數(shù)為1礼殊,那么我們就把該類存在1這個位置驹吮,如果ID是13,求得的余數(shù)是5晶伦,那么我們就把該類放在5這個位置碟狞。這樣,以后在查找該類時就可以通過ID除 8求余數(shù)直接找到存放的位置了婚陪。但是如果兩個類有相同的hashCode怎么辦那(我們假設(shè)上面的類的id不是唯一的)族沃,例如9除以8和17除以8的余數(shù)都是1,那么這是不是合法的泌参,回答是:完全合法脆淹。那么如何判斷呢?在這個時候就需要定義equals了沽一。
也就是說盖溺,我們先通過 hashCode來判斷兩個類是否存放某個桶里,但這個桶里可能有很多類铣缠,那么我們就需要再通過 equals 來在這個桶里找到我們要的類烘嘱。
那么。重寫了equals()攘残,為什么還要重寫hashCode()呢拙友?
想想,你要在一個桶里找東西歼郭,你必須先要找到這個桶啊遗契,你不通過重寫hashCode()來找到桶,光重寫equals()有什么用啊病曾。
可能太過文本的東西沒有什么說服力牍蜂,那就來點干貨:
public static void main(String[] args) {
Stu s1 = new Stu("張三", 18);
Stu s2 = new Stu("張三", 18);
System.out.println("stu:" + s1.equals(s2));
Set<Stu> set = new HashSet<>();
set.add(s1);
System.out.println("s1 hashCode:" + s1.hashCode());
System.out.println("add s1 size:" + set.size());
set.add(s2);
System.out.println("s2 hashCode:" + s2.hashCode());
System.out.println("add s2 size::" + set.size());
}
輸出結(jié)果:
stu:false
s1 hashCode:1317241155
add s1 size:1
s2 hashCode:463175162
add s2 size::2
Java中的Set是不允許有重復(fù)元素的漾根,所以這里set的size由1變成了2,因為兩個Stu都是new出來的鲫竞,分配的地址不一樣辐怕,那么Set是通過equals來定義重復(fù)的嗎?
首先重寫Stu的equals方法:
@Override
public boolean equals(Object obj) {
if (obj == null){
return false;
}
if (obj.getClass() != getClass()){
return false;
}
return ((Stu)obj).getName().equals(getName());
}
輸出結(jié)果:
stu:true
s1 hashCode:713679046
add s1 size:1
s2 hashCode:1107557627
add s2 size::2
重寫equals方法从绘,name相同就讓equals返回true了寄疏,但是Set的size還是發(fā)生了改變,就說明不是有equals方法來定義重復(fù)的僵井,現(xiàn)在僅僅重寫hashCode方法:
@Override
public int hashCode() {
return getName().hashCode();
}
輸出結(jié)果:
stu:false
s1 hashCode:774889
add s1 size:1
s2 hashCode:774889
add s2 size::2
僅重寫了hashCode方法陕截,所以equals返回false,然后hashCode由name屬性的hashCode方法得到批什,所以hashCode相等农曲,但是Set的size還是改變了,這說明Set也不是僅僅依據(jù)hashCode來定義重復(fù)驻债。
那么現(xiàn)在將上述equals和hashCode兩者同時重寫乳规,輸出結(jié)果:
stu:true
s1 hashCode:774889
add s1 size:1
s2 hashCode:774889
add s2 size::1
結(jié)合上面引用的案例,可以類推合呐,hash類存儲結(jié)構(gòu)(HashSet暮的、HashMap等等)添加元素會有重復(fù)性校驗,校驗的方式就是先取hashCode判斷是否相等(找到對應(yīng)的位置淌实,該位置可能存在多個元素)青扔,然后再取equals方法比較(極大縮小比較范圍,高效判斷)翩伪,最終判定該存儲結(jié)構(gòu)中是否有重復(fù)元素微猖。
四、總結(jié)
- hashCode主要用于提升查詢效率缘屹,來確定在散列結(jié)構(gòu)中對象的存儲地址凛剥;
- 重寫equals()必須重寫hashCode(),二者參與計算的自身屬性字段應(yīng)該相同轻姿;
- hash類型的存儲結(jié)構(gòu)犁珠,添加元素重復(fù)性校驗的標(biāo)準(zhǔn)就是先取hashCode值,后判斷equals()互亮;
- equals()相等的兩個對象犁享,hashcode()一定相等;
- 反過來:hashcode()不等豹休,一定能推出equals()也不等炊昆;
- hashcode()相等,equals()可能相等,也可能不等凤巨。
五视乐、花邊:通用的hashCode重寫方案
初始化一個整形變量,為此變量賦予一個非零的常數(shù)值敢茁,比如int result = 17;
選取equals方法中用于比較的所有域佑淀,然后針對每個域的屬性進行計算:
- 如果是boolean值,則計算f ? 1:0
- 如果是byte\char\short\int,則計算(int)f
- 如果是long值彰檬,則計算(int)(f ^ (f >>> 32))
- 如果是float值伸刃,則計算Float.floatToIntBits(f)
- 如果是double值,則計算Double.doubleToLongBits(f)逢倍,然后返回的結(jié)果是long,再用規(guī)則(3)去處理long,得到int
- 如果是對象應(yīng)用奕枝,如果equals方法中采取遞歸調(diào)用的比較方式,那么hashCode中同樣采取遞歸調(diào)用hashCode的方式瓶堕。否則需要為這個域計算一個范式,比如當(dāng)這個域的值為null的時候症歇,那么hashCode 值為0
- 如果是數(shù)組郎笆,那么需要為每個元素當(dāng)做單獨的域來處理。如果你使用的是1.5及以上版本的JDK忘晤,那么沒必要自己去重新遍歷一遍數(shù)組宛蚓,java.util.Arrays.hashCode方法包含了8種基本類型數(shù)組和引用數(shù)組的hashCode計算,算法同上
給個簡單的例子:
@Override
public int hashCode() {
int result = 17;
result = 31 * result + getName().hashCode();
return result;
}