本文會對 Android 中經常使用HashMap(有紅黑樹)和ArrayMap進行源碼解析,其中 HashMap 源碼來自 Android Framework API 28 (JDK=1.8), ArrayMap 的源碼和 AndroidXnode
//AndroidX
implementation 'androidx.collection:collection:1.1.0-alpha03'
複製代碼
HashMap<K,V>
本着全世界都知道面試必定會問 HashMap 的前提下,咱們第一個分析 HashMap .android
首先說明,併發時只讀不寫是沒有問題的,可是併發時有讀有寫時會出現問題, HashMap 不是線程安全的,而且在JDK<=1.7
多線程狀況下調用put
引發擴容時還有可能致使循環鏈表問題,從而死循環使 CPU 佔用率變成 100% .因爲本文基於API 28
也就是JDK=1.8
,已經沒有了這個問題,因此這裏只是簡單提一下,不作展開討論,有興趣的同窗能夠自行查閱資料.面試
HashMap 是一種散列表,是一種典型的用空間換時間的數據結構,在內部使用拉鍊法處理 Hash 碰撞,也就是說在 Hash 後本身要放到表裏時,發現本身的坑已經被別人佔了,那就把以前的佔坑者做爲鏈表的頭結點,本身做爲下一個結點連到後面去.算法
//就像這樣
[ Node<K,V> , null , Node<K,V> , null , Node<K,V> , null , Node<K,V> , null , null ......]
↓ ↓ ↓
Node<K,V> Node<K,V> Node<K,V>
↓ ↓
Node<K,V> Node<K,V>
↓
Node<K,V>
複製代碼
在 JDK 1.8 中,鏈表長度是有一個臨界值的,由於過長的鏈表會增大平均搜索時間,因此當鏈表長度大於 8 時,將鏈表轉換爲紅黑樹(本文不會規避這個話題,會講紅黑樹),以提升搜索效率.編程
在理想狀況下 HashMap 查找元素的的時間複雜度爲O(1)
,這個複雜度會隨負載因子的變大而變大,當負載因子變大時,一樣容量的 HashMap 中可以存儲更多的元素,可是同時也會致使 Hash 碰撞變得更加頻繁,從而下降 HashMap 的搜索效率.數組
首先固然要從構造器開始提及:緩存
這裏咱們要區分容量 (Capacity) 和大小 (Size) 這兩個概念,容量是指 HashMap 中用來裝節點的桶的數量,而大小則是該 HashMap 中總共存了多少個鍵值對.安全
initialCapacity 爲初始容量,這個值並非 HashMap 實際的容量,由於 HashMap 的容量必須是 2 的冪,因此這個值在後面會被處理,變成 2 的冪, loadFactor 是負載因子,跟擴容時的臨界值有關,後面會介紹, MAXIMUM_CAPACITY 是 HashMap 的最大容量在代碼中是一個int,值爲1<<30
,由於最高位是符號位因此不能<<31
,這樣一來就能表示出用 int 存儲的 2 的冪的最大正整數bash
//構造函數內主要是作一些檢查參數的工做
//重點看tableSizeFor這個方法
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;
//初始化臨界值
//每當HashMap達到這個臨界值時就會進行擴容操做
this.threshold = tableSizeFor(initialCapacity);
}
複製代碼
構造器的末尾走到了tableSizeFor
這個方法裏.咱們進一步追蹤到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;
}
複製代碼
這麼多位移運算符這是要幹啥呢?
前面說過 HashMap 的容量必須是 2 的冪,tableSizeFor
所作的工做其實就是找到最接近於你所給 initialCapacity 的 2 的冪.
要解析這裏的代碼,咱們要先知道參數 cap 的範圍是怎樣的,經過看以前的構造函數,咱們知道參數 cap 確定是大於 0 的,因此接下來就要分兩種狀況了.
n |= n >>> 1
和下面的 4 行代碼均可以跳過,由於>>>
是無符號右移運算符,當 cap 爲 1 時,n 爲 0,對其進行>>>
和|=
後其值仍是0,最後通過下面的兩個三元運算符,得到的返回值是 1 ,也就是 2 的 0 次冪....0001001
,那麼 n 的二進制表示就是...0001000
執行>>> 1
獲得 ...0000100
執行|=
獲得n爲 ...0001100
執行>>> 2
獲得 ...0000011
執行|=
獲得n爲 ...0001111
執行>>> 4
獲得 ...0000000
執行|=
獲得n爲 ...0001111
...
複製代碼
最終咱們會獲得...0001111
也就是1 + 2 + 4 + 8 = 15,誒不是說好的 2 的冪嗎?這不是 15 嗎,看到return
語句的最後一句了沒有,還要+1
,因此返回的值是 16 仍是 2 的冪,而且是最接近於 9 的二的冪.
那若是咱們拿 8 做爲 cap 會怎樣呢? 8 是 2 冪,二進制表示爲...0001000
,進行-1
後就是...0000111
執行>>> 1
獲得 ...0000011
執行|=
獲得n爲 ...0000111
...
複製代碼
你會發現他變回去了,這就是這個算法的神奇之處,對於 2 的冪tableSizeFor
後仍是自己,若是不是 2 的冪,則求出最接近該值的 2 的冪.
這是 HashMap 的另外一個構造器,就是調用了上面的那個構造器而已. DEFAULT_LOAD_FACTOR 的值爲 0.75 ,是通過測試過的比較理想的值.
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
複製代碼
這個構造器也差很少,不過沒有設置 initialCapacity ,實際上是它在擴容函數reszie
中設置了,這樣構造的 HashMap 擁有默認容量 16.
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
複製代碼
下面繼續看下一個構造器
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
複製代碼
putMapEntries
方法除了構造器會調用,其實在其餘時候也會被調用可是構造器調用時參數 evict 爲 false ,其餘時候是 true .
這裏涉及到幾個字段,要先說一下,分別是table
,threshold
,loadFactor
//HashMap管理的節點表,在第一次使用時初始化,並根據須要調整大小
//分配時,長度老是2的冪
//這個數組的每個項你能夠把它當成一個桶
//這個桶裏面能夠裝好多節點
//咱們的鍵值對就是存在這些節點上的
transient Node<K,V>[] table;
//擴容的臨界值,由負載因子*當前容量獲得
int threshold;
//負載因子,在初始化時肯定下來
//是大小與容量的一個比例
//由此能夠計算出擴容的臨界值
final float loadFactor;
複製代碼
咱們跟着來到 putMapEntries
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
//s等於0就什麼都不作
if (s > 0) {
//table==null,也就是第一次初始化
if (table == null) { // pre-size
//加進來的Map的大小除以負載因子得出新的臨界值
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
//若是新的臨界值比當前的大,則將它轉換爲2的冪
//而後更新臨界值
if (t > threshold)
threshold = tableSizeFor(t);
}
//當前表非空,加進來的Map大小已經大於舊的臨界值,直接擴容
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);
}
}
}
複製代碼
由於上面涉及到了resize
這個方法,咱們要先來說它.這個方法很是重要,是 HashMap 的擴容方法,也是面試中常問的考點,筆者在阿里一面時就被問到了這個問題.
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) {
//而且若是容量到達上限,不進行擴容,直接返回舊錶
//並將臨界值設置爲不可能到達的Integer.MAX_VALUE
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//不然舊容量左移一位獲得新容量,也就是翻倍
//若是翻倍後新的容量仍然小於最大容量
//而且舊容量是大於默認初始容量DEFAULT_INITIAL_CAPACITY(值爲16)的
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//那麼臨界值也左移一位翻倍
newThr = oldThr << 1;
}
//若是當前表不是空表,而且已有臨界值
//這種狀況對應前面在構造函數中
//使用tableSizeFor(initialCapacity)對threshold的賦值
//表自己是空的,沒有元素,因此要進行一次擴容
else if (oldThr > 0)
newCap = oldThr;
//在沒有初始化臨界值時
//先給他設置新的容量爲DEFAULT_INITIAL_CAPACITY(值爲16)
//而後使用默認負載計算出臨界值
//這種狀況對應上文中的第三個構造函數
else {
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//防止新表的臨界值爲0,從新計算臨界值
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
//new 新表
@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) {
//並將原位置置空,減小GC壓力
oldTab[j] = null;
//若是這個節點(桶)只有一個元素
//也就是沒有發生過Hash碰撞,沒有別的節點連在後面
if (e.next == null)
//那麼直接將其Hash到新表裏
//咱們以前一直說HashMap的容量是2的冪,這時它派上了用場
//這裏也用了一種神奇的算法
//下面這行代碼至關於hash對newCap取模
//只不過使用位運算效率更高
//不相信的話咱們能夠試試
//hash=7 ...0111
//newCap=4 ...0100
//newCap-1 ...0011
//hash&(newCap - 1) ...0011
//0011等於3,沒錯就是這麼神奇,並且這並非偶然
//但在這裏有一點須要注意
//不一樣的Hash值通過上面的計算後可能會獲得相同的結果
//這也就是說
//在一個桶中連成的鏈表上的不一樣的節點的Hash值有多是不一樣的
//因此在同一個桶中並不表明他們的Hash值就必定相等了
newTab[e.hash & (newCap - 1)] = e;
//這裏是對紅黑樹的處理,這裏先暫時跳過,下面專門講
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//下面開始處理有多個節點連成鏈表的桶
else {
//要放在本來位置的鏈表
Node<K,V> loHead = null, loTail = null;
//要放在新位置的鏈表
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
//開始處理
do {
next = e.next;
//用節點的Hash與舊錶容量進行與運算
//其實也就是跟取模差很少
//只是如今用來判斷Hash值是否大於等於舊錶容量
//若計算結果爲0,則表示小於舊錶容量
//則放在原來的位置
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的容量是原來的兩倍
//若是以前的容量是32,那麼擴容後就是64
//Hash值爲16的會被放到原來的位置16
//Hash值爲48的本來是和16放一塊兒的
//可是擴容後就被放到48這個位置了
}
}
}
}
return newTab;
}
複製代碼
通常咱們都是調用put
對 HashMap 進行添加操做,今天咱們對它的源碼進行分析
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
複製代碼
跟蹤到putVal
,咱們發現上面的putMapEntries
其實也調用了該函數
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
return new Node<>(hash, key, value, next);
}
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)
tab[i] = newNode(hash, key, value, null);
//若位置不爲空
else {
Node<K,V> e; K k;
//先依次使用Hash,引用,以及equals函數比較相等性
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//若徹底相等,直接賦值給下一步須要處理的變量e
e = p;
//若爲紅黑樹,則做特殊處理
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//不然遍歷該桶的鏈表
for (int binCount = 0; ; ++binCount) {
//在桶中沒找到Key相等性徹底一致的節點
//則建立新節點對該桶的鏈表進行尾插
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//長度大於TREEIFY_THRESHOLD(值爲8)
//轉換爲紅黑樹
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;
}
}
//e不爲null,也就是有要處理的節點
if (e != null) {
//保存舊值
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
//寫入新值
e.value = value;
//空方法,LinkedHashMap實現
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//更新size,並判斷是否要擴容
if (++size > threshold)
resize();
//空方法,LinkedHashMap實現
afterNodeInsertion(evict);
return null;
}
複製代碼
咱們注意到put
還有一個hash
方法,它的實現是這樣的
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
複製代碼
HashMap 做爲 JDK 中泛用的集合,必須考慮各類極端狀況,因此是不能假設做爲 K 泛型參數的類型有良好定義的hashCode
方法的,因此在內部還要在 hash 一次,這樣作能讓 hash 碰撞更少的發生從而提高 HashMap 的效率.
咱們一般使用remove
來移除 HashMap 中的元素
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
複製代碼
跟蹤到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;
//若表非空,則用Hash計算出應該在表中的所在的桶的下標,並獲取該節點
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
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 = e;
} while ((e = e.next) != null);
}
}
//有須要處理的節點
//以前傳參時matchValue=false,不須要匹配值
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);
//從鏈表中將該節點移除
//p爲鏈表頭,移除鏈表頭
else if (node == p)
tab[index] = node.next;
//node爲鏈表的中間節點
else
p.next = node.next;
++modCount;
//減少size
--size;
afterNodeRemoval(node);
//返回舊值
return node;
}
}
return null;
}
複製代碼
可是當咱們調用另外一個重載時matchValue
爲 true ,這時就要匹配值了.
@Override
public boolean remove(Object key, Object value) {
//此時matchValue爲true
return removeNode(hash(key), key, value, true, true) != null;
}
複製代碼
咱們通常使用get
來獲取 HashMap 中的值
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
複製代碼
跟蹤到getNode
方法,相比起前幾個方法,這個方法就簡單許多
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//判斷表是否非空,該Hash位置的桶中是否有節點
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//查看第頭節點是否相等性是否徹底匹配
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;
}
複製代碼
當咱們使用某種類型做爲 HashMap 泛型參數 K (也是就是鍵-值對中的鍵)時,該類型的對象的hashCode
函數返回的值應該是不變的,不然當 HashMap 進行 Hash 時可能會獲得錯誤的位置,有可能致使key-value
對實際存儲到了 HashMap 中,但就是找不到的狀況.
WARNING: 前方即將進入高能區.
每次看別人在解析 HashMap 的時候一講到紅黑樹就戛然而止,要否則說後面單獨開一篇文章來說,要否則就直接太監了,因此我看別人都不說那索性我就本身研究去了.
須要讀者注意的是,看紅黑樹這一節須要你有必定二叉樹的基礎知識,而且有必定耐心去理解,筆者會盡量地講清楚,但不可能面面俱到,若是在閱讀過程當中發現有不能理解的名詞,還請自行百度.
紅黑樹是一種自平衡二叉查找樹,雖然它的實現很是複雜,但即便是在最壞狀況下運行也能有很好的效率.好比當紅黑樹上有N
個元素時,它能夠在O(log N)
時間內作查找,插入和刪除.
當 HashMap 在桶中的鏈表長度超過 8 時使用它,鏈表在作查找時的時間複雜度是O(N)
,使用紅黑樹會將效率提升很多.
做爲一種查找樹,它須要符合一些規則:
一顆紅黑樹的樣子,大概就像這樣(圖是用Process On畫的)
除了二叉查找樹所具備的一些性質,全部的紅黑樹還具備如下的 5 個性質:
null
)都是黑色的由於插入,查找,刪除操做時,最壞的狀況下的時間都與二叉樹的樹高有關,根據性質 4 咱們知道不會有兩個直接相連的紅色節點.接着,根據性質 5 咱們又能夠知道,由於全部最長的路徑都有相同數目的黑色節點,這就保證了沒有可能會有一條路徑的長度能有其餘路徑的兩倍這麼長.
上面的性質使紅黑樹達到了相對平衡,但實際上,紅黑樹也是最接近平衡的二叉樹.
以前咱們一講到TreeNode
就跳過,如今咱們對它進行分析
咱們先分析TreeNode
這個嵌套類是怎麼來的
//追根溯源
//HashMap.TreeNode
// -繼承-> LinkedHashMap.LinkedHashMapEntry
// -繼承->HashMap.Node
// -實現->Map.Entry
//這樣的好處是TreeNode既能夠當作LinkedHashMap.LinkedHashMapEntry來使用
//也能夠當作HashMap.Node來使用
//TreeNode包含如下幾個字段
//父節點
TreeNode<K,V> parent;
//左子節點
TreeNode<K,V> left;
//右子節點
TreeNode<K,V> right;
//TreeNode也是由以前的鏈表樹化而來的
//prev指向的是本來仍是鏈表時的前一個節點
//刪除時須要取消下一個連接
TreeNode<K,V> prev;
//是否是紅色節點
boolean red;
//並從HashMap.Node繼承了next
//Node是鏈表節點,next指向鏈表中的下一個節點
Node<K,V> next;
複製代碼
回憶以前的代碼,咱們知道當鏈表長度超過 8 時會調用treeifyBin
這個方法對鏈表進行樹化
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
return new TreeNode<>(p.hash, p.key, p.value, next);
}
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//MIN_TREEIFY_CAPACITY的值爲64
//這是會觸發樹化的容量最小值
//若未達到這個值
//則HashMap選擇的策略是使用resize進行擴容以減小Hash衝突,而非樹化
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
//先檢查一波這個地方是否真的要樹化(是否爲空)
else if ((e = tab[index = (n - 1) & hash]) != null) {
//其實這裏先把它轉換成了雙鏈表方便下一步操做
//hd是鏈表頭,tl是鏈表尾
TreeNode<K,V> hd = null, tl = null;
do {
//HashMap.Node將轉換爲HashMap.TreeNode
TreeNode<K,V> p = replacementTreeNode(e, null);
//若尾巴爲空說明是第一次循環
if (tl == null)
//先設置頭結點
hd = p;
else {
//將當前新生成的節點p的前驅設置爲本來的尾巴
p.prev = tl;
//而後本來的尾巴的下一個節點指向新生成的節點
tl.next = p;
}
//更新尾巴
tl = p;
} while ((e = e.next) != null);
//給節點表賦值
if ((tab[index] = hd) != null)
//並開始實際的樹化
hd.treeify(tab);
}
}
//繼續跟蹤源碼到TreeNode#treeify
final void treeify(Node<K,V>[] tab) {
//樹根
TreeNode<K,V> root = null;
//從頭開始遍歷
for (TreeNode<K,V> x = this, next; x != null; x = next) {
//取下一個節點
next = (TreeNode<K,V>)x.next;
//將左右子樹置空
x.left = x.right = null;
//若根節點爲空,則把當前節點設置爲根節點
if (root == null) {
x.parent = null;
//根節點是黑色的
x.red = false;
root = x;
}
//不然取出該節點的Hash值和Key值,準備進行插入
//變量x是帶插入節點
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
//從根節點開始遍歷紅黑樹,查找插入位置
//變量p表明的是當前遍歷到的節點
for (TreeNode<K,V> p = root;;) {
//dir表明兩個節點比較的結果
//ph是p的Hash值
int dir, ph;
//pk是p的Key值
K pk = p.key;
//若是要插入的節點的Hash值小於當前遍歷到的節點
if ((ph = p.hash) > h)
//比較結果爲-1,繼續往左子樹找
dir = -1;
else if (ph < h)
//不然爲1,繼續往右子樹找
dir = 1;
//若是出現二者相等的狀況
//則調用comparableClassFor
//瞄一眼做爲Key的類是否實現了Comparable
//若是實現了
//就繼續調用compareComparables進行比較
//若是比較結果仍是相等
//就到tieBreakOrder中去比較
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p;
//按照每次比較獲得的結果
//不是樹葉則選擇左子節點仍是右子節點
//若是該節點不是樹葉(不爲null),則繼續向下找
//不然先將x其插入到那個樹葉原有的位置
//上述過程實際上就是將其先變成一顆二叉查找樹的過程
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
//在插入結束後再作平衡處理
root = balanceInsertion(root, x);
break;
}
}
}
}
//把紅黑樹的根節點移動成爲桶中的第一個元素
moveRootToFront(tab, root);
}
//回到剛纔跳過的tieBreakOrder看看
//咱們得知是調用了System#identityHashCode
//這個函數是根據對象在JVM中的的實際地址來返回Hash的
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;
}
複製代碼
balanceInsertion
這個方法比較複雜而且會在增長元素時用到,因此咱們放到下文的增長元素中來說.
如今咱們補充一些二叉樹左旋與右旋的知識,爲後面作鋪墊.
左旋的步驟:
是否是感受像繞口令同樣?那畫個圖吧(圖是用Process On畫的)
仍是繼續畫一個圖:
Java
中是如何實現的
//左旋
//root是樹根
//p表明上圖左旋中的A
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
TreeNode<K,V> p) {
TreeNode<K,V> r, pp, rl;
//由於p是A,因此r就是C了
if (p != null && (r = p.right) != null) {
//就像上圖同樣,C的左子節點若存在
//就變成A的右子節點
if ((rl = p.right = r.left) != null)
rl.parent = p;
//C取代A變成子樹樹根
if ((pp = r.parent = p.parent) == null)
//若是A以前恰好是二叉樹的樹根
//則要保證它是黑色的
(root = r).red = false;
else if (pp.left == p)
//替換A
pp.left = r;
else
//替換A
pp.right = r;
//C的左節點變成A
r.left = p;
p.parent = r;
}
return root;
}
//右旋以此類推
//再也不贅述
static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,
TreeNode<K,V> p)
//......
複製代碼
這裏最後說個題外話,讓你們放鬆一下,在rotateLeft
方法的源碼上面有這麼一句註釋
// Red-black tree methods, all adapted from CLR
複製代碼
它的意思就是:紅黑樹的方法,均改變自CLR
看到這個的我就當場就笑出來了,由於筆者半年前仍是一個學C#
的,如今轉Android
以後,總感受Java
之於C#
有一種莫名的諷刺感,下面這段話可能可以更貼切地表達個人心情:
震驚!從響應式編程到MVVM, Microsoft 研究出來的新技術居然老是最早在Java
上大規模使用!Microsoft 居然在背後源源不斷地爲 Java 提供技術支持? Oracle 不勞而獲恐成最大贏家.
在給紅黑樹增長元素時有兩個步驟:
給二叉排序樹插入元素的思路很簡單,一句話就能講清楚.由於樹是已經排序過的,從根節點開始遍歷和比較,若是要插入的節點的Hash小於遍歷到的節點的Hash,則進入左子樹,不然進入右子樹,如此遞歸,直到找到一個空葉子把節點插入便可.
因爲插入後紅黑樹可能會退化成二叉查找樹,因此接下來對二叉查找樹進行調整使它從新變成紅黑樹.左旋和右旋不會改變二叉查找樹的性質,因此在給原紅黑樹按照二叉查找樹的排序規則插入新的節點後,咱們須要使用左旋和右旋來使這顆二叉查找樹從新調整成紅黑樹.
在插向紅黑樹插入一個元素時,咱們把要新插入的設置成紅色,至於爲何要這麼作,咱們結合紅黑樹的 5 條性質來分析一下就知道了.
null
)都是黑色的 ( √ 影響不了該性質,故知足)這樣咱們在調整紅黑樹的時候就只須要考慮性質 2 和 4 帶來的問題就能夠了.
在插入時會出現如下幾種狀況:
爲了分析第三種狀況,下面咱們設一些變量方便進一步說明, x 爲當前要處理的節點, xp 爲父 x 的父節點, xpp 爲 x 的祖父節點(父節點的父節點), xu 爲叔叔節點(祖父節點的另外一個子節點).而且在一開始,咱們把插入的節點當作 x.
注意!!: 下面的狀況只適用於 xp 是 xpp 的左子節點的狀況, xp 是 xpp 的右子節點的狀況須要進行 對稱(左右交換) 處理,若是你在本身嘗試的時候必定要注意這個大前提.
下面咱們畫兩張圖來解釋一下.下面這棵紅黑樹調整的步驟:
這個算法的核心思想就是:將影響紅黑樹性質的紅色節點向上移動到根節點,並將根節點設置成黑色.
通過上面的一通分析,咱們只是僅僅知道了原理,離源碼分析實際上還很遠,不得不說這數據結構真的太狠了,感受比 HashMap 自己還要複雜.
以前咱們對 HashMap 的源碼進行分析時,一分析到putTreeVal
時就跳過了,如今咱們在重點看看它的源碼
//獲取根節點
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
//直到父節點爲null
if ((p = r.parent) == null)
return r;
r = p;
}
}
TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
return new TreeNode<>(hash, key, value, next);
}
//能夠看到這裏和TreeNode#treeify差很少
//就是嘗試這在紅黑樹裏找已近存過指定Key的節點
//若是實在在不到,就給該Key插入一個新的節點
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
int h, K k, V v) {
//可能會用到的變量,若是Key實現了Comparable
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;
//若是遍歷到的節點的Hash值大於要插入的Key的Hash值
//則繼續dir賦值爲-1
//會繼續往左子樹搜索
if ((ph = p.hash) > h)
dir = -1;
//不然往右子樹搜索
else if (ph < h)
dir = 1;
//大於和小於的狀況已近被排除,如今Hash必然相等
//若是Key也相等,那就直接返回
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
//這裏並無直接修改節點的value
//但你若回去看putVal,就能夠知道它實際上在putVal中修改了
return p;
//如果上面那個else if沒有匹配
//則說明Hash相等,可是Key不等,這下就要進一步比較
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
//到這裏只有兩種狀況
//1.Key沒有實現Comparable
//2.實現了Comparable而且比較仍是相等了
//咱們知道經過上面的比較,Hash是已經相等了
//可是Key不等,因此就要在該節點的左右子樹繼續進行搜索
if (!searched) {
TreeNode<K,V> q, ch;
//遍歷查找只會進行一次
//待會繼續向下找時不會再遍歷
//只是找插入位置
searched = true;
//find是一個遞歸方法,比較簡單
//做用是在這個子樹裏查找與指定的Key徹底匹配的節點
//本着抓大放小的原則
//這裏我選擇跳過,節約你們時間
//感興趣的話建議本身去看源碼
//下面這裏短路求值,只要在一邊找到了,就直接返回
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;
//根據上面獲得的dir
//瞄一眼左子樹或右子樹是否爲空
//爲空就插入新節點
//若是不爲空,即p!=null
//則下一次迭代的節點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));
//返回null,則外層的putVal啥都不作
return null;
}
}
}
複製代碼
下面就是期待已久的balanceInsertion
了,若是你理解了上面紅黑樹的理論知識,看這個你會以爲很輕鬆.
//新插入的元素雖然使整棵樹依然保持爲二叉查找樹
//可是這棵樹可能還不夠平衡
//因此要進行調整
//該方法的返回值是調整完成後的樹根
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
TreeNode<K,V> x) {
//將新插入的節點初始化爲紅色
x.red = true;
//x->當前節點
//xp->父親節點
//xpp->祖父節點
//xppl和xppr->分別是祖父節點的左子節點或右子節點
//如今咱們開始回憶剛纔在理論介紹中羅列的幾種狀況
for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
//以前是空樹
if ((xp = x.parent) == null) {
//塗黑,而後返回便可
x.red = false;
return x;
}
//這裏是方法的第二個出口
//若是父節點是黑的
//又或者是祖父節點已經不存在
else if (!xp.red || (xpp = xp.parent) == null)
//算法結束,直接返回根節點
return root;
//若父節點是祖父節點的左子節點
if (xp == (xppl = xpp.left)) {
//出現三紅的狀況,變色
if ((xppr = xpp.right) != null && xppr.red) {
xppr.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
//不然就要看本身是左子節點仍是右子節點
else {
//是右子節點
if (x == xp.right) {
//更新當前節點x,並左旋
root = rotateLeft(root, x = xp);
//更新xp和xpp
xpp = (xp = x.parent) == null ? null : xp.parent;
}
//如果左子節點
if (xp != null) {
//父節點變黑
xp.red = false;
if (xpp != null) {
//祖父變紅
xpp.red = true;
//右旋,且當前節點不變
root = rotateRight(root, xpp);
}
}
}
}
//若父節點是祖父節點的右子節點
//與上面相似,只是把左右交換了而已,故再也不贅述
else {
if (xppl != null && xppl.red) {
xppl.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
if (x == xp.left) {
root = rotateRight(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
root = rotateLeft(root, xpp);
}
}
}
}
}
}
複製代碼
PS: 望理解,筆者能力有限,待往後補充.....
在查找元素時調用的是getTreeNode
方法,該方法比較簡單,調用了咱們以前分析過的find
方法.
final TreeNode<K,V> getTreeNode(int h, Object k) {
return ((parent != null) ? root() : this).find(h, k, null);
}
複製代碼
ArrayMap<K,V>
其次,在 Android 的 Map 中除了 HashMap 可能最重要的就是也實現 Map 接口的 ArrayMap 了,它也是非線程安全的.
ArrayMap 在內部使用開地址法處理Hash碰撞,也就是說存值時對Key先Hash一波,而後算出本身在數組中應該存放的位置的下標,若是本身拿着下標去存的時候發現本身的坑已經被別人給佔了,那本身就日後佔一個坑,若是坑還被佔,那就繼續往下一個坑看,直到有沒被佔的坑位置把本身放進去.
ArrayMap 中內部的數組是通過排序的,並使用二分查找法搜索元素,因此時間複雜度是O(log N)
,比 HashMap 慢,並且數據規模越大慢得越多,我的認爲在數據規模<=100
時使用 ArrayMap 是比較理想的,若是超過這個值仍是推薦你用 HashMap 吧.
注: 系統 ArrayMap 與 AndroidX 的 ArrayMap 實現上有所不一樣,下文中的 ArrayMap 來自AndroidX
Google 說 ArrayMap 是一種節省內存的 Map 實現,比較適用於 APP 開發這種 Map 中元素較少的狀況.
可是然並卵,在看開源項目的源碼時,我發現其實是不少人仍是更傾向於使用 HashMap .我我的也比較推崇使用 HashMap ,由於 ArrayMap 不夠跨平臺,節省的那點內存也實在是杯水車薪,並且 HashMap 速度更快,也能適應將來數據規模變大時的改變.
個人意見是,在本身寫的庫和內存不吃緊的APP裏,最好不要用ArrayMap.
ArrayMap 繼承自 SimpleArrayMap 並實現了 Map 接口, ArrayMap 這個類裏基本上是空的,具體實現都在 SimpleArrayMap 中.
public class ArrayMap<K, V> extends SimpleArrayMap<K, V> implements Map<K, V>
複製代碼
從 ArrayMap 追蹤源碼到 SimpleArrayMap ,查看其構造器
public ArrayMap() {
super();
}
//實際調用
public SimpleArrayMap() {
//EMPTY_INTS爲new int[0]
mHashes = ContainerHelpers.EMPTY_INTS;
//EMPTY_OBJECTS爲new Object[0]
mArray = ContainerHelpers.EMPTY_OBJECTS;
mSize = 0;
}
//涉及的兩個字段
//mHashes用來保存Key值對應的Hash值
//它的大小就是ArrayMap的容量
int[] mHashes;
//與HashMap不同,沒有Node這一說
//只使用一個數組mArray來保存Key和Value
//mArray的長度永遠是mHashes的兩倍
//Key的Hash在mHashes中所在的位置的下標index
//在沒有Hash衝突時index*2的位置就是mArray中存Key的位置
//index*2+1的位置就是mArray中存Value的位置
Object[] mArray;
//在mArray中Key和Value以如下的形式存儲
//咱們舉一個容量爲4的例子
//那麼mArray的長度就等於8
//在ArrayMap中Key和Value以相鄰的形式存在mArray中
//[ Key1 , Value1 , null , null , null , null , Key2 , Value2 ]
// ↑ ↑ ↑ ↑
// 鍵值對KV 鍵值對KV
複製代碼
能夠看到這個構造器基本沒作什麼事,那咱們繼續看下一個構造器
public ArrayMap(int capacity) {
super(capacity);
}
//實際調用
public SimpleArrayMap(int capacity) {
//若初始容量爲0,就和上面那個構造函數同樣
if (capacity == 0) {
//ContainerHelpers是一個工具類
mHashes = ContainerHelpers.EMPTY_INTS;
mArray = ContainerHelpers.EMPTY_OBJECTS;
}
//不然分配新數組
else {
allocArrays(capacity);
}
mSize = 0;
}
複製代碼
順着源碼繼續跟蹤到allocArrays
下面面涉及到幾個字段,這裏提早說明一下,分別是mBaseCache
,mBaseCacheSize
,mTwiceBaseCache
和mTwiceBaseCacheSize
.
這就不得不說到 ArrayMap 的緩存機制, ArrayMap 的空間策略十分保守,對於用來保存 Hash 的mHashes
與用來保存 Key 和 Value 的mArray
有兩個緩存,分別用來緩存容量爲 4 和 8 ArrayMap 在擴容時被換下的數組,而且連成鏈表.
//第一個緩存,其實是一個鏈表
//用來緩存長度爲4的ArrayMap在擴容時換下的數組
static @Nullable Object[] mBaseCache;
//第一個緩存的大小
static int mBaseCacheSize;
//第二個緩存,其實是一個鏈表
//用來緩存長度爲8的ArrayMap在擴容時換下的數組
static @Nullable Object[] mTwiceBaseCache;
//第二個緩存的大小
static int mTwiceBaseCacheSize;
//以mBaseCache爲例子
//連成的鏈表大概像這樣
//mBaseCache=>[ Object[] , int[] , null , null.......]
// ↑ ↑
// ↑ 對應的mHashes
// ↑
// 另外一個長度爲4的數組=>[ Object[] , int[] , null , null......]
// ↑ ↑
// ↑ next mHashes
// next...
複製代碼
上面的鏈表,不知道你看懂了沒有,反正我第一次看的的時候就在想,竟然還有這種操做?充分利用了數組空間又實現了鏈表結構.
當有 ArrayMap 實例進行擴容有換下的mHash
和mArray
恰好知足條件(也就是mHash
的大小爲 4 mArray
的大小爲 8 以及mHash
的大小爲 8 mArray
的大小爲 16 時)時,就由 ArrayMap 上的這些的靜態緩存來接收這些數組.
由於這些字段是靜態的,全部實例共享,併發的時候就可能會出問題,因此咱們看到了下面的代碼使用了synchronized
關鍵字在同步代碼塊中執行修改操做.
//該函數也是ArrayMap的擴容函數
private void allocArrays(final int size) {
//BASE_SIZE的值爲4,若要分配的大小等於8
if (size == (BASE_SIZE*2)) {
synchronized (ArrayMap.class) {
//若爲容量爲8的緩存非空
if (mTwiceBaseCache != null) {
//取出該緩存賦值到array上
final Object[] array = mTwiceBaseCache;
//由於長度合適,能夠直接mArray
mArray = array;
//更換頭結點
mTwiceBaseCache = (Object[])array[0];
//取出緩存的int[]
mHashes = (int[])array[1];
//最後清理數組
array[0] = array[1] = null;
//鏈表長度減1
mTwiceBaseCacheSize--;
if (DEBUG) System.out.println(TAG + " Retrieving 2x cache " + mHashes
+ " now have " + mTwiceBaseCacheSize + " entries");
return;
}
}
}
//長度爲4時的狀況大同小異,再也不贅述
else if (size == BASE_SIZE) {
synchronized (ArrayMap.class) {
if (mBaseCache != null) {
final Object[] array = mBaseCache;
mArray = array;
mBaseCache = (Object[])array[0];
mHashes = (int[])array[1];
array[0] = array[1] = null;
mBaseCacheSize--;
if (DEBUG) System.out.println(TAG + " Retrieving 1x cache " + mHashes
+ " now have " + mBaseCacheSize + " entries");
return;
}
}
}
//沒有合適的緩存,則給它new新的數組
mHashes = new int[size];
//size左移一位至關於乘2
mArray = new Object[size<<1];
}
複製代碼
ArrayMap 並無實現put
方法,put
方法由它的父類 SimpleArrayMap 實現.
不過咱們如今先不看put
,而是先看indexOf
,這個方法,不然接下來看put
的時候會很難受
//這個函數若是返回正值
//則表示所hash的key在表中被找到
//將返回其在數組mHashes中的下標
//若返回負值
//則表示所hash的key在表中沒有被找到
//將返回能夠在表中插入的下標用~運算符按未取反後的值
int indexOf(Object key, int hash) {
final int N = mSize;
//若是數組爲空那這個表裏就沒什麼東西好找的
if (N == 0) {
//將0按位取反
//告訴調用方若要插入值,在0這個下標進行插入
return ~0;
}
//若是表非空,則使用Hash進入二分搜索
//若是找不到就返回一個負數
//也就是用~按位取反過的插入位置
//找到了就返回該Hash在mHashes中的下標
int index = binarySearchHashes(mHashes, N, hash);
//找不到了就直接返回
if (index < 0) {
return index;
}
//若是在mHash中找到了,那麼還要比較Key
//看看本身的坑是否是已經被別人給佔了
//若是佔坑的恰好是本身,就將下標返回
//index<<1是將index乘2的意思
if (key.equals(mArray[index<<1])) {
return index;
}
//不然在mArray使用線性探索法一直向後找
//直到在下一個節點的Hash與本身的Hash不等
//或者在中mArray找到本身爲止
int end;
for (end = index + 1; end < N && mHashes[end] == hash; end++) {
//找到了就返回
if (key.equals(mArray[end << 1])) return end;
}
//向後找找不到就向前找
for (int i = index - 1; i >= 0 && mHashes[i] == hash; i--) {
//找到了就返回
if (key.equals(mArray[i << 1])) return i;
}
//沒找到,返回插入位置
//咱們能夠看到這個算法很是傾向於減元素插入到儘量靠後的位置
//這樣能夠減小插入時須要複製的數組條目的數量
return ~end;
}
複製代碼
而後咱們繼續進到剛纔沒講的binarySearchHashes
方法
//能夠看到這個方法啥都沒作
//而是把工做轉給ContainerHelpers.binarySearch去作了
//同時咱們也能夠在這裏看到ArrayMap是不容許併發編程的
//其中CONCURRENT_MODIFICATION_EXCEPTIONS的值爲true
//表示若是發生ArrayIndexOutOfBoundsException那麼必定是因爲併發引發的
private static int binarySearchHashes(int[] hashes, int N, int hash) {
try {
return ContainerHelpers.binarySearch(hashes, N, hash);
} catch (ArrayIndexOutOfBoundsException e) {
if (CONCURRENT_MODIFICATION_EXCEPTIONS) {
throw new ConcurrentModificationException();
} else {
throw e;
}
}
}
//咱們繼續看ContainerHelpers.binarySearch
//這是一個很是常規的二分搜索
static int binarySearch(int[] array, int size, int value) {
//區間頭
int lo = 0;
//區間尾
int hi = size - 1;
while (lo <= hi) {
//又是位運算,>>>爲無符號右移運算符
//(lo + hi) >>> 1表明的就是兩數相加後除2,不過效率更高
int mid = (lo + hi) >>> 1;
//取出中值
int midVal = array[mid];
//比較Hash,若傳入的Hash更大
//說明該Hash在區間的後半段
//頭lo變成中位數的位置+1
if (midVal < value) {
lo = mid + 1;
}
//不然就是在前半段
else if (midVal > value) {
hi = mid - 1;
}
//又或者說找到了
else {
return mid; // value found
}
}
//若是坑是沒被佔過的
//也就是說該Hash在mHashes中不存在
//那麼返回的是該Hash在數組中該插入位置
return ~lo; // value not present
}
複製代碼
indexOf
講完了,可是咱們如今仍是不能開始講put
......由於 ArrayMap 的 Key 能夠爲null
,因此要對爲null
的 Key 作特殊處理,那就是indexOfNull
這個方法
int indexOfNull() {
final int N = mSize;
//這裏和indexOf同樣
if (N == 0) {
return ~0;
}
//也是二分搜索,只不過如今的Hash=0
//這裏要注意的一點是雖然null的Hash=0
//但不表明其餘Key值的Hash就不能夠爲0了
//Hash只是保證在Key相等的狀況下Hash必定相等
//可是不保證Hash相等的狀況下Key必定相等
//因此其餘非null的Key依然可能獲得爲0的Hash
//因此依然可能會發生Hash碰撞
int index = binarySearchHashes(mHashes, N, 0);
//若是找不到,返回可插入的位置
if (index < 0) {
return index;
}
//若是找到的位置的Key恰好就是null
//那沒什麼好說了,直接返回
if (null == mArray[index<<1]) {
return index;
}
//依然是線性探索法,向後找
int end;
for (end = index + 1; end < N && mHashes[end] == 0; end++) {
if (null == mArray[end << 1]) return end;
}
//找不到就向前找
for (int i = index - 1; i >= 0 && mHashes[i] == 0; i--) {
if (null == mArray[i << 1]) return i;
}
//找不到就返回插入位置
return ~end;
}
複製代碼
有了上面的充足準備後,如今咱們能夠開始看put
了
public V put(K key, V value) {
//表的舊大小
final int osize = mSize;
final int hash;
//該Key的Hash在mHash中的下標
int index;
//若是Key爲null,則Hash值爲0
if (key == null) {
hash = 0;
index = indexOfNull();
} else {
hash = key.hashCode();
index = indexOf(key, hash);
}
//若是index是大於0的,那就說明Key在表中被找到
if (index >= 0) {
index = (index<<1) + 1;
//保存舊值並設置新值
final V old = (V)mArray[index];
mArray[index] = value;
return old;
}
//不然index就是新值的插入位置
index = ~index;
//若是表中的元素數量已經大於等於mHashes的大小
//此時就說明ArrayMap須要進行一輪擴容
if (osize >= mHashes.length) {
//若以前的大小>=8,則只擴容50%(>>1等價於除2)
//若以前的大小>=4<=8,則變成8
//若以前的大小<=4,則變成4
//想清楚爲何ArrayMap會省內存了嗎?
//由於HashMap擴容是翻倍
//而ArrayMap擴容時在小容量時有兩級緩存
//在大容量時也最多擴容50%
final int n = osize >= (BASE_SIZE*2) ? (osize+(osize>>1))
: (osize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE);
if (DEBUG) System.out.println(TAG + " put: grow from " + mHashes.length + " to " + n);
//先把兩個這數組存在臨時變量中
final int[] ohashes = mHashes;
final Object[] oarray = mArray;
//擴容後mHashes和mArray就被設置成新數組了
allocArrays(n);
//依然是不容許併發,這裏有併發檢查
//若是在擴容時給ArrayMap添加元素,那就會報錯
if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) {
throw new ConcurrentModificationException();
}
if (mHashes.length > 0) {
if (DEBUG) System.out.println(TAG + " put: copy 0-" + osize + " to 0");
//將舊數組內的值拷貝到新數組
System.arraycopy(ohashes, 0, mHashes, 0, ohashes.length);
System.arraycopy(oarray, 0, mArray, 0, oarray.length);
}
//釋放舊數組,若是他們符合條件,就會被回收
freeArrays(ohashes, oarray, osize);
}
//若是此次put進來的Key沒有排在數組的最後
if (index < osize) {
if (DEBUG) System.out.println(TAG + " put: move " + index + "-" + (osize-index)
+ " to " + (index+1));
//那麼就要移動數組的元素,給當前要插入的值騰出位置
//這個過程實際上就是對數組進行排序
//System.arraycopy這個方法在Android上是一個native方法
//因此它的效率會更高
System.arraycopy(mHashes, index, mHashes, index + 1, osize - index);
System.arraycopy(mArray, index << 1, mArray, (index + 1) << 1, (mSize - index) << 1);
}
if (CONCURRENT_MODIFICATION_EXCEPTIONS) {
if (osize != mSize || index >= mHashes.length) {
throw new ConcurrentModificationException();
}
}
//最後纔是給數組對應的位置賦值
mHashes[index] = hash;
mArray[index<<1] = key;
mArray[(index<<1)+1] = value;
mSize++;
return null;
}
複製代碼
下面咱們開始分析remove
@Nullable
public V remove(Object key) {
final int index = indexOfKey(key);
//大於0,說明纔有Key
if (index >= 0) {
return removeAt(index);
}
return null;
}
//跟蹤到indexOfKey,能夠發現都是上面咱們已經分析過的方法
public int indexOfKey(@Nullable Object key) {
return key == null ? indexOfNull() : indexOf(key, key.hashCode());
}
//那麼繼續跟蹤到removeAt
public V removeAt(int index) {
//先拿出舊值
final Object old = mArray[(index << 1) + 1];
final int osize = mSize;
final int nsize;
//若是當前就只存了一個元素
if (osize <= 1) {
// Now empty.
if (DEBUG) System.out.println(TAG + " remove: shrink from " + mHashes.length + " to 0");
//那麼就檢查一下可否回收這兩個數組
freeArrays(mHashes, mArray, osize);
//並將原數組置空
mHashes = ContainerHelpers.EMPTY_INTS;
mArray = ContainerHelpers.EMPTY_OBJECTS;
nsize = 0;
}
//若是存着的不僅一個元素
else {
//大小減1
nsize = osize - 1;
//若是ArrayMap的容量是大於8,而且最多隻使用了三分之一
//那麼就要從新分配空間,減小內存使用
if (mHashes.length > (BASE_SIZE*2) && mSize < mHashes.length/3) {
//計算出新的容量
//若是移除該元素以前ArrayMap所存的大小(Szie)大於8
//則容量(Capacity)收縮到原大小(Size)的1.5倍,不然收縮到8
final int n = osize > (BASE_SIZE*2) ? (osize + (osize>>1)) : (BASE_SIZE*2);
if (DEBUG) System.out.println(TAG + " remove: shrink from " + mHashes.length + " to " + n);
//暫存舊數組
final int[] ohashes = mHashes;
final Object[] oarray = mArray;
//分配新數組
allocArrays(n);
if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) {
throw new ConcurrentModificationException();
}
//和以前同樣,從舊數組拷貝
if (index > 0) {
if (DEBUG) System.out.println(TAG + " remove: copy from 0-" + index + " to 0");
System.arraycopy(ohashes, 0, mHashes, 0, index);
System.arraycopy(oarray, 0, mArray, 0, index << 1);
}
//而後移動數組元素
//將要移除的數組下標後的元素往前移覆蓋掉要移除的元素
if (index < nsize) {
if (DEBUG) System.out.println(TAG + " remove: copy from " + (index+1) + "-" + nsize
+ " to " + index);
System.arraycopy(ohashes, index + 1, mHashes, index, nsize - index);
System.arraycopy(oarray, (index + 1) << 1, mArray, index << 1,
(nsize - index) << 1);
}
}
//若是沒有達成收縮條件
//則直接移動
else {
if (index < nsize) {
if (DEBUG) System.out.println(TAG + " remove: move " + (index+1) + "-" + nsize
+ " to " + index);
System.arraycopy(mHashes, index + 1, mHashes, index, nsize - index);
System.arraycopy(mArray, (index + 1) << 1, mArray, index << 1,
(nsize - index) << 1);
}
mArray[nsize << 1] = null;
mArray[(nsize << 1) + 1] = null;
}
}
if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) {
throw new ConcurrentModificationException();
}
//更新大小
mSize = nsize;
//返回舊值
return (V)old;
}
複製代碼
最後咱們再來看一下用於回收數組的freeArrays
private static void freeArrays(final int[] hashes, final Object[] array, final int size) {
//若是長度是8,則進入同步代碼塊
if (hashes.length == (BASE_SIZE*2)) {
synchronized (ArrayMap.class) {
//最多隻會緩存10個數組
//CACHE_SIZE=10
if (mTwiceBaseCacheSize < CACHE_SIZE) {
//爲把array做爲新的頭結點作準備
array[0] = mTwiceBaseCache;
array[1] = hashes;
//除了前兩個item其餘置空
for (int i=(size<<1)-1; i>=2; i--) {
array[i] = null;
}
//更新頭結點
mTwiceBaseCache = array;
//鏈表長度加1
mTwiceBaseCacheSize++;
if (DEBUG) System.out.println(TAG + " Storing 2x cache " + array
+ " now have " + mTwiceBaseCacheSize + " entries");
}
}
}
//大小爲4時同理,不在贅述
else if (hashes.length == BASE_SIZE) {
synchronized (ArrayMap.class) {
if (mBaseCacheSize < CACHE_SIZE) {
array[0] = mBaseCache;
array[1] = hashes;
for (int i=(size<<1)-1; i>=2; i--) {
array[i] = null;
}
mBaseCache = array;
mBaseCacheSize++;
if (DEBUG) System.out.println(TAG + " Storing 1x cache " + array
+ " now have " + mBaseCacheSize + " entries");
}
}
}
}
複製代碼
如今咱們來看看get
的源碼
public V get(Object key) {
//咱們看到其實是調用getOrDefault
return getOrDefault(key, null);
}
//跳到getOrDefault,能夠發現它很是簡單
//indexOfKey以前咱們已經分析過,這裏只是對負值作判斷
//若index爲負值則返回null
public V getOrDefault(Object key, V defaultValue) {
final int index = indexOfKey(key);
return index >= 0 ? (V) mArray[(index << 1) + 1] : defaultValue;
}
複製代碼
\ | HashMap | ArrayMap |
---|---|---|
線程安全 | 否 | 否 |
數據結構 | 數組+鏈表+紅黑樹 | 數組+鏈表 |
容許是否容許Key爲null值 | 是 | 是 |
定位算法 | 對Key的hash值再hash,而後於容量-1 的值做按位與運算(實際上至關於對容量進行取模) |
hash值在數組mHashes 中已經排序,對數組mHashes 做二分搜索,找到下標index後,在數組mArray 中index<<1 爲Key,index<<1+1 爲Value |
默認容量 | 16 | 0 |
查找元素時間複雜度 | 在Key的hashCode定義良好的理想狀況下爲O(1) | O(logN) |
擴容算法 | 變成原來容量的兩倍,並將本來連成的鏈表進行拆分 | 對於容量爲4和8有緩存,對於超過8的只擴容50% |
hash衝突的處理方式 | 拉鍊法+紅黑樹 | 開地址法 |
在這裏特別感謝掘金的運營負責人@優弧大大,這篇文章在寫到7000字的時候,由於個人失誤,險些形成腰斬,謝謝大大幫我找回.
本身在寫這篇文章的時候也遇到不少困難,好比在看 HashMap 中的紅黑樹的時候,因爲網上文章質量參差不齊,不少文章都存在對紅黑樹的理解不夠透徹的問題,並且本身算法基礎也比較差,看得幾近奔潰,可是我不喜歡妥協,經過看書和參考對比大量文章堅持下來了,雖然到最後紅黑樹刪除那塊仍是鴿了,由於我以爲本身都弄不清楚的東西怎麼能跟別人講清楚呢?因此乾脆別講,省得害人害己,等往後我水平達到了再補上了,經過這一趟下來感受本身也提高了很多.
同時,在此也提醒一下各位讀者,對於複雜的數據結構和算法理解必須捧着權威書本來作參考,不能夠輕易相信網上的博客,由於紅黑樹做爲一種複雜的高級數據結構,是沒有多少人能將它徹底講清楚的,或多或少都會存在一些謬誤,這對你的理解是極其有害的.因此對於本篇的態度也是同樣,本篇文章僅供您的參考,若有謬誤還請指出.
最後,這篇文章真的很來之不易,綜上種種才讓你們能在今天看到這篇文章.
若是喜歡個人文章別忘了給我點個贊,拜託了這對我來講真的很重要.