Java學習筆記:Map集合-HashMap和HashTable(01)
在面試的過程中經常會被問到一個問題,HashMap和HashTable有什麼不同?我就大概的整理了一些,與大家分享。
一、相同點
- 都實現了Map介面,底層都是採用的雜湊表(陣列 + 單向連結串列,在JDK1.8以後又加入了紅黑樹。即當連結串列長度大於8時,單向連結串列轉換成紅黑樹--提高查詢速度)。
- 都屬於雙列集合,儲存的都是key-value(鍵值對)形式的資料。
3.雙列集合的儲存結構(雜湊表)
雙列集合儲存的都是鍵值對形式的資料,每個鍵值對都是一個Entry型別的(Map介面中的內部介面Entry<K,V>),實際上Map集合儲存的都是Entry型別的資料。
public interface Map<K,V> { interface Entry<K,V> { // 內容省略 } } public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; } }
根據原始碼可以知道Entry的實現類Node(儲存的鍵值對)有四個屬性:
-
K key:儲存的key。
-
V value:儲存的value。
-
int hash:key的雜湊值。
-
Entry entry:指向當前Entry物件在連結串列中的下一個Entry物件,可以為null,null表示當前Entry物件在連結串列的尾部。
雜湊表儲存過程
- 建立Map集合:集合內部初始化一個Entry型別的陣列(HashMap初始長度為16,HashTable初始長度為11) ---> 實際上儲存的是Node型別。
- 儲存資料:存入雜湊結構的集合的元素必須重寫equals()和hashCode()方法,否則無法保證存入資料的不可重複性。
- 根據存入key的雜湊值計算當前Entry物件的在陣列中的儲存索引。
- 判斷計算出來的索引上是否存在元素。
- 如果不存在元素,那麼將當前Entry物件存入陣列中。
- 如果存在元素,那麼使用equals()方法判斷當前Entry物件與原始的Entry物件是否相同。
- 如果相同,放棄儲存(Map集合的key不可重複)。
- 如果不同,判斷當前索引位置是紅黑樹還是連結串列。
- 紅黑樹:插入紅黑樹中儲存。
- 連結串列:判斷插入Entry物件後當前連結串列的長度是否達到了指定長度(7)。
- 達到了指定長度:轉化成紅黑樹儲存。
- 沒有達到指定長度:插入連結串列的尾部。
補充:
- 由於物件的雜湊值可能會發生重複(雜湊衝突),所以為了解決雜湊衝突的問題,在陣列的基礎之上又引入了連結串列。將雜湊值相同但不是重複的元素儲存在同一個資料索引上的連結串列中。
- 由於連結串列沒有索引,同時在記憶體中的地址值不是連續的,連結串列的這種特性導致其查詢速率比較低。當雜湊值相同的非重複元素較多時,就會導致連結串列上的查詢速度大大降低。為了提高查詢速度,當連結串列的長度大於指定長度後會將連結串列轉換成查詢速度更高效的紅黑樹結構。
二、不同點
- 繼承體系不同
- 鍵值對的null值問題
- 陣列擴容機制不同
- 執行緒安全問題
- 迭代器不同
- 繼承體系不同
根據兩者的繼承體系可以知道,兩者實現的介面都相同,卻別就在於繼承的類不同。HahsMap繼承了AbstractMap類,HashTable繼承了Dictionary類(已經不再使用)。繼承體系的不同導致兩者之間的方法有少許差別,HashTable比HashMap多了兩個方法:一個是來自Dictionary類的elements()方法,另一個是contains()方法
2、鍵值對的null值問題
(1)、HashMap的key和value都可以為null
HashMap的key為null是由於對獲取雜湊值的步驟進行了特殊處理,當key為null時,雜湊值返回0,不為null時再去呼叫hashCode()方法獲取雜湊值。value可以為null,是由於在put()方法內,直接將value進行了儲存,沒有進行判斷。
// put方法
public V put(K key, V value) {
// 儲存資料時,使用key獲取雜湊值時做了處理
return putVal(hash(key), key, value, false, true);
}
// hash方法
static final int hash(Object key) {
int h;
// 值null的key認為其雜湊值為0,即儲存在陣列的第一個元素上。
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// get方法
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
(2)、HashTable的key和value都不能為null
HashTable的key不能為null是由於需要使用key呼叫hashCode()方法獲取雜湊值,如果key為null就會丟擲空指標異常。value不能為null是由於在put()方法內,針對value做了個是否為null值的判斷。
// get方法
public synchronized V get(Object key) {
Entry<?,?> tab[] = table;
// 直接使用key呼叫hashCode()方法獲取雜湊值,如果key為null的話肯定會丟擲空指標異常。
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
// ...省略...
}
// put方法
public synchronized V put(K key, V value) {
// Make sure the value is not null
// 對存入的value做了是否為null的判斷,如果為null就丟擲空指標異常
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
// 直接使用key呼叫hashCode()方法獲取雜湊值,如果key為null的話肯定會丟擲空指標異常。
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
// ...省略...
}
3、陣列擴容機制不同
-
HashMap:初始長度為16,每次擴容為原來的2倍(2n)
-
HashTable:初始長度為11,每次擴容為原來的2倍再加一(2n + 1)
// HashMap的雜湊表預設初始大小為16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// HashMap每次擴充為原來的2倍
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
}
// HashTable的雜湊表預設初始大小為11
public Hashtable() {
this(11, 0.75f);
}
// HashTable每次擴容為原來的2倍再加一
protected void rehash() {
int oldCapacity = table.length;
Entry<K,V>[] oldMap = table;
// 每次擴容為原來的2n+1
int newCapacity = (oldCapacity << 1) + 1;
// ...
}
4、執行緒安全問題
HashTable是執行緒安全的,HashMap是執行緒不安全的。也就是在多執行緒併發訪問時,HashTable不會發生執行緒安全問題(不需要新增額外的同步機制),而HashMap會發生執行緒安全問題(需要新增同步機制)。
HashTable之所以是執行緒安全的,是因為其在儲存和獲取元素的方法上使用了synchronized關鍵字進行修飾,在遍歷檢視的方法中使用了Collections.synchronizedXXX進行了封裝。
// get方法
public synchronized V get(Object key) {
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return (V)e.value;
}
}
return null;
}
// get方法
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// .....省略
}
// 遍歷集合的方法
public Set<K> keySet() {
if (keySet == null)
keySet = Collections.synchronizedSet(new KeySet(), this);
return keySet;
}
5、迭代器不同
-
HashMap的迭代器是Iterator型別的,該型別的迭代器特點是如果在迭代過程中改變的集合的內容(增刪改)的話,就會丟擲ConcurrentModificationException(併發修改異常)異常。
-
HashTable的迭代器是Enumerator型別的