HashMap源碼解析 給jdk寫註釋系列之jdk1.6容器(4)

  前面瞭解了jdk容器中的兩種List,回憶一下怎麼從list中取值(也就是做查詢),是通過index索引位置對不對,由於存入list的元素時安裝插入順序存儲的,所以index索引也就是插入的次序。
  Map呢是這樣一種容器,它可以存儲兩個元素鍵和值,根據鍵這個關鍵字可以明確且唯一的查出一個值,這個過程很像查字典,考慮一下使用什麼樣的數據結構才能實現這種效果呢?
 
1.自己實現一個Map
 
     先來看一下jdk中map的定義:     
複製代碼
 1 public interface Map<K,V> {
 2     int size();
 3     boolean isEmpty();
 4     boolean containsKey(Object key);
 5     boolean containsValue(Object value);
 6     V get(Object key);
 7     V put(K key, V value);
 8     V remove(Object key);
 9     void putAll(Map<? extends K, ? extends V> m);
10     void clear();
11     Set<K> keySet();
12     Collection<V> values();
13     Set<Map.Entry<K, V>> entrySet();
14     interface Entry<K,V> {
15        V getValue();
16        V setValue(V value);
17         boolean equals(Object o);
18         int hashCode();
19     }
20     boolean equals(Object o);
21     int hashCode();
22 }
複製代碼
  可以看到Map並沒有實現Collection接口,也沒有實現List接口,因爲它可以保存兩個屬性key-value,和List容器一樣還是包含增刪改查等基本操作,同時可以看到Map中還定義了一個用來表示鍵值K-V的接口Entry。
     
  在瞭解了map的概念和定義後,首先我們自己先來簡單寫一個Map的實現,看看會遇到什麼樣的問題。
複製代碼
 1 public class MyMap {
 2 
 3         private Entry[] data = new Entry[100];
 4         private int size;
 5 
 6         public Object put(Object key, Object value) {
 7                // 檢查key是否存在,存在則覆蓋
 8                for (int i = 0; i < size; i++) {
 9                       if (key.equals(data [i].key)) {
10                            Object oldValue = data[i].value ;
11                             data[i].value = value;
12                             return oldValue;
13                      }
14               }
15               
16               Entry e = new Entry(key, value);
17                data[size ] = e;
18                size++;
19               
20                return null;
21        }
22 
23         public Object get(Object key) {
24                for (int i = 0; i < size; i++) {
25                       if (key.equals(data [i].key)) {
26                             return data [i].value;
27                      }
28               }
29 
30                return null;
31        }
32        
33         public int size() {
34                return size ;
35        }
36        
37         private class Entry {
38               Object key;
39               Object value;
40 
41                public Entry(Object key, Object value) {
42                       this.key = key;
43                       this.value = value;
44               }
45 
46        }
47 }
複製代碼
  
  上面我們簡單實現了一下map的put、get、size等方法,從代碼可以看到底層是使用數組來存儲數據的。
  測試一下上面的方法:
複製代碼
 1 public class Test {
 2 
 3         public static void main(String[] args) {
 4               MyMap map = new MyMap();
 5               map.put( "tstd", "angelababy" );
 6               map.put( "張三" , "李四");
 7               map.put( "tstd", "高圓圓" );
 8               
 9               System. out.println(map.size());
10               System. out.println(map.get("tstd" ));
11               System. out.println(map.get("張三" ));
12        }
13 }
複製代碼

 

  看下結果:

2
高圓圓
李四

 

  結果好像是沒有問題的對不對。但是這麼簡單嘛?我們來看一下上面的代碼存在一些什麼樣的問題。
  觀察代碼可以看到,get方法中,通過key獲取value的方式是通過遍歷數組實現,這樣顯然是非常低效的,同樣在put方法中由於要檢查key是否已經存在也是通過遍歷數組實現,想一下有沒有更好的辦法呢?能不能像數組那樣直接通過下標就可以取得對應的元素呢?
  接下來,我們看下HashMap是怎麼樣實現的。
 
