天天看點

Java 細品 重寫equals方法 和 hashcode 方法

前言

 在我們需要比較對象是否相等時,我們往往需要采取重寫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了;

Java 細品 重寫equals方法 和 hashcode 方法

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;
    }      

稍微對重寫的代碼做下解讀,請看圖:

Java 細品 重寫equals方法 和 hashcode 方法

事不宜遲,我們在重寫了equals後,我們再比較下兩個Pig對象:

Java 細品 重寫equals方法 和 hashcode 方法

可以看到,到這裡好像已經能符合我們的比較對象邏輯了,但是我們還需要重寫hashCode方法。

為什麼?

原因1. 

通用約定,

Java 細品 重寫equals方法 和 hashcode 方法

翻譯:

/*請注意,通常需要重寫{@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對象:

Java 細品 重寫equals方法 和 hashcode 方法

也就是說,最終我們重寫了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類上,使用注解:

Java 細品 重寫equals方法 和 hashcode 方法

然後編譯後,可以看到lombok幫我們重寫了equals和hashcode方法:

Java 細品 重寫equals方法 和 hashcode 方法
Java 細品 重寫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加入元素方法,再判斷是否相等。

Java 細品 重寫equals方法 和 hashcode 方法
Java 細品 重寫equals方法 和 hashcode 方法

好的,該篇介紹就到此吧。

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 。  

好吧,這篇就真的到此結束吧。