Java HashMap HashTable HashSet區別

HashMap、HashSet、HashTable之間的區別是Java程序員的一個常見面試題目,在此僅以此博客記錄,並深入源代碼進行分析:

在分析之前,先將其區別列於下面

1:HashSet底層採用的是HashMap進行實現的,但是沒有key-value,只有HashMap的key set的視圖,HashSet不容許重複的對象

2:Hashtable是基於Dictionary類的,而HashMap是基於Map接口的一個實現

3:Hashtable裏默認的方法是同步的,而HashMap則是非同步的,因此Hashtable是多線程安全的

4:HashMap可以將空值作爲一個表的條目的key或者value,HashMap中由於鍵不能重複,因此只有一條記錄的Key可以是空值,而value可以有多個爲空,但HashTable不允許null值(鍵與值均不行)

5:內存初始大小不同,HashTable初始大小是11,而HashMap初始大小是16

6:內存擴容時採取的方式也不同,Hashtable採用的是2*old+1,而HashMap是2*old。

7:哈希值的計算方法不同,Hashtable直接使用的是對象的hashCode,而HashMap則是在對象的hashCode的基礎上還進行了一些變化

源代碼分析:

對於區別1,看下面的源碼

[java] view plaincopy
  1. //HashSet類的部份源代碼  
  2. public class HashSet<E>  
  3.     extends AbstractSet<E>  
  4.     implements Set<E>, Cloneable, java.io.Serializable  
  5. {   //用於類的序列化,可以不用管它  
  6.     static final long serialVersionUID = -5024744406713321676L;  
  7.     //從這裏可以看出HashSet類裏面真的是採用HashMap來實現的  
  8.     private transient HashMap<E,Object> map;  
  9.   
  10.     // Dummy value to associate with an Object in the backing Map  
  11.     //這裏是生成一個對象,生成這個對象的作用是將每一個鍵的值均關聯於此對象,以滿足HashMap的鍵值對  
  12.     private static final Object PRESENT = new Object();  
  13.   
  14.     /** 
  15.      * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has 
  16.      * default initial capacity (16) and load factor (0.75). 
  17.      */  
  18.     //這裏是一個構造函數,開構生成一個HashMap對象,用來存放數據  
  19.     public HashSet() {  
  20.     map = new HashMap<E,Object>();  
  21.     }  
從上面的代碼中得出的結論是HashSet的確是採用HashMap來實現的,而且每一個鍵都關鍵同一個Object類的對象,因此鍵所關聯的值沒有意義,真正有意義的是鍵。而HashMap裏的鍵是不允許重複的,因此1也就很容易明白了。

對於區別2,繼續看源代碼如下

[java] view plaincopy
  1. //從這裏可以看得出Hashtable是繼承於Dictionary,實現了Map接口  
  2. public class Hashtable<K,V>  
  3.     extends Dictionary<K,V>  
  4.     implements Map<K,V>, Cloneable, java.io.Serializable {  
[java] view plaincopy
  1. //這裏可以看出的是HashMap是繼承於AbstractMap類,實現了Map接口  
  2. //因此與Hashtable繼承的父類不同  
  3. public class HashMap<K,V>  
  4.     extends AbstractMap<K,V>  
  5.     implements Map<K,V>, Cloneable, Serializable  
區別3,找一個具有針對性的方法看看,這個方法就是put

[java] view plaincopy
  1. //Hashtable裏的向集體增加鍵值對的方法,從這裏可以明顯看到的是  
  2. //採用了synchronized關鍵字,這個關鍵字的作用就是用於線程的同步操作  
  3. //因此下面這個方法對於多線程來說是安全的,但這會影響效率     
  4. public synchronized V put(K key, V value) {  
  5.     // Make sure the value is not null  
  6.     //如果值爲空的,則會拋出異常  
  7.     if (value == null) {  
  8.         throw new NullPointerException();  
  9.     }  
  10.   
  11.     // Makes sure the key is not already in the hashtable.  
  12.     Entry tab[] = table;  
  13.     //獲得鍵值的hashCode,從這裏也可以看得出key!=null,否則的話會拋出異常的呦  
  14.     int hash = key.hashCode();  
  15.     //獲取鍵據所在的哈希表的位置  
  16.     int index = (hash & 0x7FFFFFFF) % tab.length;  
  17.     //從下面這個循環中可以看出的是,內部實現採用了鏈表,即桶狀結構  
  18.     for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {  
  19.         //如果向Hashtable中增加同一個元素時,則會重新更新元素的值   
  20.         if ((e.hash == hash) && e.key.equals(key)) {  
  21.                 V old = e.value;  
  22.                 e.value = value;  
  23.                 return old;  
  24.         }  
  25.     }  
  26.     //後面的暫時不用管它,大概的意思就是內存的個數少於某個閥值時,進行重新分配內存  
  27.     modCount++;  
  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.     }  
[java] view plaincopy
  1. //HashMap中的實現則相對來說要簡單的很多了,如下代碼  
  2. //這裏的代碼中沒有synchronize關鍵字,即可以看出,這個關鍵函數不是線程安全的  
  3.     public V put(K key, V value) {  
  4.     //對於鍵是空時,將向Map中放值一個null-value構成的鍵值對  
  5.     //對值卻沒有進行判空處理,意味着可以有多個具有鍵,鍵所對應的值卻爲空的元素。  
  6.         if (key == null)  
  7.             return putForNullKey(value);  
  8.     //算出鍵所在的哈希表的位置  
  9.         int hash = hash(key.hashCode());  
  10.         int i = indexFor(hash, table.length);  
  11.     //同樣從這裏可以看得出來的是採用的是鏈表結構,採用的是桶狀  
  12.         for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
  13.             Object k;  
  14.             //對於向集體中增加具有相同鍵的情況時,這裏可以看出,並不增加進去,而是進行更新操作  
  15.             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
  16.                 V oldValue = e.value;  
  17.                 e.value = value;  
  18.                 e.recordAccess(this);  
  19.                 return oldValue;  
  20.             }  
  21.         }  
  22.         //開始增加元素  
  23.         modCount++;  
  24.         addEntry(hash, key, value, i);  
  25.         return null;  
  26.     }  
區別4在上面的代碼中,已經分析了,可以再細看一下

區別5內存初化大小不同,看看兩者的源代碼:

[java] view plaincopy
  1.  public Hashtable() {  
  2.    //從這裏可以看出,默認的初始化大小11,這裏的11並不是11個字節,而是11個Entry,這個Entry是  
  3.    //實現鏈表的關鍵結構  
  4.    //這裏的0.75代表的是裝載因子  
  5. this(110.75f);  
  6.  }  
[java] view plaincopy
  1. //這裏均是一些定義  
  2.  public HashMap() {  
  3.  //這個默認的裝載因子也是0.75  
  4.      this.loadFactor = DEFAULT_LOAD_FACTOR;  
  5.  //默認的痤爲0.75*16  
  6.      threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);  
  7.  //這裏開始是默認的初始化大小,這裏大小是16  
  8.      table = new Entry[DEFAULT_INITIAL_CAPACITY];  
  9.      init();  
  10.  }  
從上面的代碼中,可以看出的是兩者的默認大小是不同的,一個是11,一個是16

區別6內存的擴容方式,看一看源代碼也是很清楚的,其實區別是不大的,看到網上一哥們寫的,說兩者有區別,其實真正深入源碼,區別真不大,一個是2*oldCapacity+1, 一個是2*oldCapacity,你說大嗎:)

[java] view plaincopy
  1. //Hashtable中調整內存的函數,這個函數沒有synchronize關鍵字,但是protected呦  
  2. protected void rehash() {  
  3.     //獲取原來的表大小  
  4.     int oldCapacity = table.length;  
  5.     Entry[] oldMap = table;  
  6.   //設置新的大小爲2*oldCapacity+1  
  7.     int newCapacity = oldCapacity * 2 + 1;  
  8.     //開設空間  
  9.     Entry[] newMap = new Entry[newCapacity];  
  10.   //以下就不用管了。。。  
  11.     modCount++;  
  12.     threshold = (int)(newCapacity * loadFactor);  
  13.     table = newMap;  
  14.   
  15.     for (int i = oldCapacity ; i-- > 0 ;) {  
  16.         for (Entry<K,V> old = oldMap[i] ; old != null ; ) {  
  17.         Entry<K,V> e = old;  
  18.         old = old.next;  
  19.   
  20.         int index = (e.hash & 0x7FFFFFFF) % newCapacity;  
  21.         e.next = newMap[index];  
  22.         newMap[index] = e;  
  23.         }  
  24.     }  
  25.     }  
[java] view plaincopy
  1. //HashMap中要簡單的多了,看看就知道了  
  2. void addEntry(int hash, K key, V value, int bucketIndex) {  
  3. Entry<K,V> e = table[bucketIndex];  
  4.        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);  
  5.        //如果超過了閥值  
  6.        if (size++ >= threshold)  
  7.        //就將大小設置爲原來的2倍  
  8.            resize(2 * table.length);  
  9.    }  
是吧,沒什麼區別吧

對於區別7的哈希值計算方法的不同,源碼面前,同樣是了無祕密

[java] view plaincopy
  1. //Hashtable中可以看出的是直接採用關鍵字的hashcode作爲哈希值  
  2. int hash = key.hashCode();  
  3. //然後進行模運算,求出所在譁然表的位置   
  4. int index = (hash & 0x7FFFFFFF) % tab.length;  
[java] view plaincopy
  1. //HashMap中的實現  
  2. //這兩行代碼的意思是先計算hashcode,然後再求其在哈希表的相應位置        
  3. int hash = hash(key.hashCode());  
  4. int i = indexFor(hash, table.length);  
上面的HashMap中可以看出關鍵在兩個函數hash與indexFor

源碼如下:

[java] view plaincopy
  1. static int hash(int h) {  
  2.     // This function ensures that hashCodes that differ only by  
  3.     // constant multiples at each bit position have a bounded  
  4.     // number of collisions (approximately 8 at default load factor).  
  5.     //這個我就不多說了,>>>這個是無符號右移運算符,可以理解爲無符號整型  
  6.     h ^= (h >>> 20) ^ (h >>> 12);  
  7.     return h ^ (h >>> 7) ^ (h >>> 4);  
  8. }  
[java] view plaincopy
  1. //求位於哈希表中的位置  
  2.  static int indexFor(int h, int length) {  
  3.      return h & (length-1);  
  4.  } 
發佈了90 篇原創文章 · 獲贊 33 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章