1. 程式人生 > >Java HashMap, Hashtable, TreeMap, WeakHashMap總結

Java HashMap, Hashtable, TreeMap, WeakHashMap總結

概要

學完了Map的全部內容,我們再回頭開開Map的框架圖。

 

本章內容包括:
第1部分 Map概括
第2部分 HashMap和Hashtable異同
第3部分 HashMap和WeakHashMap異同

第1部分 Map概括

(01) Map 是“鍵值對”對映的抽象介面。
(02) AbstractMap 實現了Map中的絕大部分函式介面。它減少了“Map的實現類”的重複編碼。
(03) SortedMap 有序的“鍵值對”對映介面。
(04) NavigableMap 是繼承於SortedMap的,支援導航函式的介面。
(05) HashMap, Hashtable, TreeMap, WeakHashMap這4個類是“鍵值對”對映的實現類。它們各有區別!

  HashMap 是基於“拉鍊法”實現的散列表。一般用於單執行緒程式中。
  Hashtable 也是基於“拉鍊法”實現的散列表。它一般用於多執行緒程式中。
  WeakHashMap 也是基於“拉鍊法”實現的散列表,它一般也用於單執行緒程式中。相比HashMap,WeakHashMap中的鍵是“弱鍵”,當“弱鍵”被GC回收時,它對應的鍵值對也會被從WeakHashMap中刪除;而HashMap中的鍵是強鍵。
  TreeMap 是有序的散列表,它是通過紅黑樹實現的。它一般用於單執行緒中儲存有序的對映。

 

第2部分 HashMap和Hashtable異同

第2.1部分 HashMap和Hashtable的相同點

HashMapHashtable都是儲存“鍵值對(key-value)”的散列表,而且都是採用拉鍊法實現的。
儲存的思想都是:通過table陣列儲存,陣列的每一個元素都是一個Entry;而一個Entry就是一個單向連結串列Entry連結串列中的每一個節點就儲存了key-value鍵值對資料

新增key-value鍵值對:首先,根據key值計算出雜湊值,再計算出陣列索引(即,該key-value在table中的索引)。然後,根據陣列索引找到Entry(即,單向連結串列),再遍歷單向連結串列,將key和連結串列中的每一個節點的key進行對比。若key已經存在Entry連結串列中,則用該value值取代舊的value值;若key不存在Entry連結串列中,則新建一個key-value節點,並將該節點插入Entry連結串列的表頭位置。
刪除key-value鍵值對

:刪除鍵值對,相比於“新增鍵值對”來說,簡單很多。首先,還是根據key計算出雜湊值,再計算出陣列索引(即,該key-value在table中的索引)。然後,根據索引找出Entry(即,單向連結串列)。若節點key-value存在與連結串列Entry中,則刪除連結串列中的節點即可。


上面介紹了HashMap和Hashtable的相同點。正是由於它們都是散列表,我們關注更多的是“它們的區別,以及它們分別適合在什麼情況下使用”。那接下來,我們先看看它們的區別。

 

第2.2部分 HashMap和Hashtable的不同點

1 繼承和實現方式不同

HashMap 繼承於AbstractMap,實現了Map、Cloneable、java.io.Serializable介面。
Hashtable 繼承於Dictionary,實現了Map、Cloneable、java.io.Serializable介面。

HashMap的定義:

public class HashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable { ... }

Hashtable的定義:

public class Hashtable<K,V>
    extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable { ... }

從中,我們可以看出:
1.1 HashMap和Hashtable都實現了Map、Cloneable、java.io.Serializable介面。
      實現了Map介面,意味著它們都支援key-value鍵值對操作。支援“新增key-value鍵值對”、“獲取key”、“獲取value”、“獲取map大小”、“清空map”等基本的key-value鍵值對操作。
      實現了Cloneable介面,意味著它能被克隆。
      實現了java.io.Serializable介面,意味著它們支援序列化,能通過序列化去傳輸。

