關於重寫equals()和hashCode()的思考

yellowgg發表於2019-07-08

最近這幾天一直對equals()和hashCode()的事搞不清楚,雲裡霧裡的。

為什麼重寫equals(),我知道。

但是為什麼要兩個都要重寫呢,我就有點迷糊了,所以趁現在思考清楚後記錄一下。

起因

無非就是一道面試題:“你重寫過 equalshashcode 嗎,為什麼重寫equals時必須重寫hashCode方法?”

 

1.為什麼要重寫equals()

也不用多說大道理了,我們都知道Object類的equals()其實用的也是“==”。

我們也知道“==”比較的是記憶體地址。

所以當物件一樣時,它的記憶體地址也是一樣的,所以此時不管是“==”也好,equals()也好,都是返回true。

例子

public static void main(String[] args) {
        String s = "大木大木大木大木";
        String sb = s;

        System.out.println(s.equals(sb));

        System.out.println(s == sb);
    }

輸出結果

true
true

但是,我們有時候判斷兩個物件是否相等不一定是要判斷它的記憶體地址是否相等,我只想根據物件的內容判斷。

在我們人為的規定下,我看到物件的內容相等,我就認為這兩個物件是相等的,怎麼做呢?

很顯然,用“==”是做不到的,所以我們需要用到equals()方法,

我們需要重寫它,讓它達到我們上面的目的,也就是根據物件內容判斷是否相等,而不是根據物件的記憶體地址。

例子:沒有重寫equls()

public class MyClass {
public static void main(String[] args) { Student s1 = new Student("jojo", 18); Student s2 = new Student("jojo", 18); System.out.println(s1.equals(s2)); } private static class Student { String name; int age; public Student(String name, int age) { this.name = name; this.age = age; } } }

輸出結果

false

結果分析

兩個長得一樣的物件比較,為什麼equals()會返回false。

因為我們的Student類沒有重寫equals()方法,所以它呼叫的其實是Object類的equals(),其實現就是“==”。

所以雖然兩個物件長一樣,但它們的記憶體地址不一樣,兩個物件也就不相等,所以就返回了false。

 

所以我們為了達到我們先前的規定,需要重寫一下equals()方法,至於重寫此方法,有幾點原則,我就不多說了,都是些廢話。

例子:重寫了equals()方法

 private static class Student {
        String name;
        int age;

        public Student(String name, int age) {
            this.name = name;
            this.age = age;
        }

     /**
     * 重寫後的equals()是根據內容來判定相等的
     */
@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 && name.equals(student.name); } }

此時再次執行以下程式碼

public static void main(String[] args) {
        Student s1 = new Student("jojo", 18);
        Student s2 = new Student("jojo", 18);

        System.out.println(s1.equals(s2));
    }

輸出結果

true

結果分析

顯然我們此時已經達到了目的,根據內容能夠判定物件相等。

 

總結

1.Object類的equals()方法實現就是使用了"==",所以如果沒有重寫此方法,呼叫的依然是Object類的equals()

2.重寫equals()是為了讓物件根據內容判斷是否相等,而不是記憶體地址。

 

2.雜湊值

關於hashCode(),百度百科是這麼描述的。

hashCode是jdk根據物件的地址或者字串或者數字算出來的int型別的數值 
詳細瞭解請 參考 public int hashCode()返回該物件的雜湊值。
支援此方法是為了提高雜湊表(例如 java.util.Hashtable 提供的雜湊表)的效能。

雜湊表的內容我這裡也不多提,但我們需要明白的一點是,

雜湊值相當於一個元素在雜湊表中的索引位置。

其中,

元素的雜湊值相同,內容不一定相同

元素的內容相同,雜湊值一定相同。

為什麼第一句會這麼說呢,因為其就是接下來所說的雜湊衝突。

 

3.雜湊衝突

雜湊衝突就是雜湊值重複了,撞車了。

最直觀的看法是,兩個不同的元素,卻因為雜湊演算法不夠強,算出來的雜湊值是一樣的。

所以解決雜湊衝突的方法就是雜湊演算法儘可能的強。

例子:弱的雜湊演算法

public class MyClass {
    public static void main(String[] args) {
        //Student物件
        Student s1 = new Student("jojo", 18);
        Student s2 = new Student("JOJO", 18);//用equals()比較,並附帶hashCode()
        System.out.println("雜湊值:s1-->" + s1.hashCode() + "  s2-->" + s2.hashCode());
    }

    private static class Student {
        String name;
        int age;

        public Student(String name, int age) {
            this.name = name;
            this.age = age;
        }

        /**
         * 此方法只是簡單的運算了name和age。
         * @return
         */
        @Override
        public int hashCode() {
            int nameHash = name.toUpperCase().hashCode();
            return nameHash ^ age;
        }
    }
}

輸出結果

雜湊值:s1-->2282840  s2-->2282840

結果分析

我們可以看到這個兩個不同的物件卻因為簡單的雜湊演算法不夠健壯,導致了雜湊值的重複。

