HashMap(2) 源碼剖析(推薦)

 

今天看代碼,想到去年發生的HashMap發生的CPU使用率100%的事件,轉載下當時看的三個比較不錯的博客(很是推薦)html

參考:http://coolshell.cn/articles/9606.htmljava

     http://github.thinkingbar.com/hashmap-analysis/git

   http://developer.51cto.com/art/201102/246431.htmgithub

 

 

 

在 Java 集合類中,使用最多的容器類恐怕就是 HashMap 和 ArrayList 了,因此打算看一下它們的源碼,理解一下設計思想。下面咱們從 HashMap 開始提及。首先咱們看下源碼的版本:shell

1 java version "1.7.0_45" 2 Java(TM) SE Runtime Environment (build 1.7.0_45-b18) 3 Java HotSpot(TM) 64-Bit Server VM (build 24.45-b08, mixed mode)

接下來直接開始上代碼了。數組

1、HashMap 的實現原理

咱們先從總體上把握一下 HashMap 的設計,其實也很簡單,就是哈希表的使用,咱們直接上圖說明會更好。安全

img

這個圖就是 HashMap 的核心,若是把這張圖看懂了,其實 HashMap 已經理解了一大半。若是學過數據結構,這玩意就沒啥說的了。簡單總結一下:bash

HashMap 使用哈希表做爲底層數據結構,而哈希表的實現是結合數組和鏈表。咱們知道數組的優勢是分配連續內存,尋址迅速;而鏈表對於插入刪除特別高效,因此綜合使用數組和鏈表,就能進行互補。咱們在插入刪除的時候,首先根據 key 的 hash 值進行數組定位,每一個數組都掛着一個鏈表,表明相同的 key 的元素,若是 hash 函數設計得當,基本不會出現鏈表過長的問題,這樣就能夠作到 O(1)插入、查詢,因此極其高效。數據結構

2、文檔

第二步就是去看下文檔,看看能不能撈到有效的信息。這裏我直接 copy 一下官方文檔吧。app

Hash table based implementation of the Map interface(說明實現原理). This implementation provides all of the optional map operations, and permits null values and the null key. (The HashMap class is roughly equivalent to Hashtable, except that it is unsynchronized and permits nulls.)(一句話說明 HashMap 和 HashTable 的區別) This class makes no guarantees as to the order of the map(這個在講到 transfer 的 時候就知道緣由了); in particular, it does not guarantee that the order will remain constant over time.

This implementation provides constant-time performance for the basic operations (get and put)(get 和 set 都是常熟複雜度,是哈希表以空間換時間的代價), assuming the hash function disperses the elements properly among the buckets. Iteration over collection views requires time proportional to the "capacity" of the HashMap instance (the number of buckets) plus its size (the number of key-value mappings). Thus, it's very important not to set the initial capacity too high (or the load factor too low) if iteration performance is important(遍歷操做跟數組長度和鏈表長度有關,因此在遍歷操做要求性能高的狀況下,就不要將初始容量設置的太高或者裝載因子設置的過低。爲何?看下面這段).

An instance of HashMap has two parameters that affect its performance: initial capacity and load factor. The capacity is the number of buckets in the hash table, and the initial capacity is simply the capacity at the time the hash table is created. The load factor is a measure of how full the hash table is allowed to get before its capacity is automatically increased. When the number of entries in the hash table exceeds the product of the load factor and the current capacity, the hash table is rehashed (that is, internal data structures are rebuilt) so that the hash table has approximately twice the number of buckets(超過裝載因子和容量的乘積就要進行 rehash 了,rehash 後容量近似是原來的2倍).

As a general rule, the default load factor (.75) offers a good tradeoff between time and space costs. Higher values decrease the space overhead but increase the lookup cost (reflected in most of the operations of the HashMap class, including get and put). The expected number of entries in the map and its load factor should be taken into account when setting its initial capacity(放入 map 的元素個數和裝載因子在初始化的時候要考慮一下,由於 rehash 涉及到 copy 轉移,會影響性能), so as to minimize the number of rehash operations. If the initial capacity is greater than the maximum number of entries divided by the load factor, no rehash operations will ever occur.

