HashMap源碼解析


  HashMap繼承AbstractMap,實現了Map接口,Map接口定義了所有Map子類必須實現的方法。

1 public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable

  HashMap的實例有兩個參數影響其性能:初始容量和加載因子。初始容量只是哈希表在創建時的容量。加載因子是哈希表再其容量自動增加之前可以達到多滿的一種尺度。當哈希表中的條目數超出了加載因子與當前容量的乘積時,則要對該哈希表進行rehash操作(擴容操作)。

  HashMap中定義的屬性:

 1     /**
 2       * 默認的初始容量16.
 3       */
 4      static final int DEFAULT_INITIAL_CAPACITY = 16;
 5      /**
 6       * 最大容量
 7       */
 8      static final int MAXIMUM_CAPACITY = 1 << 30;
 9      /**
10       * 默認裝載因子0.75f.
11       */
12      static final float DEFAULT_LOAD_FACTOR = 0.75f;
13      /**
14       * 存儲數據的Entry數組
15       */
16      transient Entry[] table;
17      /**
18       * map中保存的鍵值對的數量
19       */
20      transient int size;
21      /**
22       * 需要調整大小的極限值(容量*裝載因子)
23       */
24      int threshold;
25      /**
26       *裝載因子,當HashMap的數據大小>=容量*加載因子時,HashMap會將容量擴容
27       */
28      final float loadFactor;
29      /**
30       * map結構被改變的次數
31       */
32      transient volatile int modCount;

  HashMap的構造方法。

 1     /**
 2      *使用默認的容量及裝載因子構造一個空的HashMap
 3      */
 4     public HashMap() {
 5         this.loadFactor = DEFAULT_LOAD_FACTOR;
 6         threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
 7         table = new Entry[DEFAULT_INITIAL_CAPACITY];//根據默認容量(16)初始化table
 8         init();
 9     }
10   /**
11      * 根據給定的初始容量的裝載因子創建一個空的HashMap
12      * 初始容量小於0或裝載因子小於等於0將報異常 
13      */
14     public HashMap(int initialCapacity, float loadFactor) {
15         if (initialCapacity < 0)
16             throw new IllegalArgumentException("Illegal initial capacity: " +
17                                                initialCapacity);
18         if (initialCapacity > MAXIMUM_CAPACITY)
19             initialCapacity = MAXIMUM_CAPACITY;
20         if (loadFactor <= 0 || Float.isNaN(loadFactor))
21             throw new IllegalArgumentException("Illegal load factor: " +
22                                                loadFactor);
23         int capacity = 1;
24         //設置capacity為大於initialCapacity且是2的冪的最小值
25         while (capacity < initialCapacity)
26             capacity <<= 1;
27         this.loadFactor = loadFactor;
28         threshold = (int)(capacity * loadFactor);
29         table = new Entry[capacity];
30         init();
31     }
32   /**
33      *根據指定容量創建一個空的HashMap
34      */
35     public HashMap(int initialCapacity) {
36         this(initialCapacity, DEFAULT_LOAD_FACTOR);//調用上面的構造方法,容量為指定的容量,裝載因子是默認值
37     }
38   /**
39      *通過傳入的map創建一個HashMap,容量為默認容量(16)和(map.zise()/DEFAULT_LOAD_FACTORY)+1的較大者,裝載因子為默認值
40      */
41     public HashMap(Map<? extends K, ? extends V> m) {
42         this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
43                       DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
44         putAllForCreate(m);
45     }

  這里需要注意一個小問題,在第二個構造方法中,capacity才是初始容量,而不是initialCapacity,即如果執行new HashMap(9,0.75),那么HashMap的初始容量是16,而不是9。

  在初始化table的時候都使用了Entry,這是HashMap的一個內部類。

  Map.Entry接口定義的方法