1.2 HashMap繼承於AbstractMap,而Hashtable繼承於Dictionary
      Dictionary是一個抽象類,它直接繼承於Object類,沒有實現任何介面。Dictionary類是JDK 1.0的引入的。雖然Dictionary也支援“新增key-value鍵值對”、“獲取value”、“獲取大小”等基本操作,但它的API函式比Map少;而且             Dictionary一般是通過Enumeration(列舉類)去遍歷,Map則是通過Iterator(迭代器)去遍歷。 然而‘由於Hashtable也實現了Map介面,所以,它即支援Enumeration遍歷,也支援Iterator遍歷。關於這點,後面還會進一步說明。
      AbstractMap是一個抽象類,它實現了Map介面的絕大部分API函式;為Map的具體實現類提供了極大的便利。它是JDK 1.2新增的類。

 

2 執行緒安全不同

Hashtable的幾乎所有函式都是同步的,即它是執行緒安全的,支援多執行緒。
而HashMap的函式則是非同步的,它不是執行緒安全的。若要在多執行緒中使用HashMap,需要我們額外的進行同步處理。 對HashMap的同步處理可以使用Collections類提供的synchronizedMap靜態方法,或者直接使用JDK 5.0之後提供的java.util.concurrent包裡的ConcurrentHashMap類。


3 對null值的處理不同

HashMap的key、value都可以為null
Hashtable的key、value都不可以為null

我們先看看HashMap和Hashtable “新增key-value”的方法

HashMap的新增key-value的方法

複製程式碼

 1 // 將“key-value”新增到HashMap中
 2 public V put(K key, V value) {
 3     // 若“key為null”,則將該鍵值對新增到table[0]中。
 4     if (key == null)
 5         return putForNullKey(value);
 6     // 若“key不為null”,則計算該key的雜湊值,然後將其新增到該雜湊值對應的連結串列中。
 7     int hash = hash(key.hashCode());
 8     int i = indexFor(hash, table.length);
 9     for (Entry<K,V> e = table[i]; e != null; e = e.next) {
10         Object k;
11         // 若“該key”對應的鍵值對已經存在,則用新的value取代舊的value。然後退出!
12         if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
13             V oldValue = e.value;
14             e.value = value;
15             e.recordAccess(this);
16             return oldValue;
17         }
18     }
19 
20     // 若“該key”對應的鍵值對不存在,則將“key-value”新增到table中
21     modCount++;
22     addEntry(hash, key, value, i);
23     return null;
24 }
25 
26 // putForNullKey()的作用是將“key為null”鍵值對新增到table[0]位置
27 private V putForNullKey(V value) {
28     for (Entry<K,V> e = table[0]; e != null; e = e.next) {
29         if (e.key == null) {
30             V oldValue = e.value;
31             e.value = value;
32             // recordAccess()函式什麼也沒有做
33             e.recordAccess(this);
34             return oldValue;
35         }
36     }
37     // 新增第1個“key為null”的元素都table中的時候,會執行到這裡。
38     // 它的作用是將“設定table[0]的key為null,值為value”。
39     modCount++;
40     addEntry(0, null, value, 0);
41     return null;
42 }

複製程式碼

Hashtable的新增key-value的方法

