面試官問我:hashcode 是什麼?和equals是兄弟嗎?

2021-02-18 程序IT圈

 

    秋招的時候還記得面試官問過我hashcode是什麼,對於int、long、string類型的hashcode有什麼區別,和equals一起是怎麼使用的,為什麼重寫hashcode的同時也要重寫equals。

      八股文背多了,也只是會表面,有空的時候還是整理一下,順便寫了幾個例子加深下印象。

hashcode 是什麼?

hash 一般翻譯做「散列」,也有直接音譯為「哈希」的,就是把任意長度的輸入,通過散列算法,變換成固定長度的輸出,該輸出就是散列值。hash 是一個函數,該函數中的實現就是一種算法,就是通過一系列的算法來得到一個 hash 值。每個對象都有 hashcode,對象的 hashcode 怎麼得來的呢?

首先一個對象肯定有物理地址,對象的物理地址跟這個 hashcode 地址不一樣,hashcode 代表對象的地址說的是對象在 hash 表中的位置,通過對象的內部地址(也就是物理地址)轉換成一個整數,然後該整數通過 hash 函數的算法就得到了 hashcode。所以,hashcode 就是在 hash 表中對應的位置。

所有散列函數都有如下一個基本特性:根據同一散列函數計算出的散列值如果不同,那麼輸入值肯定也不同。但是,根據同一散列函數計算出的散列值如果相同,輸入值不一定相同。

兩個不同的輸入值,根據同一散列函數計算出的散列值相同的現象叫做碰撞

常見的 Hash 函數有以下幾個:

直接定址法:直接以關鍵字 k 或者 k 加上某個常數(k+c)作為哈希地址。

數字分析法:提取關鍵字中取值比較均勻的數字作為哈希地址。

除留餘數法:用關鍵字 k 除以某個不大於哈希表長度 m 的數 p,將所得餘數作為哈希表地址。

分段疊加法:按照哈希表地址位數將關鍵字分成位數相等的幾部分,其中最後一部分可以比較短。然後將這幾部分相加,捨棄最高進位後的結果就是該關鍵字的哈希地址。

平方取中法:如果關鍵字各個部分分布都不均勻的話,可以先求出它的平方值,然後按照需求取中間的幾位作為哈希地址。

偽隨機數法:採用一個偽隨機數當作哈希函數。

定義

以下是關於 HashCode 的官方文檔定義:

hashcode 方法返回該對象的哈希碼值。支持該方法是為哈希表提供一些優點,例如,java.util.Hashtable 提供的哈希表。

hashCode 的常規協定是:
在 Java 應用程式執行期間,在同一對象上多次調用 hashCode 方法時,必須一致地返回相同的整數,前提是對象上 equals 比較中所用的信息沒有被修改。從某一應用程式的一次執行到同一應用程式的另一次執行,該整數無需保持一致。

如果根據 equals(Object) 方法,兩個對象是相等的,那麼在兩個對象中的每個對象上調用 hashCode 方法都必須生成相同的整數結果。

以下情況不是必需的:
如果根據 equals(java.lang.Object)方法,兩個對象不相等,那麼在兩個對象中的任一對象上調用 hashCode 方法必定會生成不同的整數結果。但是,程式設計師應該知道,為不相等的對象生成不同整數結果可以提高哈希表的性能
實際上,由 Object 類定義的 hashCode 方法確實會針對不同的對象返回不同的整數。(這一般是通過將該對象的內部地址轉換成一個整數來實現的,但是 JavaTM 程式語言不需要這種實現技巧。)當 equals 方法被重寫時,通常有必要重寫 hashCode 方法,以維護 hashCode 方法的常規協定,該協定聲明相等對象必須具有相等的哈希碼。

總結一下:
1.hashcode 一致性 同一個對象的 hashcode 肯定是一樣的,無論調用多少次 hashcode 都不會變化,隨著 equals 肯定也是一樣的
2.兩個對象的 hashCode 相同,並不一定表示兩個對象就相同,也就是不一定適用於 equals(java.lang.Object) 方法,只能夠說明這兩個對象在散列存儲結構中,如 Hashtable,他們「存放在同一個籃子裡」。
3.如果對象的 equals 方法被重寫,那麼對象的 hashCode 也儘量重寫,並且產生 hashCode 使用的對象,一定要和 equals 方法中使用的一致,

剛剛提到的碰撞,指的是不同對象的 hashcode 處在同一個 bucket 當中,這個情況發生的概率越大說明這個 hashcode 設計的不夠理想

hashcode 和 equals 的聯繫

