天天看點

面試官問我: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作為乘子

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

這個案例中隻是覆寫了 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));

    }
}