2.HashMap的定義
  
  在看HashMap定義前,我們首先需要了解hash是什麼意思,hash通常被翻譯成“散列”,簡單解析下(不對的話還請指出^_^),hash就是通過散列算法,將一個任意長度關鍵字轉換爲一個固定長度的散列值,但是有一點要指出的是,不同的關鍵字可能會散列出相同的散列值。什麼意思呢?也就是關鍵字和散列值不是一一對應的,散列值會出現衝突。但是爲什麼會出現這種情況呢,原因是hash是一種壓縮映射,舉個例子就是將一個8個字節(二進制64位)的long值轉換爲一個4個字節(二進制32位)的int值,也就是說需要砍掉4個字節(32位),坑位有限,人太多,所以只能兩個人一個坑嘍。
     ok、瞭解了hash的概念和特點後,來看下HashMap的定義:
1 public class HashMap<K,V>
2     extends AbstractMap<K,V>
3     implements Map<K,V>, Cloneable, Serializable
  可以看出HashMap集成了AbstractMap抽象類,實現了Map,Cloneable,Serializable接口,AbstractMap抽象類繼承了Map提供了一些基本的實現。
 
3.底層存儲
複製代碼
 1 // 默認初始容量爲16,必須爲2的n次冪
 2     static final int DEFAULT_INITIAL_CAPACITY = 16;
 3 
 4     // 最大容量爲2的30次方
 5     static final int MAXIMUM_CAPACITY = 1 << 30;
 6 
 7     // 默認加載因子爲0.75f
 8     static final float DEFAULT_LOAD_FACTOR = 0.75f;
 9 
10     // Entry數組,長度必須爲2的n次冪
11     transient Entry[] table;
12 
13     // 已存儲元素的數量
14     transient int size ;
15 
16     // 下次擴容的臨界值,size>=threshold就會擴容,threshold等於capacity*load factor
17     int threshold;
18 
19     // 加載因子
20     final float loadFactor ;
複製代碼

  可以看出HashMap底層是用Entry數組存儲數據,同時定義了初始容量,最大容量,加載因子等參數,至於爲什麼容量必須是2的冪,加載因子又是什麼,下面再說,先來看一下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;
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             if (k1 == k2 || (k1 != null && k1.equals(k2))) {
35                 Object v1 = getValue();
36                 Object v2 = e.getValue();
37                 if (v1 == v2 || (v1 != null && v1.equals(v2)))
38                     return true;
39             }
40             return false;
41         }
42 
43         public final int hashCode() {
44             return (key ==null   ? 0 : key.hashCode()) ^
45                    ( value==null ? 0 : value.hashCode());
46         }
47 
48         public final String toString() {
49             return getKey() + "=" + getValue();
50         }
51 
52         // 當向HashMap中添加元素的時候調用這個方法,這裏沒有實現是供子類回調用
53         void recordAccess(HashMap<K,V> m) {
54         }
55 
56         // 當從HashMap中刪除元素的時候調動這個方法 ,這裏沒有實現是供子類回調用
57         void recordRemoval(HashMap<K,V> m) {
58         }
59 }
複製代碼

  Entry是HashMap的內部類,它繼承了Map中的Entry接口,它定義了鍵(key),值(value),和下一個節點的引用(next),以及hash值。很明確的可以看出Entry是什麼結構,它是單線鏈表的一個節點。也就是說HashMap的底層結構是一個數組,而數組的元素是一個單向鏈表。

 

 

  爲什麼會有這樣的設計?我們上面自己實現的map存在一個問題就是查詢時需要遍歷所有的key,爲了解決這個問題HashMap採用hash算法將key散列爲一個int值,這個int值對應到數組的下標,再做查詢操作的時候,拿到key的散列值,根據數組下標就能直接找到存儲在數組的元素。但是由於hash可能會出現相同的散列值,爲了解決衝突,HashMap採用將相同的散列值存儲到一個鏈表中,也就是說在一個鏈表中的元素他們的散列值絕對是相同的。找到數組下標取出鏈表,再遍歷鏈表是不是比遍歷整個數組效率好的多呢?
  我們來看一下HashMap的具體實現。
 