這顯然不是我們所希望的,所以可以用更強的雜湊演算法。

例子:強的雜湊演算法

public class MyClass {
    public static void main(String[] args) {
        //Student物件
        Student s1 = new Student("jojo", 18);
        Student s2 = new Student("JOJO", 18);

        //用equals()比較,並附帶hashCode()
        System.out.println("雜湊值:s1-->" + s1.hashCode() + "  s2-->" + s2.hashCode());
    }

    private static class Student {
        String name;
        int age;

        public Student(String name, int age) {
            this.name = name;
            this.age = age;
        }

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

輸出結果

雜湊值:s1-->101306313  s2-->70768585

結果分析

上面用的雜湊演算法是IDEA自動生成的,它是使用了java.util.Objects的hash()方法,

總而言之,好的雜湊演算法能夠儘可能的減少雜湊衝突。

 

總結

雜湊衝突可以減少,但無法避免,解決方法就是雜湊演算法儘可能的強。

 

所以結合上面而言,我們可以認為,雜湊值越唯一越好,這樣在雜湊表中插入物件時就不容易在同一個位置插入了。

但是,我們希望雜湊值唯一,現實卻不會如我們希望,在雜湊表中,雜湊碼值的計算總會有撞車,有重複的,

關於雜湊值的介紹僅此這麼點,更多詳情可以繼續學習,這裡就不多提及了。

 

4.極其重要的一點

不是所有重寫equals()的都要重寫hashCode(),如果不涉及到雜湊表的話,就不用了,比如Student物件插入到List中。

涉及到雜湊表,比如HashSet, Hashtable, HashMap這些資料結構,Student物件插入就必須考慮雜湊值了。

 

5.重寫

以下的討論是設定在jdk1.8的HashSet,因為HashSet本質是雜湊表的資料結構,是Set集合,是不允許有重複元素的。

在這種情況下才需要重寫equals()和重寫hashCode()。

我們分四種情況來說明。

1.兩個方法都不重寫

例子

public class MyClass {

    public static void main(String[] args) {
        //Student物件
        Student s1 = new Student("jojo", 18);
        Student s2 = new Student("jojo", 18);

        //HashSet物件
        HashSet set = new HashSet();
        set.add(s1);
        set.add(s2);

        //輸出兩個物件
        System.out.println(s1);
        System.out.println(s2);

        //輸出equals
        System.out.println("s1.equals(s2)的結果為:" + s1.equals(s2));

        //輸出雜湊值
        System.out.println("雜湊值為:s1->" + s1.hashCode() + "  s2->" + s2.hashCode());

        //輸出set
        System.out.println(set);
    }

    private static class Student {
        String name;
        int age;

        public Student(String name, int age) {
            this.name = name;
            this.age = age;
        }

        @Override
        public String toString() {
            return "Student{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
}

輸出結果

Student{name='jojo', age=18}
Student{name='jojo', age=18}
s1.equals(s2)的結果為:false
雜湊值為:s1->2051450519 s2->99747242
[Student{name='jojo', age=18}, Student{name='jojo', age=18}]

結果分析

equals()返回 false

雜湊值          不一致

兩個都沒有重寫,HashSet將s1和s2都存進去了,明明算是重複元素,為什麼呢?

到底HashSet怎麼才算把兩個元素視為重複呢?

 

2.只重寫equals()

例子

public class MyClass {

    public static void main(String[] args) {
        //Student物件
        Student s1 = new Student("jojo", 18);
        Student s2 = new Student("jojo", 18);

        //HashSet物件
        HashSet set = new HashSet();
        set.add(s1);
        set.add(s2);

        //輸出兩個物件
        System.out.println(s1);
        System.out.println(s2);

        //輸出equals
        System.out.println("s1.equals(s2)的結果為:" + s1.equals(s2));

        //輸出雜湊值
        System.out.println("雜湊值為:s1->" + s1.hashCode() + "  s2->" + s2.hashCode());

        //輸出set
        System.out.println(set);
    }

    private static class Student {
        String name;
        int age;

        public Student(String name, int age) {
            this.name = name;
            this.age = age;
        }

        @Override
        public String toString() {
            return "Student{" +
                    "name='" + name + '\'' +
                    ", age=" + 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);
        }
    }
}

輸出結果

Student{name='jojo', age=18}
Student{name='jojo', age=18}
s1.equals(s2)的結果為:true
雜湊值為:s1->2051450519  s2->99747242
[Student{name='jojo', age=18}, Student{name='jojo', age=18}]

結果分析

equals()返回 true

雜湊值          不一致

重寫了equals()之後,HashSet依然將s1和s2都存進去了。

雜湊值不一樣,按照雜湊值即索引的說法,所以HashSet存入了兩個索引不同的元素?

又或者是HashSet判斷元素重複不是靠equals()?我們接著往下看

 

3.只重寫hashCode()

例子

public class MyClass {

