Java 基礎(chǔ)復(fù)習(xí)實踐 --- Hashcode Equals

雖然很多知識點書籍都有整理,但是記性總是不好,所以決定將一些細小容易混淆的概念,通過簡單的 Demo 實踐,加深復(fù)習(xí)。特此開一個坑,堅持就是勝利。

本章內(nèi)容主要為了理解以下幾個知識點:

  • equals() 的作用是什么?
  • equals() 與 “==”的區(qū)別是什么?
  • hashcode() 的作用是什么?
  • hashcode() 與 equals()之間有什么聯(lián)系?

目錄

0x01 equals() 的作用

Indicates whether some other object is "equal to" this one.

equals()是用來 判斷兩個對象是否相等

equals() 定義在 JDK 的 Object.java 中。通過判斷兩個對象的地址是否相等(即,是否是同一個對象)來區(qū)分它們是否相等。源碼如下:

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

既然是在 Object 類中定義了該方法,就表明了 Java 所有類都實現(xiàn)了 equals() 方法,所以所有類都可以通過該方法去判斷兩個對象是否相等。
默認(rèn)的 equals() 方法等同于 ”==” 方法,所以我們一般會重寫 equals() 方法 —-> 兩個對象的內(nèi)容相等,返回 true ,否則返回 false。

所以我們可以根據(jù)是否 重寫 equals() 方法將類分為兩類:

1.若某個類沒有覆蓋 equals() 方法,當(dāng)它的通過 equals() 比較兩個對象時,實際上是比較兩個對象是不是同一個對象。這時,等價于通過“==”去比較這兩個對象

2.覆蓋類的 equals() 方法,來讓 equals() 通過其它方式比較兩個對象是否相等。通常的做法是:若兩個對象的內(nèi)容相等,則 equals()方法返回true;否則,返回 false 。

沒有覆蓋 equals() 方法


public class HashcodeAndEquals {

    private static <T> void out(T t) {
        System.out.println(t);
    }

    public static void main(String[] args) {
        // 實例化兩個 Person 對象
        Person p1 = new Person("小明", 12);
        Person p2 = new Person("小明", 12);
        // 通過 equals() 比較他們是否相等
        out(p1.equals(p2));

    }

    private static class Person {
        int age;
        String name;

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

        @Override
        public String toString() {

            return name + "--- age:" + age;
        }

    }

}

輸出為
false

結(jié)論:

<div class="tip">
當(dāng)我們使用 p1.equals(p2) 來比較 p1 和 p2 是否相等時,實際上是調(diào)用了 object 類的 equals() 方法,即 “p1==p2”,它是比較 p1 和 p2 是否為一個對象。

由定義可知,p1 和 p2 雖然內(nèi)容相同,但是它們是兩個不同的對象。因此,返回 false 。
</div>

覆蓋 equals() 方法

我們簡單修改下 HashcodeAndEquals.java 文件,覆蓋 equals() 方法

public class HashcodeAndEquals {

    private static <T> void out(T t) {
        System.out.println(t);
    }

    public static void main(String[] args) {
        // 實例化兩個 Person 對象
        Person p1 = new Person("小明", 12);
        Person p2 = new Person("小明", 12);
        // 通過 equals() 比較他們是否相等
        out(p1.equals(p2));

    }

    private static class Person {
        int age;
        String name;

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

        @Override
        public String toString() {

            return name + "--- age:" + age;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }

            // 如果是同一對象,返回 true
            if (this == obj) {
                return true;
            }
            // 判斷是否類型相同
            if (this.getClass() != obj.getClass()) {
                return false;
            }
            // 只有一下三種情況才能通過編譯
            // 1.instanceof前面的類型與后面的類型相同
            // 2.instanceof前面的類型是后面的類型父類
            // 3.instanceof前面的類型是后面的類型子類
            // 故與我們判斷類型相同有一點偏差
            //
            // 這一點我們可以自己定義一個 Student 類繼承 Person 類來進行實驗
            // if (!(obj instanceof Person)) {
            // return false;
            // }

            Person per = (Person) obj;
            return name.equals(per.name) && age == per.age;
        }

    }

}

輸出為
true

結(jié)論:

<div class="tip">
我們在新的 HashcodeAndEquals.java 文件中重寫了 Person 類的 equals() 函數(shù):當(dāng)兩個 Person 對象 name 和 age 都相等時,則返回 true ,因此結(jié)果為 true 。
</div>

Tips

Java 對 equals() 的要求

  1. 對稱性:如果 x.equals(y) 返回是" true ",那么 y.equals(x) 也應(yīng)該返回是"true"。
  2. 反射性:x.equals(x) 必須返回是 "true" 。
  3. 類推性:如果 x.equals(y) 返回是"true",而且 y.equals(z) 返回是"true",那么 z.equals(x) 也應(yīng)該返回是"true"。
  4. 一致性:如果 x.equals(y) 返回是"true",只要x和y內(nèi)容一直不變,不管你重復(fù) x.equals(y) 多少次,返回都是"true"。
  5. 非空性,x.equals(null),永遠返回是"false";x.equals (和x不同類型的對象)永遠返回是"false"。

0x02 equals() 與 == 的區(qū)別

equals() : 它的作用也是判斷兩個對象是否相等。但它一般有兩種使用情況(前面第1部分已詳細介紹過)

== : 它的作用是判斷兩個對象的地址是不是相等。即,判斷兩個對象是否為同一個對象。

只用修改下HashcodeAndEquals.java 主函數(shù)為:

public static void main(String[] args) {
    // 實例化兩個 Person 對象
    Person p1 = new Person("小明", 12);
    Person p2 = new Person("小明", 12);
    // 分別用 equals() 和 == 來判斷
    out("equals: " + p1.equals(p2));
    out("==: " + (p1 == p2));
}

