咱們知道在Java 8中對於HashMap引入了紅黑樹從而提升操做性能,因爲在上一節咱們已經經過圖解方式分析了紅黑樹原理,因此在接下來咱們將更多精力投入到解析原理而不是算法自己,HashMap在Java中是使用比較頻繁的鍵值對數據類型,因此咱們很是有必要詳細去分析背後的具體實現原理,不管是C#仍是Java原理解析,從不打算一行行代碼解釋,我認爲最重要的是設計思路,重要的地方可能會多囉嗦兩句。java
咱們由淺入深,按部就班,首先了解下在HashMap中定義的幾個屬性,稍後會進一步講解爲什麼要定義這個值,難道是靠拍腦殼嗎。算法
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { //默認初始化容量 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //最大容量 static final int MAXIMUM_CAPACITY = 1 << 30; //默認負載因子 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; }
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; }
當實例化HashMap時,咱們不指定任何參數,此時定義負載因子爲0.75f數組
public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); }
當實例化HashMap時,咱們也能夠指定初始化容量,此時默認負載因子仍爲0.75f。ide
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); }
當實例化HashMap時,咱們既指定默認初始化容量,也可指定負載因子,很顯然初始化容量不能小於0,不然拋出異常,若初始化容量超過定義的最大容量,則將定義的最大容量賦值與初始化容量,對於負載因子不能小於或等於0,不然拋出異常。接下來根據提供的初始化容量設置閾值,咱們接下來看看上述tableSizeFor方法實現。函數
static final int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }
這個方法是在作什麼處理呢?閾值 = 2的次冪大於初始化容量的最小值。 到學習java目前爲止,咱們接觸到了模運算【%】、按位左移【<<】、按位右移【>>】,這裏咱們將學習到按位或運算【|】、無符號按位右移【>>>】。按位或運算就是二進制有1,結果就爲1,不然爲0,而無符號按位右移只是高位無正負之分而已。不要看到上述【n | = n >>> 1】一臉懵,實際上就是【n = n | n >>> 1】,和咱們正常進行四則運算一個道理,只不過是邏輯運算和位運算符號不一樣而已罷了。咱們經過以下例子來講明上述結論,假設初始化容量爲5,接下來咱們進行如上運算。源碼分析
0000 0000 0000 0000 0000 0000 0000 0101 cap = 5
0000 0000 0000 0000 0000 0000 0000 0100 n = cap - 1
0000 0000 0000 0000 0000 0000 0000 0010 n >>> 1
0000 0000 0000 0000 0000 0000 0000 0110 n |= n >>> 1
0000 0000 0000 0000 0000 0000 0000 0001 n >>> 2
0000 0000 0000 0000 0000 0000 0000 0111 n |= n >>> 2
0000 0000 0000 0000 0000 0000 0000 0000 n >>> 4
0000 0000 0000 0000 0000 0000 0000 0111 n |= n >>> 4
0000 0000 0000 0000 0000 0000 0000 0000 n >>> 8
0000 0000 0000 0000 0000 0000 0000 0111 n |= n >>> 8
0000 0000 0000 0000 0000 0000 0000 0000 n >>> 16
0000 0000 0000 0000 0000 0000 0000 0111 n |= n >>> 16
如上最終算出來結果爲7,而後加上最初計算時減去的1,因此對於初始化容量爲5的最小2次冪爲8,也就是閾值爲8,要是初始化容量爲8,那麼閾值也爲8。接下來到了咱們的重點插入操做。性能
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
上述插入操做簡短一行代碼,只不過是調用了putVal方法,可是咱們注意到首先計算了鍵的哈希值,咱們看看該方法實現。學習
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
直接理解方法大意是:若傳入的鍵爲空,則哈希值爲0,不然直接調用鍵的本地hashCode方法獲取哈希值,而後對其按位向右移16位,最後進行按位異或(只要不一樣結果就爲1)操做。好像仍是不懂,咱們暫且擱置一下,咱們繼續看看插入方法具體實現。測試
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // 步驟【1】:tab爲空擴容 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 步驟【2】:計算index,並對null作處理 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; // 步驟【3】:鍵存在,直接覆蓋值 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; // 步驟【4】:若爲紅黑樹 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { // 步驟【5】:若爲鏈表 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); //若鏈表長度大於8則轉換爲紅黑樹進行處理 if (binCount >= TREEIFY_THRESHOLD - 1) 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); return oldValue; } } ++modCount; // 步驟【6】:超過最大容量進行擴容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
咱們首先來看來步驟【2】,咱們待會再來看步驟【1】實現,咱們首先摘抄上述獲取鍵的索引邏輯優化
if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null);
上述經過計算出鍵的哈希值並與數組的長度按位與運算,散列算法直接決定鍵的存儲是否分佈均勻,不然會發生衝突或碰撞,嚴重影響性能,因此上述【 (n - 1) & hash 】是發生碰撞的關鍵所在,難道咱們直接調用鍵的本地hashCode方法獲取哈希值不就能夠了嗎,確定是不能夠的,咱們來看一個例子。假設咱們經過調用本地的hashCode方法,獲取幾個鍵的哈希值爲3一、6三、95,同時默認初始化容量爲16。而後調用(n-1 & hash),計算以下:
0000 0000 0000 0000 0000 0000 0001 1111 hash = 31 0000 0000 0000 0000 0000 0000 0000 1111 n - 1 0000 0000 0000 0000 0000 0000 0000 1111 => 15 0000 0000 0000 0000 0000 0000 0011 1111 hash = 63 0000 0000 0000 0000 0000 0000 0000 1111 n - 1 0000 0000 0000 0000 0000 0000 0000 1111 => 15 0000 0000 0000 0000 0000 0000 0111 1111 hash = 95 0000 0000 0000 0000 0000 0000 0000 1111 n - 1 0000 0000 0000 0000 0000 0000 0000 1111 => 15
由於(2 ^ n-1)的低位始終都是1,再按照按位運算(0-1始終爲0)全部最終結果都有1111,這就是爲何返回相同索引的緣由,所以,儘管咱們具備不一樣的哈希值,但結果倒是存儲到哈希桶數組相同索引位置。因此爲了解決低位根本就沒有參與到運算中的問題:經過調用上述hash方法,按位右移16位並異或,解決因低位沒有參與運算致使衝突,提升性能。咱們繼續回到上述步驟【1】,當數組爲空,內部是如何進行擴容的呢?咱們來看看resize方法實現。
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) 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; ...... }
由上可知:當實例化HashMap並沒有參時,此時默認初始化容量爲16,默認閾值爲12,負載因子爲0.75f,當指定參數(初始化容量好比爲5),此時初始化容量爲8,閾值爲8,負載因子爲0.75f。不然也指定了負載因子,則以指定負載因子爲準。同時當超過容量時,擴容後的容量爲原容量的2倍。到這裏咱們發現一個問題:hashTable中的容量可爲奇或偶數,而HashMap中的容量永遠都爲2的次冪即偶數,爲什麼要這樣設計呢?
int index = (n - 1) & hash;
如上爲HashMap計算在哈希桶數組中的索引位置,若HashMap中的容量不爲2的次冪,此時經過按與運算,索引只能爲16或0,這也就意味着將發生更多衝突,也將致使性能不好,本可經過O(1)進行檢索,如今須要O(log n),由於發生衝突時,給定存儲桶中的全部節點都將存儲在紅黑樹中,若容量爲2的次冪,此時按與運算符將和hashTable中計算索引存儲位置的方式等同,以下:
int index = (hash & 0x7FFFFFFF) % tab.length;
按照HashMap計算索引的方式,當咱們從2的次冪中減去1時,獲得的是一個二進制末位全爲1的數字,例如默認初始化容量爲16,若是從中減去1,則獲得15,其二進制表示形式是1111,此時若是對1111進行任意數字的按位與運算,咱們將獲得整數的最後4位,換句話說,等價於對16取模,可是除法運算一般是昂貴的運算,也就是說按位運算比取模運算效率更高。到此咱們知道HashMap中容量爲2的次冪的緣由在於:哈希桶數組索引存儲採起按位運算而非取模運算,因其效率比取模運算更高。進行完上述擴容後容量、閾值從新計算後,接下來則須要對哈希桶數組從新哈希(rehash),請繼續往下看。
在講解上述從新哈希以前,咱們須要重頭開始進行敘述,直到這裏,咱們知道HashMap默認初始化容量爲16,假如咱們有16個元素放入到HashMap中,若是實現了很好的散列算法,那麼在哈希桶數組中將在每一個存儲桶中放入1個元素,在此種狀況下,查找元素僅須要1次,若是是HashMap中有256元素,若是實現了很好的散列算法,那麼在哈希桶數組中將在每一個存儲桶中放入16個元素,同理,在此種狀況下,查找任何一個元素,最多也只須要16次,到這裏咱們能夠知道,若是HashMap中的哈希桶數組存儲的元素增長一倍或幾倍,那麼在每一個存儲桶中查找元素的最大時間成本並不會很大,可是,若是持續維持默認容量即16不變,若是每一個存儲桶中有大量元素,此時,HashMap的映射性能將開始降低。好比如今HashMap中有一千六百萬條數據,若是實現了很好的散列算法,將在每一個存儲桶中分配一百萬個元素,也就是說,查找任意元素,最多須要查找一百萬次。很顯然,咱們將存儲的元素放大後,將嚴重影響HashMap性能,那麼對此咱們有何解決方案呢?讓咱們回到最初的話題,由於默認存儲桶大小爲16且當存儲的元素條目少時,HashMap性能並未有什麼改變,可是當存儲桶的數量持續增長時,將影響HashMap性能,這是因爲什麼緣由致使的呢?主要是咱們一直在維持容量固定不變,咱們卻一直增長HashMap中哈希桶數組中存儲元素的大小,這徹底影響到了時間複雜度。若是咱們增長存儲桶大小,則當每一個存儲桶中的總項開始增長時,咱們將可以使得每一個存儲桶中的元素個數保持恆定,並對於查詢和插入操做保持O(1)的時間複雜度。那麼增長存儲桶大小也就是容量的時機是何時呢?存儲桶的大小(容量)由負載因子決定,負載因子是一種度量,它決定着什麼時候增長存儲桶的大小(容量),以便針對查詢和插入操做保持O(1)的時間複雜度,所以,什麼時候增長容量的大小取決於乘積(初始化容量 * 負載因子),因此容量和負載因子是影響HashMap性能的根本因素。咱們知道默認負載因子是0.75,也就是百分之75,因此增長容量大小的值爲(16 * 0.75)= 12,這個值咱們稱之爲閾值,也就意味着,在HashMap中存儲直到第12個鍵值對時,都將保持容量爲16,等到第13個鍵值對插入到HashMap中時,其容量大小將由默認的16變爲( 16 * 2)= 32。經過上述計算增長容量大小即閾值的公式,咱們從反向角度思考:負載因子比率 = 哈希桶數組中元素個數 / 哈希桶數組桶大小,舉個栗子,若默認桶大小爲16,當插入第一個元素時,其負載因子比率 = 1 / 16 = 0.0625 > 0.75 嗎?若爲否無需增長容量,當插入第13個元素時,其負載因子比率 = 13 / 16 = 0.81 > 0.75嗎?若爲是則需增長容量。講完這裏,咱們再來看看重哈希,在講解爲何要進行重哈希以前,咱們須要瞭解重哈希的概念:從新計算已存儲在哈希桶數組中元素的哈希碼的過程,當達到閾值時,將其移動到另一個更大的哈希桶數組中。當存儲到哈希桶數組中的元素超過了負載因子的限制時,此時將容量增長一倍並進行重哈希。那麼爲什麼要進行重哈希呢?由於容量增長一倍後,如若不處理已存在於哈希桶數組中鍵值對,那麼將容量增長一倍則沒有任何意義,同時呢,也是爲了保持每個存儲桶中元素保持均勻分佈,由於只有將元素均勻的分佈到每個存儲桶中才能實現O(1)時間複雜度。接下來咱們繼續進行重哈希源碼分析
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; }
從總體上分析擴容後進行重哈希分爲三種狀況: ① 哈希桶數組元素爲非鏈表即數組中只存在一個元素 ②哈希桶數組元素爲紅黑樹進行轉換 ③哈希桶數組元素爲鏈表。關於①②狀況就不用我再敘述,咱們接下來重點看看對鏈表的優化處理。也就是以下這一段代碼:
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; }
看到上述代碼咱們不由疑惑爲貌似聲明瞭兩個鏈表,一個低位鏈表(lower),一個高位鏈表(high),咱們暫且不是很理解哈,接下來咱們進入 do {} while () 循環,而後重點來了這麼一句 e.hash & oldCap == 0 ,這是幹啥玩意,根據此行代碼來分別進入到低位鏈表和高位鏈表。好了,咱們經過一例子就很好理解了:假設按照默認初始化容量爲16,而後咱們插入一個爲21的元素,根據咱們上面的敘述,首先計算出哈希值,而後計算出索引位置,爲了便於很直觀的理解,咱們仍是一步步來計算下。
static final int hash(21) { int h; return (21 == null) ? 0 : (h = 21.hashCode()) ^ (h >>> 16); }
調用如上hash方法計算出鍵21的值仍爲21,接下來經過以下按與運算計算出存儲到哈希桶數組中的索引位置。
i = (16 - 1) & 21
最終咱們計算其索引位置即i等於5,由於初始化容量爲16,此時閾值爲12,當插入第13個元素開始進行擴容,容量變爲32,此時若再次按照上述方式計算索引存儲位置爲 i = (32 - 1) & 21 ,結果爲21。從這裏咱們得出結論:當容量爲16時,插入元素21的索引位置爲5,而擴容後容量爲32,此時插入元素21的索引位置爲21,也就是說【擴容後的新的索引 = 原有索引 + 原有容量】。同理,若插入元素爲5,容量爲16,那麼索引位置爲5,若擴容後容量爲32,索引位置一樣也爲5,也就是說【擴容後的索引 = 原有索引】。由於容量始終爲原有容量的2倍(好比16到32即從0000 0000 0000 0000 0000 0000 0001 0000 =》0000 0000 0000 0000 0000 0000 0010 0000)從按位考慮則是高位由0變爲1,也就是說咱們經過計算出元素的哈希值與原有容量按位與運算,若結果等於0,則擴容後索引等於原索引,不然等於原有索引加上原有容量,也就是經過哈希值與原容量按位與運算即 e.hash & oldCap == 0 來判斷新索引位置是否發生了改變,說的更加通俗易懂一點,好比(5 & 16 = 0),那麼元素5擴容後的索引位置爲【新索引 = 原索引 + 0】,同理好比(21 & 16 = 16),那麼元素21的擴容後的索引位置爲【新索引 = 原索引 + 16】。因爲容量始終爲2次冪,如此而節省了以前版本而從新計算哈希的時間從而達到優化。到這裏,咱們能夠進一步總結出容量始終爲2次冪的意義:①哈希桶數組索引存儲採起按位運算而非取模運算,因其效率比取模運算更高 ②優化從新計算哈希而節省時間。最終將索引不變鏈表即低位鏈表和索引改變鏈表即高位鏈表分別放入擴容後新的哈希桶數組中,最終從新哈希過程到此結束。接下來咱們分析將元素如何放入到紅黑樹中的呢?
// 步驟【4】:若爲紅黑樹 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
而後咱們看看上述將值放入到紅黑樹中具體方法實現,以下:
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab, int h, K k, V v) { Class<?> kc = null; boolean searched = false; TreeNode<K,V> root = (parent != null) ? root() : this; for (TreeNode<K,V> p = root;;) { int dir, ph; K pk; if ((ph = p.hash) > h) dir = -1; else if (ph < h) dir = 1; else if ((pk = p.key) == k || (k != null && k.equals(pk))) return p; else if ((kc == null && (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0) { if (!searched) { TreeNode<K,V> q, ch; searched = true; if (((ch = p.left) != null && (q = ch.find(h, k, kc)) != null) || ((ch = p.right) != null && (q = ch.find(h, k, kc)) != null)) return q; } dir = tieBreakOrder(k, pk); } TreeNode<K,V> xp = p; if ((p = (dir <= 0) ? p.left : p.right) == null) { Node<K,V> xpn = xp.next; TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn); if (dir <= 0) xp.left = x; else xp.right = x; xp.next = x; x.parent = x.prev = xp; if (xpn != null) ((TreeNode<K,V>)xpn).prev = x; moveRootToFront(tab, balanceInsertion(root, x)); return null; } } }
咱們須要思考的是:(1)待插入元素在紅黑樹中的具體位置是在哪裏呢?(2)找到插入具體位置後,而後須要知道的究竟是左邊仍是右邊呢?。咱們按照思路理解的話仍是很是容易想明白,咱們從根節點開始遍歷樹,經過每個節點的哈希值與待插入節點哈希值比較,若待插入元素位於其父節點的左邊,則看父節點的左邊是否已存在元素,若是不存在則將其父節點的左邊節點留給待插入節點,同理對於父節點的右邊也是如此,可是若是父節點的左邊和右邊都有其引用,那麼就繼續遍歷,直到找到待插入節點的具體位置。這就是咱們在寫代碼或進行代碼測試時的正常思路,可是咱們還要考慮邊界問題,不然說明考慮不徹底,針對待插入元素插入到紅黑樹中的邊界問題是什麼呢?當遍歷的節點和待插入節點的哈希值相等,那麼此時咱們應該肯定元素的順序來保持樹的平衡呢?也就是上述中的以下代碼:
else if ((kc == null && (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0) { if (!searched) { TreeNode<K,V> q, ch; searched = true; if (((ch = p.left) != null && (q = ch.find(h, k, kc)) != null) || ((ch = p.right) != null && (q = ch.find(h, k, kc)) != null)) return q; } dir = tieBreakOrder(k, pk); }
爲了解決將元素插入到紅黑樹中,如何肯定元素順序的問題。經過兩種方案來解決:①實現Comprable接口 ②突破僵局機制。接下來咱們來看實現Comprable例子,以下:
public class PersonComparable implements Comparable<PersonComparable> { int age; public PersonComparable(int age) { this.age = age; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj instanceof PersonComparable) { PersonComparable p = (PersonComparable) obj; return (this.age == p.age); } return false; } @Override public int hashCode() { return 42; } @Override public int compareTo(PersonComparable o) { return this.age - o.age; } }
而後咱們在控制檯中,調用以下代碼進行測試:
HashMap hashMap = new HashMap(); Person p1 = new Person(1); Person p2 = new Person(2); Person p3 = new Person(3); Person p4 = new Person(4); Person p5 = new Person(5); Person p6 = new Person(6); Person p7 = new Person(7); Person p8 = new Person(8); Person p9 = new Person(9); Person p10 = new Person(10); Person p11 = new Person(11); Person p12 = new Person(12); Person p13 = new Person(13); hashMap.put(p1, "1"); hashMap.put(p2, "2"); hashMap.put(p3, "3"); hashMap.put(p4, "4"); hashMap.put(p5, "5"); hashMap.put(p6, "6"); hashMap.put(p7, "7"); hashMap.put(p8, "8"); hashMap.put(p9, "9"); hashMap.put(p10, "10"); hashMap.put(p11, "11"); hashMap.put(p12, "12"); hashMap.put(p13, "13");
反觀上述代碼,咱們實現了Comprable接口而且直接重寫了hashcode爲常量值,此時將產生衝突,插入到HashMap中每個元素的哈希值都相等即索引位置同樣,也就是最終將由鏈表轉換爲紅黑樹,既然哈希值同樣,那麼咱們如何肯定其順序呢?,此時咱們回到上述 comparableClassFor 方法和 compareComparables 方法(兩者具體實現就不一一解釋了)
// 若實現Comparable接口返回其具體實現類型,不然返回空 static Class<?> comparableClassFor(Object x) { if (x instanceof Comparable) { Class<?> c; Type[] ts, as; Type t; ParameterizedType p; if ((c = x.getClass()) == String.class) // bypass checks return c; if ((ts = c.getGenericInterfaces()) != null) { for (int i = 0; i < ts.length; ++i) { if (((t = ts[i]) instanceof ParameterizedType) && ((p = (ParameterizedType)t).getRawType() == Comparable.class) && (as = p.getActualTypeArguments()) != null && as.length == 1 && as[0] == c) // type arg is c return c; } } } return null; }
//調用自定義實現Comprable接口比較器,從而肯定順序 static int compareComparables(Class<?> kc, Object k, Object x) { return (x == null || x.getClass() != kc ? 0 : ((Comparable)k).compareTo(x)); }
可是要是上述咱們實現的Person類沒有實現Comprable接口,此時將使用突破僵局機制(我猜想做者對此方法的命名是不是來自於維基百科《https://en.wikipedia.org/wiki/Tiebreaker》,比較貼切【突破僵局制(英語:tiebreaker),是一種延長賽的制度,主要用於棒球與壘球運動,特別是採淘汰制的季後賽及國際賽,以免比賽時間太久仍沒法分出勝負。】),也就是對應以下代碼:
dir = tieBreakOrder(k, pk);
static int tieBreakOrder(Object a, Object b) { int d; if (a == null || b == null || (d = a.getClass().getName(). compareTo(b.getClass().getName())) == 0) d = (System.identityHashCode(a) <= System.identityHashCode(b) ? -1 : 1); return d; }
由於哈希值相等,同時也沒有實現Comparable接口,可是咱們又不得不解決這樣實際存在的問題,能夠說是最終採起「無可奈何「的解決方案,經過調用上述 System.identityHashCode 來獲取對象惟一且恆定的哈希值從而肯定順序。好了,那麼問題來了對於實現Comparable接口的鍵插入到樹中和未實現接口的鍵插入到樹中,兩者有何區別呢?若是鍵實現Comparable接口,查找指定元素將會使用樹特性快速查找,若是鍵未實現Comparable接口,查找到指定元素將會使用遍歷樹方式查找。問題又來了,既然經過實現Comparable接口的比較器來肯定順序,那爲什麼不直接使用突破僵局機制來做爲比較器?咱們來看看那以下例子:
Person person1 = new Person(1); Person person2 = new Person(1); System.out.println(System.identityHashCode(person1) == System.identityHashCode(person2));
到這裏咱們知道,即便是兩個相同的對象實例其identityHashCode都是不一樣的,因此不能使用identityHashCode做爲比較器。問題又來了,既然使用identityHashCode肯定元素順序,當查找元素時是採用遍歷樹的方式,徹底沒有利用到樹的特性,那麼爲什麼還要構造樹呢?由於HashMap可以包含不一樣對象實例的鍵,有些可能實現了Comparable接口,有些可能未實現。咱們來看以下混合不一樣類例子:
public class Person { int age; public Person(int age) { this.age = age; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj instanceof Person) { Person p = (Person) obj; return (this.age == p.age); } return false; } @Override public int hashCode() { return 42; } }
HashMap hashMap = new HashMap(); PersonComparable p1 = new PersonComparable (1); PersonComparable p2 = new PersonComparable (2); PersonComparable p3 = new PersonComparable (3); PersonComparable p4 = new PersonComparable (4); PersonComparable p5 = new PersonComparable (5); PersonComparable p6 = new PersonComparable (6); Person p7 = new Person(7); Person p8 = new Person(8); Person p9 = new Person(9); Person p10 = new Person(10); Person p11 = new Person(11); Person p12 = new Person(12); Person p13 = new Person(13); hashMap.put(p1, "1"); hashMap.put(p2, "2"); hashMap.put(p3, "3"); hashMap.put(p4, "4"); hashMap.put(p5, "5"); hashMap.put(p6, "6"); hashMap.put(p7, "7"); hashMap.put(p8, "8"); hashMap.put(p9, "9"); hashMap.put(p10, "10"); hashMap.put(p11, "11"); hashMap.put(p12, "12"); hashMap.put(p13, "13");
當使用混合模式時即實現了Comparable接口和未實現Comparable接口的對象實例能夠基於類名來比較鍵。
本節咱們詳細講解了HashMap實現原理細節,一些比較簡單的地方就沒有再一一分析,文中如有敘述不當或理解錯誤之處,還望指正,感謝您的閱讀,咱們下節再會。