HashMap底層實現(源碼分析)

1、數據結構  
Map將實際數據存儲在Entry類的數組中。 
代碼片斷: 
Java代碼     收藏代碼
  1. transient Entry[] table;//HashMap的成員變量,存放數據  
  2.   
  3. static class Entry<K,V> implements Map.Entry<K,V> {//內部類Entry  
  4.     final K key;  
  5.     V value;  
  6.     Entry<K,V> next;//指向下一個數據  
  7.     final int hash;  
  8.   
  9.     /** 
  10.      * Creates new entry. 
  11.      */  
  12.     Entry(int h, K k, V v, Entry<K,V> n) {  
  13.         value = v;  
  14.         next = n;  
  15.         key = k;  
  16.         hash = h;  
  17.     }  

執行下面代碼後,可能的存儲內部結構是: 
Map map = new HashMap(); 
map.put("key1","value1"); 
map.put("key2","value2"); 
map.put("key3","value3"); 


執行put方法時根據key的hash值來計算放到table數組的下標,若是hash到相同的下標,則新put進去的元素放到Entry鏈的頭部,如上圖所示。put方法的源碼後面詳細解釋。 

2、屬性和構造方法  
Java代碼     收藏代碼
  1. static final int DEFAULT_INITIAL_CAPACITY = 16;//默認的初始大小,若是執行無參的構造方法,則默認初始大小爲16  
  2. static final int MAXIMUM_CAPACITY = 1 << 30;//最大容量,1073741824  
  3. static final float DEFAULT_LOAD_FACTOR = 0.75f;//默認的負載因子,若是沒有經過構造方法傳入負載因子,則使用0.75。  
  4. transient Entry[] table; //存放具體鍵值對的Entry數組  
  5. transient int size; //HashMap的大小  
  6. int threshold;//閥值  threshold = (int)(capacity * loadFactor); 即容量*負載因子,執行put方法時若是size大於threshold則進行擴容,後面put方法將會看到  
  7. final float loadFactor; //用戶設置的負載因子  
  8. transient volatile int modCount;//HashMap實例被改變的次數,這個同ArrayList  

構造方法1、 
Java代碼     收藏代碼
  1. public HashMap() {  
  2.     this.loadFactor = DEFAULT_LOAD_FACTOR;  
  3.     threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);  
  4.     table = new Entry[DEFAULT_INITIAL_CAPACITY];  
  5.     init();  
  6. }  

使用了默認的容量和默認的負載因子。 
構造方法2、 
Java代碼     收藏代碼
  1. public HashMap(int initialCapacity) {  
  2.     this(initialCapacity, DEFAULT_LOAD_FACTOR);  
  3. }  

使用了用戶設置的初始容量和默認的負載因子。 
構造方法3、 
Java代碼     收藏代碼
  1. public HashMap(int initialCapacity, float loadFactor) {  
  2.     if (initialCapacity < 0)  
  3.         throw new IllegalArgumentException("Illegal initial capacity: " +  
  4.                                            initialCapacity);  
  5.     if (initialCapacity > MAXIMUM_CAPACITY)  
  6.         initialCapacity = MAXIMUM_CAPACITY;  
  7.     if (loadFactor <= 0 || Float.isNaN(loadFactor))  
  8.         throw new IllegalArgumentException("Illegal load factor: " +  
  9.                                            loadFactor);  
  10.   
  11.     // Find a power of 2 >= initialCapacity  
  12.     int capacity = 1;  
  13.     while (capacity < initialCapacity)  
  14.         capacity <<= 1;  
  15.   
  16.     this.loadFactor = loadFactor;  
  17.     threshold = (int)(capacity * loadFactor);  
  18.     table = new Entry[capacity];  
  19.     init();  
  20. }  

用戶傳入了初始容量和負載因子,這兩個值是HashMap性能優化的關鍵,涉及到了HashMap的擴容問題。 
HashMap的容量永遠是2的倍數,若是傳入的不是2的倍數則被調整爲大於傳入值的最近的2的倍數,例如若是傳入130,則capacity計算後是256。是這段代碼起的做用: 
Java代碼     收藏代碼
  1. while (capacity < initialCapacity)  
  2.         capacity <<= 1;  

