【轉】Java 如何重寫對象的 equals 方法和 hashCode 方法

一、需求

對比兩個對象是否相等。對於下面的 User 對象,只需姓名和年齡相等則認爲是同一個對象。

二、解決方案

需要重寫對象的 equals 方法和 hashCode 方法

public class User {
    private String id;
    private String name;
    private String age;

    public User(){}

    public User(String id, String name, String age){
        this.id = id;
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return this.id + " " + this.name + " " + this.age;
    }

    @Override
    public boolean equals(Object obj) {
        if(this == obj){
            return true;//地址相等
        }

        if(obj == null){
            return false;//非空性:對於任意非空引用x,x.equals(null)應該返回false。
        }

        if(obj instanceof User){
            User other = (User) obj;
            //需要比較的字段相等,則這兩個對象相等
            if(Objects.equals(this.name, other.name)
                    && Objects.equals(this.age, other.age)){
                return true;
            }
        }

        return false;
    }

    @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + (name == null ? 0 : name.hashCode());
        result = 31 * result + (age == null ? 0 : age.hashCode());
        return result;
    }
}

三、測試

創建兩個對象,名字和年齡相等則對象 equals 爲 true。

	@Test
    public void testEqualsObj(){
        User user1 = new User("1", "xiaohua", "14");
        User user2 = new User("2", "xiaohua", "14");
        System.out.println((user1.equals(user2)));//打印爲 true
    }

四、爲什麼要重寫 equals 方法

因爲不重寫 equals 方法,執行 user1.equals(user2) 比較的就是兩個對象的地址(即 user1 == user2),肯定是不相等的,見 Object 源碼:

 public boolean equals(Object obj) {
        return (this == obj);
    }

五、爲什麼要重寫 hashCode 方法

既然比較兩個對象是否相等,使用的是 equals 方法,那麼只要重寫了 equals 方法就好了,幹嘛又要重寫 hashCode 方法呢?

其實當 equals 方法被重寫時,通常有必要重寫 hashCode 方法,以維護 hashCode 方法的常規協定,該協定聲明相等對象必須具有相等的哈希碼。那這又是爲什麼呢?看看下面這個例子就懂了。

User 對象的 hashCode 方法如下,沒有重寫父類的 hashCode 方法

	@Override
    public int hashCode() {
        return super.hashCode();
    }

使用 hashSet

    @Test
    public void testHashCodeObj(){
        User user1 = new User("1", "xiaohua", "14");
        User user2 = new User("2", "xiaohua", "14");
        Set<User> userSet = new HashSet<>();
        userSet.add(user1);
        userSet.add(user2);
        System.out.println(user1.equals(user2));
        System.out.println(user1.hashCode() == user2.hashCode());
        System.out.println(userSet);
    }

結果
在這裏插入圖片描述
顯然,這不是我們要的結果,我們是希望兩個對象如果相等,那麼在使用 hashSet 存儲時也能認爲這兩個對象相等。

通過看 hashSet 的 add 方法能夠得知 add 方法裏面使用了對象的 hashCode 方法來判斷,所以我們需要重寫 hashCode 方法來達到我們想要的效果。

將 hashCode 方法重寫後,執行上面結果爲

    @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + (name == null ? 0 : name.hashCode());
        result = 31 * result + (age == null ? 0 : age.hashCode());
        return result;
    }

所以:hashCode 是用於散列數據的快速存取,如利用 HashSet/HashMap/Hashtable 類來存儲數據時,都會根據存儲對象的 hashCode 值來進行判斷是否相同的。

六、如何重寫 hashCode

生成一個 int 類型的變量 result,並且初始化一個值,比如17

對類中每一個重要字段,也就是影響對象的值的字段,也就是 equals 方法裏有比較的字段,進行以下操作:

  • a. 計算這個字段的值 filedHashValue = filed.hashCode();
  • b. 執行 result = 31 * result + filedHashValue;

七、爲什麼要使用 31

看一看 String hashCode 方法的源碼:

    /**
     * Returns a hash code for this string. The hash code for a
     * {@code String} object is computed as
     * <blockquote><pre>
     * s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
     * </pre></blockquote>
     * using {@code int} arithmetic, where {@code s[i]} is the
     * <i>i</i>th character of the string, {@code n} is the length of
     * the string, and {@code ^} indicates exponentiation.
     * (The hash value of the empty string is zero.)
     *
     * @return  a hash code value for this object.
     */
    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

可以從註釋看出:空字符串的 hashCode 方法返回是 0。並且註釋中也給了個公式,可以瞭解瞭解。

String 源碼中也使用的 31,然後網上說有這兩點原因:

  • 原因一:更少的乘積結果衝突
    31是質子數中一個“不大不小”的存在,如果你使用的是一個如2的較小質數,那麼得出的乘積會在一個很小的範圍,很容易造成哈希值的衝突。而如果選擇一個100以上的質數,得出的哈希值會超出int的最大範圍,這兩種都不合適。而如果對超過 50,000 個英文單詞(由兩個不同版本的 Unix 字典合併而成)進行 hash code 運算,並使用常數 31, 33, 37, 39 和 41 作爲乘子,每個常數算出的哈希值衝突數都小於7個(國外大神做的測試),那麼這幾個數就被作爲生成hashCode值得備選乘數了。
    所以從 31,33,37,39 等中間選擇了 31 的原因看原因二。

  • 原因二:31 可以被JVM優化
    JVM裏最有效的計算方式就是進行位運算了:
    左移 << : 左邊的最高位丟棄,右邊補全0(把 << 左邊的數據*2的移動次冪)。
    右移 >> : 把>>左邊的數據/2的移動次冪。
    無符號右移 >>> : 無論最高位是0還是1,左邊補齊0。   
    所以 : 31 * i = (i << 5) - i(左邊 31*2=62,右邊 2*2^5-2=62) - 兩邊相等,JVM就可以高效的進行計算啦。。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章