Java集合詳解4:一文讀懂HashMap和HashTable的區別以及常見面試題
《Java集合詳解系列》是我在完成夯實Java基礎篇的系列部落格後準備開始寫的新系列。
這些文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到我的倉庫裡檢視
喜歡的話麻煩點下Star、fork哈
文章首發於我的個人部落格:
www.how2playlife.com
今天我們來探索一下HashMap和HashTable機制與比較器的原始碼。
本文參考 http://cmsblogs.com/?p=176
HashMap
HashMap也是我們使用非常多的Collection,它是基於雜湊表的 Map 介面的實現,以key-value的形式存在。在HashMap中,key-value總是會當做一個整體來處理,系統會根據hash演算法來來計算key-value的儲存位置,我們總是可以通過key快速地存、取value。下面就來分析HashMap的存取。
定義
HashMap實現了Map介面,繼承AbstractMap。其中Map介面定義了鍵對映到值的規則,而AbstractMap類提供 Map 介面的骨幹實現,以最大限度地減少實現此介面所需的工作,其實AbstractMap類已經實現了Map,這裡標註Map LZ覺得應該是更加清晰吧!
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
建構函式
HashMap提供了三個建構函式:
HashMap():構造一個具有預設初始容量 (16) 和預設載入因子 (0.75) 的空 HashMap。
HashMap(int initialCapacity):構造一個帶指定初始容量和預設載入因子 (0.75) 的空 HashMap。
HashMap(int initialCapacity, float loadFactor):構造一個帶指定初始容量和載入因子的空 HashMap。
在這裡提到了兩個引數:初始容量,載入因子。
這兩個引數是影響HashMap效能的重要引數,其中容量表示雜湊表中桶的數量,初始容量是建立雜湊表時的容量,載入因子是雜湊表在其容量自動增加之前可以達到多滿的一種尺度,它衡量的是一個雜湊表的空間的使用程度,負載因子越大表示雜湊表的裝填程度越高,反之愈小。
對於使用連結串列法的雜湊表來說,查詢一個元素的平均時間是O(1+a),因此如果負載因子越大,對空間的利用更充分,然而後果是查詢效率的降低;如果負載因子太小,那麼雜湊表的資料將過於稀疏,對空間造成嚴重浪費。系統預設負載因子為0.75,一般情況下我們是無需修改的。
HashMap是一種支援快速存取的資料結構,要了解它的效能必須要了解它的資料結構。
資料結構
我們知道在Java中最常用的兩種結構是陣列和模擬指標(引用),幾乎所有的資料結構都可以利用這兩種來組合實現,HashMap也是如此。實際上HashMap是一個“連結串列雜湊”,如下是它的資料結構:
HashMap資料結構圖
下圖的table陣列的每個格子都是一個桶。負載因子就是map中的元素佔用的容量百分比。比如負載因子是0.75,初始容量(桶數量)為16時,那麼允許裝填的元素最大個數就是16*0.75 = 12,這個最大個數也被成為閾值,就是map中定義的threshold。超過這個閾值時,map就會自動擴容。
儲存實現:put(key,vlaue)
首先我們先看原始碼
public V put(K key, V value) {
//當key為null,呼叫putForNullKey方法,儲存null與table第一個位置中,這是HashMap允許為null的原因
if (key == null)
return putForNullKey(value);
//計算key的hash值,此處對原來元素的hashcode進行了再次hash
int hash = hash(key.hashCode()); ------(1)
//計算key hash 值在 table 陣列中的位置
int i = indexFor(hash, table.length); ------(2)
//從i出開始迭代 e,找到 key 儲存的位置
for (Entry<K, V> e = table[i]; e != null; e = e.next) {
Object k;
//判斷該條鏈上是否有hash值相同的(key相同)
//若存在相同,則直接覆蓋value,返回舊value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value; //舊值 = 新值
e.value = value;
e.recordAccess(this);
return oldValue; //返回舊值
}
}
//修改次數增加1
modCount++;
//將key、value新增至i位置處
addEntry(hash, key, value, i);
return null;
}
通過原始碼我們可以清晰看到HashMap儲存資料的過程為:首先判斷key是否為null,若為null,則直接呼叫putForNullKey方法。
若不為空則先計算key的hash值,然後根據hash值搜尋在table陣列中的索引位置,如果table陣列在該位置處有元素,則通過比較是否存在相同的key,若存在則覆蓋原來key的value,==否則將該元素儲存在鏈頭(最先儲存的元素放在鏈尾)==。
若table在該處沒有元素,則直接儲存。這個過程看似比較簡單,其實深有內幕。有如下幾點:
1、 先看迭代處。此處迭代原因就是為了防止存在相同的key值,若發現兩個hash值(key)相同時,HashMap的處理方式是用新value替換舊value,這裡並沒有處理key,這就解釋了HashMap中沒有兩個相同的key。
2、 在看(1)、(2)處。這裡是HashMap的精華所在。首先是hash方法,該方法為一個純粹的數學計算,就是計算h的hash值。
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
我們知道對於HashMap的table而言,資料分佈需要均勻(最好每項都只有一個元素,這樣就可以直接找到),不能太緊也不能太鬆,太緊會導致查詢速度慢,太鬆則浪費空間。計算hash值後,怎麼才能保證table元素分佈均與呢?我們會想到取模,但是由於取模的消耗較大,HashMap是這樣處理的:呼叫indexFor方法。
static int indexFor(int h, int length) {
return h & (length-1);
}
HashMap的底層陣列長度總是2的n次方,在建構函式中存在:capacity <<= 1;這樣做總是能夠保證HashMap的底層陣列長度為2的n次方。當length為2的n次方時,h&(length - 1)就相當於對length取模,而且速度比直接取模快得多,這是HashMap在速度上的一個優化。至於為什麼是2的n次方下面解釋。
==對length取模來得到hash是常用的hash索引方法,這裡採用位運算的話效率更高。==
我們回到indexFor方法,該方法僅有一條語句:h&(length - 1),這句話除了上面的取模運算外還有一個非常重要的責任:均勻分佈table資料和充分利用空間。
這裡我們假設length為16(2^n)和15,h為5、6、7。
當n=15時,6和7的結果一樣,這樣表示他們在table儲存的位置是相同的,也就是產生了碰撞,6、7就會在一個位置形成連結串列,這樣就會導致查詢速度降低。誠然這裡只分析三個數字不是很多,那麼我們就看0-15。
而當length = 16時,length – 1 = 15 即1111,那麼進行低位&運算時,值總是與原來hash值相同,而進行高位運算時,其值等於其低位值。所以說當length = 2^n時,不同的hash值發生碰撞的概率比較小,這樣就會使得資料在table陣列中分佈較均勻,查詢速度也較快。
這裡我們再來複習put的流程:當我們想一個HashMap中新增一對key-value時,系統首先會計算key的hash值,然後根據hash值確認在table中儲存的位置。若該位置沒有元素,則直接插入。否則迭代該處元素連結串列並依此比較其key的hash值。
如果兩個hash值相等且key值相等(e.hash == hash && ((k = e.key) == key || key.equals(k))),則用新的Entry的value覆蓋原來節點的value。如果兩個hash值相等但key值不等 ,則將該節點插入該連結串列的鏈頭。具體的實現過程見addEntry方法,如下:
void addEntry(int hash, K key, V value, int bucketIndex) {
//獲取bucketIndex處的Entry
Entry<K, V> e = table[bucketIndex];
//將新建立的 Entry 放入 bucketIndex 索引處,並讓新的 Entry 指向原來的 Entry
table[bucketIndex] = new Entry<K, V>(hash, key, value, e);
//若HashMap中元素的個數超過極限了,則容量擴大兩倍
if (size++ >= threshold)
resize(2 * table.length);
}
這個方法中有兩點需要注意:
後面新增的entry反而會接到前面。
一、是鏈的產生。
這是一個非常優雅的設計。系統總是將新的Entry物件新增到bucketIndex處。如果bucketIndex處已經有了物件,那麼新新增的Entry物件將指向原有的Entry物件,形成一條Entry鏈,但是若bucketIndex處沒有Entry物件,也就是e==null,那麼新新增的Entry物件指向null,也就不會產生Entry鏈了。
二、擴容問題。
隨著HashMap中元素的數量越來越多,發生碰撞的概率就越來越大,所產生的連結串列長度就會越來越長,這樣勢必會影響HashMap的速度,為了保證HashMap的效率,系統必須要在某個臨界點進行擴容處理。
該臨界點在當HashMap中元素的數量等於table陣列長度*載入因子。但是擴容是一個非常耗時的過程,因為它需要重新計算這些資料在新table陣列中的位置並進行復制處理。所以如果我們已經預知HashMap中元素的個數,那麼預設元素的個數能夠有效的提升HashMap的效能。
JDK1.8的hashmap:put方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果p是紅黑樹節點,則用另外的處理方法
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//當連結串列節點數超過8個,則直接進行紅黑樹化。
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
JDK1.8在連結串列長度超過8時會轉換為紅黑樹。
轉換方法如下:
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
//如果節點數變小小於紅黑樹的節點數閾值時,調整空間
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
//該方法直接返回一個紅黑樹結點。
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
//從連結串列頭開始依次插入紅黑樹
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
// For treeifyBin
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
return new TreeNode<>(p.hash, p.key, p.value, next);
}
擴容
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
//如果原容量大於最大空間,則讓閾值為最大值。因為不能再擴容了,最大容量就是整數最大值。
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//兩倍擴容,閾值也跟著變為兩倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
//當後面沒有節點時,直接插入即可 //每個元素重新計算索引位置,此處的hash值並沒有變,只是改變索引值
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
//否則,就從頭到尾依次將節點進行索引然後插入新陣列,這樣插入後的連結串列順序會和原來的順序相反。
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
讀取實現:get(key)
相對於HashMap的存而言,取就顯得比較簡單了。通過key的hash值找到在table陣列中的索引處的Entry,然後返回該key對應的value即可。
public V get(Object key) {
// 若為null,呼叫getForNullKey方法返回相對應的value
if (key == null)
return getForNullKey();
// 根據該 key 的 hashCode 值計算它的 hash 碼
int hash = hash(key.hashCode());
// 取出 table 陣列中指定索引處的值
for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
Object k;
//若搜尋的key與查詢的key相同,則返回相對應的value
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
在這裡能夠根據key快速的取到value除了和HashMap的資料結構密不可分外,還和Entry有莫大的關係,在前面就提到過,HashMap在儲存過程中並沒有將key,value分開來儲存,而是當做一個整體key-value來處理的,這個整體就是Entry物件。
同時value也只相當於key的附屬而已。在儲存的過程中,系統根據key的hashcode來決定Entry在table陣列中的儲存位置,在取的過程中同樣根據key的hashcode取出相對應的Entry物件。
在java中與有兩個類都提供了一個多種用途的hashTable機制,他們都可以將可以key和value結合起來構成鍵值對通過put(key,value)方法儲存起來,然後通過get(key)方法獲取相對應的value值。
HashTable
一個是前面提到的HashMap,還有一個就是馬上要講解的HashTable。對於HashTable而言,它在很大程度上和HashMap的實現差不多,如果我們對HashMap比較瞭解的話,對HashTable的認知會提高很大的幫助。他們兩者之間只存在幾點的不同,這個後面會闡述。
定義
HashTable在Java中的定義如下:
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable
從中可以看出HashTable繼承Dictionary類,實現Map介面。其中Dictionary類是任何可將鍵對映到相應值的類(如 Hashtable)的抽象父類。每個鍵和每個值都是一個物件。在任何一個 Dictionary 物件中,每個鍵至多與一個值相關聯。Map是"key-value鍵值對"介面。
HashTable採用"拉鍊法"實現雜湊表,它定義了幾個重要的引數:table、count、threshold、loadFactor、modCount。
table:為一個Entry[]陣列型別,Entry代表了“拉鍊”的節點,每一個Entry代表了一個鍵值對,雜湊表的"key-value鍵值對"都是儲存在Entry陣列中的。
count:HashTable的大小,注意這個大小並不是HashTable的容器大小,而是他所包含Entry鍵值對的數量。
threshold:Hashtable的閾值,用於判斷是否需要調整Hashtable的容量。threshold的值="容量*載入因子"。
loadFactor:載入因子。
modCount:用來實現“fail-fast”機制的(也就是快速失敗)。所謂快速失敗就是在併發集合中,其進行迭代操作時,若有其他執行緒對其進行結構性的修改,這時迭代器會立馬感知到,並且立即丟擲ConcurrentModificationException異常,而不是等到迭代完成之後才告訴你(你已經出錯了)。
構造方法
在HashTabel中存在5個建構函式。通過這5個建構函式我們構建出一個我想要的HashTable。
public Hashtable() {
this(11, 0.75f);
}
預設建構函式,容量為11,載入因子為0.75。
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);
}
用指定初始容量和預設的載入因子 (0.75) 構造一個新的空雜湊表。
public Hashtable(int initialCapacity, float loadFactor) {
//驗證初始容量
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
//驗證載入因子
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal Load: "+loadFactor);
if (initialCapacity==0)
initialCapacity = 1;
this.loadFactor = loadFactor;
//初始化table,獲得大小為initialCapacity的table陣列
table = new Entry[initialCapacity];
//計算閥值
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
//初始化HashSeed值
initHashSeedAsNeeded(initialCapacity);
}
用指定初始容量和指定載入因子構造一個新的空雜湊表。其中initHashSeedAsNeeded方法用於初始化hashSeed引數,其中hashSeed用於計算key的hash值,它與key的hashCode進行按位異或運算。這個hashSeed是一個與例項相關的隨機值,主要用於解決hash衝突。
private int hash(Object k) {
return hashSeed ^ k.hashCode();
}
構造一個與給定的 Map 具有相同對映關係的新雜湊表。
public Hashtable(Map<? extends K, ? extends V> t) {
//設定table容器大小,其值==t.size * 2 + 1
this(Math.max(2*t.size(), 11), 0.75f);
putAll(t);
}
主要方法
HashTable的API對外提供了許多方法,這些方法能夠很好幫助我們操作HashTable,但是這裡我只介紹兩個最根本的方法:put、get。
首先我們先看put方法:將指定 key 對映到此雜湊表中的指定 value。注意這裡鍵key和值value都不可為空。
public synchronized V put(K key, V value) {
// 確保value不為null
if (value == null) {
throw new NullPointerException();
}
/*
* 確保key在table[]是不重複的
* 處理過程:
* 1、計算key的hash值,確認在table[]中的索引位置
* 2、迭代index索引位置,如果該位置處的連結串列中存在一個一樣的key,則替換其value,返回舊值
*/
Entry tab[] = table;
int hash = hash(key); //計算key的hash值
int index = (hash & 0x7FFFFFFF) % tab.length; //確認該key的索引位置
//迭代,尋找該key,替換
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
V old = e.value;
e.value = value;
return old;
}
}
modCount++;
if (count >= threshold) { //如果容器中的元素數量已經達到閥值,則進行擴容操作
rehash();
tab = table;
hash = hash(key);
index = (hash & 0x7FFFFFFF) % tab.length;
}
// 在索引位置處插入一個新的節點
Entry<K,V> e = tab[index];
tab[index] = new Entry<>(hash, key, value, e);
//容器中元素+1
count++;
return null;
}
put方法的整個處理流程是:計算key的hash值,根據hash值獲得key在table陣列中的索引位置,然後迭代該key處的Entry連結串列(我們暫且理解為連結串列),若該連結串列中存在一個這個的key物件,那麼就直接替換其value值即可,否則在將改key-value節點插入該index索引位置處
在HashTabled的put方法中有兩個地方需要注意:
1、HashTable的擴容操作,在put方法中,如果需要向table[]中新增Entry元素,會首先進行容量校驗,如果容量已經達到了閥值,HashTable就會進行擴容處理rehash(),如下:
protected void rehash() {
int oldCapacity = table.length;
//元素
Entry<K,V>[] oldMap = table;
//新容量=舊容量 * 2 + 1
int newCapacity = (oldCapacity << 1) + 1;
if (newCapacity - MAX_ARRAY_SIZE > 0) {
if (oldCapacity == MAX_ARRAY_SIZE)
return;
newCapacity = MAX_ARRAY_SIZE;
}
//新建一個size = newCapacity 的HashTable
Entry<K,V>[] newMap = new Entry[];
modCount++;
//重新計算閥值
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
//重新計算hashSeed
boolean rehash = initHashSeedAsNeeded(newCapacity);
table = newMap;
//將原來的元素拷貝到新的HashTable中
for (int i = oldCapacity ; i-- > 0 ;) {
for (Entry<K,V> old = oldMap[i] ; old != null ; ) {
Entry<K,V> e = old;
old = old.next;
if (rehash) {
e.hash = hash(e.key);
}
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
e.next = newMap[index];
newMap[index] = e;
}
}
}
在這個rehash()方法中我們可以看到容量擴大兩倍+1,同時需要將原來HashTable中的元素一一複製到新的HashTable中,這個過程是比較消耗時間的,同時還需要重新計算hashSeed的,畢竟容量已經變了。
這裡對閥值囉嗦一下:比如初始值11、載入因子預設0.75,那麼這個時候閥值threshold=8,當容器中的元素達到8時,HashTable進行一次擴容操作,容量 = 8 2 + 1 =17,而閥值threshold=170.75 = 13,當容器元素再一次達到閥值時,HashTable還會進行擴容操作,依次類推。
下面是計算key的hash值,這裡hashSeed發揮了作用。
private int hash(Object k) {
return hashSeed ^ k.hashCode();
}
相對於put方法,get方法就會比較簡單,處理過程就是計算key的hash值,判斷在table陣列中的索引位置,然後迭代連結串列,匹配直到找到相對應key的value,若沒有找到返回null。
public synchronized V get(Object key) {
Entry tab[] = table;
int hash = hash(key);
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return e.value;
}
}
return null;
}
HashTable與HashMap的異同點
HashTable和HashMap存在很多的相同點,但是他們還是有幾個比較重要的不同點。
>
第一:我們從他們的定義就可以看出他們的不同,HashTable基於Dictionary類,而HashMap是基於AbstractMap。Dictionary是什麼?它是任何可將鍵對映到相應值的類的抽象父類,而AbstractMap是基於Map介面的骨幹實現,它以最大限度地減少實現此介面所需的工作。
第二:HashMap可以允許存在一個為null的key和任意個為null的value,但是HashTable中的key和value都不允許為null。如下:
當HashMap遇到為null的key時,它會呼叫putForNullKey方法來進行處理。對於value沒有進行任何處理,只要是物件都可以。
if (key == null) return putForNullKey(value); 而當HashTable遇到null時,他會直接丟擲NullPointerException異常資訊。
if (value == null) {
throw new NullPointerException();
}
第三:Hashtable的方法是同步的,而HashMap的方法不是。所以有人一般都建議如果是涉及到多執行緒同步時採用HashTable,沒有涉及就採用HashMap,但是在Collections類中存在一個靜態方法:synchronizedMap(),該方法建立了一個執行緒安全的Map物件,並把它作為一個封裝的物件來返回,所以通過Collections類的synchronizedMap方法是可以我們你同步訪問潛在的HashMap。這樣君該如何選擇呢???
面試題:HashMap和HashTable的區別
HashMap執行緒不安全,HashTable是執行緒安全的。HashMap內部實現沒有任何執行緒同步相關的程式碼,所以相對而言效能要好一點。如果在多執行緒中使用HashMap需要自己管理執行緒同步。HashTable大部分對外介面都使用synchronized包裹,所以是執行緒安全的,但是效能會相對差一些。
二者的基類不一樣。HashMap派生於AbstractMap,HashTable派生於Dictionary。它們都實現Map, Cloneable, Serializable這些介面。AbstractMap中提供的基礎方法更多,並且實現了多個通用的方法,而在Dictionary中只有少量的介面,並且都是abstract型別。
key和value的取值範圍不同。HashMap的key和value都可以為null,但是HashTablekey和value都不能為null。對於HashMap如果get返回null,並不能表明HashMap不存在這個key,如果需要判斷HashMap中是否包含某個key,就需要使用containsKey這個方法來判斷。
演算法不一樣。HashMap的initialCapacity為16,而HashTable的initialCapacity為11。HashMap中初始容量必須是2的冪,如果初始化傳入的initialCapacity不是2的冪,將會自動調整為大於出入的initialCapacity最小的2的冪。HashMap使用自己的計算hash的方法(會依賴key的hashCode方法),HashTable則使用key的hashCode方法得到。
參考文章
http://mini.eastday.com/mobile/180310183019559.html#
https://blog.csdn.net/lihua5419/article/details/87691965
https://www.cnblogs.com/aeolian/p/8468632.html
微信公眾號
Java技術江湖
如果大家想要實時關注我更新的文章以及分享的乾貨的話,可以關注我的公眾號【Java技術江湖】一位阿里 Java 工程師的技術小站,作者黃小斜,專注 Java 相關技術:SSM、SpringBoot、MySQL、分散式、中介軟體、叢集、Linux、網路、多執行緒,偶爾講點Docker、ELK,同時也分享技術乾貨和學習經驗,致力於Java全棧開發!
Java工程師必備學習資源: 一些Java工程師常用學習資源,關注公眾號後,後臺回覆關鍵字 “Java” 即可免費無套路獲取。
個人公眾號:黃小斜
黃小斜是跨考軟體工程的 985 碩士,自學 Java 兩年,拿到了 BAT 等近十家大廠 offer,從技術小白成長為阿里工程師。
作者專注於 JAVA 後端技術棧,熱衷於分享程式設計師乾貨、學習經驗、求職心得和程式人生,目前黃小斜的CSDN部落格有百萬+訪問量,知乎粉絲2W+,全網已有10W+讀者。
黃小斜是一個斜槓青年,堅持學習和寫作,相信終身學習的力量,希望和更多的程式設計師交朋友,一起進步和成長!關注公眾號【黃小斜】後回覆【原創電子書】即可領取我原創的電子書《菜鳥程式設計師修煉手冊:從技術小白到阿里巴巴Java工程師》
程式設計師3T技術學習資源: 一些程式設計師學習技術的資源大禮包,關注公眾號後,後臺回覆關鍵字 “資料” 即可免費無套路獲取。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69906029/viewspace-2659621/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 集合類HashMap,HashTable,ConcurrentHashMap區別?HashMap
- 夯實Java基礎系列16:一文讀懂Java IO流和常見面試題Java面試題
- Java集合詳解2:一文讀懂Queue和LinkedListJava
- java複習之HashMap和Hashtable的區別JavaHashMap
- 一文讀懂JDK7,8,9的hashmap,hashtable,concurrenthashmap及他們的區別JDKHashMap
- 夯實Java基礎系列19:一文搞懂Java集合類框架,以及常見面試題Java框架面試題
- 集合常見的面試題面試題
- HashMap常見面試題整理HashMap面試題
- 一文讀懂JDK7,8,9的hashmap,hashtable,concJDKHashMap
- Java集合系列(四):HashMap、Hashtable、LinkedHashMap、TreeMap的使用方法及區別JavaHashMap
- HashMap、HashTable、ConcurrentHashMap的區別HashMap
- C#中Hashtable和HashMap的區別C#HashMap
- 超詳細的Java面試題總結(三)之Java集合篇常見問題Java面試題
- 一文讀懂JDK1.7,JDK1.8,JDK1.9的hashmap,hashtable,concurrenthashmap及他們的區別JDKHashMap
- 搞懂 HashSet & LinkedHashSet 原始碼以及集合常見面試題目原始碼面試題
- Java常見面試真題之中級進階(HashMap篇)Java面試HashMap
- 五分鐘看懂Hashtable原始碼以及與HashMap的區別原始碼HashMap
- Java常見的面試題Java面試題
- HashMap、Hashtable、ConcurrentHashMap的原理與區別HashMap
- 【Java面試】Java常見IO面試題!Java面試題
- 【Java面試】JSP常見面試題!JavaJS面試題
- 【Java面試】Servlet常見面試題!JavaServlet面試題
- 【Java面試】 Javascript常見面試題!JavaScript面試題
- 一文讀懂BeanFactory和FactoryBean區別Bean
- 一文詳解面試常考的TopK問題面試TopK
- 常見的Java面試問題Java面試
- Java集合詳解(五):Hashtable原理解析Java
- HashMap底層實現原理/HashMap與HashTable區別/HashMap與HashSet區別HashMap
- 一文搞懂所有Java集合面試題Java面試題
- JAVA系列:HashMap常見問題JavaHashMap
- 常見面試題學習(4)面試題
- Hashtable和HashMapHashMap
- Java經典面試題: == 和 equals( )的區別Java面試題
- 一文讀懂常見HTTP狀態碼HTTP
- 一文讀懂DNS解析故障的排查思路,輕鬆解決解析常見問題DNS
- Java集合類常見面試知識點總結Java面試
- HashTable、ConcurrentHashMap、TreeMap、HashMap關於鍵值的區別HashMap
- Java集合詳解3:一文讀懂Iterator,fail-fast機制與比較器JavaAIAST