構造方法4、 
Java代碼     收藏代碼
  1. public HashMap(Map<? extends K, ? extends V> m) {  
  2.     this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,  
  3.                   DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);//計算Map的大小  
  4.     putAllForCreate(m);//初始化  
  5. }  
  6.   
  7. private void putAllForCreate(Map<? extends K, ? extends V> m) {  
  8.     for (Iterator<? extends Map.Entry<? extends K, ? extends V>> i = m.entrySet().iterator(); i.hasNext(); ) {//經過entryset進行遍歷  
  9.         Map.Entry<? extends K, ? extends V> e = i.next();  
  10.         putForCreate(e.getKey(), e.getValue());  
  11.     }  
  12. }  

根據傳入的map進行初始化。 

3、關鍵方法  
1)put 
Java代碼     收藏代碼
  1. public V put(K key, V value) {  
  2.     if (key == null)  
  3.         return putForNullKey(value);//單獨處理,老是放到table[0]中  
  4.     int hash = hash(key.hashCode());//計算key的hash值,後面介紹性能的時候再說這個hash方法。  
  5.     int i = indexFor(hash, table.length);//將hash和length-1取&來獲得數組的下表  
  6.     for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
  7.         Object k;  
  8.         if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
  9.             V oldValue = e.value;  
  10.             e.value = value;  
  11.             e.recordAccess(this);  
  12.             return oldValue;  
  13.         }  
  14.     }//若是這個key值,原來已經則替換後直接返回。  
  15.     modCount++;  
  16.     addEntry(hash, key, value, i);  
  17.     return null;  
  18. }  
  19. void addEntry(int hash, K key, V value, int bucketIndex) {  
  20. Entry<K,V> e = table[bucketIndex];  
  21.        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);//若是table[bucketIndex]中已經存在Entry則放到頭部。  
  22.        if (size++ >= threshold)//若是大於了閥值,則擴容到原來大小的2倍。  
  23.            resize(2 * table.length);  
  24.    }  
  25.   
  26. void resize(int newCapacity) {  
  27.     Entry[] oldTable = table;  
  28.     int oldCapacity = oldTable.length;  
  29.     if (oldCapacity == MAXIMUM_CAPACITY) {  
  30.         threshold = Integer.MAX_VALUE;  
  31.         return;  
  32.     }  
  33.   
  34.     Entry[] newTable = new Entry[newCapacity];  
  35.     transfer(newTable);//賦值到新的table中,注意轉移後會從新hash,因此位置可能會跟以前不一樣,目的是均勻分不到新的table中。  
  36.     table = newTable;  
  37.     threshold = (int)(newCapacity * loadFactor);  
  38. }  

2)get方法 
Java代碼     收藏代碼
  1. public V get(Object key) {  
  2.     if (key == null)  
  3.         return getForNullKey();  
  4.     int hash = hash(key.hashCode());  
  5.     for (Entry<K,V> e = table[indexFor(hash, table.length)];//找到數組的下表,進行遍歷  
  6.          e != null;  
  7.          e = e.next) {  
  8.         Object k;  
  9.         if (e.hash == hash && ((k = e.key) == key || key.equals(k)))  
  10.             return e.value;//找到則返回  
  11.     }  
  12.     return null;//不然,返回null  
  13. }  

3)remove方法 
Java代碼     收藏代碼
  1. public V remove(Object key) {  
  2.     Entry<K,V> e = removeEntryForKey(key);  
  3.     return (e == null ? null : e.value);  
  4. }  
  5.   
  6. final Entry<K,V> removeEntryForKey(Object key) {  
  7.     int hash = (key == null) ? 0 : hash(key.hashCode());  
  8.     int i = indexFor(hash, table.length);  
  9.     Entry<K,V> prev = table[i];  
  10.     Entry<K,V> e = prev;  
  11.   
  12.     while (e != null) {//Entry鏈未遍歷完則一直遍歷  
  13.         Entry<K,V> next = e.next;  
  14.         Object k;  
  15.         if (e.hash == hash &&  
  16.             ((k = e.key) == key || (key != null && key.equals(k)))) {  
  17.             modCount++;  
  18.             size--;  
  19.             if (prev == e)//若是是第一個,則將table[i]執行e.next  
  20.                 table[i] = next;  
  21.             else //不然將前一個的next指向e.next  
  22.                 prev.next = next;   
  23.             e.recordRemoval(this);  
  24.             return e;  
  25.         }  
  26.         prev = e;//未找到則繼續日後遍歷  
  27.         e = next;  
  28.     }  
  29.   
  30.     return e;  
  31. }  