1 K getKey();
2 V getValue();
3 V setValue();
4 boolean equals(Object o);
5 int hashCode();

  HashMap.Entry類的具體實現

 1 static class Entry<K,V> implements Map.Entry<K,V> {
 2          final K key;
 3          V value;
 4          Entry<K,V> next;//對下一個節點的引用
 5          final int hash;
 6  
 7          Entry(int h, K k, V v, Entry<K,V> n) {
 8              value = v;
 9              next = n;
10              key = k;
11              hash = h;
12          }
13  
14          public final K getKey() {
15              return key;
16          }
17  
18          public final V getValue() {
19              return value;
20          }
21  
22          public final V setValue(V newValue) {
23            V oldValue = value;
24              value = newValue;
25              return oldValue;//返回的是之前的Value
26          }
27  
28          public final boolean equals(Object o) {
29              if (!(o instanceof Map.Entry))//先判斷類型是否一致
30                  return false;
31              Map.Entry e = (Map.Entry)o;
32              Object k1 = getKey();
33              Object k2 = e.getKey();
34         // Key相等且Value相等則兩個Entry相等
35              if (k1 == k2 || (k1 != null && k1.equals(k2))) {
36                  Object v1 = getValue();
37                  Object v2 = e.getValue();
38                  if (v1 == v2 || (v1 != null && v1.equals(v2)))
39                      return true;
40              }
41              return false;
42          }
43          // hashCode是Key的hashCode和Value的hashCode的異或的結果
44          public final int hashCode() {
45              return (key==null   ? 0 : key.hashCode()) ^
46                     (value==null ? 0 : value.hashCode());
47          }
48          // 重寫toString方法,是輸出更清晰
49          public final String toString() {
50              return getKey() + "=" + getValue();
51          }
52  
56          void recordAccess(HashMap<K,V> m) {
57          }
58  
62          void recordRemoval(HashMap<K,V> m) {
63          }
64      }

  put()

 1 public V put(K key, V value) {
 2          if (key == null)
 3              return putForNullKey(value);
 4          int hash = hash(key.hashCode());
 5          int i = indexFor(hash, table.length);
 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          }
15  
16          modCount++;
17          addEntry(hash, key, value, i);
18          return null;
19      }

  當待put的key為null的時候會調用putForNullKey(value)方法,暫且繞過,先來看看如何hash。HashMap並不是直接將對象的hashcode作為哈希值,而是把key的hashcode做一些運算以得到最終的哈希值,而且得到的哈希值也不是在數組中的位置,無論是get還是put還是別的方法,計算哈希值都是:

1 int hash = hash(key.hashCode());
1 static int hash(int h) {
2          // This function ensures that hashCodes that differ only by
3          // constant multiples at each bit position have a bounded
4          // number of collisions (approximately 8 at default load factor).
5          h ^= (h >>> 20) ^ (h >>> 12);
6          return h ^ (h >>> 7) ^ (h >>> 4);
7      }

  hash方法為什么要這樣取值,有待探討,但是作用肯定是防止沖突。如何確定數據在數組中的位置:

1 int hash = hash(k.hashCode());
2 int i = indexFor(hash,table.length);

  第一行,得到哈希值,第二行,根據哈希值計算元素在數組中的位置。

1 static int indexFor(int h,int length){
2        return h & (length - 1);  
3 }

  "h&(length-1)"其實這里有點小巧妙,為什么是做與運算?

  首先我們要確定,HashMap的數組長度永遠是偶數,所以length-1一定是一個奇數,假設現在的長度是16,length-1就是15,對應的二進制是:1111。

  假設有兩個元素,一個哈希值是8,二進制是1000,一個哈希值是9,二進制是1001。和1111與運算后,分別還是1000和1001,他們被分配在了數組的不同位置,這樣,哈希的分布非常均勻。

  那么如果數組的長度是奇數,減去1后就是偶數了,偶數對應的二進制最低位一定是0,例如14二進制1110。對上面兩個數字分別與運算,得到1000和1000。這樣,哈希值8和9的元素會被存儲在數組同一個位置的鏈表中。在操作的時候,鏈表中的元素越多,效率就會越低,因為要不停的對鏈表循環比較。

  在找到元素在數組中的索引位置以后,會循環遍歷table[i]所在的鏈表,如果找到key值與傳入的key值相同的對象,則替換並返回原對象;若找不到,則通過addEntry(hash,key,value,i)添加新的對象。

