今天,打算寫一篇HashMap的源碼解析,主要是針對增刪改查操作,廢話不多說,直接開始。
先看看hashMap在jdk 1.8的結構,如下圖,用的是數組+鏈表+紅黑樹的結構,也叫哈希桶,在jdk 1.8之前都是數組+鏈表的結構,因爲在鏈表的查詢操作都是O(N)的時間複雜度,而且hashMap中查詢操作也是佔了很大比例的,如果當節點數量多,轉換爲紅黑樹結構,那麼將會提高很大的效率,因爲紅黑樹結構中,增刪改查都是O(log n)。
哈希桶就是數組裏面的一個位置中所佔所有數據,例如,下圖中,綠色節點所佔的該數組的位置,以及它連接的鏈表,整體爲一個哈希桶。
hashMap的屬性:
上面簡單的說了一下其結構,可能大家還不是很理解,下面從源碼開始看,就應該很容易去理解。
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
//序列號,序列化的時候使用。
private static final long serialVersionUID = 362498820763181265L;
/**默認容量,1向左移位4個,00000001變成00010000,也就是2的4次方爲16,使用移位是因爲移位是計算機基礎運算,效率比加減乘除快。**/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量,2的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;
//當整個hashMap中元素數量大於64時,也會進行轉爲紅黑樹結構。
static final int MIN_TREEIFY_CAPACITY = 64;
//存儲元素的數組,transient關鍵字表示該屬性不能被序列化
transient Node<K,V>[] table;
//將數據轉換成set的另一種存儲形式,這個變量主要用於迭代功能。
transient Set<Map.Entry<K,V>> entrySet;
//元素數量
transient int size;
//統計該map修改的次數
transient int modCount;
//臨界值,也就是元素數量達到臨界值時,會進行擴容。
int threshold;
//也是加載因子,只不過這個是變量。
final float loadFactor;
這裏講講爲什麼默認容量大小爲16,加載因子爲0.75,主要原因是這兩個常量的值都是經過大量的計算和統計得出來的最優解,僅僅是這樣而已。
上面是hashMap的屬性,儘量的解釋給大家,下面再說說它裏面的內部類,並不是所有的內部類,只說常用的。
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent;
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev;
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
}
使用靜態內部類,是爲了方便調用,而不用每次調用裏面的屬性或者方法都需要new一個對象。這是一個紅黑樹的結構,如果沒有學過紅黑樹的同學,自己去看一下,內容太多,就不在這裏闡述了。
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;
}
}
裏面還包含了一個結點內部類,是一個單向鏈表。上面這兩個內部類再加上之前的Node<K,V>[] table屬性,組成了hashMap的結構,哈希桶。
構造方法:
大致懂了hashMap的結構,我們來看看構造方法,一共有3個。
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
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);
}
第一個,空參構造,使用默認的加載因子0.75;第二個,設置初始容量,並使用默認的加載因子;第三個,設置初始容量和加載因子,其實第二個構造方法也是調用了第三個。下面,在看看最後一個構造函數。
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) {
//獲取該map的實際長度
int s = m.size();
if (s > 0) {
//判斷table是否初始化,如果沒有初始化
if (table == null) { // pre-size
/**求出需要的容量,因爲實際使用的長度=容量*0.75得來的,+1是因爲小數相除,基本都不會是整數,容量大小不能爲小數的,後面轉換爲int,多餘的小數就要被丟掉,所以+1,例如,map實際長度22,22/0.75=29.3,所需要的容量肯定爲30,有人會問如果剛剛好除得整數呢,除得整數的話,容量大小多1也沒什麼影響**/
float ft = ((float)s / loadFactor) + 1.0F;
//判斷該容量大小是否超出上限。
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
/**對臨界值進行初始化,tableSizeFor(t)這個方法會返回大於t值的,且離其最近的2次冪,例如t爲29,則返回的值是32**/
if (t > threshold)
threshold = tableSizeFor(t);
}
//如果table已經初始化,則進行擴容操作,resize()就是擴容。
else if (s > threshold)
resize();
//遍歷,把map中的數據轉到hashMap中。
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);
}
}
}
該構造函數,傳入一個Map,然後把該Map轉爲hashMap,resize方法在下面添加元素的時候會詳細講解,在上面中entrySet方法會返回一個Set<Map.Entry<K, V>>,泛型爲Map的內部類Entry,它是一個存放key-value的實例,也就是Map中的每一個key-value就是一個Entry實例,爲什麼使用這個方式進行遍歷,因爲效率高,具體自己百度一波,putVal方法把取出來的每個key-value存入到hashMap中,待會會仔細講解。
構造函數和屬性講得差不多了,下面要講解的是增刪改查的操作以及常用的、重要的方法,畢竟裏面的方法太多了,其它的就自己去看看吧。
添加元素:
在講解put方法之前,先看看hash方法,看怎麼計算哈希值的。
static final int hash(Object key) {
int h;
/**先獲取到key的hashCode,然後進行移位再進行異或運算,爲什麼這麼複雜,不用想肯定是爲了減少hash衝突**/
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
下面來看看put方法。
public V put(K key, V value) {
/**四個參數,第一個hash值,第四個參數表示如果該key存在值,如果爲null的話,則插入新的value,最後一個參數,在hashMap中沒有用,可以不用管,使用默認的即可**/
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//tab 哈希數組,p 該哈希桶的首節點,n hashMap的長度,i 計算出的數組下標
Node<K,V>[] tab; Node<K,V> p; int n, i;
//獲取長度並進行擴容,使用的是懶加載,table一開始是沒有加載的,等put後纔開始加載
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
/**如果計算出的該哈希桶的位置沒有值,則把新插入的key-value放到此處,此處就算沒有插入成功,也就是發生哈希衝突時也會把哈希桶的首節點賦予p**/
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//發生哈希衝突的幾種情況
else {
// e 臨時節點的作用, k 存放該當前節點的key
Node<K,V> e; K k;
//第一種,插入的key-value的hash值,key都與當前節點的相等,e = p,則表示爲首節點
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//第二種,hash值不等於首節點,判斷該p是否屬於紅黑樹的節點
else if (p instanceof TreeNode)
/**爲紅黑樹的節點,則在紅黑樹中進行添加,如果該節點已經存在,則返回該節點(不爲null),該值很重要,用來判斷put操作是否成功,如果添加成功返回null**/
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//第三種,hash值不等於首節點,不爲紅黑樹的節點,則爲鏈表的節點
else {
//遍歷該鏈表
for (int binCount = 0; ; ++binCount) {
//如果找到尾部,則表明添加的key-value沒有重複,在尾部進行添加
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//判斷是否要轉換爲紅黑樹結構
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
//如果鏈表中有重複的key,e則爲當前重複的節點,結束循環
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//有重複的key,則用待插入值進行覆蓋,返回舊值。
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
//到了此步驟,則表明待插入的key-value是沒有key的重複,因爲插入成功e節點的值爲null
//修改次數+1
++modCount;
//實際長度+1,判斷是否大於臨界值,大於則擴容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
//添加成功
return null;
}
上面就是具體的元素添加,在元素添加里面涉及到擴容,我們來看看擴容方法resize。
final Node<K,V>[] resize() {
//把沒插入之前的哈希數組做我誒oldTal
Node<K,V>[] oldTab = table;
//old的長度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//old的臨界值
int oldThr = threshold;
//初始化new的長度和臨界值
int newCap, newThr = 0;
//oldCap > 0也就是說不是首次初始化,因爲hashMap用的是懶加載
if (oldCap > 0) {
//大於最大值
if (oldCap >= MAXIMUM_CAPACITY) {
//臨界值爲整數的最大值
threshold = Integer.MAX_VALUE;
return oldTab;
}
//標記##,其它情況,擴容兩倍,並且擴容後的長度要小於最大值,old長度也要大於16
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//臨界值也擴容爲old的臨界值2倍
newThr = oldThr << 1;
}
/**如果oldCap<0,但是已經初始化了,像把元素刪除完之後的情況,那麼它的臨界值肯定還存在,
如果是首次初始化,它的臨界值則爲0
**/
else if (oldThr > 0)
newCap = oldThr;
//首次初始化,給與默認的值
else {
newCap = DEFAULT_INITIAL_CAPACITY;
//臨界值等於容量*加載因子
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//此處的if爲上面標記##的補充,也就是初始化時容量小於默認值16的,此時newThr沒有賦值
if (newThr == 0) {
//new的臨界值
float ft = (float)newCap * loadFactor;
//判斷是否new容量是否大於最大值,臨界值是否大於最大值
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
table = newTab;
//此處自然是把old中的元素,遍歷到new中
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
//臨時變量
Node<K,V> e;
//當前哈希桶的位置值不爲null,也就是數組下標處有值,因爲有值表示可能會發生衝突
if ((e = oldTab[j]) != null) {
//把已經賦值之後的變量置位null,當然是爲了好回收,釋放內存
oldTab[j] = null;
//如果下標處的節點沒有下一個元素
if (e.next == null)
//把該變量的值存入newCap中,e.hash & (newCap - 1)並不等於j
newTab[e.hash & (newCap - 1)] = e;
//該節點爲紅黑樹結構,也就是存在哈希衝突,該哈希桶中有多個元素
else if (e instanceof TreeNode)
//把此樹進行轉移到newCap中
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { /**此處表示爲鏈表結構,同樣把鏈表轉移到newCap中,就是把鏈表遍歷後,把值轉過去,在置位null**/
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;
}
}
}
}
}
//返回擴容後的hashMap
return newTab;
}
上部分內容就是整個擴容過程的操作,下面再來看看刪除方法,remove。
刪除元素:
public V remove(Object key) {
//臨時變量
Node<K,V> e;
/**調用removeNode(hash(key), key, null, false, true)進行刪除,第三個value爲null,表示,把key的節點直接都刪除了,不需要用到值,如果設爲值,則還需要去進行查找操作**/
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
/**第一參數爲哈希值,第二個爲key,第三個value,第四個爲是爲true的話,則表示刪除它key對應的value,不刪除key,第四個如果爲false,則表示刪除後,不移動節點**/
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
//tab 哈希數組,p 數組下標的節點,n 長度,index 當前數組下標
Node<K,V>[] tab; Node<K,V> p; int n, index;
//哈希數組不爲null,且長度大於0,然後獲得到要刪除key的節點所在是數組下標位置
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
//nodee 存儲要刪除的節點,e 臨時變量,k 當前節點的key,v 當前節點的value
Node<K,V> node = null, e; K k; V v;
//如果數組下標的節點正好是要刪除的節點,把值賦給臨時變量node
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
//也就是要刪除的節點,在鏈表或者紅黑樹上,先判斷是否爲紅黑樹的節點
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
//遍歷紅黑樹,找到該節點並返回
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else { //表示爲鏈表節點,一樣的遍歷找到該節點
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
/**注意,如果進入了鏈表中的遍歷,那麼此處的p不再是數組下標的節點,而是要刪除結點的上一個結點**/
p = e;
} while ((e = e.next) != null);
}
}
//找到要刪除的節點後,判斷!matchValue,我們正常的remove刪除,!matchValue都爲true
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);
//如果是鏈表結構,且刪除的節點爲數組下標節點,也就是頭結點,直接讓下一個作爲頭
else if (node == p)
tab[index] = node.next;
else /**爲鏈表結構,刪除的節點在鏈表中,把要刪除的下一個結點設爲上一個結點的下一個節點**/
p.next = node.next;
//修改計數器
++modCount;
//長度減一
--size;
/**此方法在hashMap中是爲了讓子類去實現,主要是對刪除結點後的鏈表關係進行處理**/
afterNodeRemoval(node);
//返回刪除的節點
return node;
}
}
//返回null則表示沒有該節點,刪除失敗
return null;
}
刪除還有clear方法,把所有的數組下標元素都置位null,下面在來看看較爲簡單的獲取元素與修改元素操作。
獲取元素:
public V get(Object key) {
Node<K,V> e;
//也是調用getNode方法來完成的
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
//first 頭結點,e 臨時變量,n 長度,k 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))))
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;
}
修改元素:
元素的修改也是put方法,因爲key是唯一的,所以修改元素,是把新值覆蓋舊值。
hashMap的源碼分析暫時到這裏,主要的增刪改查操作都已經分析完了,如果對於紅黑樹結構中的增刪改查不懂的話,可以去看看,因爲這部分也夠寫好幾篇內容了,我就不在這裏闡述了,因個人能力有限,如果裏面的內容有錯,歡迎提出,指點。後面會繼續有源碼分析系列的內容,大家也可以多關注我。