4)HashMap的遍歷方法 
Java代碼     收藏代碼
  1. Map map = new HashMap();  
  2. map.put("key1","value1");  
  3. map.put("key2","value2");  
  4. map.put("key3""value3");  
  5. for(Iterator it = map.entrySet().iterator();it.hasNext();){  
  6.     Map.Entry e = (Map.Entry)it.next();  
  7.     System.out.println(e.getKey() + "=" + e.getValue());  
  8. }  
  9. System.out.println("-----------------------------------------");  
  10. for(Iterator it = map.keySet().iterator();it.hasNext();){  
  11.     Object key = it.next();  
  12.     Object value = map.get(key);  
  13.     System.out.println(key+"="+value);  
  14. }  
  15. System.out.println("-----------------------------------------");  
  16. System.out.println(map.values());  
  17.   
  18. 輸出爲:  
  19. key3=value3  
  20. key2=value2  
  21. key1=value1  
  22. -----------------------------------------  
  23. key3=value3  
  24. key2=value2  
  25. key1=value1  
  26. -----------------------------------------  
  27. [value3, value2, value1]  

4、性能相關  
1)hash 
若是總計算到相同的數組下標,則得進行Entry的遍從來取值和存放值,必然會影響性能。 
因此HashMap提供了hash方法,來解決key的hashCode方法質量不高問題。 
Java代碼     收藏代碼
  1. public V put(K key, V value) {  
  2. ...  
  3. int hash = hash(key.hashCode());  
  4. ...  
  5. }  
  6.   
  7. static int hash(int h) {  
  8.     // This function ensures that hashCodes that differ only by  
  9.     // constant multiples at each bit position have a bounded  
  10.     // number of collisions (approximately 8 at default load factor).  
  11.     h ^= (h >>> 20) ^ (h >>> 12);  
  12.     return h ^ (h >>> 7) ^ (h >>> 4);  
  13. }  


2)初始容量和負載因子 
由於put的時候可能須要作擴容,擴容會致使性能損耗,因此若是能夠預知Map大小的話,能夠設置合理的初始大小和負載因子來避免HashMap的頻繁擴容致使的性能消耗。 
Java代碼     收藏代碼
  1. void addEntry(int hash, K key, V value, int bucketIndex) {  
  2. ntry<K,V> e = table[bucketIndex];  
  3.       table[bucketIndex] = new Entry<K,V>(hash, key, value, e);  
  4.       if (size++ >= threshold)  
  5.           resize(2 * table.length);  
  6.   }  

5、同Hashtable的區別  
1)HashMap容許key和value均可覺得null,Hashtable都不能夠爲空。 
HashMap的put方法,代碼片斷: 

Java代碼     收藏代碼
  1. if (key == null)  
  2.         return putForNullKey(value);  

Hashtable的put方法,代碼片斷: 
Java代碼     收藏代碼
  1. if (value == null) {  
  2.     throw new NullPointerException();  
  3. }  
  4. Entry tab[] = table;  
  5.     int hash = key.hashCode();  


2)HashMap是非線程安全的,Hashtable是線程安全的。 
Hashtable的put和get方法均爲synchronized的。 
6、ConcurrentHashMap  
ConcurrentHashMap是Doug Lea寫的線程安全的HashMap實現,將HashMap默認劃分爲了16個Segment,減小了鎖的爭用,另外經過寫時加鎖讀時不加鎖減小了鎖的持有時間,優雅的解決了高併發下鎖的高競爭問題。感興趣的可參見筆者的另外一篇博客 http://frank1234.iteye.com/blog/2162490
相關文章
相關標籤/搜索