007Java集合006詳解HashMap


注意:本文基於JDK1.8進行記錄。

1 簡介

不允許插入key值相同的元素,允許插入null的key值。

底層由數組、鏈表、紅黑樹組成,數組中存儲鏈表或紅黑樹,將一個key到value的映射作為一個元素,不能保證插入順序和輸出順序一致。

線程不安全。

2 擴容機制

數組結構會有容量的概念,HashMap的默認容量為16,默認負載因子是0.75,表示當插入元素后個數超出長度的0.75倍時會進行擴增,默認擴容增量是1,所以擴增后容量為2倍。

最好指定初始容量值,避免過多的進行擴容操作而浪費時間和效率。

3 方法說明

3.1 構造方法

1 // 指定長度和負載因子的構造器。
2 public HashMap(int initialCapacity, float loadFactor);
3 // 指定長度的構造器,使用默認負載因子。
4 public HashMap(int initialCapacity);
5 // 空參構造器,使用默認負載因子。
6 public HashMap();
7 // 傳入了一個集合的構造器,使用默認負載因子,添加指定集合。
8 public HashMap(Map<? extends K, ? extends V> m);

3.2 常用方法

 1 // 獲取個數。
 2 public int size();
 3 // 判斷是否為空。
 4 public boolean isEmpty();
 5 // 根據key獲取value,不存在會返回null。
 6 public V get(Object key);
 7 // 設置key和value鍵值對,返回原value,不存在會返回null。
 8 public V put(K key, V value);
 9 // 根據key刪除鍵值對,返回原value,不存在會返回null。
10 public V remove(Object key);
11 // 清除所有元素。
12 public void clear();

4 源碼分析

4.1 屬性

靜態屬性:

 1 // 默認容量為16,是2的整數次冪。
 2 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
 3 // 最大容量是2的30次方,傳入容量過大將被這個值替換。
 4 static final int MAXIMUM_CAPACITY = 1 << 30;
 5 // 默認負載因子為0.75。
 6 static final float DEFAULT_LOAD_FACTOR = 0.75f;
 7 // 樹化閾值為8,鏈表中元素的個數超過8時會轉換為紅黑樹
 8 static final int TREEIFY_THRESHOLD = 8;
 9 // 反樹化閾值為6,紅黑樹中元素的個數小於6時會轉換為鏈表。
10 static final int UNTREEIFY_THRESHOLD = 6;
11 // 樹化時哈希表最小的容量為64。為了避免沖突,該值至少為樹化閾值和4的乘積。
12 static final int MIN_TREEIFY_CAPACITY = 64;

普通屬性:

 1 // 數組,用於存儲鏈表和紅黑樹。
 2 transient Node<K,V>[] table;
 3 // 存儲key和value鍵值對的集合。
 4 transient Set<Map.Entry<K,V>> entrySet;
 5 // 鍵值對的個數。
 6 transient int size;
 7 // 修改次數,用於快速失敗機制。
 8 transient int modCount;
 9 // 擴容閾值。
10 int threshold;
11 // 負載因子。
12 final float loadFactor;

4.2 工具方法

 1 // 根據key的hashCode重新計算hash值。
 2 static final int hash(Object key) {
 3     int h;
 4     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
 5 }
 6 // 根據長度計算閾值。
 7 static final int tableSizeFor(int cap) {
 8     int n = cap - 1;
 9     n |= n >>> 1;
10     n |= n >>> 2;
11     n |= n >>> 4;
12     n |= n >>> 8;
13     n |= n >>> 16;
14     return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
15 }

4.3 構造方法

 1 // 指定長度和負載因子的構造器。
 2 public HashMap(int initialCapacity, float loadFactor) {
 3     if (initialCapacity < 0)
 4         throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
 5     if (initialCapacity > MAXIMUM_CAPACITY)
 6         initialCapacity = MAXIMUM_CAPACITY;
 7     if (loadFactor <= 0 || Float.isNaN(loadFactor))
 8         throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
 9     this.loadFactor = loadFactor;
10     // 根據長度設置閾值。
11     this.threshold = tableSizeFor(initialCapacity);
12 }
13 // 指定長度的構造器,使用默認負載因子。
14 public HashMap(int initialCapacity) {
15     this(initialCapacity, DEFAULT_LOAD_FACTOR);
16 }
17 // 空參構造器,使用默認負載因子。
18 public HashMap() {
19     this.loadFactor = DEFAULT_LOAD_FACTOR;
20 }
21 // 傳入了一個集合的構造器,使用默認負載因子,添加指定集合。
22 public HashMap(Map<? extends K, ? extends V> m) {
23     this.loadFactor = DEFAULT_LOAD_FACTOR;
24     putMapEntries(m, false);
25 }

4.4 常用方法

  1 // 獲取個數。
  2 public int size() {
  3     return size;
  4 }
  5 // 判斷是否為空。
  6 public boolean isEmpty() {
  7     return size == 0;
  8 }
  9 // 根據key獲取value,不存在會返回null。
 10 public V get(Object key) {
 11     Node<K,V> e;
 12     return (e = getNode(hash(key), key)) == null ? null : e.value;
 13 }
 14 // 根據key獲取節點。
 15 final Node<K,V> getNode(int hash, Object key) {
 16     // 節點數組tab,數組首節點first,目標節點e,數組長度n,目標節點key值k。
 17     Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
 18     // 賦值並判斷,如果數組已初始化,並且數組首節點不為空,才獲取元素。
 19     if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
 20         // 判斷數組首節點的key和value是否滿足,滿足則返回數組首節點。
 21         if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
 22             return first;
 23         // 數組首節點不滿足,賦值並遍歷鏈表和紅黑樹。
 24         if ((e = first.next) != null) {
 25             // 如果是紅黑樹節點,則通過紅黑樹節點方式查詢。
 26             if (first instanceof TreeNode)
 27                 return ((TreeNode<K,V>)first).getTreeNode(hash, key);
 28             // 如果是鏈表節點,則遍歷鏈表節點查詢。
 29             do {
 30                 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
 31                     return e;
 32             } while ((e = e.next) != null);
 33         }
 34     }
 35     return null;
 36 }
 37 // 設置key和value鍵值對,返回原value,不存在會返回null。
 38 public V put(K key, V value) {
 39     return putVal(hash(key), key, value, false, true);
 40 }
 41 // 設置key和value鍵值對,返回原value,不存在會返回null。
 42 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
 43     // 節點數組tab,指針節點p,數組長度n,數組位置i。
 44     Node<K,V>[] tab; Node<K,V> p; int n, i;
 45     // 賦值並判斷,如果數組未初始化,則初始化數組。
 46     if ((tab = table) == null || (n = tab.length) == 0)
 47         n = (tab = resize()).length
 48     // 如果數組已初始化,並且指針節點不存在,則創建新節點存儲key和value。
 49     if ((p = tab[i = (n - 1) & hash]) == null)
 50         tab[i] = newNode(hash, key, value, null);
 51     // 如果數組已初始化,並且指針節點存在,則查找key值相同節點並替換value。
 52     else {
 53         // 目標節點e,目標節點key值k。
 54         Node<K,V> e; K k;
 55         // 判斷指針節點的key和value是否滿足,滿足則將指針節點作為目標節點。
 56         if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
 57             e = p;
 58         // 指針節點不滿足,並且是紅黑樹節點,遍歷紅黑樹並返回目標節點。
 59         else if (p instanceof TreeNode)
 60             e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
 61         // 指針節點不滿足,並且是鏈表節點,遍歷鏈表並返回目標節點。
 62         else {
 63             // 記錄鏈表節點個數並遍歷鏈表。
 64             for (int binCount = 0; ; ++binCount) {
 65                 // 將下一節點作為目標節點,不存在則表示遍歷完成且不存在目標節點。
 66                 if ((e = p.next) == null) {
 67                     // 創建新節點存儲key和value。
 68                     p.next = newNode(hash, key, value, null);
 69                     // 如果新增后鏈表節點個數超過樹化閾值,則嘗試進行樹化操作。
 70                     if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
 71                         treeifyBin(tab, hash);
 72                     // 此時目標節點不存在,跳出循環。
 73                     break;
 74                 }
 75                 // 遍歷過程中,找到滿足的目標節點。
 76                 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
 77                     // 此時目標節點存在,跳出循環。
 78                     break;
 79                 // 將目標節點作為新的指針節點進入循環。
 80                 p = e;
 81             }
 82         }
 83         // 如果目標節點存在,不需要個數自增和擴容,替換value並返回原值。
 84         if (e != null) {
 85             V oldValue = e.value;
 86             if (!onlyIfAbsent || oldValue == null)
 87                 e.value = value;
 88             afterNodeAccess(e);
 89             return oldValue;
 90         }
 91     }
 92     // 執行到這里,說明增加了新節點,操作數自增。
 93     ++modCount;
 94     // 個數自增,如果自增后的個數超過了閾值則進行擴容。
 95     if (++size > threshold)
 96         resize();
 97     // 添加新節點之后的后置處理。
 98     afterNodeInsertion(evict);
 99     // 返回null。
100     return null;
101 }
102 // 根據key刪除鍵值對,返回原value,不存在會返回null。
103 public V remove(Object key) {
104     Node<K,V> e;
105     return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value;
106 }
107 // 根據key刪除鍵值對,並返回原節點。
108 final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {
109     // 節點數組tab,指針節點p,數組長度n,數組位置index。
110     Node<K,V>[] tab; Node<K,V> p; int n, index;
111     // 賦值並判斷,如果數組已初始化,並且數組首節點不為空,才刪除元素。
112     if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) {
113         // 原節點node,目標節點e,目標節點key值k,目標節點value值v。
114         Node<K,V> node = null, e; K k; V v;
115         // 判斷指針節點的key和value是否滿足,滿足則將指針節點作為原節點。
116         if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
117             node = p;
118         // 將指針節點下一節點作為目標節點,如果目標節點存在則繼續遍歷節點。
119         else if ((e = p.next) != null) {
120             // 如果指針節點是紅黑樹節點,則通過紅黑樹節點方式查詢原節點。
121             if (p instanceof TreeNode)
122                 node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
123             // 如果指針節點是鏈表節點,則通過鏈表節點方式查詢原節點。
124             else {
125                 do {
126                     // 遍歷過程中,找到滿足的目標節點。
127                     if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
128                         // 將目標節點作為原節點。
129                         node = e;
130                         // 此時原節點存在,跳出循環。
131                         break;
132                     }
133                     // 用指針節點保存目標節點,跳出循環時,指針節點保存的是目標節點的上一節點。
134                     p = e;
135                 } while ((e = e.next) != null);
136             }
137         }
138         // 原節點存在,並且滿足value值的判斷規則,那就繼續執行。
139         if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) {
140             // 如果原節點是紅黑樹節點,則通過紅黑樹節點方式刪除原節點。
141             if (node instanceof TreeNode)
142                 ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
143             // 如果原節點是鏈表節點,並且原節點和指針節點相同,則將原節點的下一節點作為數組首節點。
144             else if (node == p)
145                 tab[index] = node.next;
146             // 如果原節點是鏈表節點,並且原節點和指針節點不同,則將原節點的下一節點作為指針節點的下一節點。
147             else
148                 p.next = node.next;
149             // 執行到這里,說明刪除了原節點,操作數自增。
150             ++modCount;
151             // 個數自減。
152             --size;
153             afterNodeRemoval(node);
154             // 返回原節點。
155             return node;
156         }
157     }
158     return null;
159 }
160 // 清除所有元素。
161 public void clear() {
162     // 定義節點數組tab。
163     Node<K,V>[] tab;
164     // 操作數自增。
165     modCount++;
166     // 如果數組已初始化,則設置個數為0,並且將每個節點置空。
167     if ((tab = table) != null && size > 0) {
168         size = 0;
169         for (int i = 0; i < tab.length; ++i)
170             tab[i] = null;
171     }
172 }

4.5 擴容方法

  1 final Node<K,V>[] resize() {
  2     // 記錄原節點數組。
  3     Node<K,V>[] oldTab = table;
  4     // 記錄原數組容量。
  5     int oldCap = (oldTab == null) ? 0 : oldTab.length;
  6     // 記錄原閾值。
  7     int oldThr = threshold;
  8     // 定義新數組容量,定義新數組閾值。
  9     int newCap, newThr = 0;
 10     // 如果原容量大於0,則進行擴容。
 11     if (oldCap > 0) {
 12         // 如果原容量大於等於容量最大值。
 13         if (oldCap >= MAXIMUM_CAPACITY) {
 14             // 將原閾值設為整數最大值,並返回原數組。
 15             threshold = Integer.MAX_VALUE;
 16             return oldTab;
 17         }
 18         // 原容量擴容一倍並賦值給新容量,如果新容量小於容量最大值,並且原容量大於等於默認容量。
 19         else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
 20             // 將原閾值擴容一倍並賦值給新閾值。
 21             newThr = oldThr << 1;
 22     }
 23     // 原容量為0,判斷原閾值是否大於0。
 24     else if (oldThr > 0)
 25         // 首次初始化,將原閾值賦值給新容量。
 26         newCap = oldThr;
 27     // 原容量為0,並且原閾值也是0。
 28     else {
 29         // 設置默認容量。
 30         newCap = DEFAULT_INITIAL_CAPACITY;
 31         // 設置默認閾值。
 32         newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
 33     }
 34     // 判斷新閾值是否為0。
 35     if (newThr == 0) {
 36         // 計算新閾值。
 37         float ft = (float)newCap * loadFactor;
 38         // 新容量小於容量最大值並且閾值小於容量最大值,則使用新閾值,否則使用最大值。
 39         newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
 40     }
 41     // 確定新閥值。
 42     threshold = newThr;
 43     // 開始構造新的節點數組。
 44     Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
 45     table = newTab;
 46     // 如果原節點數組已初始化,則將原節點放置到新節點數組。
 47     if (oldTab != null) {
 48         // 遍歷原節點數組。
 49         for (int j = 0; j < oldCap; ++j) {
 50             // 定義原節點。
 51             Node<K,V> e;
 52             // 如果原節點不為空,記錄原節點並移動。
 53             if ((e = oldTab[j]) != null) {
 54                 // 將原節點置空。
 55                 oldTab[j] = null;
 56                 // 如果原節點沒有子節點,表示數組中只有一個節點,直接放置到新節點數組。
 57                 if (e.next == null)
 58                     newTab[e.hash & (newCap - 1)] = e;
 59                 // 如果原節點是紅黑樹節點,則在紅黑樹中將原節點存儲到新節點數組。
 60                 else if (e instanceof TreeNode)
 61                     ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
 62                 // 如果原節點是鏈表節點,則進行鏈表節點的移動。
 63                 else {
 64                     // 定義低位鏈表的頭節點和尾節點。
 65                     Node<K,V> loHead = null, loTail = null;
 66                     // 定義高位鏈表的頭節點和尾節點。
 67                     Node<K,V> hiHead = null, hiTail = null;
 68                     // 定義下一節點。
 69                     Node<K,V> next;
 70                     // 循環遍歷節點鏈表。
 71                     do {
 72                         // 給下一節點賦值。
 73                         next = e.next;
 74                         // 確定原節點在新節點數組中的位置,為0則放在低位鏈表。
 75                         if ((e.hash & oldCap) == 0) {
 76                             if (loTail == null)
 77                                 loHead = e;
 78                             else
 79                                 loTail.next = e;
 80                             loTail = e;
 81                         }
 82                         // 為1則放在高位鏈表。
 83                         else {
 84                             if (hiTail == null)
 85                                 hiHead = e;
 86                             else
 87                                 hiTail.next = e;
 88                             hiTail = e;
 89                         }
 90                     } while ((e = next) != null);
 91                     // 如果低位鏈表不為空,則將整個低位鏈表放到原位置。
 92                     if (loTail != null) {
 93                         loTail.next = null;
 94                         newTab[j] = loHead;
 95                     }
 96                     // 如果高位鏈表不為空,則將整個高位鏈表放到新增的空間中。
 97                     if (hiTail != null) {
 98                         hiTail.next = null;
 99                         newTab[j + oldCap] = hiHead;
100                     }
101                 }
102             }
103         }
104     }
105     // 返回新的節點數組。
106     return newTab;
107 }

5 補充說明

5.1 數組長度為2的倍數

長度減一后的二進制位全為1,可以用來計算節點在數組中的位置,不會造成浪費。

5.2 指定長度不為2的倍數

在構造方法中的最后一步會根據指定長度計算容量,會通過移位運算得到大於等於指定長度的,並且為2的倍數的最小正整數。

5.3 先使用hashCode()方法,再使用equals()方法

在Object類中有一個hashCode()方法,用來獲取對象的哈希值,也被稱作為散列值。

hashCode()方法被native修飾,意味着這個方法和平台有關。大多數情況下,hashCode()方法返回的是與對象信息(存儲地址和字段等)有關的數值。

當向集合中插入對象時,如果調用equals()逐個進行比較,雖然可行但是這樣做的效率很低。因此,先調用hashCode()進行判斷,如果相同再調用equals()判斷,就會提高效率。

5.4 使用hash()方法處理hashCode

在計算節點在數組中的下標時,一般是通過與節點有關的數值除以數組長度取余得到的。當數組長度為2的倍數時,取余操作相當於數值同數組長度減一進行與運算。

根據數組長度減一得到的結果,將二進制位分為高位和低位,將左邊全為0的部分作為高位,將右邊全為1的部分作為低位。在與運算時,任何數字與0相與只能得到0,所以高位無效,只用到了低位。

如果使用hashCode進行與運算,兩個hashCode不同但是低位相同的節點會被分到一個數組中,哈希碰撞發生的可能性較大。因此,需要對hashCode進行處理,使兩個高位不同低位相同的節點得到的結果也不同。

hash()方法被稱為擾動函數,是用來對hashCode進行處理的方法。在hash()方法處理后,hashCode高位的變化也會影響低位,這時再使用低位計算下標就能使元素的分布更加合理,哈希碰撞的可能性也會降低。

在JDK1.8版本中,只使用了hashCode進行了一次與運算:

1 static final int hash(Object key) {
2     int h;
3     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
4 }

5.5 何時進行擴容

1)插入第一個節點時,初始化數組時進行擴容。

2)插入節點后,長度超過閾值時進行擴容。

5.6 擴容時對鏈表結構的處理

使用高低位鏈表,將節點的hash值同原數組長度進行與運算,根據結果0和1放到低位鏈表和高位鏈表。

將低位鏈表放置到新數組的低位,將高位鏈表放置到新數組的高位,低位的位置加上原數組長度就是高位的位置。

5.7 重寫equals方法和hashCode方法

一般在重寫equals()方法的時候,也會盡量重寫hashCode()方法,就是為了在equals()方法判斷相等的時候保證讓hashCode()方法判斷相等。

 


免責聲明!

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



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