Java集合(8):Hashtable

一.Hashtable介紹

  和HashMap同樣,Hashtable 也是一個散列表,它存儲的內容是鍵值對(key-value)映射,它在很大程度上和HashMap的實現差很少。html

  Hashtable 的函數都是同步的,這意味着它是線程安全的。它的key、value都不能夠爲null。此外,Hashtable中的映射不是有序的。java

1.Hashtable的繼承關係

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

2.Hashtable的類圖關係

  HashTable繼承Dictionary類,實現Map接口。其中Dictionary類是任何可將鍵映射到相應值的類的抽象父類。每一個鍵和每一個值都是一個對象。在任何一個 Dictionary 對象中,每一個鍵至多與一個值相關聯。Map是」key-value鍵值對」接口。數組

二.Hashtable源碼解析

1.Hashtable的私有屬性

1 private transient Entry<?,?>[] table;//table是一個Entry[]數組類型,而Entry實際上就是一個單向鏈表。
2 private transient int count;//count是Hashtable的大小,它是Hashtable保存的鍵值對的數量。 
3 private int threshold;//threshold是Hashtable的閾值,用於判斷是否須要調整Hashtable的容量。threshold的值="容量*加載因子"。
4 private float loadFactor;//loadFactor就是加載因子。 
5 private transient int modCount = 0;//modCount是用來實現fail-fast機制的

  所謂快速失敗就是在併發集合中,其進行迭代操做時,如有其餘線程對其進行結構性的修改,這時迭代器會立馬感知到,而且當即拋出ConcurrentModificationException異常,而不是等到迭代完成以後才告訴你(你已經出錯了)。安全

2.Hashtable的構造方法

 1 // 默認構造函數。
 2 public Hashtable() {
 3     this(11, 0.75f);
 4 }
 5 
 6 // 指定「容量大小」的構造函數
 7 public Hashtable(int initialCapacity) {
 8     this(initialCapacity, 0.75f);
 9 }
10 
11 // 指定「容量大小」和「加載因子」的構造函數
12 public Hashtable(int initialCapacity, float loadFactor) {
13     if (initialCapacity < 0)//驗證初始容量
14         throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
15     if (loadFactor <= 0 || Float.isNaN(loadFactor))//驗證加載因子
16         throw new IllegalArgumentException("Illegal Load: "+loadFactor);
17     if (initialCapacity==0)
18         initialCapacity = 1;
19     this.loadFactor = loadFactor;
20     table = new Entry[initialCapacity];//初始化table,得到大小爲initialCapacity的table數組
21     threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);//計算閥值
22     initHashSeedAsNeeded(initialCapacity);//初始化HashSeed值
23 }
24 
25 // 包含「子Map」的構造函數
26 public Hashtable(Map<? extends K, ? extends V> t) {
27     this(Math.max(2*t.size(), 11), 0.75f);//設置table容器大小,其值==t.size * 2 + 1
28     putAll(t);
29 }

3.存儲數據put

將指定 key 映射到此哈希表中的指定 value。注意這裏鍵key和值value都不可爲空數據結構

 1 public synchronized V put(K key, V value) {
 2     if (value == null) {// 確保value不爲null
 3         throw new NullPointerException();
 4     }
 5 
 6     /*
 7      * 確保key在table[]是不重複的
 8      * 處理過程:
 9      * 一、計算key的hash值,確認在table[]中的索引位置
10      * 二、迭代index索引位置,若是該位置處的鏈表中存在一個同樣的key,則替換其value,返回舊值
11      */
12     Entry tab[] = table;
13     int hash = hash(key);    //計算key的hash值
14     int index = (hash & 0x7FFFFFFF) % tab.length;     //確認該key的索引位置
15     for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) { //迭代,尋找該key,替換
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     modCount++;
24     if (count >= threshold) {  //若是容器中的元素數量已經達到閥值,則進行擴容操做
25         rehash();
26         tab = table;
27         hash = hash(key);
28         index = (hash & 0x7FFFFFFF) % tab.length;
29     }
30 
31     Entry<K,V> e = tab[index];// 在索引位置處插入一個新的節點
32     tab[index] = new Entry<>(hash, key, value, e);
33     count++;//容器中元素+1
34     return null;
35 }

  put方法的整個處理流程是:計算key的hash值,根據hash值得到key在table數組中的索引位置,而後迭代該key處的Entry鏈表(咱們暫且理解爲鏈表),若該鏈表中存在一個這個的key對象,那麼就直接替換其value值便可,不然在將改key-value節點插入該index索引位置處。多線程

過程演示以下:併發

  首先咱們假設一個容量爲5的table,存在以下的鍵值對:框架

  而後咱們插入一個數:put(16,22),key=16在table的索引位置爲1,同時在1索引位置有兩個數,程序對該「鏈表」進行迭代,發現存在一個key=16,這時要作的工做就是用newValue=22替換oldValue16,並將oldValue=16返回。函數

  在put(33,33),key=33所在的索引位置爲3,而且在該鏈表中也沒有存在某個key=33的節點,因此就將該節點插入該鏈表的第一個位置。性能

  擴容操做:在put方法中,若是須要向table[]中添加Entry元素,會首先進行容量校驗,若是容量已經達到了閥值,HashTable就會進行rehash()擴容處理

 1 protected void rehash() {
 2     int oldCapacity = table.length;
 3     Entry<K,V>[] oldMap = table;
 4 
 5     int newCapacity = (oldCapacity << 1) + 1;//新容量=舊容量 * 2 + 1
 6     if (newCapacity - MAX_ARRAY_SIZE > 0) {
 7         if (oldCapacity == MAX_ARRAY_SIZE)
 8             return;
 9         newCapacity = MAX_ARRAY_SIZE;
10     }
11     Entry<K,V>[] newMap = new Entry[];//新建一個size = newCapacity 的HashTable
12     modCount++;
13     threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);//從新計算閥值
14     boolean rehash = initHashSeedAsNeeded(newCapacity);//從新計算hashSeed
15     table = newMap;
16     for (int i = oldCapacity ; i-- > 0 ;) {//將原來的元素拷貝到新的HashTable中
17         for (Entry<K,V> old = oldMap[i] ; old != null ; ) {
18             Entry<K,V> e = old;
19             old = old.next;
20             if (rehash) {
21                 e.hash = hash(e.key);
22             }
23             int index = (e.hash & 0x7FFFFFFF) % newCapacity;
24             e.next = newMap[index];
25             newMap[index] = e;
26         }
27     }
28 }

  經過上面rehash代碼咱們能夠看到容量擴大兩倍+1,同時須要將原來HashTable中的元素一一複製到新的HashTable中,這個過程是比較消耗時間的,同時還須要從新計算hashSeed的,畢竟容量已經變了。

  關於閥值:好比初始值十一、加載因子默認0.75,那麼這個時候閥值threshold=8,當容器中的元素達到8時,HashTable進行一次擴容操做,容量 = 8 * 2 + 1 =17,而閥值threshold=17*0.75 = 13,當容器元素再一次達到閥值時,HashTable還會進行擴容操做,一次類推。

4.數據讀取get()

  相對於put方法,get方法就會比較簡單,處理過程就是計算key的hash值,判斷在table數組中的索引位置,而後迭代鏈表,匹配直到找到相對應key的value,若沒有找到返回null。

 1 public synchronized V get(Object key) {
 2     Entry tab[] = table;
 3     int hash = hash(key);
 4     int index = (hash & 0x7FFFFFFF) % tab.length;
 5     for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
 6         if ((e.hash == hash) && e.key.equals(key)) {
 7             return e.value;
 8         }
 9     }
10     return null;
11 }

5.其餘方法

三.Hashtable的遍歷

1.遍歷Hashtable的鍵值對(效率較高)

第一步:根據entrySet()獲取Hashtable的「鍵值對」的Set集合。
第二步:經過Iterator迭代器遍歷「第一步」獲得的集合。

 1 // 假設table是Hashtable對象
 2 // table中的key是String類型,value是Integer類型
 3 Integer integ = null;
 4 Iterator iter = table.entrySet().iterator();
 5 while(iter.hasNext()) {
 6     Map.Entry entry = (Map.Entry)iter.next();
 7     // 獲取key
 8     key = (String)entry.getKey();
 9         // 獲取value
10     integ = (Integer)entry.getValue();
11 }

2.經過Iterator遍歷Hashtable的鍵(效率較低)

第一步:根據keySet()獲取Hashtable的「鍵」的Set集合。
第二步:經過Iterator迭代器遍歷「第一步」獲得的集合。

 1 // 假設table是Hashtable對象
 2 // table中的key是String類型,value是Integer類型
 3 String key = null;
 4 Integer integ = null;
 5 Iterator iter = table.keySet().iterator();
 6 while (iter.hasNext()) {
 7         // 獲取key
 8     key = (String)iter.next();
 9         // 根據key,獲取value
10     integ = (Integer)table.get(key);
11 }

3.經過Iterator遍歷Hashtable的值

第一步:根據value()獲取Hashtable的「值」的集合。
第二步:經過Iterator迭代器遍歷「第一步」獲得的集合。

1 // 假設table是Hashtable對象
2 // table中的key是String類型,value是Integer類型
3 Integer value = null;
4 Collection c = table.values();
5 Iterator iter= c.iterator();
6 while (iter.hasNext()) {
7     value = (Integer)iter.next();
8 }

4.經過Enumeration遍歷Hashtable的鍵(效率較高)

第一步:根據keys()獲取Hashtable的集合。
第二步:經過Enumeration遍歷「第一步」獲得的集合。

Enumeration enu = table.keys();
while(enu.hasMoreElements()) {
    System.out.println(enu.nextElement());
}  

5.經過Enumeration遍歷Hashtable的值(效率較高)

第一步:根據elements()獲取Hashtable的集合。
第二步:經過Enumeration遍歷「第一步」獲得的集合。

Enumeration enu = table.elements();
while(enu.hasMoreElements()) {
    System.out.println(enu.nextElement());
}

四.HashTable和HashMap的比較

  HashTable的應用很是普遍,HashMap是新框架中用來代替HashTable的類,也就是說建議使用HashMap

下面着重比較一下兩者的區別:

1.繼承不一樣

Hashtable是基於陳舊的Dictionary類的,HashMap是java 1.2引進的Map接口的一個實現。

2.同步

Hashtable 中的方法是同步的,保證了Hashtable中的對象是線程安全的。

HashMap中的方法在缺省狀況下是非同步的,HashMap中的對象並非線程安全的。在多線程併發的環境下,能夠直接使用Hashtable,可是要使用HashMap的話就要本身增長同步處理了。

3.效率

單線程中, HashMap的效率大於Hashtable。由於同步的要求會影響執行的效率,因此若是你不須要線程安全的集合,HashMap是Hashtable的輕量級實現,這樣能夠避免因爲同步帶來的沒必要要的性能開銷,從而提升效率。

4.null值

Hashtable中,key和value都不容許出現null值,不然出現NullPointerException。

在HashMap中,null能夠做爲鍵,這樣的鍵只有一個;能夠有一個或多個鍵所對應的值爲null。當get()方法返回null值時,便可以表示 HashMap中沒有該鍵,也能夠表示該鍵所對應的值爲null。所以,在HashMap中不能由get()方法來判斷HashMap中是否存在某個鍵,而應該用containsKey()方法來判斷。

5.遍歷方式

Hashtable、HashMap都使用了 Iterator。而因爲歷史緣由,Hashtable還使用了Enumeration的方式。

6.容量

Hashtable和HashMap它們兩個內部實現方式的數組的初始大小和擴容的方式。

HashTable中hash數組默認大小是11,增長的方式是 old*2+1。

HashMap中hash數組的默認大小是16,並且必定是2的指數。 

小結:

  不管何時有多個線程訪問相同實例的可能時,就應該使用Hashtable,反之使用HashMap。非線程安全的數據結構能帶來更好的性能。

  若是在未來有一種可能—你須要按順序得到鍵值對的方案時,HashMap是一個很好的選擇,由於有HashMap的一個子類 LinkedHashMap。因此若是你想可預測的按順序迭代(默認按插入的順序),你能夠很方便用LinkedHashMap替換HashMap。反觀要是使用的Hashtable就沒那麼簡單了。同時若是有多個線程訪問HashMap,Collections.synchronizedMap()能夠代替,總的來講HashMap更靈活

 

參考:http://cmsblogs.com/?p=618

http://blog.csdn.net/zheng0518/article/details/42199477

http://www.cnblogs.com/devinzhang/archive/2012/01/13/2321481.html

相關文章
相關標籤/搜索