4.構造方法
複製代碼
 1 /**
 2      * 構造一個指定初始容量和加載因子的HashMap
 3      */
 4     public HashMap( int initialCapacity, float loadFactor) {
 5         // 初始容量和加載因子合法校驗
 6         if (initialCapacity < 0)
 7             throw new IllegalArgumentException( "Illegal initial capacity: " +
 8                                                initialCapacity);
 9         if (initialCapacity > MAXIMUM_CAPACITY)
10             initialCapacity = MAXIMUM_CAPACITY;
11         if (loadFactor <= 0 || Float.isNaN(loadFactor))
12             throw new IllegalArgumentException( "Illegal load factor: " +
13                                                loadFactor);
14 
15         // Find a power of 2 >= initialCapacity
16         // 確保容量爲2的n次冪,是capacity爲大於initialCapacity的最小的2的n次冪
17         int capacity = 1;
18         while (capacity < initialCapacity)
19             capacity <<= 1;
20 
21         // 賦值加載因子
22         this.loadFactor = loadFactor;
23         // 賦值擴容臨界值
24         threshold = (int)(capacity * loadFactor);
25         // 初始化hash表
26         table = new Entry[capacity];
27         init();
28     }
29 
30     /**
31      * 構造一個指定初始容量的HashMap
32      */
33     public HashMap( int initialCapacity) {
34         this(initialCapacity, DEFAULT_LOAD_FACTOR);
35     }
36 
37     /**
38      * 構造一個使用默認初始容量(16)和默認加載因子(0.75)的HashMap
39      */
40     public HashMap() {
41         this.loadFactor = DEFAULT_LOAD_FACTOR;
42         threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
43         table = new Entry[DEFAULT_INITIAL_CAPACITY];
44         init();
45     }
46 
47     /**
48      * 構造一個指定map的HashMap,所創建HashMap使用默認加載因子(0.75)和足以容納指定map的初始容量。
49      */
50     public HashMap(Map<? extends K, ? extends V> m) {
51         // 確保最小初始容量爲16,並保證可以容納指定map
52         this(Math.max(( int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
53                       DEFAULT_INITIAL_CAPACITY ), DEFAULT_LOAD_FACTOR);
54         putAllForCreate(m);
55 }
複製代碼

  

  最後一個構造方法引入一下三個方法進行map元素添加,具體內容不多看了,邏輯和put一樣但是少了數組擴容邏輯,直接跳過去看增加方法。

複製代碼
 1 private void putAllForCreate(Map<? extends K, ? extends V> m) {
 2       for(Iterator<?extendsMap.Entry<?extendsK, ?extendsV>> i = m.entrySet().iterator(); i.hasNext(); ) {
 3             Map.Entry<? extends K, ? extends V> e = i.next();
 4             putForCreate(e.getKey(), e.getValue());
 5         }
 6     }
 7 
 8     /**
 9      * This method is used instead of put by constructors and
10      * pseudoconstructors (clone, readObject).  It does not resize the table,
11      * check for comodification, etc.  It calls createEntry rather than
12      * addEntry.
13      */
14     private void putForCreate(K key, V value) {
15         int hash = (key == null) ? 0 : hash(key.hashCode());
16         int i = indexFor(hash, table.length );
17 
18         for (Entry<K,V> e = table [i]; e != null; e = e. next) {
19             Object k;
20             if (e.hash == hash &&
21                 ((k = e. key) == key || (key != null && key.equals(k)))) {
22                 e. value = value;
23                 return;
24             }
25         }
26 
27         createEntry(hash, key, value, i);
28     }
29    
30    void createEntry(int hash, K key, V value, int bucketIndex) {
31        Entry<K,V> e = table[bucketIndex];
32         table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
33         size++;
34 }
複製代碼
  看完構造方法有一個疑問一直存在,代碼一直確認初始容量和數組長度必須爲2的n次冪,而加載因子是爲了計算擴容臨界值,那麼到底HashMap是怎麼進行擴容的呢?
 
5.增加
複製代碼
 1 public V put(K key, V value) {
 2         // 如果key爲null,調用putForNullKey方法進行存儲
 3         if (key == null)
 4             return putForNullKey(value);
 5         // 使用key的hashCode計算key對應的hash值
 6         int hash = hash(key.hashCode());
 7         // 通過key的hash值查找在數組中的index位置
 8         int i = indexFor(hash, table.length );
 9         // 取出數組index位置的鏈表,遍歷鏈表找查看是有已經存在相同的key
10         for (Entry<K,V> e = table [i]; e != null; e = e. next) {
11             Object k;
12             // 通過對比hash值、key判斷是否已經存在相同的key
13             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
14                 // 如果存在,取出當前key對應的value,供返回
15                 V oldValue = e. value;
16                 // 用新value替換之舊的value
17                 e. value = value;
18                 e.recordAccess( this);
19                 // 返回舊value,退出方法
20                 return oldValue;
21             }
22         }
23 
24         // 如果不存在相同的key
25         // 修改版本+1
26         modCount++;
27         // 在數組i位置處添加一個新的鏈表節點
28         addEntry(hash, key, value, i);
29         // 沒有相同key的情況,返回null
30         return null;
31     }
32 
33     private V putForNullKey(V value) {
34         // 取出數組第1個位置(下標等於0)的節點,如果存在則覆蓋不存在則新增,和上面的put一樣不多講,
35         for (Entry<K,V> e = table [0]; e != null; e = e. next) {
36             if (e.key == null) {
37                 V oldValue = e. value;
38                 e. value = value;
39                 e.recordAccess( this);
40                 return oldValue;
41             }
42         }
43         modCount++;
44         // 如果key等於null,則hash值等於0
45         addEntry(0, null, value, 0);
46         return null;
47 }
複製代碼

  增加和我們上面分析的一樣,通過將key做hash取得一個散列值,將散列值對應到數組下標,然後將k-v組成鏈表節點存進數組中。

  上面有三個方法需要重點關注,計算hash值的hash方法,計算數組索引位置的indexFor方法,添加新鏈表節點的addEntry方法,下面我們逐一的看一下。

