一、基礎知識
1、註釋
註釋中對hashmap進行了一些簡單介紹
- 允許空值和空鍵;
- 無序:不保證map中的順序,不保證順序一直不變;
- 兩個重要因素:初始大小和負載因子(初始大小默認16,負載因子默認爲0.75);當已存儲的數量 > 容量 * 負載因子,hashmap自動增大爲原來大小的兩倍,重新散列(rehash,消耗大)。負載因子越高,空間消耗越小,查詢map中元素消耗時間越多。當需要一個較大空間時,最好給一個大的初始容量,避免rehash。
- 基本操作 (get and put) 恆定時間性能:O(log n);
- 使用相同的鍵存儲值會直接降低hashtable的效率。爲了減輕這樣的情況,會對key進行比較,確保key的重複性低,但是這樣也會降低性能。
- hashmap不同步,需要自行在外部同步。一般是將map封裝在一個對象中,然後對這個對象進行同步;也可以如下確保同步:
Map m = Collections.synchronizedMap(new HashMap(...));
大致相當於 hashtable, 只是hashmap不同步, 並允許空值;
- 當hashmap過大時(鏈表長度大於8)會轉爲紅黑樹,支持更快的查詢,樹節點的大小是常規節點的兩倍。
2、內部結構
我們先來看一下hashmap的內部結構,大多數人多少都知道一些,畫成圖更加直觀的表現:
這是hashmap的內部結構,用數組加鏈表的形式,先使用散列,把節點分佈到數組的每個位置,發生衝突時,使用鏈表解決
這裏散列的大小爲2^n,事實上這並不是一個很好的選擇,碰撞概率會增大。一般情況下,散列的大小最好取2的n次方-1(素數)。hashmap這樣做是爲了之後運算(位運算)方便,同時在hash時選擇更好的hash函數,以抵消2的n次方帶來的不便。
這是每一個node:
當鏈表長度大於8時,每一個node都會變成treeNode,形成紅黑樹。
3、補充
- 要求map中存儲的對象有hashcode()和equals()方法,且有不變性,所以使用Integer和String更好,它們都是final,不會變,而且有hashcode()和equals()方法。
- fail-fast機制:map中有一個modcount,用於存儲版本號,每次對map進行結構上的修改,modcount就會+1;修改時,檢查版本號,如果期待的版本號和當前版本號不同,則直接拋出異常,不再進行後續步驟。問題在於fail-fast並不保證每次都能檢查出異常,所以並不能依賴它,hashmap依舊是線程不安全的。
- 序列化的時候,先寫入大小,負載因子等參數,再寫入每一個節點,讀取時按相同順序。
二、常用方法
1、字段
hashmap中的字段如下,可以在初始化時進行設置,如果不設置,則按照默認的處理
//大小爲2^n,首次使用時初始化,有時長度可以爲0
transient Node<K,V>[] table;
//緩存節點,AbstractMap字段在keySet() and values()中使用
transient Set<Entry<K,V>> entrySet;
//map中存儲節點數量
transient int size;
//版本號,結構修改時增加,fail-fast機制
transient int modCount;
//大於該值,rehash
int threshold;
//負載因子
final float loadFactor;
默認配置
//默認初始化容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//負載因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//鏈表長度大於該值,轉爲紅黑樹
static final int TREEIFY_THRESHOLD = 8;
//最多可存儲數量:CAPACITY * LOAD_FACTOR。大於該值,rehash。
static final int UNTREEIFY_THRESHOLD = 6;
//變成樹的最小容量
static final int MIN_TREEIFY_CAPACITY = 64;
2、計算hash
先得到key的hashcode,然後讓高16位和低16位異或,結果就是hash,
index = (n - 1) & hash,也就是hash對錶大小取餘。
/*計算hash
由於map的大小爲2^n,更容易出現碰撞,所以需要高位與低位異或,減少碰撞
*/
static final int hash(Object key) {
int h;
// >>>:無符號右移16位
//高位與低位異或
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
3、存入值:
afterNodeInsertion,afterNodeAccess這些是linkhashmap會做的事情,此處不討論
/** 存入值
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*
*/
//todo
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//table爲空,resize
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;//table的長度
//該節點應該存入的位置爲空,新建節點,存入
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//不爲空,p指向鏈表或紅黑樹
else {
Node<K,V> e; K k;
//判斷第一個節點,如果第一個節點就是要存儲的節點,將p的值給e
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) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);//轉爲紅黑樹
break;
}
//如果e是要存儲的節點,停止
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.7加入節點用的是頭插法,所以tab[index] = 最新加入的節點,因爲認爲最新加入的節點用到的可能性會更大。
jdk1.8採用尾插
4、查找node:
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//table爲null,table長度爲0,index = (n - 1) & hash,對應位置爲null
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//first = table[index],此位置存的是一串鏈表
//first存的是第一個節點
//先判斷第一個節點(first)是不是
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;
}
經過這兩個方法其他方法基本都大同小異,先得到index對應的鏈表/樹,根據不同的情況進行處理。是樹,交給樹處理,是鏈表遍歷,自行處理。
三、擴容
//擴容
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; // 兩倍
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { //初始化
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)
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;
}
jdk1.7:擴容時,鏈表會逆置
鏈表逆置可以避免尾部遍歷,但存在一個很嚴重的問題:多線程的時候會導致死循環!!!
jdk1.8:不會
除此之外,jdk1.8有一個很好的改進,在原來擴容時,需要重新rehash,但是看看這部分代碼,我們並沒有發現rehash。
我們來看一下處理鏈表這一部分:
if ((e.hash & oldCap) == 0){
loTail...
} else {
hiTail...
}
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
擴容,就是給原來的容量乘2,也就是把原來容量oldCap左移一位,這時2^n的好處就表現出來了。
index = (n - 1) & hash,如果這一位是0,則index = index,否則,index = index + oldCap。
這樣很快就能得到新的index而且避免了rehash(rehash是一個消耗比較大的方法,避免它,可以提高性能)。
四、 jdk1.8新加的方法
/**
* 添加一個節點,若該節點已存在,不改變原來的值
* */
@Override
public V putIfAbsent(K key, V value) {
return putVal(hash(key), key, value, true, true);
}
/**
* 有值,設置新值,返回新值
* 無值,頭插
* */
//TODO afterNodeInsertion mappingFunction.apply treeifyBin afterNodeInsertion
@Override
public V computeIfAbsent(K key,
Function<? super K, ? extends V> mappingFunction) {
if (mappingFunction == null)
throw new NullPointerException();
int hash = hash(key);
Node<K,V>[] tab; Node<K,V> first; int n, i;
int binCount = 0;
TreeNode<K,V> t = null;
Node<K,V> old = null;
//設置大小resize
if (size > threshold || (tab = table) == null ||
(n = tab.length) == 0)
n = (tab = resize()).length;
//獲取舊節點
if ((first = tab[i = (n - 1) & hash]) != null) {
if (first instanceof TreeNode)
old = (t = (TreeNode<K,V>)first).getTreeNode(hash, key);
else {
Node<K,V> e = first; K k;
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
old = e;
break;
}
++binCount;
} while ((e = e.next) != null);
}
//舊的值和節點都存在,返回舊的值
V oldValue;
if (old != null && (oldValue = old.value) != null) {
afterNodeAccess(old);
return oldValue;
}
}
V v = mappingFunction.apply(key);
//新值不存在
if (v == null) {
return null;
} else if (old != null) {//新值存在且舊節點存在
old.value = v;//設置新值
afterNodeAccess(old);
return v;
}
//按照紅黑樹處理
else if (t != null)
t.putTreeVal(this, tab, hash, key, v);
else {//頭插法給鏈表加入一個節點
tab[i] = newNode(hash, key, v, first);
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
}
++modCount;
++size;
afterNodeInsertion(true);
return v;
}
/**
* 新值存在則設置新值,不存在 則刪除節點
* */
public V computeIfPresent(K key,
BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
if (remappingFunction == null)
throw new NullPointerException();
Node<K,V> e; V oldValue;
int hash = hash(key);
if ((e = getNode(hash, key)) != null &&
(oldValue = e.value) != null) {
V v = remappingFunction.apply(key, oldValue);
if (v != null) {
e.value = v;
afterNodeAccess(e);
return v;
}
else
removeNode(hash, key, null, false, true);
}
return null;
}
@Override
public V compute(K key,
BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
if (remappingFunction == null)
throw new NullPointerException();
int hash = hash(key);
Node<K,V>[] tab; Node<K,V> first; int n, i;
int binCount = 0;
TreeNode<K,V> t = null;
Node<K,V> old = null;
if (size > threshold || (tab = table) == null ||
(n = tab.length) == 0)
n = (tab = resize()).length;
//獲取舊節點
if ((first = tab[i = (n - 1) & hash]) != null) {
if (first instanceof TreeNode)
old = (t = (TreeNode<K,V>)first).getTreeNode(hash, key);
else {
Node<K,V> e = first; K k;
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
old = e;
break;
}
++binCount;
} while ((e = e.next) != null);
}
}
V oldValue = (old == null) ? null : old.value;
V v = remappingFunction.apply(key, oldValue);
if (old != null) {//有節點
if (v != null) {//新值存在
old.value = v;//設置新值
afterNodeAccess(old);
}
else//無值,刪除節點
removeNode(hash, key, null, false, true);
}
//沒有節點,但新值存在則添加節點
else if (v != null) {
//紅黑樹添加
if (t != null)
t.putTreeVal(this, tab, hash, key, v);
//鏈表添加
else {
tab[i] = newNode(hash, key, v, first);
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
}
++modCount;
++size;
afterNodeInsertion(true);
}
return v;
}
@Override
public V merge(K key, V value,
BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
if (value == null)
throw new NullPointerException();
if (remappingFunction == null)
throw new NullPointerException();
int hash = hash(key);
Node<K,V>[] tab; Node<K,V> first; int n, i;
int binCount = 0;
TreeNode<K,V> t = null;
Node<K,V> old = null;
//設置大小resize
if (size > threshold || (tab = table) == null ||
(n = tab.length) == 0)
n = (tab = resize()).length;
//first爲對應的鏈表/樹
//得到需要的節點old
if ((first = tab[i = (n - 1) & hash]) != null) {
//紅黑樹
if (first instanceof TreeNode)
old = (t = (TreeNode<K,V>)first).getTreeNode(hash, key);
else {//鏈表
Node<K,V> e = first; K k;
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
old = e;
break;
}
++binCount;
} while ((e = e.next) != null);
}
}
//節點存在
if (old != null) {
V v;
//根據舊值和value,得到設置的新值
if (old.value != null)
v = remappingFunction.apply(old.value, value);
else
v = value;
//新值存在
if (v != null) {
old.value = v;//設置新值
afterNodeAccess(old);
}
else//不存在則刪除節點
removeNode(hash, key, null, false, true);
return v;
}
//新的值存在則添加節點
if (value != null) {
if (t != null)
t.putTreeVal(this, tab, hash, key, value);
else {
tab[i] = newNode(hash, key, value, first);
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
}
++modCount;
++size;
afterNodeInsertion(true);
}
return value;
}