    public static void main(String[] args) {
        //Student物件
        Student s1 = new Student("jojo", 18);
        Student s2 = new Student("jojo", 18);

        //HashSet物件
        HashSet set = new HashSet();
        set.add(s1);
        set.add(s2);

        //輸出兩個物件
        System.out.println(s1);
        System.out.println(s2);

        //輸出equals
        System.out.println("s1.equals(s2)的結果為:" + s1.equals(s2));

        //輸出雜湊值
        System.out.println("雜湊值為:s1->" + s1.hashCode() + "  s2->" + s2.hashCode());

        //輸出set
        System.out.println(set);
    }

    private static class Student {
        String name;
        int age;

        public Student(String name, int age) {
            this.name = name;
            this.age = age;
        }

        @Override
        public String toString() {
            return "Student{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }

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

輸出結果

Student{name='jojo', age=18}
Student{name='jojo', age=18}
s1.equals(s2)的結果為:false
雜湊值為:s1->101306313  s2->101306313
[Student{name='jojo', age=18}, Student{name='jojo', age=18}]

結果分析

equals()返回 false

雜湊值          一致

這次我們只重寫hashCode(),沒有重寫equals(),

我們可以看到s1和s2的雜湊值是相同的,怎麼還會儲存兩個物件呢,不應該啊。

 

難道之前雜湊值是索引的說法是錯的?不是這樣的,這個說法沒有錯。

但也不是絕對正確,在同一個雜湊值下,如果兩個元素的內容不一樣,依然會都被儲存起來。

但是先不說在強的雜湊演算法下很小很小概率會有雜湊衝突,

就算有了雜湊衝突,java也不會就這麼簡單的認為兩個元素是相等的。

所以從巨集觀上來看,從java方面來看,雜湊即索引這個說法是行得通的。

 

其實這裡是這樣的,問題在於HashSet的add()。

我們可以追蹤其原始碼,發現了以下片段

恍然大悟,我們可以看到HashSet新增元素時,呼叫的是HashMap的put(),

它不僅涉及了元素的雜湊值,即hashCode(),還涉及到了equals()。

所以為什麼上面只重寫hashCode()之後HashSet還能新增s1和s2,就是因為equals()沒有重寫。

導致了雖然雜湊值相同,但equals()不同,所以認為s1和s2是兩個不同的元素。

 

所以綜上所述,我們可以知道,對於這些雜湊結構的東西,

它們判斷元素重複是先判斷雜湊值然後再判斷equals()的。

也就是說

先判斷雜湊值,如果雜湊值相等,內容不一定等,此時繼續判斷equals()。

如果雜湊值不等,那麼此時內容一定不等,就不用再判斷equals()了,直接操作。

 

4.兩個都重寫

例子

public class MyClass {

    public static void main(String[] args) {
        //Student物件
        Student s1 = new Student("jojo", 18);
        Student s2 = new Student("jojo", 18);

        //HashSet物件
        HashSet set = new HashSet();
        set.add(s1);
        set.add(s2);

        //輸出兩個物件
        System.out.println(s1);
        System.out.println(s2);

        //輸出equals
        System.out.println("s1.equals(s2)的結果為:" + s1.equals(s2));

        //輸出雜湊值
        System.out.println("雜湊值為:s1->" + s1.hashCode() + "  s2->" + s2.hashCode());

        //輸出set
        System.out.println(set);
    }

    private static class Student {
        String name;
        int age;

        public Student(String name, int age) {
            this.name = name;
            this.age = age;
        }

        @Override
        public String toString() {
            return "Student{" +
                    "name='" + name + '\'' +
                    ", age=" + 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);
        }

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

輸出結果

Student{name='jojo', age=18}
Student{name='jojo', age=18}
s1.equals(s2)的結果為:true
雜湊值為:s1->101306313  s2->101306313
[Student{name='jojo', age=18}]

結果分析

equals()返回 true

雜湊值          一致

重寫了兩個方法後,equals()返回了true,雜湊值也因為內容一樣而一樣,

更重要的是,HashSet只插入了一個元素。

 

6.最最最最後的總結

1.為什麼要重寫equals()

從正常角度而言,重寫equals()是為了讓兩個內容一樣的元素相等。

從java的雜湊結構設計而言,雜湊結構對元素的操作跟雜湊值以及equals()有關,所以必須重寫。

 

2.為什麼要重寫hashCode()

重寫hashCode()是為了讓雜湊值跟內容產生關聯,從而保證了雜湊值跟內容一一對應,

提高雜湊值的唯一性,減少雜湊衝突。

:重寫hashCode()不是必須的,只有跟雜湊結構有關時才需要重寫。

 

3.為什麼重寫equals()時必須重寫hashCode()方法

其一

在跟雜湊結構有關的情況下,判斷元素重複是先判斷雜湊值再判斷equals()。所以得兩個都重寫,

其二

重寫了hashCode()減少了雜湊衝突,就能直接判斷元素的重複,而不用再繼續判斷equals(),從而提高了效率。 

相關文章