一、前言
前段時(shí)間使用list.remove(obj)的時(shí)候重寫了obj的equals方法,因?yàn)閘ist的remove是以equals來判斷標(biāo)準(zhǔn)的。但是,今天被公司的代碼掃描工具提示未重寫hashCode方法??!之前準(zhǔn)備面試時(shí)也多少看過,但是沒有細(xì)細(xì)研究過這個(gè)hashCode和equals到底背后是什么個(gè)關(guān)系,趁此機(jī)會(huì),總結(jié)一波。
本文章所用到的自定義測試對(duì)象類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的方法,所以只能用于對(duì)象間,基本類型之間比較用“==”,反則他們的封裝類型可以用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實(shí)現(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因?yàn)闆]有重寫equals方法,所以直接使用的父類Object的equals方法,后面Integer和String都各自實(shí)現(xiàn)了自己的equals方法,所以Integer(基本類型)的equals實(shí)際上都是用的自己的實(shí)際值比較,String則是逐個(gè)char比較相等于否。
三、hashCode的具體作用
hashcode方法返回該對(duì)象的哈希碼值。支持該方法是為哈希表提供一些優(yōu)點(diǎn),例如,java.util.Hashtable 提供的哈希表。
hashCode 的常規(guī)協(xié)定是:
在 Java 應(yīng)用程序執(zhí)行期間,在同一對(duì)象上多次調(diào)用 hashCode 方法時(shí),必須一致地返回相同的整數(shù),前提是對(duì)象上 equals 比較中所用的信息沒有被修改。從某一應(yīng)用程序的一次執(zhí)行到同一應(yīng)用程序的另一次執(zhí)行,該整數(shù)無需保持一致。以下情況不 是必需的:如果根據(jù) equals(java.lang.Object) 方法,兩個(gè)對(duì)象不相等,那么在兩個(gè)對(duì)象中的任一對(duì)象上調(diào)用 hashCode 方法必定會(huì)生成不同的整數(shù)結(jié)果。但是,程序員應(yīng)該知道,為不相等的對(duì)象生成不同整數(shù)結(jié)果可以提高哈希表的性能。
實(shí)際上,由 Object 類定義的 hashCode 方法確實(shí)會(huì)針對(duì)不同的對(duì)象返回不同的整數(shù)。(這一般是通過將該對(duì)象的內(nèi)部地址轉(zhuǎn)換成一個(gè)整數(shù)來實(shí)現(xiàn)的,但是 JavaTM 編程語言不需要這種實(shí)現(xiàn)技巧。)
當(dāng)equals方法被重寫時(shí),通常有必要重寫 hashCode 方法,以維護(hù) hashCode 方法的常規(guī)協(xié)定,該協(xié)定聲明相等對(duì)象必須具有相等的哈希碼。
上面是引用的官方文檔上面的一段話,我們需要他說人話:
- 對(duì)象equals方法參與運(yùn)算的自身屬性attr不能被修改,并且同一個(gè)對(duì)象的hashCode值任何時(shí)候的返回值都應(yīng)該相等;
- hashCode不等的兩個(gè)對(duì)象equals一定不相等,但是hashCode相等的兩個(gè)對(duì)象equals不一定相等;
- 根據(jù)規(guī)定,重寫對(duì)象的equals方法必須重寫hashCode方法,盡管不寫也能通過編譯;
這里引用網(wǎng)上一個(gè)很容易理解的例子:
hashcode是用來查找的,如果你學(xué)過數(shù)據(jù)結(jié)構(gòu)就應(yīng)該知道,在查找和排序這一章有
例如內(nèi)存中有這樣的位置
0 1 2 3 4 5 6 7
而我有個(gè)類,這個(gè)類有個(gè)字段叫id,我要把這個(gè)類存放在以上8個(gè)位置之一,如果不用hashcode而任意存放,那么當(dāng)查找時(shí)就需要到這八個(gè)位置里挨個(gè)去找,或者用二分法一類的算法。
但如果用hashCode那就會(huì)使效率提高很多。
我們這個(gè)類中有個(gè)字段叫id,那么我們就定義我們的hashCode為id%8,然后把我們的類存放在取得得余數(shù)那個(gè)位置。比如我們的ID為9,9除8的余數(shù)為1,那么我們就把該類存在1這個(gè)位置,如果ID是13,求得的余數(shù)是5,那么我們就把該類放在5這個(gè)位置。這樣,以后在查找該類時(shí)就可以通過ID除 8求余數(shù)直接找到存放的位置了。但是如果兩個(gè)類有相同的hashCode怎么辦那(我們假設(shè)上面的類的id不是唯一的),例如9除以8和17除以8的余數(shù)都是1,那么這是不是合法的,回答是:完全合法。那么如何判斷呢?在這個(gè)時(shí)候就需要定義equals了。
也就是說,我們先通過 hashCode來判斷兩個(gè)類是否存放某個(gè)桶里,但這個(gè)桶里可能有很多類,那么我們就需要再通過 equals 來在這個(gè)桶里找到我們要的類。
那么。重寫了equals(),為什么還要重寫hashCode()呢?
想想,你要在一個(gè)桶里找東西,你必須先要找到這個(gè)桶啊,你不通過重寫hashCode()來找到桶,光重寫equals()有什么用啊。
可能太過文本的東西沒有什么說服力,那就來點(diǎn)干貨:
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,因?yàn)閮蓚€(gè)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兩者同時(shí)重寫,輸出結(jié)果:
stu:true
s1 hashCode:774889
add s1 size:1
s2 hashCode:774889
add s2 size::1
結(jié)合上面引用的案例,可以類推,hash類存儲(chǔ)結(jié)構(gòu)(HashSet、HashMap等等)添加元素會(huì)有重復(fù)性校驗(yàn),校驗(yàn)的方式就是先取hashCode判斷是否相等(找到對(duì)應(yīng)的位置,該位置可能存在多個(gè)元素),然后再取equals方法比較(極大縮小比較范圍,高效判斷),最終判定該存儲(chǔ)結(jié)構(gòu)中是否有重復(fù)元素。
四、總結(jié)
- hashCode主要用于提升查詢效率,來確定在散列結(jié)構(gòu)中對(duì)象的存儲(chǔ)地址;
- 重寫equals()必須重寫hashCode(),二者參與計(jì)算的自身屬性字段應(yīng)該相同;
- hash類型的存儲(chǔ)結(jié)構(gòu),添加元素重復(fù)性校驗(yàn)的標(biāo)準(zhǔn)就是先取hashCode值,后判斷equals();
- equals()相等的兩個(gè)對(duì)象,hashcode()一定相等;
- 反過來:hashcode()不等,一定能推出equals()也不等;
- hashcode()相等,equals()可能相等,也可能不等。
五、花邊:通用的hashCode重寫方案
初始化一個(gè)整形變量,為此變量賦予一個(gè)非零的常數(shù)值,比如int result = 17;
選取equals方法中用于比較的所有域,然后針對(duì)每個(gè)域的屬性進(jìn)行計(jì)算:
- 如果是boolean值,則計(jì)算f ? 1:0
- 如果是byte\char\short\int,則計(jì)算(int)f
- 如果是long值,則計(jì)算(int)(f ^ (f >>> 32))
- 如果是float值,則計(jì)算Float.floatToIntBits(f)
- 如果是double值,則計(jì)算Double.doubleToLongBits(f),然后返回的結(jié)果是long,再用規(guī)則(3)去處理long,得到int
- 如果是對(duì)象應(yīng)用,如果equals方法中采取遞歸調(diào)用的比較方式,那么hashCode中同樣采取遞歸調(diào)用hashCode的方式。否則需要為這個(gè)域計(jì)算一個(gè)范式,比如當(dāng)這個(gè)域的值為null的時(shí)候,那么hashCode 值為0
- 如果是數(shù)組,那么需要為每個(gè)元素當(dāng)做單獨(dú)的域來處理。如果你使用的是1.5及以上版本的JDK,那么沒必要自己去重新遍歷一遍數(shù)組,java.util.Arrays.hashCode方法包含了8種基本類型數(shù)組和引用數(shù)組的hashCode計(jì)算,算法同上
給個(gè)簡單的例子:
@Override
public int hashCode() {
int result = 17;
result = 31 * result + getName().hashCode();
return result;
}