複製程式碼

 1 // 將“key-value”新增到Hashtable中
 2 public synchronized V put(K key, V value) {
 3     // Hashtable中不能插入value為null的元素!!!
 4     if (value == null) {
 5         throw new NullPointerException();
 6     }
 7 
 8     // 若“Hashtable中已存在鍵為key的鍵值對”,
 9     // 則用“新的value”替換“舊的value”
10     Entry tab[] = table;
11     // Hashtable中不能插入key為null的元素!!!
12     // 否則,下面的語句會丟擲異常!
13     int hash = key.hashCode();
14     int index = (hash & 0x7FFFFFFF) % tab.length;
15     for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
16         if ((e.hash == hash) && e.key.equals(key)) {
17             V old = e.value;
18             e.value = value;
19             return old;
20         }
21     }
22 
23     // 若“Hashtable中不存在鍵為key的鍵值對”,
24     // (01) 將“修改統計數”+1
25     modCount++;
26     // (02) 若“Hashtable實際容量” > “閾值”(閾值=總的容量 * 載入因子)
27     //  則調整Hashtable的大小
28     if (count >= threshold) {
29         // Rehash the table if the threshold is exceeded
30         rehash();
31 
32         tab = table;
33         index = (hash & 0x7FFFFFFF) % tab.length;
34     }
35 
36     // (03) 將“Hashtable中index”位置的Entry(連結串列)儲存到e中 Entry<K,V> e = tab[index];
37     // (04) 建立“新的Entry節點”,並將“新的Entry”插入“Hashtable的index位置”,並設定e為“新的Entry”的下一個元素(即“新Entry”為連結串列表頭)。        
38     tab[index] = new Entry<K,V>(hash, key, value, e);
39     // (05) 將“Hashtable的實際容量”+1
40     count++;
41     return null;
42 }

複製程式碼

根據上面的程式碼,我們可以看出:

Hashtable的key或value,都不能為null!否則,會丟擲異常NullPointerException。
HashMap的key、value都可以為null。 當HashMap的key為null時,HashMap會將其固定的插入table[0]位置(即HashMap散列表的第一個位置);而且table[0]處只會容納一個key為null的值,當有多個key為null的值插入的時候,table[0]會保留最後插入的value。

 

4 支援的遍歷種類不同

HashMap只支援Iterator(迭代器)遍歷。
而Hashtable支援Iterator(迭代器)和Enumeration(列舉器)兩種方式遍歷。

Enumeration 是JDK 1.0新增的介面,只有hasMoreElements(), nextElement() 兩個API介面,不能通過Enumeration()對元素進行修改 。
而Iterator 是JDK 1.2才新增的介面,支援hasNext(), next(), remove() 三個API介面。HashMap也是JDK 1.2版本才新增的,所以用Iterator取代Enumeration,HashMap只支援Iterator遍歷。

 

5 通過Iterator迭代器遍歷時,遍歷的順序不同

HashMap是“從前向後”的遍歷陣列;再對陣列具體某一項對應的連結串列,從表頭開始進行遍歷。
Hashtabl是“從後往前”的遍歷陣列;再對陣列具體某一項對應的連結串列,從表頭開始進行遍歷。

HashMap和Hashtable都實現Map介面,所以支援獲取它們“key的集合”、“value的集合”、“key-value的集合”,然後通過Iterator對這些集合進行遍歷。
由於“key的集合”、“value的集合”、“key-value的集合”的遍歷原理都是一樣的;下面,我以遍歷“key-value的集合”來進行說明。

HashMap 和Hashtable 遍歷"key-value集合"的方式是:(01) 通過entrySet()獲取“Map.Entry集合”。 (02) 通過iterator()獲取“Map.Entry集合”的迭代器,再進行遍歷。

HashMap的實現方式:先“從前向後”的遍歷陣列;對陣列具體某一項對應的連結串列,則從表頭開始往後遍歷。

複製程式碼

 1 // 返回“HashMap的Entry集合”
 2 public Set<Map.Entry<K,V>> entrySet() {
 3     return entrySet0();
 4 }
 5 // 返回“HashMap的Entry集合”,它實際是返回一個EntrySet物件
 6 private Set<Map.Entry<K,V>> entrySet0() {
 7     Set<Map.Entry<K,V>> es = entrySet;
 8     return es != null ? es : (entrySet = new EntrySet());
 9 }
