標籤:數組下標 strong puts www 封裝 inpu 應該 [] fabs
之前很早就在部落格中寫過HashMap的一些東西:徹底搞懂HashMap,HashTableConcurrentHashMap關聯:http://www.cnblogs.com/wang-meng/p/5808006.htmlHashMap和HashTable的區別:http://www.cnblogs.com/wang-meng/p/5720805.html 今天來講HashMap是分JDK7和JDK8 對比著來講的, 因為JDK8中針對於HashMap有些小的改動, 這也是一些面試會經常問到的點。
一:JDK7中的HashMap: HashMap底層維護一個數組table, 數組中的每一項是一個key,value形式的Entry。 我們往HashMap中所放置的對象實際是儲存在該數組中。Map中的key,value則以Entry的形式存放在數組中。 這個Entry應該放在數組的哪一個位置上, 是通過key的hashCode來計算的。這個位置也成為hash桶。
通過hash計算出來的值將通過indexFor方法找到它所在的table下標:
這個方法其實是對table.length模數,
當兩個key通過
hashCode計算相同時,則發生了hash衝突(碰撞),HashMap解決hash衝突的方式是用鏈表。當發生hash衝突時,則將存放在數組中的Entry設定為新值的next(這裡要注意的是,比如A和B都hash後都映射到下標i中,之前已經有A了,當map.put(B)時,將B放到下標i中,A則為B的next,所以新值存放在數組中,舊值在新值的鏈表上)。
例如, 一個長度為16的數組中,每個元素儲存的是一個鏈表的頭結點。那麼這些元素是按照什麼樣的規則儲存到數組中呢。一般情況是通過hash(key)%len獲得,也就是元素的key的雜湊值對數組長度模數得到。比如上述雜湊表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都儲存在數組下標為12的位置。它的內部其實是用一個Entity數組來實現的,屬性有key、value、next。 接著看看put方法: 469行, 如果key為空白, 則把這個對象放到第一個數組上。471行, 計算key的hash值472行, 通過indexFor方法返回分散到數組table中的下標473行, 通過table[i]擷取新Entry的值, 如果值不為空白,則判斷key的hash值和equals來判斷新的Entry和舊的Entry值是否相同, 如果相同則覆蓋舊Entry的值並返回。484行, 往數組上添加新的Entry。 添加Entry時,當table的容量大於theshold(
(int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1)), 這裡實際上就是16*0.75=12 如上, 當滿足一定條件後 table就開始擴容, 這個過程也稱為rehash, 具體請看: 559行: 建立一個新的Entry數組564行: 將數組轉移到新的Entry數組中565行: 修改resize的條件threshold再具體的實現大家可以看下jdk7中HashMap的相關源碼。
二:JDK8中的HashMap:一直到JDK7為止,HashMap的結構都是這麼簡單,基於一個數組以及多個鏈表的實現,hash值衝突的時候,就將對應節點以鏈表的形式儲存。這樣子的HashMap效能上就抱有一定疑問,如果說成百上千個節點在hash時發生碰撞,儲存一個鏈表中,那麼如果要尋找其中一個節點,那就不可避免的花費O(N)的尋找時間,這將是多麼大的效能損失。這個問題終於在JDK8中得到瞭解決。再最壞的情況下,鏈表尋找的時間複雜度為O(n),而紅/黑樹狀結構一直是O(logn),這樣會提高HashMap的效率。JDK7中HashMap採用的是位桶+鏈表的方式,即我們常說的
散列鏈表的方式,而JDK8中採用的是位
桶+鏈表/紅/黑樹狀結構的方式,也是非安全執行緒的。當
某個位桶的鏈表的長度達到某個閥值的時候,這個鏈表就將轉換成紅/黑樹狀結構。 JDK8中,當同一個hash值的節點數大於等於8時,將不再以單鏈表的形式儲存了,會被調整成一顆紅/黑樹狀結構(中null節點沒畫)。這就是JDK7與JDK8中HashMap實現的最大區別。
public V put(K key, V value) { return putVal(hash(key), key, value, false, true);}final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; //如果當前map中無資料,執行resize方法。並且返回n if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //如果要插入的索引值對要存放的這個位置剛好沒有元素,那麼把他封裝成Node對象,放在這個位置上就完事了 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); //否則的話,說明這上面有元素 else { Node<K,V> e; K k; //如果這個元素的key與要插入的一樣,那麼就替換一下,也完事。 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //1.如果當前節點是TreeNode類型的資料,執行putTreeVal方法 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { //還是遍曆這條鏈子上的資料,跟jdk7沒什麼區別 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); //2.完成了操作後多做了一件事情,判斷,並且可能執行treeifyBin方法 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) //true || -- e.value = value; //3. afterNodeAccess(e); return oldValue; } } ++modCount; //判斷閾值,決定是否擴容 if (++size > threshold) resize(); //4. afterNodeInsertion(evict); return null;} treeifyBin()就是將鏈錶轉換成紅/黑樹狀結構。之前的indefFor()方法消失 了,直接用(tab.length-1)&hash,所以看到這個,代表的就是數組的角標。 具體紅/黑樹狀結構的實現大家可以看下JDK8中HashMap的實現。
三:需要注意的地方:
再談HashCode的重要性前面講到了,HashMap中對Key的HashCode要做一次rehash,防止一些糟糕的Hash演算法產生的糟糕的HashCode,那麼為什麼要防止糟糕的HashCode?糟糕的HashCode意味著的是Hash衝突,即多個不同的Key可能得到的是同一個HashCode,糟糕的Hash演算法意味著的就是Hash衝突的機率增大,這意味著HashMap的效能將下降,表現在兩方面: 1、有10個Key,可能6個Key的HashCode都相同,另外四個Key所在的Entry均勻分布在table的位置上,而某一個位置上卻串連了6個Entry。這就失去了HashMap的意義,HashMap這種資料結構性高效能的前提是,Entry均勻地分布在table位置上,但現在確是1 1 1 1 6的分布。所以,我們要求HashCode有很強的隨機性,這樣就儘可能地可以保證了Entry分布的隨機性,提升了HashMap的效率。 2、HashMap在一個某個table位置上遍曆鏈表的時候的代碼:if (e.hash == hash && ((k = e.key) == key || key.equals(k)))看到,由於採用了"&&"運算子,因此先比較HashCode,HashCode都不相同就直接pass了,不會再進行equals比較了。HashCode因為是int值,比較速度非常快,而equals方法往往會對比一系列的內容,速度會慢一些。Hash衝突的機率大,意味著equals比較的次數勢必增多,必然降低了HashMap的效率了。
HashMap的table為什麼是transient的一個非常細節的地方:transient Entry[] table;看到table用了transient修飾,也就是說table裡面的內容全都不會被序列化,不知道大家有沒有想過這麼寫的原因? 在我看來,這麼寫是非常必要的。因為HashMap是基於HashCode的,HashCode作為Object的方法,是native的:
public native int hashCode();這意味著的是:HashCode和底層實現相關,不同的虛擬機器可能有不同的HashCode演算法。再進一步說得明白些就是,可能同一個Key在虛擬機器A上的HashCode=1,在虛擬機器B上的HashCode=2,在虛擬機器C上的HashCode=3。 這就有問題了,Java自誕生以來,就以跨平台性作為最大賣點,好了,如果table不被transient修飾,在虛擬機器A上可以用的程式到虛擬機器B上可以用的程式就不能用了,失去了跨平台性,因為:1、Key在虛擬機器A上的HashCode=100,連在table[4]上2、Key在虛擬機器B上的HashCode=101,這樣,就去table[5]上找Key,明顯找不到整個代碼就出問題了。因此,為了避免這一點,Java採取了重寫自己序列化table的方法,在writeObject選擇將key和value追加到序列化的檔案最後面:
private void writeObject(java.io.ObjectOutputStream s) throws IOException{ Iterator<Map.Entry<K,V>> i = (size > 0) ? entrySet0().iterator() : null; // Write out the threshold, loadfactor, and any hidden stuff s.defaultWriteObject(); // Write out number of buckets s.writeInt(table.length); // Write out size (number of Mappings) s.writeInt(size); // Write out keys and values (alternating) if (size > 0) { for(Map.Entry<K,V> e : entrySet0()) { s.writeObject(e.getKey()); s.writeObject(e.getValue()); } }} 而在readObject的時候重構HashMap資料結構:
private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException{ // Read in the threshold (ignored), loadfactor, and any hidden stuff s.defaultReadObject(); if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new InvalidObjectException("Illegal load factor: " + loadFactor); // set hashSeed (can only happen after VM boot) Holder.UNSAFE.putIntVolatile(this, Holder.HASHSEED_OFFSET, sun.misc.Hashing.randomHashSeed(this)); // Read in number of buckets and allocate the bucket array; s.readInt(); // ignored // Read number of mappings int mappings = s.readInt(); if (mappings < 0) throw new InvalidObjectException("Illegal mappings count: " + mappings); int initialCapacity = (int) Math.min( // capacity chosen by number of mappings // and desired load (if >= 0.25) mappings * Math.min(1 / loadFactor, 4.0f), // we have limits... HashMap.MAXIMUM_CAPACITY); int capacity = 1; // find smallest power of two which holds all mappings while (capacity < initialCapacity) { capacity <<= 1; } table = new Entry[capacity]; threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); useAltHashing = sun.misc.VM.isBooted() && (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD); init(); // Give subclass a chance to do its thing. // Read the keys and values, and put the mappings in the HashMap for (int i=0; i<mappings; i++) { K key = (K) s.readObject(); V value = (V) s.readObject(); putForCreate(key, value); }} 一種麻煩的方式,但卻保證了跨平台性。這個例子也告訴了我們:儘管使用的虛擬機器大多數情況下都是HotSpot,但是也不能對其它虛擬機器不管不顧,有跨平台的思想是一件好事。
HashMap和Hashtable的區別HashMap和Hashtable是一組相似的索引值對集合,它們的區別也是面試常被問的問題之一,我這裡簡單總結一下HashMap和Hashtable的區別: 1、Hashtable是安全執行緒的,Hashtable所有對外提供的方法都使用了synchronized,也就是同步,而HashMap則是線程非安全的2、Hashtable不允許空的value,空的value將導致null 指標異常,而HashMap則無所謂,沒有這方面的限制3、上面兩個缺點是最主要的區別,另外一個區別無關緊要,我只是提一下,就是兩個的rehash演算法不同,Hashtable的是: 這個hashSeed是使用sun.misc.Hashing類的randomHashSeed方法產生的。HashMap的rehash演算法上面看過了,也就是:
Java中常見資料結構Map之HashMap