Java equals 和 hashCode 的這幾個問題可以說明白嗎?


Java equals 和 hashCode 的這幾個問題可以說明白嗎?


前言

上一篇文章 https://dayarch.top/p/spring-data-binding-mechanism.html ,靈魂追問 環節留下了一個有關 equals 和 hashcode 問題 。基礎面試經常會碰到與之相關的問題,這不是一個複雜的問題,但很多朋友都苦於說明他們二者的關係和約束,於是寫本文做單獨說明,本篇文章將循序漸進 ( 通過舉例,讓記憶與理解更輕鬆 ) 說明這些讓你有些苦惱的問題,Let's go .......

面試問題


1. Java 裡面有了 == 運算符,為什麼還需要 equals ?

== 比較的是對象地址,equals 比較的是對象值

先來看一看 Object 類中 equals 方法:

<code>public boolean equals(Object obj) {
    return (this == obj);
}/<code>

我們看到 equals 方法同樣是通過 == 比較對象地址,並沒有幫我們比較值。Java 世界中 Object 絕對是"老祖宗" 的存在,== 號我們沒辦法改變或重寫。但 equals 是方法,這就給了我們重寫 equals 方法的可能,讓我們實現其對值的比較:

<code>@Override
public boolean equals(Object obj) {
    //重寫邏輯
}/<code>

新買的電腦,每個電腦都有唯一的序列號,通常情況下,兩個一模一樣的電腦放在面前,你會說由於序列號不一樣,這兩個電腦不一樣嗎?

如果我們要說兩個電腦一樣,通常是比較其「品牌/尺寸/配置 」(值) ,比如這樣:

<code>@Override
public boolean equals(Object obj) {
    return 品牌相等 && 尺寸相等 && 配置相等
}/<code>

當遇到如上場景時,我們就需要重寫 equals 方法。這就解釋了 Java 世界為什麼有了 == 還有equals 這個問題了.

2. equals相等 和 hashcode 相等問題

關於二者,你經常會碰到下面的兩個問題:

  • 兩個對象 equals 相等,那他們 hashCode 相等嗎?
  • 兩個對象 hashCode 相等,那他們 equals 相等嗎?

為了說明上面兩個問題的結論,這裡舉一個不太恰當的例子,只為方便記憶,我們將 equals 比作一個單詞的拼寫;hashCode 比作一個單詞的發音,在相同語境下:

sea / sea 「大海」,兩個單詞拼寫一樣,所以 equals 相等,他們讀音 /siː/ 也一樣,所以 hashCode 就相等,這就回答了第一個問題:

兩個對象 equals 相等,那他們 hashCode 一定也相等

sea / see 「大海/看」,兩個單詞的讀音 /siː/ 一樣,顯然單詞是不一樣的,這就回答了第二個問題:

兩個對象 hashCode 相等,那他們 equals 不一定相等

查看 Object 類的 hashCode 方法:

<code>public native int hashCode();/<code>

繼續查看該方法的註釋,明確寫明關於該方法的約束

Java equals 和 hashCode 的這幾個問題可以說明白嗎?

其實在這個結果的背後,還有的是關於重寫 equals 方法的約束

3. 重寫 equals 有哪些約束?

關於重寫 equals 方法的約束,同樣在該方法的註釋中寫的很清楚了,我在這裡再說明一下:

Java equals 和 hashCode 的這幾個問題可以說明白嗎?

赤橙紅綠青藍紫,七彩以色列;哆來咪發唆拉西, 一曲安哥拉 ,這些規則不是用來背誦的,只是在你需要重寫 equals 方法時,打開 JDK 查看該方法,按照準則重寫就好

4. 什麼時候需要我們重寫 hashCode?

為了比較值,我們重寫 equals 方法,那什麼時候又需要重寫 hashCode 方法呢?

通常只要我們重寫 equals 方法就要重寫 hashCode 方法

為什麼會有這樣的約束呢?按照上面講的原則,兩個對象 equals 相等,那他們的 hashCode 一定也相等。如果我們只重寫 equals 方法而不重寫 hashCode 方法,看看會發生什麼,舉個例子來看:

定義學生類,並通過 IDE 只幫我們生成 equals 方法:

<code>public class Student {

    private String name;

    private int age;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return age == student.age &&
                Objects.equals(name, student.name);
    }
}/<code>

編寫測試代碼:

<code>Student student1 = new Student();
student1.setName("日拱一兵");
student1.setAge(18);

Student student2 = new Student();
student2.setName("日拱一兵");
student2.setAge(18);

