1. 程式人生 > >HashMap原始碼分析(史上最詳細的原始碼分析)

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