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

秋招的時候還記得面試官問過我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 的聯繫

當且僅當兩個對象的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,這是最無法容忍的現象。

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

和Jerry哥聊技術,聊生活

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