若且唯若兩個對象的hashcode和equals相同時,這兩個對象才是同一個對象,否則不是。

那麼快速判斷兩個對象的步驟是怎麼樣的呢,我們假設這裡有兩個不同的對象A和B。當我們的hashcode設計合理的時候,這兩個對象的hashcode(A)是和hashcode(B)不相等的,那麼這個時候我們就可以直接判斷A和B不是同一對象。但如果hashcode(A)==hashcode(B)呢? 這個時候就要繼續通過equals方法進行比較了,但是整個equals方法比hashcode複雜,所以設計一個好的hashcode函數至少可以節約90%以上的時間。

java 中的 hashcode 是如何實現的

我們知道 java 內部 HashSet 和 HashMap 都是基於 hash 算法去實現的
hash算法的好壞,直接影響這個hashcode碰撞的機率,好的hashcode可以使得所有對象均勻地分布在bucket中

1.Integer、Byte、Short、Character都是轉換為int類型作為hashcode
    public static int hashCode(int value) {
        return value;
    }

可以看到hashcode在遇到Integer、Byte、Short、Character直接返回原數,不做處理。

public class HashMapTest {
    public static void main(String[] args) {
        Integer a = 1;
        Integer b = 1;
        System.out.println(a.hashCode()+"  "+b.hashCode());
        System.out.println(a.equals(b));
    }
}

1  1
true

2.Long  取高32位和低32位與值轉成int類型作為hashcode

Double  將64bit值轉成long類型,然後按照Long類型進行獲取hashcode**

    public static int hashCode(double value) {
        long bits = doubleToLongBits(value);
        return (int)(bits ^ (bits >>> 32));
    }

long型是將自己的高32位和低32位拆成兩個部分,然後兩個部份直接做與操作得出的數字作為hashcode。

demo中a=1,b=1<<32,均為Long型,按照我們的設計兩者的hashcode應該是一樣的,但是不equals。看看我們的實驗是否驗證這個說法。

public class HashMapTest {
    public static void main(String[] args) {
        long number = 1;
        Long a =(long)1;
        Long b = number<<32;
        System.out.println(a+" "+b);
        System.out.println(a.hashCode()+" "+b.hashCode() );
        System.out.println(a.equals(b));
    }
}
---
1 4294967296
1 1
false

看來還是逃不過equals。

doubleToLongBits該方法可以將double類型數據轉換成long類型數據,從而可以使double類型數據按照long的方法判斷大小(<, >, ==)。

3.String 將字符串裡面的字符以31進位求和,既hash=hash*31+value[i]
    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;
    }

為什麼選擇31作為乘子

31是一個不大不小的質數,是作為 hashCode 乘子的優選質數之一。另外一些相近的質數,比如37、41、43等等,也都是不錯的選擇。那麼為啥偏偏選中了31呢?請看第二個原因。

31可以被 JVM 優化,31 * i = (i << 5) - i。

4.Boolean true值hashcode為1231,false值hashcode為1237
      public static int hashCode(boolean value) {
        return value ? 1231 : 1237;
    }

5.hashmap 放置元素的hashcode
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

由於和(length-1)運算,length 絕大多數情況小於2的16次方。所以始終是hashcode 的低16位(甚至更低)參與運算。要是高16位也參與運算,會讓得到的下標更加散列

所以這樣高16位是用不到的,如何讓高16也參與運算呢。所以才有hash(Object key)方法。讓他的hashCode()和自己的高16位^運算。所以(h >>> 16)得到他的高16位與hashCode()進行^運算。

值得注意的是hashset存放的時候也是聲明了一個hashmap進行存儲,所以原理等同於hashmap

demo demo1 案例 不重寫 hashcode 和 equals:
public class HashCodeTest {
    private int number;
    public HashCodeTest(int number){
        this.number =number;
    }



    public static void main(String[] args) {
        HashCodeTest a = new HashCodeTest(1);
        HashCodeTest b = new HashCodeTest(1);
        System.out.println(a.hashCode());
        System.out.println(b.hashCode());
        System.out.println(a.equals(b));
    }
}

輸出

460141958
1163157884
false

分析:

兩個不同的對象hashcode(a)和hashcode(b)肯定不同,a 對象和 b 對象調用 equal 函數肯定返回 false

demo2 案例 只重寫 hashcode:
public class HashCodeTest {
    private int number;
    public HashCodeTest(int number){
        this.number =number;
    }

    @Override
    public int hashCode() {
        return number%8;
    }

