首先爲什麼使用哈希樹模型來實現hashmap?
如果是一個正常的鏈表來存儲數據,只有幾個數據可能不會有太大的性能問題,每次取數據也只能遍歷整個鏈表取尋找。如果數據量非常龐大的時候想要在幾萬幾十萬個數據中遍歷找到一個key性能是非常差的。
後來科學家設計了這種數據結構來存放數據,將整個數據的存放區域分成n份node(節點),將node存放在hashmap內部的一個table中維護起來,每次有新的數據put進來的時候會計算出一個table對應的index來將新數據存放進去,同樣每次取get數據的時候也不需要取遍歷整個數據池,只需要根據index去對應的node,而node中又使用元素next維護着鏈表的下一個節點,所以會在這個單向的鏈表中去尋找該元素,大大的提高了性能。
hashmap內部實際上是一個數組,數組的每個元素是一個單向鏈表。
java7之前數組節點是單向鏈表
java8開始節點會動態的改變,當數據量到達一定的數量,會變成紅黑樹的方式。
然後開始看代碼
然後看看開始都定義了一些什麼
/**
* 數組默認的初始容量-必須是2的冪。
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
最大容量,用於隱式指定較高值的情況
必須是2≤1≤30的冪。
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 負載因子
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
當鏈表長度大於8的時候就會轉化爲紅黑樹的數據結構
*/
static final int TREEIFY_THRESHOLD = 8;
/**
如果發現數的結構小於6的時候就會退回成爲鏈表
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
在轉變成樹之前,還會有一次判斷,只有鍵值對數量大於 64 纔會發生轉換。
這是爲了避免在哈希表建立初期,多個鍵值對恰好被放入了同一個鏈表中而導致不
必要的轉 化。
*/
static final int MIN_TREEIFY_CAPACITY = 64;
然後看一下構造函數
/**
這個構造函數會自定義負載因子和初始的容量來構造一個hashmap
*/
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; // all other fields defaulted
}
/**
此方法會構造一個映射的hashmap
*/
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
接着看一些重要的方法
這個是我們最常用的put方法,調用put方法最終會走到這個方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; //用於局部使用table數組
Node<K,V> p; //通過計算得出的key位於tab中的位置
int n, //tab的size
int i;//位置index
//首先如果第一次存放數據的時候,table使用懶加載初始化擴容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//然後使用 (n - 1) & hash]來計算出一個i(index)來獲取數組節點
//如果獲取到的爲空,那麼通過方法newNode來創建一個節點
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//如果葉子節點不爲空
Node<K,V> e;
K k;
//如果該節點的hash與新元素的hash相同,並且他們的key相同
//或者key相同
//那麼說明該節點與存放的元素是同一個東西,直接覆蓋
if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
e = p;
/**
這裏,如果他們的key不相同,但是hashcode卻相同,那麼說明發生了哈希碰
撞,再或者他們的key也不相同,就會就緒往下執行其他的判斷
*/
/**
如果發現節點元素是樹節點,那麼將元素存放到樹節點中
*/
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
/**
如果不是樹節點,那麼就需要不斷的執行next去獲取鏈條上的下一個node
*/
else {
for (int binCount = 0; ; ++binCount) {
//如果下一個節點是null,就創建一個新的節點
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//binCount爲一共遍歷了多少次,即爲該節點的鏈條的長度,如果到達
//了轉化爲樹節點的閥值那麼就將整個鏈條轉化爲紅黑樹,接着跳出循
//環
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
/**
此時我們已經創建了一個新的節點,並且將該節點保存在了鏈條最後一
個節點的next中。
然後會比較e的hash與目標元素的hash,之前e = p.next已經爲e進行了
賦值,即爲鏈條的最後一個節點,如果此時發現新創建的node和e可以
看作同一個東西的話,那麼直接就將e覆蓋了p。否則e就成爲了該鏈條的
最後一個節點。
*/
if (e.hash == hash && ((k = e.key) == key ||(key!=null&&key.equals(k))))
break;
p = e;
}
}
/**
再然後前面已經將新的節點創建,基本工作已經完成了
onlyIfAbsent 這個參數代表的是如果爲ture,不要更改現有值
*/
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方法掠清楚了,再看一下get方法再幹什麼
//這裏沒有什麼好解釋的,調用getNode去獲取鍵值對返回
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){
//根據tab[(n -1)&hash]拿到一個根節點,如果這個key的hash和根節點的hash
//相同,並且key相同,那麼直接返回根節點的node元素
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//否則
if ((e = first.next) != null) {
//如果節點是紅黑樹,那麼會去紅黑樹中獲取node
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;
}
然後之前的一個方法resize(),看看hashmap是如何進行擴容操作的
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;//將擴容前的node數組存放起來
int oldCap = (oldTab == null) ? 0 : oldTab.length;//舊數組的長度
int oldThr = threshold;//舊的容量
int newCap, newThr = 0;//新數組的長度,新的容量
if (oldCap > 0) {
//判斷,擴容的數組的最大容量不能大於int的最大值
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//當舊的數組的2倍長度小於最大閥值,並且舊的長度大於初始容量的時候進行
//擴容,即爲2倍的進行擴容
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//否則將初始的閥值進行擴容
else if (oldThr > 0)
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;
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//創建了一個新的table
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;
}
接着還有一個重要的方法需要看removeNode(T)
實際上hashmap重寫了AbstractMap的remove方法,最終會調用removeNode這個方法
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab;
Node<K,V> p;
int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
//這裏如果直接再根節點找到了這個key
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
//如果根節點沒有找到
else if ((e = p.next) != null) {
//如果是樹節點,去紅黑樹中把這個node取出來
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
//如果不是樹節點,那麼遍歷整個單向鏈條去找到這個key
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
//這裏進行node節點的移除操作
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
//如果是樹節點,那麼再紅黑書中將其移除
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
//否則,如果是根節點,那麼將原來根節點的元素指向node的下一個node
else if (node == p)
tab[index] = node.next;
//再否則,將node的父節點的next指向node的next
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
//之後整個hashmap的數據結構中已經找不到這個node了
}
return null;
}
下面說個概念,就是hash這個東西
爲什麼會發生哈希碰撞?
因爲兩個元素的哈希值是可能會相同的,哈希不是唯一的,雖然說這個概率很低。
所以當倆個元素hashcode相同的時候,這倆個元素並不一定相同。
反之,當倆個元素equase相同,則他的hash一定是相同的。
hashmap中是使用的key的hashcode。
然後就會發生哈希碰撞。