包括:
一. Map 介紹
二. HashMap 介紹
三. HashTable 介紹
一. Map
HashMap 有兩個參數影響性能:初始容量和加載因子。加載因子表示哈希表在其容量自動增加之前可以達到多滿的一種尺度。當哈希表中的條目超過了容量和加載因子的乘積的時候,就會進行重哈希操作。
加載因子默認爲0.75,容量默認爲16。加載因子過高,容易產生哈希衝突,加載因子過小,容易浪費空間,0.75是一種折中。
注意:HashMap 不是同步的。
HashMap 的整體思路就是先創建一個 table 數組,然後算出哈希值,找到table 數組特定的位置,找到或者存放該值。另外,由於哈希衝突,該位置可能有一個或者多個值(使用鏈表法進行連接),還需要進一步判斷。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
在 put() 方法中, 首先會根據 key 算出哈希值,算出之後,再在固定位置進行存放,方法如下: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);
<span style="color:#3366ff;">//如果此時已經有節點,那麼實際上就是出現了哈希衝突,此時再使用鏈表法解決哈希衝突 </span>
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = 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) {
<span style="color:#3366ff;">p.next = newNode(hash, key, value, null); </span>
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
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;
}
從上面的代碼可以分析出,當put 一個元素的時候,首先檢查 該table 數組的該位置上的第一個元素是否相同,如果不同,那麼就使用了 for 循環遍歷 該位置上的元素(因爲可能已經出現了hash 衝突,存在鏈表)。最後,由 p.next=newNode(....) 可以直達,把新的元素插在該鏈表的最後。
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;
//如果不符合,其實這個位置已經有了哈希衝突,那麼只能 e = e.next() 一個一個查找
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;
}
- 可以有空值,可以有空鍵,但是鍵不能重複,非同步。
- HashMap由於使用了數組+鏈表(解決Hash衝突)的方式,所以增刪查改的效率比較高。
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;
//取得key 的哈希值,也就是在 table 數組中的位置
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
// 取出該值,判斷是不是之前是不是有了,有可的話,那麼就替換,並且返回舊值
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
//如果沒有,那麼就把該節點插入
addEntry(hash, key, value, index);
return null;
}
private void addEntry(int hash, K key, V value, int index) {
modCount++;
Entry<?,?> tab[] = table;
//判斷需不需要進行重哈希
if (count >= threshold) {
// Rehash the table if the threshold is exceeded
rehash();
tab = table;
hash = key.hashCode();
index = (hash & 0x7FFFFFFF) % tab.length;
}
// Creates the new entry.
@SuppressWarnings("unchecked")
//把節點插入到第一個位置上,如果已經有值,那麼就插在原來的前面;如果沒有,那麼就直接放入
Entry<K,V> e = (Entry<K,V>) tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
這裏注意,在JDK8 中, HashMap 的 put 如果重現hash 衝突,是把新的元素放在最後面,而Hashtable 則是把最新的元素放在第一位。這裏會有點區別。get() 方法:
@SuppressWarnings("unchecked")
public synchronized V get(Object key) {
Entry<?,?> tab[] = table;
//定位到該位置
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
//如果該位置上有哈希衝突,也就是有多個值,那麼就 for 循環取合適的那個
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return (V)e.value;
}
}
return null;
}
- HashMap 允許空值 和空鍵, 但是 HashTable 則不允許空值 和空鍵。
- HashMap 不是線程安全, Hashtable 是線程安全的。
- 對於hash衝突,HashMap把最新的元素放在了最後一位,Hashtable 則是把最新的元素放在了最前一位。