10 // EntrySet對應的集合
11 // EntrySet繼承於AbstractSet,說明該集合中沒有重複的EntrySet。
12 private final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
13     ...
14     public Iterator<Map.Entry<K,V>> iterator() {
15         return newEntryIterator();
16     }
17     ...
18 }
19 // 返回一個“entry迭代器”
20 Iterator<Map.Entry<K,V>> newEntryIterator()   {
21     return new EntryIterator();
22 }
23 // Entry的迭代器
24 private final class EntryIterator extends HashIterator<Map.Entry<K,V>> {
25     public Map.Entry<K,V> next() {
26         return nextEntry();
27     }
28 }
29 private abstract class HashIterator<E> implements Iterator<E> {
30     // 下一個元素
31     Entry<K,V> next;
32     // expectedModCount用於實現fail-fast機制。
33     int expectedModCount;
34     // 當前索引
35     int index;
36     // 當前元素
37     Entry<K,V> current;
38 
39     HashIterator() {
40         expectedModCount = modCount;
41         if (size > 0) { // advance to first entry
42             Entry[] t = table;
43             // 將next指向table中第一個不為null的元素。
44             // 這裡利用了index的初始值為0,從0開始依次向後遍歷,直到找到不為null的元素就退出迴圈。
45             while (index < t.length && (next = t[index++]) == null)
46                 ;
47         }
48     }
49 
50     public final boolean hasNext() {
51         return next != null;
52     }
53 
54     // 獲取下一個元素
55     final Entry<K,V> nextEntry() {
56         if (modCount != expectedModCount)
57             throw new ConcurrentModificationException();
58         Entry<K,V> e = next;
59         if (e == null)
60             throw new NoSuchElementException();
61 
62         // 注意!!!
63         // 一個Entry就是一個單向連結串列
64         // 若該Entry的下一個節點不為空,就將next指向下一個節點;
65         // 否則,將next指向下一個連結串列(也是下一個Entry)的不為null的節點。
66         if ((next = e.next) == null) {
67             Entry[] t = table;
68             while (index < t.length && (next = t[index++]) == null)
69                 ;
70         }
71         current = e;
72         return e;
73     }
74 
75     ...
76 }

複製程式碼

Hashtable的實現方式:先從“後向往前”的遍歷陣列;對陣列具體某一項對應的連結串列,則從表頭開始往後遍歷。

複製程式碼

 1 public Set<Map.Entry<K,V>> entrySet() {
 2     if (entrySet==null)
 3         entrySet = Collections.synchronizedSet(new EntrySet(), this);
 4     return entrySet;
 5 }
 6 
 7 private class EntrySet extends AbstractSet<Map.Entry<K,V>> {
 8     public Iterator<Map.Entry<K,V>> iterator() {
 9         return getIterator(ENTRIES);
10     }
11     ...
12 }
13 
14 private class Enumerator<T> implements Enumeration<T>, Iterator<T> {
15     // 指向Hashtable的table
16     Entry[] table = Hashtable.this.table;
17     // Hashtable的總的大小
18     int index = table.length;
19     Entry<K,V> entry = null;
20     Entry<K,V> lastReturned = null;
21     int type;
22 
23     // Enumerator是 “迭代器(Iterator)” 還是 “列舉類(Enumeration)”的標誌
24     // iterator為true,表示它是迭代器;否則,是列舉類。
25     boolean iterator;
26 
27     // 在將Enumerator當作迭代器使用時會用到,用來實現fail-fast機制。
28     protected int expectedModCount = modCount;
29 
30     Enumerator(int type, boolean iterator) {
31         this.type = type;
32         this.iterator = iterator;
33     }
34 
35     // 從遍歷table的陣列的末尾向前查詢,直到找到不為null的Entry。
36     public boolean hasMoreElements() {
37         Entry<K,V> e = entry;
38         int i = index;
39         Entry[] t = table;
40         /* Use locals for faster loop iteration */
41         while (e == null && i > 0) {
42             e = t[--i];
43         }
44         entry = e;
45         index = i;
46         return e != null;
47     }
48 
49     // 獲取下一個元素
50     // 注意:從hasMoreElements() 和nextElement() 可以看出“Hashtable的elements()遍歷方式”
51     // 首先,從後向前的遍歷table陣列。table陣列的每個節點都是一個單向連結串列(Entry)。
52     // 然後,依次向後遍歷單向連結串列Entry。
53     public T nextElement() {
54         Entry<K,V> et = entry;
55         int i = index;
56         Entry[] t = table;
57         /* Use locals for faster loop iteration */
58         while (et == null && i > 0) {
59             et = t[--i];
60         }
61         entry = et;
62         index = i;
63         if (et != null) {
64             Entry<K,V> e = lastReturned = entry;
65             entry = e.next;
66             return type == KEYS ? (T)e.key : (type == VALUES ? (T)e.value : (T)e);
67         }
68         throw new NoSuchElementException("Hashtable Enumerator");
69     }
70 
71     // 迭代器Iterator的判斷是否存在下一個元素
72     // 實際上,它是呼叫的hasMoreElements()
73     public boolean hasNext() {
74         return hasMoreElements();
75     }
76 
77     // 迭代器獲取下一個元素
78     // 實際上,它是呼叫的nextElement()
79     public T next() {
80         if (modCount != expectedModCount)
81             throw new ConcurrentModificationException();
82         return nextElement();
83     }
84 
85     ...
86 
87 }

