秋招的時候還記得面試官問過我hashcode是什麼,對於int、long、string類型的hashcode有什麼區別,和equals一塊兒是怎麼使用的,爲何重寫hashcode的同時也要重寫equals。
八股文背多了,也只是會表面,有空的時候仍是整理一下,順便寫了幾個例子加深下印象。java
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相同時,這兩個對象纔是同一個對象,不然不是。
那麼快速判斷兩個對象的步驟是怎麼樣的呢,咱們假設這裏有兩個不一樣的對象A和B。當咱們的hashcode設計合理的時候,這兩個對象的hashcode(A)是和hashcode(B)不相等的,那麼這個時候咱們就能夠直接判斷A和B不是同一對象。但若是hashcode(A)==hashcode(B)呢? 這個時候就要繼續經過equals方法進行比較了,可是整個equals方法比hashcode複雜,因此設計一個好的hashcode函數至少能夠節約90%以上的時間。編程
咱們知道 java 內部 HashSet 和 HashMap 都是基於 hash 算法去實現的
hash算法的好壞,直接影響這個hashcode碰撞的概率,好的hashcode可使得全部對象均勻地分佈在bucket中編程語言
public static int hashCode(int value) { return value; }
能夠看到hashcode在遇到Integer、Byte、Short、Character直接返回原數,不作處理。ide
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
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是一個不大不小的質數,是做爲 hashCode 乘子的優選質數之一。另一些相近的質數,好比3七、4一、43等等,也都是不錯的選擇。那麼爲啥恰恰選中了31呢?請看第二個緣由。
31能夠被 JVM 優化,31 * i = (i << 5) - i。
public static int hashCode(boolean value) { return value ? 1231 : 1237; }
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
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
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 同時覆寫。
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,這是最沒法容忍的現象。