深入分析HashMap与Hashtable的区别
一、前言
??上個月花了點時間研究了一下HashMap的源碼,對HashMap的實現原理有了一個較為深入的了解,今天突然想到有一個??嫉拿嬖囶}——HashMap與Hashtable的區別,于是又花了點時間研究了一下Hashtable的源碼。今天這篇博客就來簡單介紹一下兩者的區別,以下內容將主要從Hashtable的角度,描述它和HashMap有什么不同,而且將分兩個方面來敘述——使用方面和實現細節方面。如果對HashMap不是很了解的,可以閱讀一下這篇博客:HashMap源碼解讀——深入理解HashMap高效的原因。
二、正文
?2.1 使用上的不同
?(1)Hashtable線程安全,HashMap線程不安全
??這是這兩個容器最根本的區別。HashMap是一個線程不安全的容器,它并沒有實現線程同步,所以不應該在多線程的環境下使用。而Hashtable實現了線程同步,是一個線程安全的容器,但是它實現同步的方式比較拙劣——直接在大部分的public方法上添加synchronized關鍵字進行同步,這樣對于同一個Hashtable對象,每次只能有一個線程調用它的實例方法。這種拙劣的同步方式使得Hashtable的效率要遠遠低于HashMap,而且在很多情況下,這種同步是沒有意義的,比如在沒有寫操作發生的情況下,完全可以多個線程同時對Hashtable進行讀操作,并不會造成危害。
?(2)Hashtable不允許key或value為null,HashMap可以
??在Hashtable的很多方法中(比如put方法),特判了value為null的情況,此時將直接拋出一個NullPointerException;而對于key為null的情況卻沒有特判,但是若在其中加入一個key為null的元素,依舊會拋出NullPointerException,因為Hashtable中獲取key的hash值是直接調用key.hashCode方法,所有key不能為null。我們看Hashtable中的put方法即可知曉(多余的代碼被刪除):
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// ......
int hash = key.hashCode();
// ......
}
??但是HashMap中,無論是key還是value,都可以為null,因為它對null做了特殊處理。HashMap實現中,判斷key為null,則不會調用key.hashCode獲取它的hash值,而是將0作為它的hash值;而在get方法獲取value時,如果找到的節點為null,則直接返回null,否則返回node.value。同樣看HashMap中的兩段代碼來驗證這一點:
// 此方法為HashMap中獲取key的hash值的方法,特判了key == null的情況
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// HashMap中的get方法,特判了value為null的情況
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
?2.2 實現細節上的不同
?(1)Hashtable使用頭插法,JDK1.8之后的HashMap使用尾插法
??先簡單說一說什么是頭插和尾插:
頭插法:在鏈表中插入一個新的節點時,將新節點作為鏈表的頭節點,然后將新節點的next指向舊頭節點;
尾插法:在鏈表中插入一個新的節點時,讓鏈表的尾節點的next指向新節點;
??Hashtable使用的是頭插法,而HashMap在JDK1.8之前,使用的也是頭插法。但是,有人發現在并發情況下,若沒有進行線程同步,頭插法有很小的概率會導致死循環(這個問題我就不展開描述了,可以自己去查一查),于是從JDK1.8開始,為HashMap添加節點改為了尾插法。
?(2)HashMap的容量必須是2^n,而Hashtable的容量沒有這個限制
??這是兩者之間非常大的一個區別,就這一點,讓HashMap和Hashtble在實現上有很大的不同。HashMap的容量一定是2^n,其原因我在前言中推薦的博客里面做了詳細的描述,這里就簡單說一下。容量定為2^n是為了對取余運算做優化,因為2^n滿足一個性質:
num % 2^n == num & (2^n - 1)
??不論是HashMap還是Hashtable,要獲取節點在數組中的下標,都需要用key的hash值對數組長度取余,而HashMap將數組長度限制為2^n,就是為了用上面公式中的&替代%進行取余操作,因為&的效率要比%更高,而獲取下標又是一個頻繁的操作,所以這一步優化讓HashMap的查找速度有了很大的提升。而在Hashtable中,就是直接用%取余。我們看兩者取余的源碼:
// HashMap中的取余:tab為存放節點的數組,n為數組長度,hash變量存的是key的哈希值
// 這一步就是獲取哈希值為hash的key在tab中的位置
// HashMap中的hash值不會是負數,因為HashMap中自己實現了hash方法求hash值
tab[(n - 1) & hash]
// Hashtable中的取余:hash & 0x7FFFFFFF其實就是取hash的絕對值,然后直接對tab.length取余
// 0x7FFFFFFF轉換成二進制就是第一位為0,后31位為1,這個操作也就是將符號位轉為0(第一位為符號位),
// 而二進制中,符號位為0表示表示正數,這一步是防止計算出負數的下標,因為hash值可能為負
// 不用Math.abs是因為Math.abs(Integer.MAX_VALUE)越界,結果還是負數
int index = (hash & 0x7FFFFFFF) % tab.length;
??而正是這個優化,導致了HashMap和Hashtable的許多其他區別:
HashMap的默認初始容量為16,也就是2^4,如果指定了一個容量,則HashMap不會直接用它,而是先找出第一個大于這個指定容量的2^n;而Hashtable的默認初始容量是11,11是一個質數,根據hash求下標時,質數能夠讓下標分配更加均勻,Hashtable會直接接收指定的初始容量;
HashMap擴容時直接將數組大小乘2,因為(2^n)×2 == 2^(n+1),仍然滿足2^n;而Hashtable擴容是乘2加1,因為這樣可以讓一個質數很大概率還是一個質數;
?(3)JDK1.8開始,HashMap結構變為數組+鏈表+紅黑樹
??在JDK1.7以前,HashMap和Hashtable的結構都是數組+鏈表的實現,但是從JDK1.8開始,HashMap變成了數組+鏈表+紅黑樹。這是因為,在hash取余的結果不均勻的情況下,節點可能集中在數組中的某幾條鏈表上,導致鏈表過長。比如舉一個極端的例子,所有的節點通過hash值計算出的下標都相等,此時所有的節點都在一條鏈表上,這個時候HashMap或者Hashtable就退化成了一個鏈表,查詢的時間復雜度退化為了O(n)。為了避免這種情況的發生,從JDK1.8開始,HashMap中若某條鏈表的節點數達到了8個,就會將這條鏈表轉換為紅黑樹,此時,對這條鏈表的查詢效率就從O(n)提升為O(logn)。而Hashtable作為一個已經過時的容器,自然不會花時間對它進行復雜的優化。
三、總結
??Hashtable是一個過時的容器,在開發當中,我們應該少去使用它,甚至根本不要使用它。在單線程的環境下,我們最好使用HashMap,而在多線程的環境下,應該使用ConcurrentHashMap,它的效率要遠遠高于Hashtable。上面最后提到的幾點都和HashMap的實現有關,建議閱讀我在前言中推薦的博客,了解HashMap的具體實現。
四、參考
https://blog.csdn.net/weixin_43871333/article/details/100888713
總結
以上是生活随笔為你收集整理的深入分析HashMap与Hashtable的区别的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 荣耀智慧屏怎么连接手机文件夹
- 下一篇: redis通过key模糊搜索_Redis