複製代碼
 1 /**
 2      * Applies a supplemental hash function to a given hashCode, which
 3      * defends against poor quality hash functions.  This is critical
 4      * because HashMap uses power -of- two length hash tables, that
 5      * otherwise encounter collisions for hashCodes that do not differ
 6      * in lower bits. Note: Null keys always map to hash 0, thus index 0.
 7      */
 8     static int hash(int h) {
 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         h ^= (h >>> 20) ^ (h >>> 12);
13         return h ^ (h >>> 7) ^ (h >>> 4);
14     }
15 
16     /**
17      * Returns index for hash code h.
18      */
19     static int indexFor(int h, int length) {
20         return h & (length-1);
21 }
複製代碼
  
  
     上面這兩個方法好難懂啊,又是位移又是異或又是與操作,如果讓我們自己來寫會怎麼寫呢,hash方法中直接使用hashCode就好了,indexFor直接取模(h % length)就好了,這兩種有什麼區別嗎,哪個更好呢?來簡單分析下(分析的不好請拍磚)。
     首先要明白&操作:把兩個操作數分別轉換爲二進制,如果兩個操作數的位都是1則爲1,否則爲0,舉個例子:兩個數8和9的二進制分別爲1000和1001,1000 & 1001 = 1000。
     先看下indexFor方法中的h & (length-1) ,這是什麼鬼東西。。。
     不懂原理只能反着推了。。。我們先來看下一個神奇的推論。。。
 
     2^n轉換爲二進制是什麼樣子呢:
     2^1 = 10
     2^2 = 100
     2^3 = 1000
     2^n = 1(n個0)
 
     再來看下2^n-1的二進制是什麼樣子的:
     2^1 - 1 = 01
     2^2 - 1 = 011
     2^3 - 1 = 0111
     2^n - 1 = 0(n個1)
 
  我們發現一個喫驚的結果,就是當length=2的n次冪的時候,h & (length-1)的結果,就是0~(length-1)之間的數,而這個結果和h % length是一樣的,但當length!=2^n的時候,這個就特點不成立了。解釋下就是:2^n - 1轉換成二進制就是0+n個1,比如16的二進制10000,15的二進制01111,按照&操作,都是1則爲1,否則爲0,所以在低位運算的時候(小於等於2^n - 1),值總是與hash相同,而進行高位運算時(大於2^n - 1),其值等於其低位值。
     只要知道這個結果,就ok,如果還想更加深入,有個大神寫了一篇文章可以參考下http://yananay.iteye.com/blog/910460
     但是爲什麼不直接取模呢,當然是因爲&操作要比除法操作效率高了。
 
     知道了 h & (length-1)的結果等同於h % length後,再來看看上面的hash()方法是怎麼回事呢?如果hashCode的低位相同(尤其是等於length位數的部分),那麼經過散列後衝突的概率比較大,於是jdk給hash的各位加入了一些隨機性。
 
     上面那兩個還沒懂的話,只要明白含義,然後忘掉他來看增加節點的方法。
 
