HashMap原始碼分析(史上最詳細的原始碼分析)
HashMap簡介
HashMap是開發中使用頻率最高的用於對映(鍵值對 key value)處理的資料結構,我們經常把hashMap資料結構叫做雜湊連結串列;
ObjectI entry<Key,Value>,entry<Key,Value>] 可以將資料通過鍵值對形式存起來
特點
- HashMap根據鍵的hashcode值儲存資料,大多數情況可以直接定位到它的值,因而具有很快的訪問速度,但遍歷順序是不確定的
想要使得遍歷的順序就是插入的順序,可以使用LinkedHashMap,LinkedHashMap是HashMap的一個子類,儲存了記錄的插入順序,在用Iterator遍歷LinkedHashMap時,先得到的記錄肯定是先插入的,也可以在構造時帶引數,按照訪問次序排序。
public class HashMapTest { public static void main(String[] args) { HashMap hashMap = new HashMap(); hashMap.put(2,"bbb"); hashMap.put(3,"ccc"); hashMap.put(1,"aaa"); System.out.println("HashMap的遍歷順序:"+hashMap); LinkedHashMap linkedHashMap = new LinkedHashMap(); linkedHashMap.put(2,"bbb"); linkedHashMap.put(3,"ccc"); linkedHashMap.put(1,"aaa"); System.out.println("LinkedHashMap的遍歷順序:"+linkedHashMap); } } HashMap的遍歷順序:{1=aaa, 2=bbb, 3=ccc} LinkedHashMap的遍歷順序:{2=bbb, 3=ccc, 1=aaa}
執行緒不安全的HashMap
因為多執行緒環境下,使用Hashmap進行put操作會引起死迴圈,導致CPU利用率接近100%,所以在併發情況下不能使用HashMap。
- HashMap最多隻允許一條記錄的鍵為null,允許多條記錄的值為null
- HashMap非執行緒安全,如果需要滿足執行緒安全,可以一個Collections的synchronizedMap方法使HashMap具有執行緒安全能力,或者使用ConcurrentHashMap
效率低下的HashTable容器
HashTable容器使用synchronized來保證執行緒安全,但線上程競爭激烈的情況下HashTable的效率非常低下。因為當一個執行緒訪問HashTable的同步方法時,其他執行緒訪問HashTable的同步方法時,可能會進入阻塞或輪詢狀態。如執行緒1使用put進行新增元素,執行緒2不但不能使用put方法新增元素,並且也不能使用get方法來獲取元素,所以競爭越激烈效率越低。
ConcurrentHashMap的鎖分段技術
HashTable容器在競爭激烈的併發環境下表現出效率低下的原因,是因為所有訪問HashTable的執行緒都必須競爭同一把鎖,那假如容器裡有多把鎖,每一把鎖用於鎖容器其中一部分資料,那麼當多執行緒訪問容器裡不同資料段的資料時,執行緒間就不會存在鎖競爭,從而可以有效的提高併發訪問效率,這就是ConcurrentHashMap所使用的鎖分段技術,首先將資料分成一段一段的儲存,然後給每一段資料配一把鎖,當一個執行緒佔用鎖訪問其中一個段資料的時候,其他段的資料也能被其他執行緒訪問。
通常會見到資料庫優化,具體就是索引優化,那什麼是索引優化呢?
舉個例子,微信通訊錄,最右側會有a-z的排序,這就是索引,把資料分組,用到了hashMap資料結構
為什麼鍵一個set集合,值是一個value集合
public abstract class AbstractMap<K,V>implements Map<K,V>{ Set<K>keyset; Collection<V>valuescollection;
set資料結構:元素不能相同
collection資料結構:元素可以相同
因為在hashMap中,key(鍵)不能相同,value(值)是可以相同的
HashMap原始碼分析
核心成員變數
transient HashMapEntry<k, V>[] table; //鍵值對的陣列,存著每一個鍵值對 transient HashMapEntry<K,V>entryForNullKey; //沒有鍵的鍵值對 private transient Set<Map.Entry<K, V>> entrySet; //HashMap將資料轉換成set的另一種儲存形式,這個變數主要用於迭代功能。 transient int size; //HashMap中實際存在的Node數量,注意這個數量不等於table的長度,甚至可能大於它,因為在table的每個節點上是一個連結串列(或RBT)結構,可能不止有一個Node元素存在。 transient int modCount; //HashMap的資料被修改的次數,這個變數用於迭代過程中的Fail-Fast機制,其存在的意義在於保證發生了執行緒安全問題時,能及時的發現(操作前備份的count和當前modCount不相等)並丟擲異常終止操作。 private transient int threshold; //HashMap的擴容閾值,在HashMap中儲存的鍵值對超過這個數量時,自動擴容容量為原來的二倍。 final float loadFactor; //HashMap的負載因子,可計算出當前table長度下的擴容閾值:threshold = loadFactor * table.length。
hashMap常量
private static final int MINIMUM_cAPACITY = 4; //最小容量 private static final int MAXIAMM_CAPACITY = 1<<30; //最大容量,即2的30次方 (左移乘2,右移除2) static final float DEFAULT_LOAD_FACTOR = 0.75f; //載入因子,用於擴容,容量達到三分之二時,就準備擴容了 static final int MIN_TREEIFY_CAPACITY = 64;//預設的最小的擴容量64,為避免重新擴容衝突,至少為4 * TREEIFY_THRESHOLD=32,即預設初始容量的2倍 private static final Entry[] EMPTY_TABLE = new HashMapEntry[MINIMUM CARACITY >>>1];//鍵值對陣列最小容量(空的時候)
構造方法
//空參構造,使用預設的載入因子0.75
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); }
常用方法
public interface Map<K,V>{} public static interface Entry<k,V){} public boolean equals(Object object); //要重寫,因為要相等的話,鍵和值都要相同 public K getkey(); //獲取鍵 public V getValue(); //獲取值 public V setValue(V object); //設定值 public void clear(); //清除這個map public boolean containskey(Object key); //是否包含某個鍵 public boolean containsValue(Object value); //是否包含某個值 public Seft<Map. Entry<K,V>>entryset(); //獲取實體集合 public Set<k>keyset(); //獲取鍵的集合 public Set<k>Valueset(); //獲取值的集合 public V put(K key,V vale); //往裡面新增一個鍵值對 public void putAll(Map<? extends K,?extends V>map); //新增一個鍵值對的、小的map public V remove(Object key); //通過一個鍵移除一個值 public int size(); //鍵值對的數量 public Collectign<V>values(); //值的集合
什麼是HASH
是根據檔案的內容的資料 通過邏輯運算得到的數值, 不同的檔案(即使是相同的檔名)得到的HASH值是不同的, 所以HASH值就成了每一個檔案在EMULE裡的身份證.
secondary : 第二的
table:儲存鍵值對的陣列
tab.lenth=下標最大值
e:tab[index] : 一維陣列第一個元素,整個連結串列的頭結點
put方法
下1 代表下一個程式碼塊有此方法 下下2 代表下下一個程式碼塊有此方法 依次類推
@Override public V put(k key, V value) { if (key == null) { return putValueForNu1lKey(value); //放一個空key的值 注:hashMap的值是可以為空的
} int hash = Collections.下下2secondaryHash(key); //首先拿到鍵的hash值,這個key傳進來之後進行兩次hash:先拿到下 key.hashCode()本身hash值,再把它作為引數(object key)傳進來,就是二次hash
目的:為了不能key一直在一個數據域裡,要分散一些,均勻排列,在0-9 下下下1HashMapEntry<K, V>[] tab = table; //為了安全問題宣告區域性變數tab
int index = hash & (tab.length - 1); //通過計算hash值再進行一個與運算,獲取下標要放在哪個地方。 就相當於微信索引,計算出所在位置,
打個比方,成--c,放在c區域裡 一個區域可以有多個鍵只需要兩行程式碼,就能找到key(n)所在的雜湊,
比如有10個連結串列,之前需要查10次,現在只需要查10分之1次,效率提高10倍,再通過迭代找具體元素,100個連結串列效率就提高100倍
for (HashMapEntry<K, V> e = tab[index]; e! = null; e = e.next) { //遍歷整個下標下面整個連結串列,e:頭結點 ,如果頭結點不等於空,就讓頭結點等於他的下一個結點
if (e.hash == hash && key.equals(e.key)) { //鍵值對裡的hash等於算出來的hash ,然後發現放進來的這個key和連結串列裡的這個key相同,就覆蓋 preModify(e);覆蓋 V oldValue = e.value; //以前的值oldValue賦值給新的value e.value = value; return oldValue; } }
modCount++; //找不到計數+1
if(size++ > threshold){ //數量有大小,也就是size,如果size++大於容量因子極限,就擴充
tab = doubleCapacity(); //容量乘以2,擴大兩倍。最小是4,22 23 24 25 26 . . .
index = hash &(tab.1ength-1); //擴完容再重新計算一遍下標的值
}
addNewEntry(key, value, hash, index);
return nul1;
}
public static int ①secondaryHash(Object key){ return ②secondaryHash(key. hashCode()); //先獲取key本身的hashcode,再經過一次hash,呼叫secondaryHash
}
private static int 上上2secondaryHash(int h) { h += (h << 15) ^ 0xffffcd7d; h ^= (h >>> 10); h += (h << 3); h ^= (h >>> 6); h += (h << 2) + (h << 14); return h ^ (h >>> 16);
}
hashMap不僅僅是一種單純的一維陣列,有鍵key,有值value,還有next指標,這樣的好處是HashMap根據鍵的hashcode值儲存資料,大多數情況可以直接定位到它的值,因而具有很快的訪問速度,但遍歷順序是不確定的
static class 上上上1HashMapEntry<K, V> implements Entry<K, V> { final K key; V value; final int hash; HashMapEntry<K, V> next; //如果出現相同的鍵,就通過這個指標指向下一個
HashMapEntry(K key, V value, int hash, HashMapEntry<K, V> next) { this.key = key; this.value = value; this.hash = hash; this.next = next;
}
oldCapacity:擴容之前的長度
newTable:
private HashMapEntry<K,V>[] doubleCapacity(){ HashMapEntry<K,V>[] oldTable=table; //作為區域性變數賦值 int oldCapacity =oldTable.1engtth; if(oldCapacity == MAXTMUM_CAPACITY){ //現在的長度就等於最大就不擴容了 return oldTable;
} int newCapacity = oldCapacity*2; //否則,擴容2倍 HashMapEntry<K,V>[] newTable = makeTable(newCapacity); //聲明瞭一個newTable,讓他的長度等於newCapacity
if(size==0){ //沒元素在裡面 return newTable; }
for(int j=Q;j<b1dcapacity;j++){ //遍歷,為了把老數組裡的元素放到新數組裡面來
HashMapEntry<K,V>e=oldTable[j]; //拿到老的數組裡的每一個鍵值對
if(e==nul1){
continue;
} //鍵值對為空,則不管
int highBit=e. hash & oldCapacity; //拿到鍵值對裡的hash和oldCapacity長度,進行取小(highBit)
HashMapEntry<K,V>broken=null;
newTable[j | highBit]=e; //把highBit放在newTable裡面,和j進行一個或運算,其實就是把元素丟到新的裡面來
for(HashMapEntry<K,V>n=e. next;n!=null;e=n,n=n. next){ //把串裡的資料全部拿出來,重新計算下標
int nextHighBit=n. hash & oldCapacity;
if(nextHighBit!=highBit){ //如果後面子串和前面這個串,計算出來的下標不同,不能再放在這個陣列(相當於微信的一個索引)裡了
if(broken==null) //不相等
newTable[j | nextHighBit]=n; //應該放在newTable新的下標去 或運算的時候分成兩個區間
else
broken.next = n; //如果相等,放在next後面,繼續串起來
broken=e;
heigBit = nextHighBit;
}
}
if(broken !=null)
broken. next=null;
}
return newTable;
}
table[index] = next 也就是連結串列中下一個元素
table[index]就相當於微信同一個索引下的某個元素,有兩個了,再新增,就用next指向下一個元素
串只需要用頭結點來表示,要做到是先把新結點連線到串裡面來,然後再讓tab[index]等於這個串,這個串本身就是這個頭結點,比如現在有三個串(頭結點也有一個),新進來的串放在前面
void addNewEntry(K key,V value,int hash,int index){ table[index]=new HashMapEntry<K,V>(key,value,hash,table[index](next指標)); 藍色為新結點,放在tab[index]裡面來,就是頭結點,相當於新結點變成了頭結點,
而新結點作為頭結點的next指標,作為一個引用引進來了
,這個圖就是,tab[index]這個一維陣列中的某個元素或儲存區域,讓它等於新加進來的元素--newHashMap,讓新進來的元素的next指標指向tab[index]
remove
tab[index]:頭結點
prev=null 預設為空
@override二次遍歷
public V remove(object key) {
if (key == null) {
return removeNullke(); //移除空鍵但有值的鍵值對
}
int hash = Collections.secondaryHash(key);
HashMapEntry<K, V>[] tab = table;
int index = hash & (tab.length - 1);
for (HashMapEntry<K, V> e = tab[index], prev = null; //遍歷串裡的每一個元素,讓頭結點等於e
e != null;prev = e, e = e.next){ //讓頭結點e等於prev,又讓e.next(頭結點的下一個元素等於e)
if (e.hash == hash && key.equals(e.key)) { hash相等,又能equals,說明找到了這個結點
if (prev == null) {
tab[index] = e.next; 讓頭結點不再等於之前的prev,把e放在頭結點位置,然後e.next就是tab[index](頭結點),成功上位了哈哈
} else {
prev.next = e.next; prev.next不指向下一個元素了,指向下一個的下一個(e.next),就表示把e刪除了
modCount++;
size++;
要刪除一個key1,找到下標索引就是index=0 第一列,但是這個index=0有很多鍵值對,不能直接把index=0裡的所有鍵值對刪除吧,所以要先查詢找出來,刪除需要的鍵值對
修改元素:
元素的修改也是put方法,因為key是唯一的,所以修改元素,是把新值覆蓋舊值。
第一排,只有最後4位才有效,因為與運算全是1才為1,所以 0000=1001=0(最小值) 1001+1001=9(最大值)
hash%10也是(0~9),因為hash不固定
與運算lenth-1均勻的分佈成0~9 或運算分成兩個區間
hash相同 key不一定相同 >> key1 key2產生的hash很有可能是相同的,如果key真的相同,就不會存在雜湊連結串列了,雜湊連結串列是很多不同的鍵算出的hash值和index相同的
key相同 經過兩次hash hash一定相同
tips
想理解資料結構原始碼,得理清楚當一個新的元素被新增進來以後,會和之前的老的元素產生什麼關係
首先看繼承的關係,看成員變數,看元素之間的關係,看元素之間的關係就是在新增元素的時候,這組元素和之前的元素有什麼關係,put方法
&n