Java基礎之HashMap的原理

講到HashMap不得不講到HashCode,百度HashCode,它的釋義以下
哈希碼並非徹底惟一的,它是一種算法,讓同一個類的對象按照本身不一樣的特徵儘可能的有不一樣的哈希碼,但不表示不一樣的對象哈希碼徹底不一樣。也有相同的狀況,看程序員如何寫哈希碼的算法。
HashCode是用來在散列存儲結構中肯定對象的存儲地址,HashMap正是利用HashCode來快速定位存儲對象的。
上面說過HashCode並非惟一的,他取決於設計的哈希算法,因此在HashMap會出現Hash衝突的狀況,那HashMap是怎麼處理這種問題的呢?答案是在產生衝突的地方使用鏈表和紅黑樹。
在JDK1.8以前解決hash衝突使用的是鏈表,實際上初始化後的HashMap就是由長度爲1的單向鏈表組成的數組,在發生hash衝突時,該節點的單向鏈表保存具備相同hashcode的對象。在JDK1.8以後,當節點的單向鏈表長度大於8時,改成使用紅黑樹,提升查找效率。
HashMap是平常開發中很是經常使用的容器,HashMap實現了Map接口,底層的實現原理是哈希表,HashMap不是一個線程安全的容器,jdk8對HashMap作了一些改進,做爲開發人員須要對HashMap的原理有所瞭解,如今就經過源碼來了解HashMap的實現原理。
首先看HashMap中的屬性
//Node數組
transient Node<K,V>[] table;
//當前哈希表中k-v對個數,實際就是node的個數
transient int size;
//修改次數
transient int modCount;
//元素閾值
int threshold;
//負載因子
final float loadFactor;
這裏的threshold = loadFactor * table.length,hash表若是想要保持比較好的性能,數組的長度一般要大於元素個數,默認的負載因子是0.75,用戶能夠自行修改,不過最好使用默認的負載因子。
Node是用來存儲KV的節點,每次put(k,v)的時候就會包裝成一個新的Node, Node定義
static class Node<K,V> implements Map.Entry<K,V> {
//hash值
final int hash;
final K key;
V value;
//hash & (capacity - 1) 相同的Node會造成一個鏈表
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
put操做
寫入操做是map中最經常使用的方法,這裏看看hashmap的put方法代碼
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
這裏先計算key的hash值,而後調用putVal()方法,其中hash方法是內部自帶的一個算法,會對key的hashcode再作一次hash操做html

static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
pubVal方法
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) //若是對應的數組爲空的話,那麼就直接new一個node而後塞進去
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; //若是頭結點的key和要插入的key相同,那麼就說明找到了以前插入的節點
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) { //若是以前沒有put過這個節點,那麼就new一個新的節點
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1)
//另外要檢查一下當前鏈表的長度,若是超過8那麼就將鏈表轉化成紅黑樹
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
//若是找到了以前的節點,那麼就跳出
break;
p = e;
}
}
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e); //在當前類中NOOP
return oldValue;
}
}
++modCount;
//若是當前元素數量大於門限值,就要resize整個hash表,實際上就是把數組擴大一倍,而後將全部元素從新塞到新的hash表中
if (++size > threshold)
resize();
afterNodeInsertion(evict); //在該類中NOOP
return null;
}
在hashtable中默認的出現衝突的時候就會將衝突的元素造成一個鏈表,當鏈表長度大於8的時候就會將鏈表變成一個二叉樹,這是java8中作出的改進,由於在使用hash表的時候在key特殊的狀況下最壞的時候hash表會退化成一個鏈表,那麼原有的O(1)的時間複雜度就變成了O(n),性能就會大打折扣,可是引用了紅黑樹以後那麼在最好的狀況下時間複雜度就變成了O(log(n))。
resize方法
final Node<K, V> [] resize() {
......
//去掉了一些代碼,只關注最核心的node遷移
//resize會新建一個數組,數組的長度是原來數組長度的兩倍
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; //若是沒有造成鏈表的話,就直接塞到新的hash表中
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) { //若是hash值小於oldCap的時候,那麼就還在原來那個數組的位置,就把這個節點放到low鏈表中
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else { //不然的話就是由於擴展數組長度,就把原來的節點放到high鏈表中
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead; //low鏈表還放在原來的位置
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead; //high鏈表放到j+oldCap位置上
}
}
}
}
}
resize操做就是建立一個先的數組,而後把老的數組中的元素塞到新的數組中,注意java8中的hashMap中數組長度都是2的n次冪,二、四、、八、16….. 這樣的好處就是能夠經過與操做來替代求餘操做。當數組擴大以後,那麼每一個元素所在的位置是能夠預期的,就是要不就待在原來的位置,要不就是到j+oldCap位置上,舉個栗子,若是原來數組長度爲4,那麼hash爲3和7 的元素都會放在index爲3的位置上,當數組長度變成8的時候,hash爲3的元素還待在index爲3的位置,hash爲7的元素此時就要放到index爲7的位置上。
resize操做是一個很重要的操做,resize會很消耗性能,所以在建立hashMap的時候最好先預估容量,防止重複建立拷貝。
另外hashmap也是非線程安全的,在多線程操做的時候可能會產生cpu100%的狀況,主要的緣由也是由於在多個線程resize的時候致使鏈表產生了環,這樣下次get操做的時候就會容易進入死循環。
get方法()
get的實現比較簡單
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 && ((k = first.key) == key || (key != null && key.equals(k)))) //若是節點不爲空並且頭結點與查找的key相同就返回
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); //遍歷鏈表查找key相同的node
}
}
return null;
}
HashMap的常量和構造函數
//默認初始容量,這個值必須是2的次冪
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16
//最大容量,也必須設置成2的次冪
static final int MAXIMUM_CAPACITY = 1 << 30; // 1 073 741 824
//負載因子默認大小
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//節點保存數據數量超過這個值,由鏈表轉爲紅黑樹
static final int TREEIFY_THRESHOLD = 8;
//當節點保存數據數量小於該值,紅黑樹轉爲鏈表存儲
static final int UNTREEIFY_THRESHOLD = 6;
//紅黑樹最小長度
static final int MIN_TREEIFY_CAPACITY = 64;
//HashMap中實際就是用這樣一個元素類型爲Node<K,V>的數組來存儲數據,該數組長度必須爲2的次冪
//每一個Node本質上就是一個單向鏈表
transient Node<K,V>[] table;
//HashMap大小,它表明HashMap保存的鍵值對的多少
transient int size;
//HashMap被改變的次數
transient int modCount;
//HashMap擴容的閾值,保存鍵值對個數大於它就要擴容
int threshold;
//存儲負載因子的常量
final float loadFactor;
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
if (table == null) { // pre-size
//根據loadFactor以及MAXIMUM_CAPACITY再次計算出新的threshold
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
threshold = tableSizeFor(t);
}
//數量超過閾值擴容
else if (s > threshold)
resize();
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
HashMap中的元素單向鏈表Node是Map.Entry<K,V>的實現,能夠看到每個Node只有next屬性,沒有pre屬性,是一個單向鏈表
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
HashMap中的紅黑樹也是本身實現的,這裏就不詳細列出其實現
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
//省略...
}
HashMap主要經常使用API get和put方法
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;
//使用key參與hash運算後得出的hash,找出該位置的鏈表節點的第一個元素first
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//老是先檢查鏈表第一個元素first是否匹配,匹配上就返回
if (first.hash == hash &&
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//匹配不上,就使用first的next屬性找到下一個節點e
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);
}
}
//以上都沒法匹配就返回null
return null;
}
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
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;
//若是使用該hash計算出XM返傭www.fx61.com/brokerlist/xm.html的位置沒有元素,就把元素保存在這個位置
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//若是該位置有數據,且該節點Node的hash和key都與傳入值相同或調用傳入鍵對象的equals方法與節點Node的key比較相同,則覆蓋該數據
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//若是上面的if判斷不知足,且該位置節點Node是紅黑樹類型,使用紅黑樹保存
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//若是if知足也不是紅黑樹類型節點,則找出該位置單向鏈表最後一個位置的元素,把它的next指向新傳入數據保存位置
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//添加完後,若該節點鏈表長度超過8,則轉換爲紅黑樹
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;
}
爲何HashMap的容量必須設置爲2的次冪呢?
其實主要是爲了在哈希算法中減小碰撞,使數據均勻分佈。這裏就不深究了。java

相關文章
相關標籤/搜索