複製代碼
 1     /**
 2      * 增加一個k-v,hash組成的節點在數組內,同時可能會進行數組擴容。
 3      */
 4     void addEntry( int hash, K key, V value, int bucketIndex) {
 5         // 下面兩行行代碼的邏輯是,創建一個新節點放到單向鏈表的頭部,舊節點向後移
 6         // 取出索引bucketIndex位置處的鏈表節點,如果節點不存在那就是null,也就是說當數組該位置處還不曾存放過節點的時候,這個地方就是null,
 7        Entry<K,V> e = table[bucketIndex];
 8        // 創建一個節點,並放置在數組的bucketIndex索引位置處,並讓新的節點的next指向原來的節點
 9         table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
10        // 如果當前HashMap中的元素已經到達了臨界值,則將容量擴大2倍,並將size計數+1
11         if (size ++ >= threshold)
12             resize(2 * table.length );
13 }
複製代碼
  這裏面有一個需要注意的地方,將新節點指向原來的節點,這裏雖然是next,但是卻是往回指向的,而不是像上面圖中畫的由數組第1個節點往後指向,就是說第1個節點指向null,第2個節點指向第1個,第3個節點指向第2個。也就是新節點一直插入在最前端,新節點始終是單向列表的頭節點。
    
  再看下擴容的方法:
複製代碼
 1     /**
 2      * Rehashes the contents of this map into a new array with a
 3      * larger capacity.  This method is called automatically when the
 4      * number of keys in this map reaches its threshold.
 5      *
 6      * If current capacity is MAXIMUM_CAPACITY, this method does not
 7      * resize the map, but sets threshold to Integer.MAX_VALUE.
 8      * This has the effect of preventing future calls.
 9      *
10      * @param newCapacity the new capacity, MUST be a power of two;
11      *        must be greater than current capacity unless current
12      *        capacity is MAXIMUM_CAPACITY (in which case value
13      *        is irrelevant).
14      */
15     void resize( int newCapacity) {
16         // 當前數組
17         Entry[] oldTable = table;
18         // 當前數組容量
19         int oldCapacity = oldTable.length ;
20         // 如果當前數組已經是默認最大容量MAXIMUM_CAPACITY ,則將臨界值改爲Integer.MAX_VALUE 返回
21         if (oldCapacity == MAXIMUM_CAPACITY) {
22             threshold = Integer.MAX_VALUE;
23             return;
24         }
25 
26         // 使用新的容量創建一個新的鏈表數組
27         Entry[] newTable = new Entry[newCapacity];
28         // 將當前數組中的元素都移動到新數組中
29         transfer(newTable);
30         // 將當前數組指向新創建的數組
31         table = newTable;
32         // 重新計算臨界值
33         threshold = (int)(newCapacity * loadFactor);
34     }
35 
36     /**
37      * Transfers all entries from current table to newTable.
38      */
39     void transfer(Entry[] newTable) {
40         // 當前數組
41         Entry[] src = table;
42         // 新數組長度
43         int newCapacity = newTable.length ;
44         // 遍歷當前數組的元素,重新計算每個元素所在數組位置
45         for (int j = 0; j < src. length; j++) {
46             // 取出數組中的鏈表第一個節點
47             Entry<K,V> e = src[j];
48             if (e != null) {
49                 // 將舊鏈表位置置空
50                 src[j] = null;
51                 // 循環鏈表,挨個將每個節點插入到新的數組位置中
52                 do {
53                     // 取出鏈表中的當前節點的下一個節點
54                     Entry<K,V> next = e. next;
55                     // 重新計算該鏈表在數組中的索引位置
56                     int i = indexFor(e. hash, newCapacity);
57                     // 將下一個節點指向newTable[i]
58                     e. next = newTable[i];
59                     // 將當前節點放置在newTable[i]位置
60                     newTable[i] = e;
61                     // 下一次循環
62                     e = next;
63                 } while (e != null);
64             }
65         }
66 }
複製代碼
  
  
   transfer方法中,由於數組的容量已經變大,也就導致hash算法indexFor已經發生變化,原先在一個鏈表中的元素,在新的hash下可能會產生不同的散列值,so所有元素都要重新計算後安頓一番。注意在do while循環的過程中,每次循環都是將下個節點指向newTable[i] ,是因爲如果有相同的散列值i,上個節點已經放置在newTable[i]位置,這裏還是下一個節點的next指向上一個節點(不知道這裏是否能理解,畫個圖理解下吧)。
 
  Map中的元素越多,hash衝突的機率也就越大,數組長度是固定的,所以導致鏈表越來越長,那麼查詢的效率當然也就越低下了。還記不記得同時數組容器的ArrayList怎麼做的,擴容!而HashMap的擴容resize,需要將所有的元素重新計算後,一個個重新排列到新的數組中去,這是非常低效的,和ArrayList一樣,在可以預知容量大小的情況下,提前預設容量會減少HashMap的擴容,提高性能。
  再來看看加載因子的作用,如果加載因子越大,數組填充的越滿,這樣可以有效的利用空間,但是有一個弊端就是可能會導致衝突的加大,鏈表過長,反過來卻又會造成內存空間的浪費。所以只能需要在空間和時間中找一個平衡點,那就是設置有效的加載因子。我們知道,很多時候爲了提高查詢效率的做法都是犧牲空間換取時間,到底該怎麼取捨,那就要具體分析了。
 
