感謝大神HashCodejava
感謝大神HashMapnode
@Test public void Integer_HashCode(){ Integer one = new Integer(20); System.out.println(one.hashCode()); //20 } /** * Integer 的 hashCode 就是它的value * * public int hashCode() { * return Integer.hashCode(value); * } */
@Test public void String_HashCode(){ String str1 ="123"; System.out.println(str1.hashCode()); // 48690 } /** * ASCII http://tool.oschina.net/commons?type=4 * String 類的散列值就是依次遍歷其每一個字符成員, * 遞歸的將當前獲得的hashCode乘以31而後加上下一個字符成員的ASCII值 (h = 31 * h + val[i];) * * h 初始爲 0 * '1' 49 h = 31 * 0 + 49 = 49 * '2' 50 h = 31 * 49 + 50 = 1569 * '3' 51 h = 31 * 1569 + 51 = 48690 * * 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; * } */
關於網上的一些解釋git
Why does Java’s hashCode() in String use 31 as a multiplier?github
排名第一的答案web
The value 31 was chosen because it is an odd prime. If it were even and the multiplication overflowed, information would be lost, as multiplication by 2 is equivalent to shifting. The advantage of using a prime is less clear, but it is traditional. A nice property of 31 is that the multiplication can be replaced by a shift and a subtraction for better performance: 31 * i == (i << 5) - i`. Modern VMs do this sort of optimization automatically. 選擇數字31是由於它是一個奇質數,,相對來講,若是選擇一個偶數會在乘法運算中產生溢出,致使數值信息丟失,由於乘二至關於移位運算。 選擇質數的優點並非特別的明顯,但這是一個傳統。同時,數字31有一個很好的特性,即乘法運算能夠被移位和減法運算取代,來獲取更好的性能:31 * i == (i << 5) - i,現代的 Java 虛擬機能夠自動的完成這個優化。 (h = 31 * h + val[i];)
排名第二的答案 ,後面有可視化驗證算法
As Goodrich and Tamassia point out, If you take over 50,000 English words (formed as the union of the word lists provided in two variants of Unix), using the constants 31, 33, 37, 39, and 41 will produce less than 7 collisions in each case. Knowing this, it should come as no surprise that many Java implementations choose one of these constants. 正如 Goodrich 和 Tamassia 指出的那樣,若是你對超過 50,000 個英文單詞(由兩個不一樣版本的 Unix 字典合併而成)進行 hash code 運算,並使用常數 31, 33, 37, 39 和 41 做爲乘子,每一個常數算出的哈希值衝突數都小於7個,因此在上面幾個常數中,常數 31 被 Java 實現所選用也就不足爲奇了。
31 * i = (i << 5) - i
。現代的 Java 虛擬機能夠自動的完成這個優化假設 n=3 i=0 -> h = 31 * 0 + val[0] i=1 -> h = 31 * (31 * 0 + val[0]) + val[1] i=2 -> h = 31 * (31 * (31 * 0 + val[0]) + val[1]) + val[2] h = 31*31*31*0 + 31*31*val[0] + 31*val[1] + val[2] h = 31^(n-1)*val[0] + 31^(n-2)*val[1] + val[2] 這裏先分析質數2。-------僅計算公式中次數最高的那一項----------- 首先,假設 n = 6,而後把質數2和 n 帶入上面的計算公式。結果是2^5 = 32,是否是很小。因此這裏能夠判定,當字符串長度不是很長時,用質數2作爲乘子算出的哈希值,數值不會很大。也就是說,哈希值會分佈在一個較小的數值區間內,分佈性不佳,最終可能會致使衝突率上升,質數2作爲乘子會致使哈希值分佈在一個較小區間內 那麼若是用一個較大的大質數101會產生什麼樣的結果呢?根據上面的分析,我想你們應該能夠猜出結果了。就是不用再擔憂哈希值會分佈在一個小的區間內了,由於101^5 = 10,510,100,501。可是要注意的是,這個計算結果太大了。若是用 int 類型表示哈希值,結果會溢出,最終致使數值信息丟失。若是不在乎質數101容易致使數據信息丟失問題,或許其是一個更好的選擇。 儘管數值信息丟失並不必定會致使衝突率上升,可是咱們暫且先認爲質數101(或者更大的質數)也不是很好的選擇。最後,咱們再來看看質數31的計算結果: 31^5 = 28629151,結果值相對於32和10,510,100,501來講。是否是很nice,不大不小。
計算哈希算法衝突率並不難,好比能夠一次性將全部單詞的 hash code 算出,並放入 Set 中去除重複值。以後拿單詞數減去 set.size() 便可得出衝突數,有了衝突數,衝突率就能夠算出來了。固然,若是使用 JDK8 提供的流式計算 API,則可更方便算出,代碼片斷以下:segmentfault
public static Integer hashCode(String str, Integer multiplier) { int hash = 0; for (int i = 0; i < str.length(); i++) { hash = multiplier * hash + str.charAt(i); } return hash; } /** * 計算 hash code 衝突率,順便分析一下 hash code 最大值和最小值,並輸出 * @param multiplier * @param hashs */ public static void calculateConflictRate(Integer multiplier, List<Integer> hashs) { Comparator<Integer> cp = (x, y) -> x > y ? 1 : (x < y ? -1 : 0); int maxHash = hashs.stream().max(cp).get(); int minHash = hashs.stream().min(cp).get(); // 計算衝突數及衝突率 int uniqueHashNum = (int) hashs.stream().distinct().count(); int conflictNum = hashs.size() - uniqueHashNum; double conflictRate = (conflictNum * 1.0) / hashs.size(); System.out.println(String.format("multiplier=%4d, minHash=%11d, maxHash=%10d, conflictNum=%6d, conflictRate=%.4f%%", multiplier, minHash, maxHash, conflictNum, conflictRate * 100)); }
從上圖能夠看出,數組
簡單總結微信
一、 質數二、衝突率達到了 55%以上,並且hash值分佈不是很普遍,,僅僅分佈在了整個哈希空間的正半軸部分,即 0 ~ 2^31-1。而負半軸 -2^31 ~ -1,則無分佈。這也證實了咱們上面斷言,即質數2做爲乘子時,對於短字符串,生成的哈希值分佈性不佳。less
二、奇質數,10一、109 表現也不錯,衝突率很低,說明了哈希值溢出不必定致使衝突率比較高,可是溢出的話,咱們不認爲是咱們的優選乘子 ,若是不在乎質數101容易致使數據信息丟失問題,或許其是一個更好的選擇。
三、偶數 3二、36 這兩個並很差,32的衝突率已經超過了50%,儘管36表現好一些,可是和31,37相比,衝突率仍是比較高的,可是**偶數也不必定做爲乘子衝突率就比較高 **
四、奇質數 3一、37 、41 表現都不出,衝突數都小於7個,使用較小的質數作爲乘子時,衝突率會很高。 選擇比較大的質數做爲乘子時,衝突率會下降,可是可能會再次哈希值溢出
上面的2.2介紹了不一樣數字做爲乘子的衝突率狀況,下面分析一下不一樣數字做爲乘子時,hash值得分佈狀況
http://www.javashuo.com/article/p-wpfvgnad-v.html
從Object角度看,JVM每new一個Object,它都會將這個Object丟到一個Hash表中去,這樣的話,下次作Object的比較或者取這個對象的時候(讀取過程),它會根據對象的HashCode再從Hash表中取這個對象。這樣作的目的是提升取對象的效率。若HashCode相同再去調用equal。
(1)HashCode的存在主要是用於查找的快捷性,如Hashtable,HashMap等,HashCode是用來在散列存儲結構中肯定對象的存儲地址的;
(2)若是兩個對象相同, equals方法必定返回true,而且這兩個對象的HashCode必定相同;除非重寫了方法
(3)若是對象的equals方法被重寫,那麼對象的HashCode也儘可能重寫,而且產生HashCode使用的對象,必定要和equals方法中使用的一致,不然就會違反上面提到的第2點;
(4)兩個對象的HashCode相同,並不必定表示兩個對象就相同,也就是equals方法不必定返回true,只可以說明這兩個對象在散列存儲結構中,如Hashtable,他們存放在同一個籃子裏。
Java中的集合(Collection)有兩類,一類是List,再有一類是Set。前者集合內的元素是有序的,元素能夠重複;後者元素無序,但元素不可重複。 equals方法可用於保證元素不重複,可是,若是每增長一個元素就檢查一次,若是集合中如今已經有1000個元素,那麼第1001個元素加入集合時,就要調用1000次equals方法。這顯然會大大下降效率。
因而,Java採用了哈希表的原理。
哈希算法也稱爲散列算法,是將數據依特定算法直接指定到一個地址上。這樣一來,當集合要添加新的元素時,先調用這個元素的HashCode方法,就一會兒能定位到它應該放置的物理位置上。
(1)若是這個位置上沒有元素,它就能夠直接存儲在這個位置上,不用再進行任何比較了;
(2)若是這個位置上已經有元素了,就調用它的equals方法與新元素進行比較,相同的話就不存了;
(3)不相同的話,也就是發生了Hash key相同致使衝突的狀況,那麼就在這個Hash key的地方產生一個鏈表,將全部產生相同HashCode的對象放到這個單鏈表上去,串在一塊兒(不多出現)。這樣一來實際調用equals方法的次數就大大下降了,幾乎只須要一兩次。 (下面一、的實例就爲這裏的測試實例)
(1)例如內存中有這樣的位置 :0 1 2 3 4 5 6 7
而我有個類,這個類有個字段叫ID,我要把這個類存放在以上8個位置之一,若是不用HashCode而任意存放,那麼當查找時就須要到這八個位置裏挨個去找,或者用二分法一類的算法。
但若是用HashCode那就會使效率提升不少。 定義咱們的HashCode爲ID%8,好比咱們的ID爲9,9除8的餘數爲1,那麼咱們就把該類存在1這個位置,若是ID是13,求得的餘數是5,那麼咱們就把該類放在5這個位置。依此類推。
(2)可是若是兩個類有相同的HashCode,例如9除以8和17除以8的餘數都是1,也就是說,咱們先經過 HashCode來判斷兩個類是否存放某個桶裏,但這個桶裏可能有不少類,好比hashtable,那麼咱們就須要再經過 equals 在這個桶裏找到咱們要的類。
public class HashTest { private int i; public int getI() { return i; } public void setI(int i) { this.i = i; } @Override public int hashCode() { return i % 10; } /** * 對象的內存地址與hashcode有關係,但並非hashcode值相等,就是表明內存地址相同,這種想法是幼稚的 * 好比hashtable中hashcode值相等, * 可是存了不少的對象,這代表對象的== 確定不相等,Ojbect逆向推理,equals不相等,==確定不相等 * */ public final static void main(String[] args) { HashTest a = new HashTest(); HashTest b = new HashTest(); System.out.println(a.hashCode() == b.hashCode()); //true 人爲製造hashcode值相同 System.out.println(a==b); //false //== 比較對象的相等比較對象引用地址是否相等。還要要比較對象內容是否相等 System.out.println(a.equals(b)); //false 不一樣的對象 object中 == 和euqals是同樣的 a.setI(1); b.setI(1); Set<HashTest> set = new HashSet<HashTest>(); set.add(a); set.add(b); //沒有 equels 重寫的狀況 System.out.println(a.hashCode() == b.hashCode()); //true hashcode相同 System.out.println(a.equals(b)); //false 不一樣的對象 ,建立出來的是地址就不一樣了 //2 這個時候會發想存入了兩個值 set中存放是根據hashcode值存放,若是hashcode值相同, //再比較equals值,若是equals值也相同,則產生一個單鏈表放進去 System.out.println(set.size()); }
public class HashTest { private int i; public int getI() { return i; } public void setI(int i) { this.i = i; } @Override public boolean equals(Object object) { if (object == null) { return false; } if (object == this) { return true; } if (!(object instanceof HashTest)) { return false; } HashTest other = (HashTest) object; if (other.getI() == this.getI()) { return true; } return false; } @Override public int hashCode() { return i % 10; } public final static void main(String[] args) { HashTest a = new HashTest(); HashTest b = new HashTest(); System.out.println(a.hashCode() == b.hashCode()); System.out.println(a==b); System.out.println(a.equals(b)); a.setI(1); b.setI(1); Set<HashTest> set = new HashSet<HashTest>(); set.add(a); set.add(b); System.out.println(a.hashCode() == b.hashCode()); System.out.println(a.equals(b)); System.out.println(set.size()); } } }
package com.hlj.util.Z009HashCode; import java.util.HashMap; import java.util.List; import java.util.Map; /** * @Description * @Author HealerJean * @Date 2018/4/12 */ public class D02_Object { public static void main(String[] args) { Map <Object,Object> map = new HashMap<>(); Object o = new Object(); map.put(o,"Stromg"); Object o2 = new Object(); map.put(o2,3); System.out.println(map.get(o)); //Stromg System.out.println(map.get(o2)); //3 o = 456; System.out.println(map.get(o)); //null 由於提供的key的hashcode的地址不是咱們存儲的地址,因此不能找到,可是以前存儲的還在 Person person = new Person(); person.setAge(1); map.put(person,3); System.out.println(map.get(person)); //3 person.setName("HealerJean"); System.out.println(map.get(person)); //3 System.out.println("map的大小"+map.size()); //map的大小3 person = null ; // person設置爲null, map中仍是具備該person對象 System.out.println(map.get(person)); //null System.out.println("map的大小"+map.size()); //map的大小3 System.out.println("打印map的結果集"); Object[] list = map.keySet().toArray(); for (Object object :list){ System.out.println("key="+object.toString()+"||||| value="+map.get(object).toString()); } } } class Person { int age; String name; public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "Person{" + "age=" + age + ", name='" + name + '\'' + '}'; } } 打印結果 /** * Stromg * 3 * null * 3 * 3 * map的大小3 * null * map的大小3 * 打印map的結果集 * key=java.lang.Object@9807454||||| value=3 * key=Person{age=1, name='HealerJean'}||||| value=3 * key=java.lang.Object@4fca772d||||| value=Stromg */
上面明白了hashcode的生成原理了,如今咱們來看看 hash算法
hash值的做用,知道hash是爲了獲取數組下標的,很明顯就知道該hash值是爲了均勻散列表的下標,仔細看看,就知道下面使用了 hashcode 右移16位再和本身異或獲得的hash值
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
觀察下HashMap查找到數組的
// 重點關注 first = tab[(n - 1) & hash]) != null final Node<K,V> getNode(int hash, Object key) { // 該處的hash其實是調用的上面的hash方法 Node<K,V>[] tab; Node<K,V> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && // 咱們須要關注下面這一行 (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }
舉例說明
對象 A 的 hashCode 爲 0100 0010 0011 1000 1000 0011 1100 0000
對象 B 的 hashCode 爲 0011 1011 1001 1100 0101 0000 1010 0000
若是不通過hash運算,若是數組長度是16(默認就是16),也就是 15 與運算這兩個數,
15 的二進制數 0000 0000 0000 0000 0000 0000 0000 1111 ,發現結果都是0。這樣的話數組小標就都是0了,這樣的結果應該不是咱們想看到的,由於這種狀況其實出現的次數挺多的。
解決
若是咱們將 hashCode 值右移 16 位,也就是取 int 類型的一半,恰好將該二進制數對半切開。而且使用位異或運算(若是兩個數對應的位置相反,則結果爲1,反之爲0),這樣的話,就能避免咱們上面的狀況的發生,即(h = key.hashCode()) ^ (h >>> 16)。 至關於讓本身的前半段16位和後半段16位作一個異或的運算
總的來講,使用位移 16 位和 異或 就是防止這種極端狀況。可是,該方法在一些極端狀況下仍是有問題,好比:10000000000000000000000000 和 1000000000100000000000000 這兩個數,若是數組長度是16,那麼即便右移16位,在異或,hash 值仍是會重複。可是爲了性能,對這種極端狀況,JDK 的做者選擇了性能。畢竟這是少數狀況,爲了這種狀況去增長 hash 時間,性價比不高。
當數組吃長度n爲 16 的時候數組下標: 1111 & 101010100101001001000(隨便寫的) 1000 = 8
其中 n 是數組的長度。其實該算法的結果和模運算的結果是相同的。可是,對於現代的處理器來講,**除法和求餘數(模運算)**是最慢的動做,
經過上面的4.1末尾,能夠看到,能夠看到,當 n 爲 2 的冪次方的時候,減一以後就會獲得 一堆1111…… 的數字,這個數字正好能夠掩碼 (都是一堆 1111……),而且獲得的結果取決於 hash 值。由於 hash 位值是1,那麼最終的結果也是1 ,hash位 值是0,最終的結果也是0。
tab[ (n - 1) & hash ];
接4.二、hash 算法的目的是爲了讓hash值均勻的分佈在桶中(數組),那麼,如何作到呢?試想一下,若是不使用 2 的冪次方做爲數組的長度會怎麼樣?
假設咱們的數組長度是10,仍是上面的公式:
1001 & 101010100101001001000 結果:1000 = 8
1001 & 101000101101001001001 結果:1001 = 9
1001 & 101010101101101001010 結果: 1000 = 8
1001 & 101100100111001101100 結果: 1000 = 8
結論
因此說,咱們必定要保證 & 中的二進制位全爲 1,才能最大限度的利用 hash 值,並更好的散列,只有全是1 ,纔能有更多的散列結果。若是是 1001,有的散列結果是永遠都不會出現的,好比 1111,1010,1011,…,只要 & 以前的數有 0, 對應的 1 確定就不會出現(由於只有都是1纔會爲1)。大大限制了散列的範圍。
那咱們如何自定義呢?自從有了阿里的規約插件,每次樓主都要初始化容量,若是咱們預計咱們的散列表中有2個數據,那麼我就初始化容量爲2嘛
絕對不行,若是你們看過源碼就會發現,若是Map中已有數據的容量達到了初始容量的 75%,那麼散列表就會擴容,而擴容將會從新將全部的數據從新散列,性能損失嚴重,因此,咱們能夠必需要大於咱們預計數據量的 1.34 倍,若是是2個數據的話,就須要初始化 2.68 個容量。固然這是開玩笑的,2.68 不能夠,3 可不能夠呢?確定也是不能夠的,我前面說了,若是不是2的冪次方,散列結果將會大大降低。致使出現大量鏈表。那麼我能夠將初始化容量設置爲4。 固然了,若是你預計大概會插入 12 條數據的話,那麼初始容量爲16簡直是完美,一點不浪費,並且也不會擴容。
項目經驗總計:
一、好比鏈表數不少,確定是數組初始化長度不對,這樣的話會形成擁擠
二、若是某個map很大,注意,確定是事先沒有定義好初始化長度
假設,某個Map存儲了10000個數據,那麼他會擴容到 20000,實際上,根本不用 20000,只須要 10000* 1.34= 13400 個,而後向上找到一個2 的冪次方,也就是 16384 初始容量足夠。
哈,博主很樂意和各路好友交流,若是滿意,請打賞博主任意金額,感興趣的在微信轉帳的時候,備註您的微信或者其餘聯繫方式。添加博主微信哦。
請下方留言吧。可與博主自由討論哦
微信 | 微信公衆號 | 支付寶 |
---|---|---|
![]() |
![]() |
![]() |