前言
在我們需要比較對象是否相等時,我們往往需要采取重寫equals方法和hashcode方法。
該篇,就是從比較對象的場景結合通過代碼執行個體以及部分源碼解讀,去跟大家品一品這個重寫equals方法和hashcode方法。
正文
場景:
我們現在需要比較兩個對象 Pig 是否相等 。
而Pig 對象裡面包含 三個字段, name,age,nickName ,我們現在隻需要認為如果兩個pig對象的name名字和age年齡一樣,那麼這兩個pig對象就是一樣的,nickName昵稱不影響相等的比較判斷。
代碼示例:
Pig.java:
/**
* @Author : JCccc
* @CreateTime : 2020/4/21
* @Description :
**/
public class Pig {
private String name;
private Integer age;
private String nickName;
public Pig() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getNickName() {
return nickName;
}
public void setNickName(String nickName) {
this.nickName = nickName;
}
}
可以看到上面的Pig對象,沒有重寫equals方法 和 hashcode 方法,那麼我們去比較兩個Pig對象,就算都設定了三個一樣的屬性字段,都是傳回 false:
public static void main(String[] args) {
Pig pig1=new Pig();
pig1.setName("A");
pig1.setAge(11);
pig1.setNickName("a");
String name= new String("A");
Pig pig2=new Pig();
pig2.setName(name);
pig2.setAge(11);
pig2.setNickName("B");
System.out.println(pig1==pig2); //false
System.out.println(pig1.equals(pig2)); //false
System.out.println(pig1.hashCode() ==pig2.hashCode()); //false
}
為什麼false? 很簡單,因為pig1和pig2都是新new出來的,記憶體位址都是不一樣的。
== : 比較記憶體位址 ,那肯定是false了 ;
equals: 預設調用的是Object的equals方法,看下面源碼圖,顯然還是使用了== ,那就還是比較記憶體位址,那肯定是false了;
hashCode: 這是根據一定規則例如對象的存儲位址,屬性值等等映射出來的一個散列值,不同的對象存在可能相等的hashcode,但是機率非常小(兩個對象equals傳回true時,hashCode傳回肯定是true;而兩個對象hashCode傳回true時,這兩個對象的equals不一定傳回true; 還有,如果兩個對象的hashCode不一樣,那麼這兩個對象一定不相等!)。
一個好的雜湊演算法,我們肯定是盡可能讓不同對象的hashcode也不同,相同的對象hashcode也相同。這也是為什麼我們比較對象重寫equals方法後還會一起重寫hashcode方法。接下來會有介紹到。
好的,上面啰嗦了很多,接下來我們開始去重寫equals方法和hashCode方法,實作我們這個Pig對象的比較,隻要能保證name和age兩個字段屬性一緻,就傳回相等true。
首先是重寫equals方法(看上去我似乎寫的很啰嗦吧,我覺得這樣去寫更容易幫助新手去了解):
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Pig pig = (Pig) o;
boolean nameCheck=false;
boolean ageCheck=false;
if (this.name == pig.name) {
nameCheck = true;
} else if (this.name != null && this.name.equals(pig.name)) {
nameCheck = true;
}
if (this.age == pig.age) {
ageCheck = true;
} else if (this.age != null && this.age.equals(pig.age)) {
ageCheck = true;
}
if (nameCheck && ageCheck){
return true;
}
return false;
}
稍微對重寫的代碼做下解讀,請看圖:
事不宜遲,我們在重寫了equals後,我們再比較下兩個Pig對象:
可以看到,到這裡好像已經能符合我們的比較對象邏輯了,但是我們還需要重寫hashCode方法。
為什麼?
原因1.
通用約定,
翻譯:
/*請注意,通常需要重寫{@code hashCode}
*方法,以便維護
*{@code hashCode}方法的正常約定,它聲明
*相等的對象必須有相等的哈希碼。
*/
原因2.
為了我們使用hashmap存儲對象 (下面有介紹)
沒錯,就是文章開頭我們講到的,相同的對象的hashCode 的散列值最好保持相等, 而不同對象的散列值,我們也使其保持不相等。
而目前我們已經重寫了equals方法,可以看到,隻要兩個pig對象的name和age都相等,那麼我們的pig的equals就傳回true了,也就是說,此時此刻,我們也必須使兩個pig的hashCode 的散列值保持相等,這樣才是對象相等的結果。
事不宜遲,我們繼續重寫hashCode方法:
@Override
public int hashCode() {
int result = 17;
result = 31 * result + name.hashCode();
result = 31 * result + age;
return result;
}
然後我們再比較下兩個pig對象:
也就是說,最終我們重寫了equals和hashCode方法後, Pig.java:
/**
* @Author : JCccc
* @CreateTime : 2020/4/21
* @Description :
**/
public class Pig {
private String name;
private Integer age;
private String nickName;
public Pig() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getNickName() {
return nickName;
}
public void setNickName(String nickName) {
this.nickName = nickName;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Pig pig = (Pig) o;
boolean nameCheck=false;
boolean ageCheck=false;
if (this.name == pig.name) {
nameCheck = true;
} else if (this.name != null && this.name.equals(pig.name)) {
nameCheck = true;
}
if (this.age == pig.age) {
ageCheck = true;
} else if (this.age != null && this.age.equals(pig.age)) {
ageCheck = true;
}
if (nameCheck && ageCheck){
return true;
}
return false;
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + name.hashCode();
result = 31 * result + age;
return result;
}
}
看到這裡,應該有不少人覺得,重寫怎麼有點麻煩,有沒有簡單點的模闆形式的?
有的,其實在java 7 有在Objects裡面新增了我們需要重新的這兩個方法,是以我們重寫equals和hashCode還可以使用java自帶的Objects,如:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Pig pig = (Pig) o;
return Objects.equals(name, pig.name) &&
Objects.equals(age, pig.age);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
那麼如果還是覺得有點麻煩呢?
那就使用lombok的注解,讓它幫我們寫,我們自己就寫個注解!
導入lombok依賴:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
</dependency>
然後在Pig類上,使用注解:
然後編譯後,可以看到lombok幫我們重寫了equals和hashcode方法:
其實如果咱們使用到了lombok的話,用@Data注解應該基本就夠用了,它結合了@ToString,@EqualsAndHashCode,@Getter和@Setter的功能。
ps:再啰嗦幾句, 我們自己重寫了hashCode方法後,能夠確定兩個對象傳回的的hashCode散列值是不一樣的,這樣一來,
在我們使用hashmap 去存儲對象, 在進行驗重邏輯的時候,咱們的性能就特别好了。
為啥說這樣性能好, 我們再啰嗦地補充一下hashmap在插入值時對key(這裡key是對象)的驗重:
HashMap中的比較key:
先求出key的hashcode(),比較其值是否相等;
-若相等再比較equals(),若相等則認為他們是相等 的。
-若equals()不相等則認為他們不相等。
如果隻重寫hashcode()不重寫equals()方法,當比較equals()時隻是看他們是否為 同一對象(即進行記憶體位址的比較),是以必定要兩個方法一起重寫。
而HashSet,
用來判斷key是否相等的方法,其實也是調用了HashMap加入元素方法,再判斷是否相等。
好的,該篇介紹就到此吧。
ps:其實沒有結束,我還想啰嗦一下,因為我們在重寫hashcode方法的時候,我們看到了一個數字 31 。 想必會有很多人奇怪,為什麼要寫個31啊?
這裡引用一下《Effective Java》 裡面的解釋:
之是以使用 31, 是因為他是一個奇素數。
如果乘數是偶數,并且乘法溢出的話,資訊就會丢失,因為與2相乘等價于移位運算(低位補0)。
使用素數的好處并不很明顯,但是習慣上使用素數來計算散列結果。
31 有個很好的性能,即用移位和減法來代替乘法,
可以得到更好的性能: 31 * i == (i << 5)- i,
現代的 VM 可以自動完成這種優化。這個公式可以很簡單的推導出來。
那麼可能還有眼尖的人看到了,為什麼還要個數字17?
這個其實道理一樣,在《Effective Java》裡,作者推薦使用的就是 基于17和31的散列碼的算法 ,而在Objects裡面的hash方法裡,17換做了1 。
好吧,這篇就真的到此結束吧。