6.刪除
複製代碼
 1 /**
 2      * 根據key刪除元素
 3      */
 4     public V remove(Object key) {
 5         Entry<K,V> e = removeEntryForKey(key);
 6         return (e == null ? null : e. value);
 7     }
 8 
 9     /**
10      * 根據key刪除鏈表節點
11      */
12     final Entry<K,V> removeEntryForKey(Object key) {
13         // 計算key的hash值
14         int hash = (key == null) ? 0 : hash(key.hashCode());
15         // 根據hash值計算key在數組的索引位置
16         int i = indexFor(hash, table.length );
17         // 找到該索引出的第一個節點
18         Entry<K,V> prev = table[i];
19         Entry<K,V> e = prev;
20 
21         // 遍歷鏈表(從鏈表第一個節點開始next),找出相同的key,
22         while (e != null) {
23             Entry<K,V> next = e. next;
24             Object k;
25             // 如果hash值和key都相等,則認爲相等
26             if (e.hash == hash &&
27                 ((k = e. key) == key || (key != null && key.equals(k)))) {
28                 // 修改版本+1
29                 modCount++;
30                 // 計數器減1
31                 size--;
32                 // 如果第一個就是要刪除的節點(第一個節點沒有上一個節點,所以要分開判斷)
33                 if (prev == e)
34                     // 則將下一個節點放到table[i]位置(要刪除的節點被覆蓋)
35                     table[i] = next;
36                 else
37                  // 否則將上一個節點的next指向當要刪除節點下一個(要刪除節點被忽略,沒有指向了)
38                     prev. next = next;
39                 e.recordRemoval( this);
40                 // 返回刪除的節點內容
41                 return e;
42             }
43             // 保存當前節點爲下次循環的上一個節點
44             prev = e;
45             // 下次循環
46             e = next;
47         }
48 
49         return e;
50 }
複製代碼
 
7.修改
     想一下Map中爲什麼沒有修改方法,1,2,3想好了,對於Map,put相同的key,value會被覆蓋掉,這是不是就相當於修改呀。
 