If many mappings are to be stored in a HashMap instance, creating it with a sufficiently large capacity will allow the mappings to be stored more efficiently than letting it perform automatic rehashing as needed to grow the table. Note that using many keys with the same hashCode() is a sure way to slow down performance of any hash table. To ameliorate impact, when keys are Comparable, this class may use comparison order among keys to help break ties.

Note that this implementation is not synchronized(HashMap 不是線程安全的,這點極其重要。我打算再寫篇文章分析一下). If multiple threads access a hash map concurrently, and at least one of the threads modifies the map structurally, it must be synchronized externally. (A structural modification is any operation that adds or deletes one or more mappings; merely changing the value associated with a key that an instance already contains is not a structural modification.) (這裏說明了什麼狀況須要加鎖)This is typically accomplished by synchronizing on some object that naturally encapsulates the map. If no such object exists, the map should be "wrapped" using the Collections.synchronizedMap method. This is best done at creation time, to prevent accidental unsynchronized access to the map:

Map m = Collections.synchronizedMap(new HashMap(...));

The iterators returned by all of this class's "collection view methods" are fail-fast: if the map is structurally modified at any time after the iterator is created, in any way except through the iterator's own remove method, the iterator will throw a ConcurrentModificationException. Thus, in the face of concurrent modification, the iterator fails quickly and cleanly, rather than risking arbitrary, non-deterministic behavior at an undetermined time in the future.(講的是 fail-fast 機制,當迭代過程當中哈希表被增或者刪,不包括改,就會拋出異常,而不會推遲到程序出問題)

Note that the fail-fast behavior of an iterator cannot be guaranteed as it is, generally speaking, impossible to make any hard guarantees in the presence of unsynchronized concurrent modification. Fail-fast iterators throw ConcurrentModificationException on a best-effort basis. Therefore, it would be wrong to write a program that depended on this exception for its correctness: the fail-fast behavior of iterators should be used only to detect bugs.(程序不該該依賴 fail-fast 機制,使用 fail-fast 的惟一用處就是檢測 bug)

This class is a member of the Java Collections Framework.

上面就是 HashMap 的文檔,重要的地方我用黑體字註明並進行了簡短的說明。下面就從代碼開始剖析了。

3、源碼剖析

1. 類的聲明

首先咱們看這個類都繼承、實現了哪些東西:

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

咱們能夠看到它實現了 Map 接口,繼承了 AbstactMap 類。同時也實現了 Cloneable 和序列化接口。咱們知道 HashMap 本質就是 Map 接口的實現,爲何又繼承了 AbstractMap 呢?由於大部分的 AbstractXX 都是實現了一部分的 XX 接口,只留下一部分重要的方法讓咱們實現。因此當我的須要實現 Map,可是又不想實現所有方法,就能夠去繼承 AbstractMap 類了。

2. 類的成員屬性

仍是直接先上源碼:

1 /**
 2      * 默認 HashMap 容量爲16。必須是2的冪
 3      */
 4     static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
 5 
 6     /**
 7      * 最大容量
 8      */
 9     static final int MAXIMUM_CAPACITY = 1 << 30;