1 void addEntry(int hash, K key, V value, int bucketIndex) {     
    Entry<K,V> e = table[bucketIndex]; 2 table[bucketIndex] = new Entry<K,V>(hash, key, value, e); 3 if (size++ >= threshold) 4   resize(2 * table.length); 5 }

  以上過程就是新建一個Entry對象,並放在當前位置的Entry鏈表的頭部。然后判斷size是否達到了需要擴容的界限並讓size增加1,如果達到了擴容的界限則調用resize(int capacity)方法。

 1 void resize(int newCapacity) {
 2          Entry[] oldTable = table;
 3          int oldCapacity = oldTable.length;
 4          // 這個if塊表明,如果容量已經到達允許的最大值,即MAXIMUN_CAPACITY,則不再拓展容量,而將裝載拓展的界限值設為計算機允許的最大值。
 5          // 不會再觸發resize方法,而是不斷的向map中添加內容,即table數組中的鏈表可以不斷變長,但數組長度不再改變
 6          if (oldCapacity == MAXIMUM_CAPACITY) {
 7              threshold = Integer.MAX_VALUE;
 8              return;
 9          }
10          // 創建新數組,容量為指定的容量
11          Entry[] newTable = new Entry[newCapacity];
12          transfer(newTable);
13          table = newTable;
14          // 設置下一次需要調整數組大小的界限
15          threshold = (int)(newCapacity * loadFactor);
16      }

  這里需要重點看看transfer方法:

 1 void transfer(Entry[] newTable) {
 2          // 保留原數組的引用到src中,
 3          Entry[] src = table;
 4          // 新容量使新數組的長度
 5          int newCapacity = newTable.length;
 6       // 遍歷原數組
 7          for (int j = 0; j < src.length; j++) {
 8              // 獲取元素e
 9              Entry<K,V> e = src[j];
10              if (e != null) {
11                  // 將原數組中的元素置為null
12                  src[j] = null;
13                  // 遍歷原數組中j位置指向的鏈表
14                  do {
15                      Entry<K,V> next = e.next;
16                      // 根據新的容量計算e在新數組中的位置
17                      int i = indexFor(e.hash, newCapacity);
18                      // 將e插入到newTable[i]指向的鏈表的頭部
19                      e.next = newTable[i];
20                      newTable[i] = e;
21                      e = next;
22                  } while (e != null);
23              }
24          }
25      }

  tranfer方法將所有的元素重新哈希,因為新的容量變大,所以每個元素的哈希值和位置都是不一樣的。

  如果key值為空,我們來看看putForNullKey的處理過程:

 1 private V putForNullKey(V value) {
 2          for (Entry<K,V> e = table[0]; e != null; e = e.next) {
 3              if (e.key == null) {
 4                  V oldValue = e.value;
 5                  e.value = value;
 6                  e.recordAccess(this);
 7                  return oldValue;
 8              }
 9          }
10          modCount++;
11          addEntry(0, null, value, 0);
12          return null;
13      }

  這是一個私有方法,在put方法中被調用。它首先遍歷table數組,如果找到key為null的元素,則替換值並返回oldValue;否則通過addEntry方法添加元素,並返回null。

  正確的使用HashMap

  1.不要再並發場景中使用HashMap

   HashMap是線程不安全的,如果被多個線程共享的操作,將會引發不可預知的問題。據sun的說法,在擴容時,會引起鏈表的閉環,在get元素時,就會無限循環。

 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;
13     }

  2.如果數據大小是固定的,那么最好給HashMap設定一個合理的容量值。

  本文大量參考借鑒杭州.Mark童鞋HashMap源碼分析這篇文章,如有冒犯,還請見諒。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM