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,看下面的源碼
- //HashSet類的部份源代碼
- public class HashSet<E>
- extends AbstractSet<E>
- implements Set<E>, Cloneable, java.io.Serializable
- { //用於類的序列化,可以不用管它
- static final long serialVersionUID = -5024744406713321676L;
- //從這里可以看出HashSet類里面真的是采用HashMap來實現的
- private transient HashMap<E,Object> map;
- // Dummy value to associate with an Object in the backing Map
- //這里是生成一個對象,生成這個對象的作用是將每一個鍵的值均關聯於此對象,以滿足HashMap的鍵值對
- private static final Object PRESENT = new Object();
- /**
- * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
- * default initial capacity (16) and load factor (0.75).
- */
- //這里是一個構造函數,開構生成一個HashMap對象,用來存放數據
- public HashSet() {
- map = new HashMap<E,Object>();
- }
從上面的代碼中得出的結論是HashSet的確是采用HashMap來實現的,而且每一個鍵都關鍵同一個Object類的對象,因此鍵所關聯的值沒有意義,真正有意義的是鍵。而HashMap里的鍵是不允許重復的,因此1也就很容易明白了。
對於區別2,繼續看源代碼如下
- //從這里可以看得出Hashtable是繼承於Dictionary,實現了Map接口
- public class Hashtable<K,V>
- extends Dictionary<K,V>
- implements Map<K,V>, Cloneable, java.io.Serializable {
- //這里可以看出的是HashMap是繼承於AbstractMap類,實現了Map接口
- //因此與Hashtable繼承的父類不同
- public class HashMap<K,V>
- extends AbstractMap<K,V>
- implements Map<K,V>, Cloneable, Serializable
區別3,找一個具有針對性的方法看看,這個方法就是put
- //Hashtable里的向集體增加鍵值對的方法,從這里可以明顯看到的是
- //采用了synchronized關鍵字,這個關鍵字的作用就是用於線程的同步操作
- //因此下面這個方法對於多線程來說是安全的,但這會影響效率
- public synchronized V put(K key, V value) {
- // Make sure the value is not null
- //如果值為空的,則會拋出異常
- if (value == null) {
- throw new NullPointerException();
- }
- // Makes sure the key is not already in the hashtable.
- Entry tab[] = table;
- //獲得鍵值的hashCode,從這里也可以看得出key!=null,否則的話會拋出異常的呦
- int hash = key.hashCode();
- //獲取鍵據所在的哈希表的位置
- int index = (hash & 0x7FFFFFFF) % tab.length;
- //從下面這個循環中可以看出的是,內部實現采用了鏈表,即桶狀結構
- for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
- //如果向Hashtable中增加同一個元素時,則會重新更新元素的值
- if ((e.hash == hash) && e.key.equals(key)) {
- V old = e.value;
- e.value = value;
- return old;
- }
- }
- //后面的暫時不用管它,大概的意思就是內存的個數少於某個閥值時,進行重新分配內存
- modCount++;
- if (count >= threshold) {
- // Rehash the table if the threshold is exceeded
- rehash();
- tab = table;
- index = (hash & 0x7FFFFFFF) % tab.length;
- }
- //HashMap中的實現則相對來說要簡單的很多了,如下代碼
- //這里的代碼中沒有synchronize關鍵字,即可以看出,這個關鍵函數不是線程安全的
- public V put(K key, V value) {
- //對於鍵是空時,將向Map中放值一個null-value構成的鍵值對
- //對值卻沒有進行判空處理,意味着可以有多個具有鍵,鍵所對應的值卻為空的元素。
- if (key == null)
- return putForNullKey(value);
- //算出鍵所在的哈希表的位置
- int hash = hash(key.hashCode());
- int i = indexFor(hash, table.length);
- //同樣從這里可以看得出來的是采用的是鏈表結構,采用的是桶狀
- for (Entry<K,V> e = table[i]; e != null; e = e.next) {
- Object k;
- //對於向集體中增加具有相同鍵的情況時,這里可以看出,並不增加進去,而是進行更新操作
- if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
- V oldValue = e.value;
- e.value = value;
- e.recordAccess(this);
- return oldValue;
- }
- }
- //開始增加元素
- modCount++;
- addEntry(hash, key, value, i);
- return null;
- }
區別4在上面的代碼中,已經分析了,可以再細看一下
區別5內存初化大小不同,看看兩者的源代碼:
- public Hashtable() {
- //從這里可以看出,默認的初始化大小11,這里的11並不是11個字節,而是11個Entry,這個Entry是
- //實現鏈表的關鍵結構
- //這里的0.75代表的是裝載因子
- this(11, 0.75f);
- }
- //這里均是一些定義
- public HashMap() {
- //這個默認的裝載因子也是0.75
- this.loadFactor = DEFAULT_LOAD_FACTOR;
- //默認的痤為0.75*16
- threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
- //這里開始是默認的初始化大小,這里大小是16
- table = new Entry[DEFAULT_INITIAL_CAPACITY];
- init();
- }
從上面的代碼中,可以看出的是兩者的默認大小是不同的,一個是11,一個是16
區別6內存的擴容方式,看一看源代碼也是很清楚的,其實區別是不大的,看到網上一哥們寫的,說兩者有區別,其實真正深入源碼,區別真不大,一個是2*oldCapacity+1, 一個是2*oldCapacity,你說大嗎:)
- //Hashtable中調整內存的函數,這個函數沒有synchronize關鍵字,但是protected呦
- protected void rehash() {
- //獲取原來的表大小
- int oldCapacity = table.length;
- Entry[] oldMap = table;
- //設置新的大小為2*oldCapacity+1
- int newCapacity = oldCapacity * 2 + 1;
- //開設空間
- Entry[] newMap = new Entry[newCapacity];
- //以下就不用管了。。。
- modCount++;
- threshold = (int)(newCapacity * loadFactor);
- table = newMap;
- for (int i = oldCapacity ; i-- > 0 ;) {
- for (Entry<K,V> old = oldMap[i] ; old != null ; ) {
- Entry<K,V> e = old;
- old = old.next;
- int index = (e.hash & 0x7FFFFFFF) % newCapacity;
- e.next = newMap[index];
- newMap[index] = e;
- }
- }
- }
- //HashMap中要簡單的多了,看看就知道了
- void addEntry(int hash, K key, V value, int bucketIndex) {
- Entry<K,V> e = table[bucketIndex];
- table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
- //如果超過了閥值
- if (size++ >= threshold)
- //就將大小設置為原來的2倍
- resize(2 * table.length);
- }
是吧,沒什么區別吧
對於區別7的哈希值計算方法的不同,源碼面前,同樣是了無秘密
- //Hashtable中可以看出的是直接采用關鍵字的hashcode作為哈希值
- int hash = key.hashCode();
- //然后進行模運算,求出所在嘩然表的位置
- int index = (hash & 0x7FFFFFFF) % tab.length;
- //HashMap中的實現
- //這兩行代碼的意思是先計算hashcode,然后再求其在哈希表的相應位置
- int hash = hash(key.hashCode());
- int i = indexFor(hash, table.length);
上面的HashMap中可以看出關鍵在兩個函數hash與indexFor
源碼如下:
- static int hash(int h) {
- // This function ensures that hashCodes that differ only by
- // constant multiples at each bit position have a bounded
- // number of collisions (approximately 8 at default load factor).
- //這個我就不多說了,>>>這個是無符號右移運算符,可以理解為無符號整型
- h ^= (h >>> 20) ^ (h >>> 12);
- return h ^ (h >>> 7) ^ (h >>> 4);
- }
- //求位於哈希表中的位置
- static int indexFor(int h, int length) {
- return h & (length-1);
- }