輸出為:
equals: true
==: false

結(jié)果與我們預(yù)想的一樣,因為我們是復(fù)寫了 Person 類的 equals() 方法,而且 p1 p2 內(nèi)容相同所以返回 true ,而 p1 p2 并是兩個不同對象,所以 == 判斷它們地址不相同,返回 false。

0x03 hashcode() 的作用

hashCode() 的作用是獲取哈希碼,也稱為散列碼;它實際上是返回一個int整數(shù)。這個哈希碼的作用是確定該對象在哈希表中的索引位置。

hashCode() 定義在 JDK 的 Object.java 中,這就意味著Java中的任何類都包含有 hashCode() 函數(shù)。

雖然,每個 Java 類都包含 hashCode() 函數(shù)。但是,僅僅當(dāng)創(chuàng)建并某個“類的散列表”(關(guān)于“散列表”見下面說明)時,該類的 hashCode() 才有用(作用是:確定該類的每一個對象在散列表中的位置;其它情況下(例如,創(chuàng)建類的單個對象,或者創(chuàng)建類的對象數(shù)組等等),類的 hashCode() 沒有作用。

上面的散列表,指的是:Java集合中本質(zhì)是散列表的類,如HashMap,Hashtable,HashSet。
也就是說:hashCode() 在散列表中才有用,在其它情況下沒用。
在散列表中 hashCode() 的作用是獲取對象的散列碼,進而確定該對象在散列表中的位置。

散列碼的解釋:

我們都知道,散列表存儲的是鍵值對(key-value),它的特點是:能根據(jù)“鍵”快速的檢索出對應(yīng)的“值”。這其中就利用到了散列碼!
散列表的本質(zhì)是通過數(shù)組實現(xiàn)的。當(dāng)我們要獲取散列表中的某個“值”時,實際上是要獲取數(shù)組中的某個位置的元素。而數(shù)組的位置,就是通過“鍵”來獲取的;更進一步說,數(shù)組的位置,是通過“鍵”對應(yīng)的散列碼計算得到的。

我們以 Hashset 為例來說明下 hashcode() 的作用:
首先我們都知道 HashSet 是 Set 的集合,不允許有重復(fù)的元素。當(dāng)該 Set 已經(jīng)有了 1000 個元素時,當(dāng)插入第1001個元素時,需要怎么處理? “將第1001個元素逐個的和前面1000個元素進行比較”?顯然,這個效率是相等低下的。散列表很好的解決了這個問題,它根據(jù)元素的散列碼計算出元素在散列表中的位置,然后將元素插入該位置即可。對于相同的元素,自然是只保存了一個。由此可知,若兩個元素相等,它們的散列碼一定相等;但反過來確不一定。
在散列表中,

  1. 如果兩個對象相等,那么它們的hashCode()值一定要相同;
  2. 如果兩個對象hashCode()相等,它們并不一定相等。
    注意:這是在散列表中的情況。在非散列表中一定如此!

0x04 hashCode() 和 equals() 的關(guān)系

我們修改主函數(shù)為:

public static void main(String[] args) {
    Person p1 = new Person("小明", 12);
    // HashMap
    HashMap<Person, Integer> map = new HashMap<>();
    map.put(p1, 1);
    out(map.get(new Person("小明", 12)));

}

按照理想中,我們輸出的結(jié)果應(yīng)該為 “1”,因為我們存入的 Person 和查找的 Person 都是“小明”,是同一個人。
但是最終運行該程序輸出結(jié)果為:

null

所以按照設(shè)計標(biāo)準(zhǔn)我們應(yīng)該在重寫 equals() 方法的同時也要寫重寫 hashcode()。

雖然通過重寫equals方法使得邏輯上姓名和年齡相同的兩個對象被判定為相等的對象(跟String類類似),但是要知道默認(rèn)情況下,hashCode 方法是將對象的存儲地址進行映射。那么上述代碼的輸出結(jié)果為“null”就不足為奇了。

因為 p1 對象和 new Person("小明", 12) 生成的對象是兩個不同的對象,它們的存儲地址肯定不同,所以得到的 hashcode 值不同(不絕對,因為有哈希沖突的情況)。

所以現(xiàn)在重寫下我們的 Person 類的 hashcode() 方法,利用 eclipse 自動生成如下:

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

現(xiàn)在我們再次運行程序得到結(jié)果:

1

與預(yù)期一致。

摘自Effective Java一書:

  • 在程序執(zhí)行期間,只要equals方法的比較操作用到的信息沒有被修改,那么對這同一個對象調(diào)用多次,hashCode方法必須始終如一地返回同一個整數(shù)。
  • 如果兩個對象根據(jù)equals方法比較是相等的,那么調(diào)用兩個對象的hashCode方法必須返回相同的整數(shù)結(jié)果。
  • 如果兩個對象根據(jù)equals方法比較是不等的,則hashCode方法不一定得返回不同的整數(shù)。

對于第一條,我們通過一個例子來驗證:

public static void main(String[] args) {
    Person p1 = new Person("小明", 12);
    out("初始值:" + p1.hashCode());
    HashMap<Person, Integer> map = new HashMap<>();
    map.put(p1, 1);
    p1.age = 13;
    out("更新后:" + p1.hashCode());
    out(map.get(p1));
}

輸出結(jié)果為:

初始值:758036
更新后:758067
null

其中原因我就不用多說了,因此,在設(shè)計 hashCode 方法和 equals 方法的時候,如果對象中的數(shù)據(jù)易變,則最好在 equals 方法和 hashCode 方法中不要依賴于該字段。

參考文檔

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容