10 
11     /**
12      * 默認裝載因子,當前元素個數/總容量超過這個值就會進行 rehash
13      */
14     static final float DEFAULT_LOAD_FACTOR = 0.75f;
15 
16     /**
17      * 在 table 沒有擴張以前,就用這個初始化
18      */
19     static final Entry<?,?>[] EMPTY_TABLE = {};
20 
21     /**
22      * HashMap 中的關鍵地方就是這個 table 了。容量必須是2的冪
23      */
24     transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
25 
26     /**
27      * 鍵值對的個數
28      */
29     transient int size;
30 
31     /**
32      達到這個閾值就要進行 rehash 了,默認初始化的閾值是16 * 0.75 = 12
33     int threshold;
34 
35     /**
36      * 實際裝載因子,若是沒有指定,就使用默認裝載因子0.75
37      *
38      * @serial
39      */
40     final float loadFactor;
41 
42     /**
43      * HashMap 結構改變的次數,用於 fail-fast 機制
44      */
45     transient int modCount;
46 
47     /**
48      * 默認的 threshold
49      */
50     static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
51 
52     /**
53      * 虛擬機各自實現中,也會有對應的 threshold 設置
54      */
55     private static class Holder {
56 
57         /**
58          * Table capacity above which to switch to use alternative hashing.
59          */
60         static final int ALTERNATIVE_HASHING_THRESHOLD;
61 
62         static {
63             String altThreshold = java.security.AccessController.doPrivileged(
64                 new sun.security.action.GetPropertyAction(
65                     "jdk.map.althashing.threshold")); //讀取 Sun 定義的 threshold 的值
66 
67             int threshold;
68             try {
69                 threshold = (null != altThreshold) //修改了threshold 的值
70                         ? Integer.parseInt(altThreshold)
71                         : ALTERNATIVE_HASHING_THRESHOLD_DEFAULT;
72 
73                 // disable alternative hashing if -1
74                 if (threshold == -1) {
75                     threshold = Integer.MAX_VALUE;
76                 }
77 
78                 if (threshold < 0) {
79                     throw new IllegalArgumentException("value must be positive integer.");
80                 }
81             } catch(IllegalArgumentException failed) {
82                 throw new Error("Illegal value for 'jdk.map.althashing.threshold'", failed);
83             }
84 
85             ALTERNATIVE_HASHING_THRESHOLD = threshold;
86         }
87     }
88 
89     /**
90      * hash 種子
91      */
92     transient int hashSeed = 0;

  從源碼中咱們能夠看到,HashMap 的關鍵之處就在於數組和鏈表,table 是一個 Entry 數組,每個數組元素保存一個 Entry 節點,而 Entry 節點內部又鏈接着一樣 key 的下一個 Entry 節點,就構成了鏈表。而若是有了科學的 hashSeed 保證碰撞的概率很是小,就能夠近似的認爲只有數組,沒有鏈表,那麼查找 key 的時候就是 O(1)複雜度了。下面咱們來看一下 Entry 的源碼:

  

1 //實現了 Map 的 Entry 接口,有沒有想到 C++中的 Pair 類型?是的,它就是一個 key-value 的最小單位
 2 static class Entry<K,V> implements Map.Entry<K,V> {
 3         final K key;
 4         V value;
 5         Entry<K,V> next; //鏈表就靠他了
 6         int hash;
 7 
 8         /**
 9          * Creates new entry.
10          */
11         Entry(int h, K k, V v, Entry<K,V> n) {
12             value = v;
13             next = n;
14             key = k;
15             hash = h;
16         }
17 
18         public final K getKey() {
19             return key;
20         }
21 
22         public final V getValue() {
23             return value;
24         }
25 
26         public final V setValue(V newValue) {
27             V oldValue = value;
28             value = newValue;
29             return oldValue;
30         }
31 
32         public final boolean equals(Object o) {
33             if (!(o instanceof Map.Entry))
34                 return false;
35             Map.Entry e = (Map.Entry)o;
36             Object k1 = getKey();
37             Object k2 = e.getKey();
38             if (k1 == k2 || (k1 != null && k1.equals(k2))) {
39                 Object v1 = getValue();
40                 Object v2 = e.getValue();
41                 if (v1 == v2 || (v1 != null && v1.equals(v2)))
42                     return true;
43             }
44             return false;
45         }
46 
47         public final int hashCode() { //key 和 value 的 hashcode,而後再求異或
48             return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
49         }
50 
51         public final String toString() {
52             return getKey() + "=" + getValue();
53         }
54 
55         /**
56          * This method is invoked whenever the value in an entry is
57          * overwritten by an invocation of put(k,v) for a key k that's already
58          * in the HashMap.
59          */
60         void recordAccess(HashMap<K,V> m) {
61         }
62 
63         /**
64          * This method is invoked whenever the entry is
65          * removed from the table.
66          */
67         void recordRemoval(HashMap<K,V> m) {
68         }
69     }

  

3. 構造函數

既然看完了 HashMap 的構造,下面咱們就來看看如何初始化一個 HashMap,也就是構造函數。一共有4種:

1 //1. 指定初始化容量和裝載因子
 2  public HashMap(int initialCapacity, float loadFactor) {
 3         if (initialCapacity < 0)
 4             throw new IllegalArgumentException("Illegal initial capacity: " +
 5                                                initialCapacity);
 6         if (initialCapacity > MAXIMUM_CAPACITY)
 7             initialCapacity = MAXIMUM_CAPACITY;
 8         if (loadFactor <= 0 || Float.isNaN(loadFactor))
 9             throw new IllegalArgumentException("Illegal load factor: " +
10                                                loadFactor);
11 
12         this.loadFactor = loadFactor;
13         threshold = initialCapacity;
14         init(); //init()是一個空方法,在之後版本會添加具體實現
15     }
16 
17     //2. 只指定初始化容量,裝載因子使用默認值
18     public HashMap(int initialCapacity) {
19         this(initialCapacity, DEFAULT_LOAD_FACTOR);
20     }
21 
22     //3. 初始化容量和裝載因子使用默認值(16和0.75)
23     public HashMap() {
24         this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
25     }
26 
27     //4. 用一個 Map 來初始化,會先判斷容量是否須要擴張,而後把元素複製一遍
28     public HashMap(Map<? extends K, ? extends V> m) {
29         this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
30                       DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
31         inflateTable(threshold);
32 
33         putAllForCreate(m);
34     }

  上面就是4種構造方法,通常狀況下用的最多的是第一種和第四種。第一種沒什麼說的,咱們先看下第四種的擴張函數inflateTable(threshold):

1 /**
 2     * 1. 找到最小的大於等於容量的2的倍數
 3     * 2. 肯定 threshold,容量*裝載因子,若是大於最大容量1<<30,就選擇+1做爲閾值
 4     * 3. 初始化底層的 table 索引數組
 5     * 4. 選擇最合適的 hash 種子,使碰撞的概率最低
 6     */
 7  private void inflateTable(int toSize) {
 8         // Find a power of 2 >= toSize
 9         int capacity = roundUpToPowerOf2(toSize);
10 
11         threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
12         table = new Entry[capacity];
13         initHashSeedAsNeeded(capacity); 
14     }

  

4. put()方法——核心之一

在使用 HashMap 時,最經常使用的確定就是 get/set 操做了。固然,要先說 put,由於選擇如何放入,才能決定 get 怎樣進行。下面是 put()方法的源碼:

1 /**
 2      * 1. 初始化的時候 table 是一個空數組,因而要根據 threshold 進行初始化。結合上面的`inflateTable()`源碼,
 3      *    咱們知道默認狀況下容量是16(threshold 是12)
 4      * 2. key 爲 null 的時候,也能夠進行 put 操做。而 HashTable 是不能處理 key/value 爲 null 的狀況
 5      * 3. 計算 hash 值
 6      * 4. i 就是在 table 中找到對應的位置,本質就是 HashMap 的索引了
 7      * 5. 在鏈表中插入元素
 8      */
 9  public V put(K key, V value) {
10         if (table == EMPTY_TABLE) {
11             inflateTable(threshold);
12         }
13         if (key == null)
14             return putForNullKey(value);
15         int hash = hash(key);
16         int i = indexFor(hash, table.length);
17 
18         //若是已經有了這個 key,就進行更新操做。返回舊 value
19         for (Entry<K,V> e = table[i]; e != null; e = e.next) {
20             Object k;
21             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
22                 V oldValue = e.value;
23                 e.value = value;
24                 e.recordAccess(this);
25                 return oldValue;
26             }
27         }
28 
29         //若是不存在這個 key,就改變告終構。添加一個 Entry
30         modCount++;
31         addEntry(hash, key, value, i);
32         return null;
33     }

  

能夠看到,put()的返回值是 V,就是當key 存在的時候,put 會返回 oldValue。而 put()涉及到的函數主要有:

  • putForNullKey()
  • hash()
  • indexFor()
  • addEntry()

下面咱們先看一下 putForNullKey() 函數的源碼,很簡單的。由於 null 也能夠做爲 key 的。一個特別的地方在於 null 的 key 是佔據 table 的第一個位置。因此直接在 table[0]的鏈表中進行操做。

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     }

  而後是 hash()的源碼,首先 h 是 hash 種子,根據這個值獲得 key 在 table 中的索引位置(hash 種子要儘量保證不出現碰撞)。其中涉及到了 unsigned right shift 技巧,能夠參考我前面寫過的文章:Java右移操做

1 final int hash(Object k) {
 2         int h = hashSeed;
 3         if (0 != h && k instanceof String) {
 4             return sun.misc.Hashing.stringHash32((String) k);
 5         }
 6 
 7         h ^= k.hashCode();
 8 
 9         // This function ensures that hashCodes that differ only by
10         // constant multiples at each bit position have a bounded
11         // number of collisions (approximately 8 at default load factor).
12         //下面這種方法能使得鏈表的長度上界爲8,那麼get()操做都在9次如下
13         h ^= (h >>> 20) ^ (h >>> 12);
14         return h ^ (h >>> 7) ^ (h >>> 4);
15     }

  

PS:

經評論網友指正,上面說法有誤。不是使得鏈表的長度上界爲8,get()在9次如下。做用詳見 stackoverflow 的討論:Understanding strange Java hash function)

而後接着看indexFor()函數的源碼:

1 static int indexFor(int h, int length) {
2         //一句話,length 是數組最大值+1,很少解釋了。真個函數功能就是尋找在 table 中的位置
3         return h & (length-1);
4     }

  最後一個是addEntry(),從名字就能夠看出來。就是往 Entry 數組中添加一個 key-value 元素唄:

1 //當元素個數達到閾值或者有衝突,就 resize 擴容 了。
 2     void addEntry(int hash, K key, V value, int bucketIndex) {
 3         if ((size >= threshold) && (null != table[bucketIndex])) {
 4             resize(2 * table.length);
 5             hash = (null != key) ? hash(key) : 0;
 6             bucketIndex = indexFor(hash, table.length);
 7         }
 8         
 9         //實際添加元素,源碼在下面
10         createEntry(hash, key, value, bucketIndex);
11     }
12 
13     void createEntry(int hash, K key, V value, int bucketIndex) {
14         Entry<K,V> e = table[bucketIndex];
15         //結合 Entry 的源碼,會發現是在鏈表頭部添加新元素
16         table[bucketIndex] = new Entry<>(hash, key, value, e);
17         size++;
18     }

  resize()函數是一個很是關鍵的函數,由於它會引發底層數據結構的大洗牌。因此,咱們要單獨分析一下:

1 void resize(int newCapacity) {
 2         Entry[] oldTable = table;
 3         int oldCapacity = oldTable.length;
 4 
 5         //若是已達最大容量,就直接返回。再也不擴容。MAX 是1<<30,閾值設爲最大值
 6         if (oldCapacity == MAXIMUM_CAPACITY) {
 7             threshold = Integer.MAX_VALUE;
 8             return;
 9         }
10 
11         Entry[] newTable = new Entry[newCapacity];
12         transfer(newTable, initHashSeedAsNeeded(newCapacity));
13         table = newTable;
14         threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
15     }

  

首先是聲明瞭一個 temp Entry 數組 oldTable,這樣新舊引用都指向了相同的數組內存地址。可是爲何要畫蛇添足呢?後面代碼用到 oldTable 的地方僅僅求了一下長度,沒作任何其餘操做,徹底能夠用 table 來計算呀。這點非常奇怪,並且不少地方都是先聲明一個新引用,對新引用進行操做,而不是在緣由用上進行。有機會搞清楚這個設計的緣由。

下面就是整個函數的流程:

  1. 首先申請了一個新數組 newTable
  2. 對新數組進行 transfer 操做
  3. 將 table 指向擴容後的新數組

重點確定是第2步了,上代碼:

 1 //
 2     void transfer(Entry[] newTable, boolean rehash) {
 3         int newCapacity = newTable.length;
 4         for (Entry<K,V> e : table) {
 5             while(null != e) {
 6                 Entry<K,V> next = e.next;
 7                 if (rehash) {
 8                     e.hash = null == e.key ? 0 : hash(e.key);
 9                 }
10                 int i = indexFor(e.hash, newCapacity);
11 
12                 //這裏是文檔裏說不能保持元素順序的緣由。由於 transfer 過程將元素逆序插入到
13                 //newTable 中
14                 e.next = newTable[i];
15                 newTable[i] = e;
16                 e = next;
17             }
18         }
19     }

  其實到這裏,put()就已經講完了,可是有一個比較putAll()有時候會用到,咱們也來看一下:

1 public void putAll(Map< ? extends K, ? extends V> m) {
 2         int numKeysToBeAdded = m.size();
 3         if (numKeysToBeAdded == 0)
 4             return;
 5 
 6         if (table == EMPTY_TABLE) {
 7             inflateTable((int) Math.max(numKeysToBeAdded * loadFactor, threshold));
 8         }
 9 
10         /*
11          * Expand the map if the map if the number of mappings to be added
12          * is greater than or equal to threshold.  This is conservative; the
13          * obvious condition is (m.size() + size) >= threshold, but this
14          * condition could result in a map with twice the appropriate capacity,
15          * if the keys to be added overlap with the keys already in this map.
16          * By using the conservative calculation, we subject ourself
17          * to at most one extra resize.
18          * 
19          * 上面解釋的很是清楚了,採起了保守擴容。由於存在 key 相同的狀況只須要覆蓋,不須要從新申請新空間
20          */
21         if (numKeysToBeAdded > threshold) {
22             int targetCapacity = (int)(numKeysToBeAdded / loadFactor + 1);
23             if (targetCapacity > MAXIMUM_CAPACITY)
24                 targetCapacity = MAXIMUM_CAPACITY;
25             int newCapacity = table.length;
26             while (newCapacity < targetCapacity)
27                 newCapacity <<= 1;
28             if (newCapacity > table.length)
29                 resize(newCapacity);
30         }
31 
32         for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
33             put(e.getKey(), e.getValue());
34     }

  

5. get()方法——核心之二

下面是二個核心——get()方法,咱們來看一下源碼:

1 //簡單的不行吧?由於put()已經搞定了全部事情,我只須要判斷 key 是否是 null 便可
 2  //若是是 null 就 getForNullKey();不然就是 getEntry()。具體過程就是先在數組找,
 3  //而後再在鏈表找就 ok 了
 4     public V get(Object key) {
 5         if (key == null)
 6             return getForNullKey();
 7         Entry<K,V> entry = getEntry(key);
 8 
 9         return null == entry ? null : entry.getValue();
10     }

  其中有兩個附加的 containsKey() 和 containsValue()函數,要注意它們的複雜度:

1 public boolean containsKey(Object key) {
 2         return getEntry(key) != null;
 3     }
 4 
 5     /**
 6      * Returns the entry associated with the specified key in the
 7      * HashMap.  Returns null if the HashMap contains no mapping
 8      * for the key.
 9      */
10     final Entry<K,V> getEntry(Object key) {
11         if (size == 0) {
12             return null;
13         }
14 
15         //這個是 O(鏈表長度K,通常 <= 8)
16         int hash = (key == null) ? 0 : hash(key);
17         for (Entry<K,V> e = table[indexFor(hash, table.length)];
18              e != null;
19              e = e.next) {
20             Object k;
21             if (e.hash == hash &&
22                 ((k = e.key) == key || (key != null && key.equals(k))))
23                 return e;
24         }
25         return null;
26     }
27 
28     //這個但是 O(N*N)的,謹慎使用!!!
29     public boolean containsValue(Object value) {
30         if (value == null)
31             return containsNullValue();
32 
33         Entry[] tab = table;
34         for (int i = 0; i < tab.length ; i++)
35             for (Entry e = tab[i] ; e != null ; e = e.next)
36                 if (value.equals(e.value))
37                     return true;
38         return false;
39     }

  

6. 刪除元素

對於刪除,很簡單。就是先 get 到 index,而後從鏈表中刪除這個節點便可。源碼以下:

 

1 public V remove(Object key) {
 2         Entry<K,V> e = removeEntryForKey(key);
 3         return (e == null ? null : e.value);
 4     }
 5 
 6     /**
 7      * Removes and returns the entry associated with the specified key
 8      * in the HashMap.  Returns null if the HashMap contains no mapping
 9      * for this key.
10      */
11     final Entry<K,V> removeEntryForKey(Object key) {
12         if (size == 0) {
13             return null;
14         }
15         int hash = (key == null) ? 0 : hash(key);
16         int i = indexFor(hash, table.length);
17         Entry<K,V> prev = table[i];
18         Entry<K,V> e = prev;
19 
20         //找到那個節點,若是要刪除的是第一個節點,就須要把 table[i] 指向第二個元素;不然直接刪除
21         while (e != null) {
22             Entry<K,V> next = e.next;
23             Object k;
24             if (e.hash == hash &&
25                 ((k = e.key) == key || (key != null && key.equals(k)))) {
26                 modCount++;
27                 size--;
28                 if (prev == e)
29                     table[i] = next;
30                 else
31                     prev.next = next;
32                 e.recordRemoval(this);
33                 return e;
34             }
35             prev = e;
36             e = next;
37         }
38 
39         return e;
40     }

  若是想刪除整個 HashMap,就可使用clear()函數,源碼很簡單,就是調用 Arrays 的 fill()方法:

1 public void clear() {
 2         modCount++;
 3         Arrays.fill(table, null);
 4         size = 0;
 5     }
 6 
 7     //這個是 Arrays 類的靜態方法
 8     public static void fill(Object[] a, Object val) {
 9         for (int i = 0, len = a.length; i < len; i++)
10             a[i] = val;
11     }

  

7. 迭代器

下面是 HashMap 內部實現的迭代器,在看《Thinking In Java》第十章內部類的時候,就提到過了容器的迭代類都是內部類,外部定義了一個迭代器的接口。每一個容器在內部實現這個接口,那麼使用容器的時候就有了統一的接口。HashMap 一共實現了下列幾個迭代器:

  • HashIterator(僅僅爲迭代器模板,使用抽象類實現)
  • ValueIterator
  • KeyIterator
  • EntryIterator

下面就是它們的源碼:

1 /**
 2     * 咱們會 HashMap 將迭代器進行了抽象,以後定義了 HashIterator 迭代器,
 3     * 可是注意它是一個抽象類。(實現 Iterator 接口須要實現 hashNext(),next(),remove()方法)
 4     * 而 HashIterator 是一個抽象類,沒有實現 next(),而是由實際工做的迭代器完成
 5     * 
 6     * HashIterator 裏面有一個 expectedModCount 字段,保證了 fail-fast 機制
 7     */
 8     private abstract class HashIterator<E> implements Iterator<E> {
 9         Entry<K,V> next;        // next entry to return
10         int expectedModCount;   // For fast-fail
11         int index;              // current slot
12         Entry<K,V> current;     // current entry
13 
14         HashIterator() {
15             expectedModCount = modCount;
16             if (size > 0) { // advance to first entry
17                 Entry[] t = table;
18                 while (index < t.length && (next = t[index++]) == null)
19                     ;
20             }
21         }
22 
23         public final boolean hasNext() {
24             return next != null;
25         }
26 
27         final Entry<K,V> nextEntry() {
28             if (modCount != expectedModCount)
29                 throw new ConcurrentModificationException();
30             Entry<K,V> e = next;
31             if (e == null)
32                 throw new NoSuchElementException();
33 
34             if ((next = e.next) == null) {
35                 Entry[] t = table;
36                 while (index < t.length && (next = t[index++]) == null)
37                     ;
38             }
39             current = e;
40             return e;
41         }
42 
43         public void remove() {
44             if (current == null)
45                 throw new IllegalStateException();
46             if (modCount != expectedModCount)
47                 throw new ConcurrentModificationException();
48             Object k = current.key;
49             current = null;
50             HashMap.this.removeEntryForKey(k);
51             expectedModCount = modCount;
52         }
53     }
54 
55     private final class ValueIterator extends HashIterator<V> {
56         public V next() {
57             return nextEntry().value;
58         }
59     }
60 
61     private final class KeyIterator extends HashIterator<K> {
62         public K next() {
63             return nextEntry().getKey();
64         }
65     }
66 
67     private final class EntryIterator extends HashIterator<Map.Entry<K,V>> {
68         public Map.Entry<K,V> next() {
69             return nextEntry();
70         }
71     }
72 
73     // Subclass overrides these to alter behavior of views' iterator() method
74     Iterator<K> newKeyIterator()   {
75         return new KeyIterator();
76     }
77     Iterator<V> newValueIterator()   {
78         return new ValueIterator();
79     }
80     Iterator<Map.Entry<K,V>> newEntryIterator()   {
81         return new EntryIterator();
82     }

  感受 HashMap 設計的迭代器很巧妙,若是是我,應該是單獨實現3種。但 HashMap 卻抽象了迭代器模型,將 next()的實現統一成 nextEntry(),以後實際迭代器只須要調用這個函數就能夠返回正確的值。

相關文章
相關標籤/搜索