System.out.println("student1.equals(student2)的結果是:" + student1.equals(student2));

Set students = new HashSet();
students.add(student1);
students.add(student2);
System.out.println("Student Set 集合長度是:" + students.size());

Map map = new HashMap();
map.put(student1, "student1");
map.put(student2, "student2");
System.out.println("Student Map 集合長度是:" + map.keySet().size());/<code>

查看運行結果:

<code>student1.equals(student2)的結果是:true
Student Set 集合長度是:2
Student Map 集合長度是:2/<code>


很顯然,按照集合 Set 和 Map 加入元素的標準來看,student1 和 student2 是兩個對象,因為在調用他們的 put (Set add 方法的背後也是 HashMap 的 put)方法時, 會先判斷 hash 值是否相等,這個小夥伴們打開 JDK 自行查看吧

所以我們繼續重寫 Student 類的 hashCode 方法:

<code>@Override
public int hashCode() {
    return Objects.hash(name, age);
}/<code>

重新運行上面的測試,查看結果:

<code>student1.equals(student2)的結果是:true
Student Set 集合長度是:1
Student Map 集合長度是:1/<code>

得到我們預期的結果,這也就是為什麼通常我們重寫 equals 方法為什麼最好也重寫 hashCode 方法的原因

  • 如果你在使用 Lombok,不知道你是否注意到 Lombok 只有一個 @EqualsAndHashCode 註解,而沒有拆分成 @Equals 和 @HashCode 兩個註解,想了解更多 Lombok 的內容,也可以查看我之前寫的文章 https://dayarch.top/p/lombok-usage.html
  • 另外通過 IDE 快捷鍵生成重寫方法時,你也會看到這兩個方法放在一起,而不是像 getter 和 setter 那樣分開

以上兩點都是隱形的規範約束,希望大家也嚴格遵守這個規範,以防帶來不必要的麻煩,記憶的方式有多樣,如果記不住這個文字約束,腦海中記住上面的圖你也就懂了

5. 重寫 hashCode 為什麼總有 31 這個數字?

細心的朋友可能注意到,我上面重寫 hashCode的方法很簡答, 就是用了 Objects.hash 方法,進去查看裡面的方法:

<code>public static int hashCode(Object a[]) {
    if (a == null)
        return 0;

    int result = 1;

    for (Object element : a)
        result = 31 * result + (element == null ? 0 : element.hashCode());

    return result;
}/<code>

這裡通過 31 來計算對象 hash 值

在 https://dayarch.top/p/spring-data-binding-mechanism.html 文章末尾提到的在 HandlerMethodArgumentResolverComposite 類中有這樣一個成員變量:

<code>private final Map argumentResolverCache =
            new ConcurrentHashMap(256);/<code>

Map 的 key 是 MethodParameter ,根據我們上面的分析,這個類一定也會重寫 equals 和 hashCode 方法,進去查看發現,hashCode 的計算也用到了 31 這個數字

<code>@Override
public boolean equals(Object other) {
    if (this == other) {
        return true;
    }
    if (!(other instanceof MethodParameter)) {
        return false;
    }
    MethodParameter otherParam = (MethodParameter) other;
    return (this.parameterIndex == otherParam.parameterIndex && getMember().equals(otherParam.getMember()));
}

@Override
public int hashCode() {
    return (getMember().hashCode() * 31 + this.parameterIndex);
}/<code>

為什麼計算 hash 值要用到 31 這個數字呢?我在網上看到一篇不錯的文章,分享給大家,作為科普,可以簡單查看一下:https://www.cnblogs.com/nullllun/p/8350178.html

總結

如果還對equals 和 hashCode 關係及約束含混,我們只需要按照上述步驟逐步回憶即可,更好的是直接查看 JDK 源碼;另外拿出實際的例子來反推驗證是非常好的辦法。如果你還有相關疑問,也可以留言探討.


靈魂追問

  1. Thread 類就沒有重寫 equals 方法,你還知道哪些情況沒必要重寫 equals 方法嗎?
  2. 從上面 HandlerMethodArgumentResolverComposite 類中定義的 Map 成員變量,你注意到哪些知識點,比如 final,ConcurrentHashMap,初識容量,為什麼要這樣寫?你能解釋出原因嗎?

趣味原創解析Java技術棧問題,將複雜問題簡單化,將抽象問題圖形化落地

如果對我的專題內容感興趣,或搶先看更多內容,歡迎訪問我的博客 https://dayarch.top


分享到:


相關文章: