HashTable和HashMap的區別詳解

 

1、HashMap簡介

      HashMap是基於哈希表實現的,每個元素是一個key-value對,其內部經過單鏈表解決衝突問題,容量不足(超過了閥值)時,一樣會自動增加。html

      HashMap是非線程安全的,只是用於單線程環境下,多線程環境下能夠採用concurrent併發包下的concurrentHashMap。java

      HashMap 實現了Serializable接口,所以它支持序列化,實現了Cloneable接口,能被克隆。數組

      HashMap存數據的過程是:安全

      HashMap內部維護了一個存儲數據的Entry數組,HashMap採用鏈表解決衝突,每個Entry本質上是一個單向鏈表。當準備添加一個key-value對時,首先經過hash(key)方法計算hash值,而後經過indexFor(hash,length)求該key-value對的存儲位置,計算方法是先用hash&0x7FFFFFFF後,再對length取模,這就保證每個key-value對都能存入HashMap中,當計算出的位置相同時,因爲存入位置是一個鏈表,則把這個key-value對插入鏈表頭。多線程

      HashMap中key和value都容許爲null。key爲null的鍵值對永遠都放在以table[0]爲頭結點的鏈表中。併發

      瞭解了數據的存儲,那麼數據的讀取也就很容易就明白了。app

      HashMap的存儲結構,以下圖所示:函數

 

      圖中,紫色部分即表明哈希表,也稱爲哈希數組,數組的每一個元素都是一個單鏈表的頭節點,鏈表是用來解決衝突的,若是不一樣的key映射到了數組的同一位置處,就將其放入單鏈表中。oop

      HashMap內存儲數據的Entry數組默認是16,若是沒有對Entry擴容機制的話,當存儲的數據一多,Entry內部的鏈表會很長,這就失去了HashMap的存儲意義了。因此HasnMap內部有本身的擴容機制。HashMap內部有:post

      變量size,它記錄HashMap的底層數組中已用槽的數量;

      變量threshold,它是HashMap的閾值,用於判斷是否須要調整HashMap的容量(threshold = 容量*加載因子)    

      變量DEFAULT_LOAD_FACTOR = 0.75f,默認加載因子爲0.75

      HashMap擴容的條件是:當size大於threshold時,對HashMap進行擴容  

      擴容是是新建了一個HashMap的底層數組,然後調用transfer方法,將就HashMap的所有元素添加到新的HashMap中(要從新計算元素在新的數組中的索引位置)。 很明顯,擴容是一個至關耗時的操做,由於它須要從新計算這些元素在新的數組中的位置並進行復制處理。所以,咱們在用HashMap的時,最好能提早預估下HashMap中元素的個數,這樣有助於提升HashMap的性能。

      HashMap共有四個構造方法。構造方法中提到了兩個很重要的參數:初始容量和加載因子。這兩個參數是影響HashMap性能的重要參數,其中容量表示哈希表中槽的數量(即哈希數組的長度),初始容量是建立哈希表時的容量(從構造函數中能夠看出,若是不指明,則默認爲16),加載因子是哈希表在其容量自動增長以前能夠達到多滿的一種尺度,當哈希表中的條目數超出了加載因子與當前容量的乘積時,則要對該哈希表進行 resize 操做(即擴容)。

      下面說下加載因子,若是加載因子越大,對空間的利用更充分,可是查找效率會下降(鏈表長度會愈來愈長);若是加載因子過小,那麼表中的數據將過於稀疏(不少空間還沒用,就開始擴容了),對空間形成嚴重浪費。若是咱們在構造方法中不指定,則系統默認加載因子爲0.75,這是一個比較理想的值,通常狀況下咱們是無需修改的。

       另外,不管咱們指定的容量爲多少,構造方法都會將實際容量設爲不小於指定容量的2的次方的一個數,且最大值不能超過2的30次方

      對HashMap想進一步深刻了解的朋友推薦看一下HashMap源碼剖析:http://blog.csdn.net/ns_code/article/details/36034955

 

2、Hashtable簡介

 

      Hashtable一樣是基於哈希表實現的,一樣每一個元素是一個key-value對,其內部也是經過單鏈表解決衝突問題,容量不足(超過了閥值)時,一樣會自動增加。

      Hashtable也是JDK1.0引入的類,是線程安全的,能用於多線程環境中。

      Hashtable一樣實現了Serializable接口,它支持序列化,實現了Cloneable接口,能被克隆。

      Hashtable和HashMap比較類似,感興趣的朋友能夠看「Hashtable源碼剖析」這篇博客:http://blog.csdn.net/ns_code/article/details/36191279

下面主要介紹一下HashTable和HashMap區別

 

3、HashTable和HashMap區別

      一、繼承的父類不一樣

 

      Hashtable繼承自Dictionary類,而HashMap繼承自AbstractMap類。但兩者都實現了Map接口。

      二、線程安全性不一樣

      javadoc中關於hashmap的一段描述以下:此實現不是同步的。若是多個線程同時訪問一個哈希映射,而其中至少一個線程從結構上修改了該映射,則它必須保持外部同步。

 

      Hashtable 中的方法是Synchronize的,而HashMap中的方法在缺省狀況下是非Synchronize的。在多線程併發的環境下,能夠直接使用Hashtable,不須要本身爲它的方法實現同步,但使用HashMap時就必需要本身增長同步處理。(結構上的修改是指添加或刪除一個或多個映射關係的任何操做;僅改變與實例已經包含的鍵關聯的值不是結構上的修改。)這通常經過對天然封裝該映射的對象進行同步操做來完成。若是不存在這樣的對象,則應該使用 Collections.synchronizedMap 方法來「包裝」該映射。最好在建立時完成這一操做,以防止對映射進行意外的非同步訪問,以下所示:

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

      Hashtable 線程安全很好理解,由於它每一個方法中都加入了Synchronize。這裏咱們分析一下HashMap爲何是線程不安全的:

 

      HashMap底層是一個Entry數組,當發生hash衝突的時候,hashmap是採用鏈表的方式來解決的,在對應的數組位置存放鏈表的頭結點。對鏈表而言,新加入的節點會從頭結點加入。

咱們來分析一下多線程訪問:

 

      (1)在hashmap作put操做的時候會調用下面方法:

 

[java]  view plain  copy
 
  1. // 新增Entry。將「key-value」插入指定位置,bucketIndex是位置索引。      
  2.     void addEntry(int hash, K key, V value, int bucketIndex) {      
  3.         // 保存「bucketIndex」位置的值到「e」中      
  4.         Entry<K,V> e = table[bucketIndex];      
  5.         // 設置「bucketIndex」位置的元素爲「新Entry」,      
  6.         // 設置「e」爲「新Entry的下一個節點」      
  7.         table[bucketIndex] = new Entry<K,V>(hash, key, value, e);      
  8.         // 若HashMap的實際大小 不小於 「閾值」,則調整HashMap的大小      
  9.         if (size++ >= threshold)      
  10.             resize(2 * table.length);      
  11.     }  

 

      在hashmap作put操做的時候會調用到以上的方法。如今假如A線程和B線程同時對同一個數組位置調用addEntry,兩個線程會同時獲得如今的頭結點,而後A寫入新的頭結點以後,B也寫入新的頭結點,那B的寫入操做就會覆蓋A的寫入操做形成A的寫入操做丟失

(      2)刪除鍵值對的代碼

 

[java]  view plain  copy
 
  1. <span style="font-size: 18px;">      </span>// 刪除「鍵爲key」的元素      
  2.     final Entry<K,V> removeEntryForKey(Object key) {      
  3.         // 獲取哈希值。若key爲null,則哈希值爲0;不然調用hash()進行計算      
  4.         int hash = (key == null) ? 0 : hash(key.hashCode());      
  5.         int i = indexFor(hash, table.length);      
  6.         Entry<K,V> prev = table[i];      
  7.         Entry<K,V> e = prev;      
  8.      
  9.         // 刪除鏈表中「鍵爲key」的元素      
  10.         // 本質是「刪除單向鏈表中的節點」      
  11.         while (e != null) {      
  12.             Entry<K,V> next = e.next;      
  13.             Object k;      
  14.             if (e.hash == hash &&      
  15.                 ((k = e.key) == key || (key != null && key.equals(k)))) {      
  16.                 modCount++;      
  17.                 size--;      
  18.                 if (prev == e)      
  19.                     table[i] = next;      
  20.                 else     
  21.                     prev.next = next;      
  22.                 e.recordRemoval(this);      
  23.                 return e;      
  24.             }      
  25.             prev = e;      
  26.             e = next;      
  27.         }      
  28.      
  29.         return e;      
  30.     }  

 

      當多個線程同時操做同一個數組位置的時候,也都會先取得如今狀態下該位置存儲的頭結點,而後各自去進行計算操做,以後再把結果寫會到該數組位置去,其實寫回的時候可能其餘的線程已經就把這個位置給修改過了,就會覆蓋其餘線程的修改

      (3)addEntry中當加入新的鍵值對後鍵值對總數量超過門限值的時候會調用一個resize操做,代碼以下:

[java]  view plain  copy
 
  1. // 從新調整HashMap的大小,newCapacity是調整後的容量      
  2.     void resize(int newCapacity) {      
  3.         Entry[] oldTable = table;      
  4.         int oldCapacity = oldTable.length;     
  5.         //若是就容量已經達到了最大值,則不能再擴容,直接返回    
  6.         if (oldCapacity == MAXIMUM_CAPACITY) {      
  7.             threshold = Integer.MAX_VALUE;      
  8.             return;      
  9.         }      
  10.      
  11.         // 新建一個HashMap,將「舊HashMap」的所有元素添加到「新HashMap」中,      
  12.         // 而後,將「新HashMap」賦值給「舊HashMap」。      
  13.         Entry[] newTable = new Entry[newCapacity];      
  14.         transfer(newTable);      
  15.         table = newTable;      
  16.         threshold = (int)(newCapacity * loadFactor);      
  17.     }  

      這個操做會新生成一個新的容量的數組,而後對原數組的全部鍵值對從新進行計算和寫入新的數組,以後指向新生成的數組。

 

      當多個線程同時檢測到總數量超過門限值的時候就會同時調用resize操做,各自生成新的數組並rehash後賦給該map底層的數組table,結果最終只有最後一個線程生成的新數組被賦給table變量,其餘線程的均會丟失。並且當某些線程已經完成賦值而其餘線程剛開始的時候,就會用已經被賦值的table做爲原始數組,這樣也會有問題。

      三、是否提供contains方法

 

      HashMap把Hashtable的contains方法去掉了,改爲containsValue和containsKey,由於contains方法容易讓人引發誤解。

      Hashtable則保留了contains,containsValue和containsKey三個方法,其中contains和containsValue功能相同。

咱們看一下Hashtable的ContainsKey方法和ContainsValue的源碼:

[java]  view plain  copy
 
  1. public boolean containsValue(Object value) {      
  2.      return contains(value);      
  3.  }  

[java]  view plain  copy
 
  1. // 判斷Hashtable是否包含「值(value)」      
  2.  public synchronized boolean contains(Object value) {      
  3.      //注意,Hashtable中的value不能是null,      
  4.      // 如果null的話,拋出異常!      
  5.      if (value == null) {      
  6.          throw new NullPointerException();      
  7.      }      
  8.     
  9.      // 從後向前遍歷table數組中的元素(Entry)      
  10.      // 對於每一個Entry(單向鏈表),逐個遍歷,判斷節點的值是否等於value      
  11.      Entry tab[] = table;      
  12.      for (int i = tab.length ; i-- > 0 ;) {      
  13.          for (Entry<K,V> e = tab[i] ; e != null ; e = e.next) {      
  14.              if (e.value.equals(value)) {      
  15.                  return true;      
  16.              }      
  17.          }      
  18.      }      
  19.      return false;      
  20.  }  

 

[java]  view plain  copy
 
  1. // 判斷Hashtable是否包含key      
  2.  public synchronized boolean containsKey(Object key) {      
  3.      Entry tab[] = table;      
  4. /計算hash值,直接用key的hashCode代替    
  5.      int hash = key.hashCode();        
  6.      // 計算在數組中的索引值     
  7.      int index = (hash & 0x7FFFFFFF) % tab.length;      
  8.      // 找到「key對應的Entry(鏈表)」,而後在鏈表中找出「哈希值」和「鍵值」與key都相等的元素      
  9.      for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {      
  10.          if ((e.hash == hash) && e.key.equals(key)) {      
  11.              return true;      
  12.          }      
  13.      }      
  14.      return false;      
  15.  }  

      下面咱們看一下HashMap的ContainsKey方法和ContainsValue的源碼:

[java]  view plain  copy
 
  1. // HashMap是否包含key      
  2.     public boolean containsKey(Object key) {      
  3.         return getEntry(key) != null;      
  4.     }  

[java]  view plain  copy
 
  1. // 返回「鍵爲key」的鍵值對      
  2.     final Entry<K,V> getEntry(Object key) {      
  3.         // 獲取哈希值      
  4.         // HashMap將「key爲null」的元素存儲在table[0]位置,「key不爲null」的則調用hash()計算哈希值      
  5.         int hash = (key == null) ? 0 : hash(key.hashCode());      
  6.         // 在「該hash值對應的鏈表」上查找「鍵值等於key」的元素      
  7.         for (Entry<K,V> e = table[indexFor(hash, table.length)];      
  8.              e != null;      
  9.              e = e.next) {      
  10.             Object k;      
  11.             if (e.hash == hash &&      
  12.                 ((k = e.key) == key || (key != null && key.equals(k))))      
  13.                 return e;      
  14.         }      
  15.         return null;      
  16.     }  

[java]  view plain  copy
 
  1. // 是否包含「值爲value」的元素      
  2.     public boolean containsValue(Object value) {      
  3.     // 若「value爲null」,則調用containsNullValue()查找      
  4.     if (value == null)      
  5.             return containsNullValue();      
  6.      
  7.     // 若「value不爲null」,則查找HashMap中是否有值爲value的節點。      
  8.     Entry[] tab = table;      
  9.         for (int i = 0; i < tab.length ; i++)      
  10.             for (Entry e = tab[i] ; e != null ; e = e.next)      
  11.                 if (value.equals(e.value))      
  12.                     return true;      
  13.     return false;      
  14.     }  

經過上面源碼的比較,咱們能夠獲得第四個不一樣的地方

 

      四、key和value是否容許null值

 

      其中key和value都是對象,而且不能包含重複key,但能夠包含重複的value。

      經過上面的ContainsKey方法和ContainsValue的源碼咱們能夠很明顯的看出:

      Hashtable中,key和value都不容許出現null值。可是若是在Hashtable中有相似put(null,null)的操做,編譯一樣能夠經過,由於key和value都是Object類型,但運行時會拋出NullPointerException異常,這是JDK的規範規定的。
HashMap中,null能夠做爲鍵,這樣的鍵只有一個;能夠有一個或多個鍵所對應的值爲null。當get()方法返回null值時,多是 HashMap中沒有該鍵,也可能使該鍵所對應的值爲null。所以,在HashMap中不能由get()方法來判斷HashMap中是否存在某個鍵, 而應該用containsKey()方法來判斷。

      五、兩個遍歷方式的內部實現上不一樣

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

      六、hash值不一樣

      哈希值的使用不一樣,HashTable直接使用對象的hashCode。而HashMap從新計算hash值。

      hashCode是jdk根據對象的地址或者字符串或者數字算出來的int類型的數值。

      Hashtable計算hash值,直接用key的hashCode(),而HashMap從新計算了key的hash值,Hashtable在求hash值對應的位置索引時,用取模運算,而HashMap在求位置索引時,則用與運算,且這裏通常先用hash&0x7FFFFFFF後,再對length取模,&0x7FFFFFFF的目的是爲了將負的hash值轉化爲正值,由於hash值有可能爲負數,而&0x7FFFFFFF後,只有符號外改變,然後面的位都不變。

      七、內部實現使用的數組初始化和擴容方式不一樣

      HashTable在不指定容量的狀況下的默認容量爲11,而HashMap爲16,Hashtable不要求底層數組的容量必定要爲2的整數次冪,而HashMap則要求必定爲2的整數次冪。
      Hashtable擴容時,將容量變爲原來的2倍加1,而HashMap擴容時,將容量變爲原來的2倍。

      Hashtable和HashMap它們兩個內部實現方式的數組的初始大小和擴容的方式。HashTable中hash數組默認大小是11,增長的方式是 old*2+1。

相關文章
相關標籤/搜索