8.查找
複製代碼
 1 public V get(Object key) {
 2         // 如果key等於null,則調通getForNullKey方法
 3         if (key == null)
 4             return getForNullKey();
 5         // 計算key對應的hash值
 6         int hash = hash(key.hashCode());
 7         // 通過hash值找到key對應數組的索引位置,遍歷該數組位置的鏈表
 8         for (Entry<K,V> e = table [indexFor (hash, table .length)];
 9              e != null;
10              e = e. next) {
11             Object k;
12             // 如果hash值和key都相等,則認爲相等
13             if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
14                 // 返回value
15                 return e.value ;
16         }
17         return null;
18     }
19 
20     private V getForNullKey() {
21         // 遍歷數組第一個位置處的鏈表
22         for (Entry<K,V> e = table [0]; e != null; e = e. next) {
23             if (e.key == null)
24                 return e.value ;
25         }
26         return null;
27 }
複製代碼

 

  從刪除和查找可以看出,在根據key查找元素的時候,還是需要通過遍歷,但是由於已經通過hash對key散列,要遍歷的只是發生衝突後生成的鏈表,這樣遍歷的結果就已經少很多了,比我們自己寫的完全遍歷效率提升了n被。
 
9.是否包含
複製代碼
 1 /**
 2      * Returns <tt>true</tt> if this map contains a mapping for the
 3      * specified key.
 4      *
 5      * @param   key   The key whose presence in this map is to be tested
 6      * @return <tt> true</tt> if this map contains a mapping for the specified
 7      * key.
 8      */
 9     public boolean containsKey(Object key) {
10         return getEntry(key) != null;
11     }
12  
13     /**
14      * Returns the entry associated with the specified key in the
15      * HashMap.  Returns null if the HashMap contains no mapping
16      * for the key.
17      */
18     final Entry<K,V> getEntry(Object key) {
19         int hash = (key == null) ? 0 : hash(key.hashCode());
20         for (Entry<K,V> e = table [indexFor (hash, table .length)];
21              e != null;
22              e = e. next) {
23             Object k;
24             if (e.hash == hash &&
25                 ((k = e. key) == key || (key != null && key.equals(k))))
26                 return e;
27         }
28         return null;
29 }
複製代碼

  containsKey的代碼邏輯和get的代碼邏輯90%是相同的啊,爲什麼沒有封裝下呢?

 
複製代碼
 1 /**
 2      * Returns <tt>true</tt> if this map maps one or more keys to the
 3      * specified value.
 4      *
 5      * @param value value whose presence in this map is to be tested
 6      * @return <tt> true</tt> if this map maps one or more keys to the
 7      *         specified value
 8      */
 9     public boolean containsValue(Object value) {
10         if (value == null)
11             return containsNullValue();
12 
13        Entry[] tab = table;
14        // 遍歷整個table查詢是否有相同的value值
15         for (int i = 0; i < tab. length ; i++)
16             // 遍歷數組的每個鏈表
17             for (Entry e = tab[i] ; e != null ; e = e.next)
18                 if (value.equals(e.value ))
19                     return true;
20         return false;
21     }
22 
23     /**
24      * Special -case code for containsValue with null argument
25      */
26     private boolean containsNullValue() {
27        Entry[] tab = table;
28         for (int i = 0; i < tab. length ; i++)
29             for (Entry e = tab[i] ; e != null ; e = e.next)
30                 if (e.value == null)
31                     return true;
32         return false;
33 }
複製代碼

  可以看到針對指定key的查找,由於HashMap在結構上的優化,查找相對是十分高效的,而對於指定value的查找,要遍歷整個hash表,這樣是非常低效費時的。。。

 

10.容量檢查

複製代碼
 1     /**
 2      * Returns the number of key -value mappings in this map.
 3      *
 4      * @return the number of key- value mappings in this map
 5      */
 6     public int size() {
 7         return size ;
 8     }
 9 
10     /**
11      * Returns <tt>true</tt> if this map contains no key -value mappings.
12      *
13      * @return <tt> true</tt> if this map contains no key -value mappings
14      */
15     public boolean isEmpty() {
16         return size == 0;
17 }
複製代碼

 

11.遍歷
 
     關於Map的遍歷,需要到後面再分析,因爲它牽扯到另外一個容器Set,Set見。
 
 
     至此,HashMap也就分析的差不多了,我們應該已經明白HashMap能過做到快速查詢時建立在其底層的存儲結構只上的,學習HashMap也就是對前面的兩種數據結構的綜合運用,另外這裏面有關hash的算法,擴容的方案也應該有所掌握,還是那句話學習應該是先觀其大略,先從整體上了解他的實現,比如先了解它的存儲結構,而不是一頭扎進代碼中,另外代碼讀不懂畫畫圖唱唱歌吧,啦啦啦
 
     HashMap 完!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章