複製程式碼

 

6 容量的初始值 和 增加方式都不一樣

HashMap預設的容量大小是16;增加容量時,每次將容量變為“原始容量x2”
Hashtable預設的容量大小是11;增加容量時,每次將容量變為“原始容量x2 + 1”。

HashMap預設的“載入因子”是0.75, 預設的容量大小是16。

 View Code

當HashMap的 “實際容量” >= “閾值”時,(閾值 = 總的容量 * 載入因子),就將HashMap的容量翻倍。

 View Code

Hashtable預設的“載入因子”是0.75, 預設的容量大小是11。 

 View Code

當Hashtable的 “實際容量” >= “閾值”時,(閾值 = 總的容量 x 載入因子),就將變為“原始容量x2 + 1”。

 View Code

 

7 新增key-value時的hash值演算法不同

HashMap新增元素時,是使用自定義的雜湊演算法。
Hashtable沒有自定義雜湊演算法,而直接採用的key的hashCode()。

HashMap新增元素時,是使用自定義的雜湊演算法。

 View Code 

Hashtable沒有自定義雜湊演算法,而直接採用的key的hashCode()。

 View Code

 

8 部分API不同

Hashtable支援contains(Object value)方法,而且重寫了toString()方法
而HashMap不支援contains(Object value)方法,沒有重寫toString()方法。


最後,再說說“HashMap和Hashtable”使用的情景。
其實,若瞭解它們之間的不同之處後,可以很容易的區分根據情況進行取捨。例如:(01) 若在單執行緒中,我們往往會選擇HashMap;而在多執行緒中,則會選擇Hashtable。(02),若不能插入null元素,則選擇Hashtable;否則,可以選擇HashMap。
但這個不是絕對的標準。例如,在多執行緒中,我們可以自己對HashMap進行同步,也可以選擇ConcurrentHashMap。當HashMap和Hashtable都不能滿足自己的需求時,還可以考慮新定義一個類,繼承或重新實現散列表;當然,一般情況下是不需要的了。

 

第3部分 HashMap和WeakHashMap異同

3.1 HashMap和WeakHashMap的相同點

1 它們都是散列表,儲存的是“鍵值對”對映。
2 它們都繼承於AbstractMap,並且實現Map基礎。
3 它們的建構函式都一樣。
   它們都包括4個建構函式,而且函式的引數都一樣。
4 預設的容量大小是16,預設的載入因子是0.75。
5 它們的“鍵”和“值”都允許為null。
6 它們都是“非同步的”。

 