    public static void main(String[] args) {
        HashCodeTest a = new HashCodeTest(1);
        HashCodeTest b = new HashCodeTest(1);
        System.out.println(a.hashCode());
        System.out.println(b.hashCode());
        System.out.println(a.equals(b));
    }
}

輸出:

1
1
false

分析:

這個案例中只是覆寫了 hashcode 這個方法,沒有覆寫 equals。
這兩個不同的對象 hashcode 相同,但是 equals 不同。
從定義上看是 可以成立的。

但是 hashcode 過於簡單,可能存在嚴重的哈希碰撞問題。而且必須滿足同一對象的 hashcode 是一致的。最好是 equals 和 hashcode 同時覆寫。

demo3 只重寫equals
public class HashCodeTest {
    private int number;
    public HashCodeTest(int number){
        this.number =number;
    }

    @Override
    public boolean equals(Object o) {
        HashCodeTest that = (HashCodeTest) o;
        return number == that.number;
    }



    public static void main(String[] args) {
        HashCodeTest a = new HashCodeTest(1);
        HashCodeTest b = new HashCodeTest(1);
        System.out.println(a.hashCode());
        System.out.println(b.hashCode());
        System.out.println(a.equals(b));

    }
}

輸出:

1956725890
356573597
true

只重寫equals,可能會因為重寫的方法不夠完善導致原本兩個hashcode不同的對象equals返回true,這是最無法容忍的現象。

相關焦點

  • 乾貨 | 名企高頻考點-Java hashCode() 方法 和 equals() 方法
    在Java中存在兩個比較方法,分別是:hashCode() 方法 和 equals() 方法。public boolean equals(Object obj) { return (this == obj); }public native int hashCode();你知道hashCode() 方法 和 equals() 方法嗎?
  • 重寫equals()時為什麼要重寫hashcode
    由於每個對象的默認hashcode值是唯一的,因此手機1和手機2的hashcode值就不相同,進而導致兩個手機的value值不同。發現問題了嗎?因為我們重寫了IphoneX的equals()方法,所以 手機1.equals(手機2) 的結果是true,;但由於沒有重寫hashcode()方法,又導致map.get(手機1)和map.get(手機2)的值又不同(一個hello,一個world)。這就混亂了,明明「手機1」和「手機2」都equals()相等了,但二者通過map.get()方法的返回值卻又不同。
  • 不懂得hashcode的重要性,程序的性能會大打折扣
    面試時是否被問到過:兩個對象值相同(x.equals(y) == true),但卻可有不同的hashcode,這句話對不對?,如果我是面試官,我必問這個問題。第二種:自己重寫equals和hashcode,這就不一定了。這得看你自己怎麼寫hashcode了。
  • 為什麼要重寫 hashcode 和 equals 方法?
    Java初級開發的時候,經常會問:你有沒有重寫過hashcode方法?當前我們先注釋掉第9行的equals方法和第16行的hashCode方法。但k1有可能僅僅是和k2具有相同的hash值,但未必和k2相等(k1和k2兩把鑰匙未必能開同一扇門),這個時候,就需要調用Key對象的equals方法來判斷兩者是否相等了。由於我們在Key對象裡沒有定義equals方法,系統就不得不調用Object類的equals方法。
  • 經典面試題:重寫equals方法時,為什麼必須重寫hashCode方法?
    用鍊表幹什麼?不同的key,hashcode可能相同,也就是說不同的key,他們可能會放在數組的相同位置,那麼如何存放呢?這個地方使用鍊表將多個元素連接起來,存放多個元素。put的過程大家知道了,我們再來看看get的過程獲取key在數組的什麼位置通過key的hashcode找到元素在數組的什麼位置,算法:key.hashCode() & (數組大小 - 1)遍歷鍊表查找元素
  • Java面試題之為什麼要重寫hashcode( )和equals( )?
    好了,請看下題:為什麼要重寫hashcode( )和equals( )?打個比方,一個名叫張三的人去住酒店,在前臺登記完名字就去了99層100號房間,此時警察來前臺找叫張三的這個人住在哪間房,經過查詢,該酒店住宿的有50個叫張三的,需要遍歷查詢,查詢起來很不方便。
  • Java為什麼重寫equals一定要重寫hashCode?
    前言最近複習,又看到了這個問題,在此記錄和整理,通過例子來說明這種情況的原因,使大家可以清晰明白這個問題。初步探索首先我們要了解equals方法是什麼,hashcode方法是什麼。書寫一個測試類輸出此時,我們發現 equals 相等的對象,hashcode卻不相等,同時在map中用不同的對象進行了存儲,map計算出來的hash值不同,但equals卻相同。這時候懵了。到底兩個對象一樣不一樣呢。
  • 『Java系列文章』2、淺談equals()和hashcode()
    對於Object類來說,equals()方法在對象上實現的是差別可能性最大的等價關係,即,對於任意非null的引用值x和y,若且唯若x和y引用的是同一個對象,該方法才會返回true。需要注意的是當equals()方法被override時,hashCode()也要被override。按照一般hashCode()方法的實現來說,相等的對象,它們的hash code一定相等。
  • 詳解equals()方法和hashCode()方法
    equals()和hashCode()都不是final方法,都可以被重寫(overwrite)。本文介紹了2種方法在使用和重寫時,一些需要注意的問題。如果覺得文章對你有幫助,歡迎點讚或轉發。文章有疏漏之處,歡迎批評指正。
  • 「原創」不重寫equals和hashcode難道就不行嗎?
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫究竟為什麼要重寫equals和hashcode???目錄1、equals()方法和hashCode()方法介紹1.1、equals()方法1.2、hashCode()方法
  • Java中重寫equals方法為什麼要重寫hashcode方法?
    當equals方法被重寫時,通常有必要重寫hashCode方法,以維護hashCode方法的常規約定:值相同的對象必須有相同的hashCode。object1.equals(object2)為true,hashCode也相同;hashCode不同時,object1.equals(object2)為false;hashCode相同時,object1.equals(object2)不一定為true
  • equals 和 hashCode 到底有什麼聯繫?一文告訴你!
    equals()和hashCode()都不是final方法,都可以被重寫(overwrite)。本文介紹了2種方法在使用和重寫時,一些需要注意的問題。通過該實現可以看出,Object類的實現採用了區分度最高的算法,即只要兩個對象不是同一個對象,那麼equals()一定返回false。
  • 淺談Java中的hashcode方法
    = markOopDesc::no_hash, "invariant") ; TEVENT (hashCode: GENERATE) ; return value; }該實現位於hotspot/src/share/vm/runtime/synchronizer.cpp文件下。因此有人會說,可以直接根據hashcode值判斷兩個對象是否相等嗎?
  • 淺談 Java 中的 hashcode 方法
    此時hashCode方法的作用就體現出來了,當集合要添加新的對象時,先調用這個對象的hashCode方法,得到對應的hashcode值,實際上在HashMap的具體實現中會用一個table保存已經存進去的對象的hashcode值,如果table中沒有該hashcode值,它就可以直接存進去,不用再進行任何比較了;如果存在該hashcode值, 就調用它的equals方法與新元素進行比較,相同的話就不存了
  • Object類中的equals和hashCode方法,你真的了解嗎?
    前言在Java中,equals和hashCode方法是Object中提供的兩個方法,這兩個方法對以後的學習有很大的幫助,本文就深度來去講解這兩個方法。多說一句,我們直接從JDK的文檔來去解釋。 一致性 : x.equals(y)的第一次調用為true,那麼x.equals(y)的第二次,第三次等多次調用也應該為true,但是前提條件是在進行比較之前,x和y都沒有被修改。 x.equals(null) 應該返回false。
  • 面試話癆(二)C:JAVA String,別以為你穿個馬甲我就不認識你了
    面試話癆系列是從技術廣度的角度去回答面試官提的問題,適合萌新觀看!面試官,別再問我火箭怎麼造了,我知道螺絲的四種擰法,你想聽嗎?
  • 面試官問:為什麼String的hashCode選擇 31 作為乘子?
    某天,我在寫代碼的時候,無意中點開了 String hashCode 方法。然後大致看了一下 hashCode 的實現,發現並不是很複雜。但是我從源碼中發現了一個奇怪的數字,也就是本文的主角31。這個數字居然不是用常量聲明的,所以沒法從字面意思上推斷這個數字的用途。後來帶著疑問和好奇心,到網上去找資料查詢一下。在看完資料後,默默的感嘆了一句,原來是這樣啊。那麼到底是哪樣呢?
  • java工程師必知必會的 hashcode 和 hash 算法!
    前言作為一個有抱負的 Java 程式設計師,在經過長期的CRUD 和 HTML 填空之後必須有所思考,因為好奇心是驅動人類進步的動力之一我們好奇,比如我們常用的 HashMap 到底是如何實現的?我想,說到這裡,稍微有點經驗的大佬都會說:擦,面試必問好嘛?怎麼可能不知道?但是,我們真的了解他嗎?
  • hashCode和identityHashCode的區別你知道嗎?
    /** * Returns the same hash code for the given object as * would be returned by the default method hashCode(), * whether or not the given object's class overrides