3.2 HashMap和WeakHashMap的不同點

HashMap實現了Cloneable和Serializable介面,而WeakHashMap沒有。
   HashMap實現Cloneable,意味著它能通過clone()克隆自己。
   HashMap實現Serializable,意味著它支援序列化,能通過序列化去傳輸。

HashMap的“鍵”是“強引用(StrongReference)”,而WeakHashMap的鍵是“弱引用(WeakReference)”。
   WeakReference的“弱鍵”能實現WeakReference對“鍵值對”的動態回收。當“弱鍵”不再被使用到時,GC會回收它,WeakReference也會將“弱鍵”對應的鍵值對刪除。
   這個“弱鍵”實現的動態回收“鍵值對”的原理呢?其實,通過WeakReference(弱引用)和ReferenceQueue(引用佇列)實現的。 首先,我們需要了解WeakHashMap中:
    第一,“鍵”是WeakReference,即key是弱鍵。
    第二,ReferenceQueue是一個引用佇列,它是和WeakHashMap聯合使用的。當弱引用所引用的物件被垃圾回收,Java虛擬機器就會把這個弱引用加入到與之關聯的引用佇列中。 WeakHashMap中的ReferenceQueue是queue。
   第三,WeakHashMap是通過陣列實現的,我們假設這個陣列是table。
 

接下來,說說“動態回收”的步驟。

(01) 新建WeakHashMap,將“鍵值對”新增到WeakHashMap中。
        將“鍵值對”新增到WeakHashMap中時,新增的鍵都是弱鍵。
        實際上,WeakHashMap是通過陣列table儲存Entry(鍵值對);每一個Entry實際上是一個單向連結串列,即Entry是鍵值對連結串列。
(02) 當某“弱鍵”不再被其它物件引用,並被GC回收時。在GC回收該“弱鍵”時,這個“弱鍵”也同時會被新增到queue佇列中。
        例如,當我們在將“弱鍵”key新增到WeakHashMap之後;後來將key設為null。這時,便沒有外部外部物件再引用該了key。
        接著,當Java虛擬機器的GC回收記憶體時,會回收key的相關記憶體;同時,將key新增到queue佇列中。
(03) 當下一次我們需要操作WeakHashMap時,會先同步table和queue。table中儲存了全部的鍵值對,而queue中儲存被GC回收的“弱鍵”;同步它們,就是刪除table中被GC回收的“弱鍵”對應的鍵值對。
        例如,當我們“讀取WeakHashMap中的元素或獲取WeakReference的大小時”,它會先同步table和queue,目的是“刪除table中被GC回收的‘弱鍵’對應的鍵值對”。刪除的方法就是逐個比較“table中元素的‘鍵’和queue中的‘鍵’”,若它們相當,則刪除“table中的該鍵值對”。

 

3.3 HashMap和WeakHashMap的比較測試程式

 View Code

執行結果:

複製程式碼

 -- HashMap --
map entry : H2 - h2
  map size:1

 -- WeakHashMap --
map entry : W2 - w2
  map size:1

 -- Self-def HashMap --
map entry : [email protected] - s4
  map size:1

 -- Self-def WeakHashMap --
GC Self: id=10 [email protected])
map entry : [email protected] - s2
  map size:1

複製程式碼

 


更多內容

01. Java 集合系列10之 HashMap詳細介紹(原始碼解析)和使用示例

02. Java 集合系列11之 Hashtable詳細介紹(原始碼解析)和使用示例

03. Java 集合系列12之 TreeMap詳細介紹(原始碼解析)和使用示例

04. Java 集合系列13之 WeakHashMap詳細介紹(原始碼解析)和使用示例

05. Java 集合系列14之 Map總結(HashMap, Hashtable, TreeMap, WeakHashMap等使用場景)

轉載請註明出處:http://www.cnblogs.com/skywang12345/admin/EditPosts.aspx?postid=3311126