深刻講解HashMap

先從構造函數講起吧java

HashMap有不少個構造函數,不過咱們比較經常使用的是不帶參數的默認構造函數,其源代碼以下:程序員

public HashMap() {

        this.loadFactor = DEFAULT_LOAD_FACTOR;

        threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);

        table = new Entry[DEFAULT_INITIAL_CAPACITY];

        init();

    }

事實上,HashMap的全部鍵值對被存放在一個Entry數組中(變量table),DEFAULT_INITIAL_CAPACITY(爲16)便是默認的Entry數組長度的初始值,固然咱們也能夠自定義entry數組的長度,可是長度必須爲2的n次方,爲何要如此,下面會講解到。算法

Entry數組每個索引處均可以存放一個Entry鏈(經過下面Entry的構造函數能夠發現Entry對象能夠指明它的下一個對象,因此多個Entry對象能夠以Entry鏈的形式存在)數組

Entry類的構造函數以下:函數

 Entry(int h, K k, V v, Entry<K,V> n) {

            value = v;

            next = n;

            key = k;

            hash = h;

        }

每一個存入HashMap的鍵值對都會以Entry對象的形式儲存,Entry類除了擁有key和value變量外,還有兩個重要的變量:性能

next指明瞭下一個Entry對象
hash是該Entry對象的哈希值(Hash碼)this

HashMap在存入鍵值對時首先會根據key的hashCode() 返回值來計算 Hash 碼,而後經過該Hash碼計算出這個這個鍵值對要放在數組的哪一個索引處,設計

若是該索引處尚未存放entry對象則用要存入的鍵值對新建一個Entry對象並將此對象存放於這個索引處,並將next變量賦爲null;指針

若是該索引已經存在entry鏈,則遍歷該entry鏈,若是entry鏈中含有key和要存入的key相等的entry對象,則將該對象的value值替換成咱們要存入的value;code

若是該索引已經存在entry鏈,且entry鏈中沒有key和要存入的key相等的entry對象,則用要存入的鍵值對新建一個entry對象,而後將此對象存入改索引處,並將next指向該索引處以前的對象,也就是說將新的entry對象放入到entry鏈的鏈頭中;

存入鍵值對的方法源代碼以下:

 public V put(K key, V value) 

 { 

  // 若是 key 爲 null,調用 putForNullKey 方法進行處理

  if (key == null) 

   return putForNullKey(value); 

  // 根據 key 的 keyCode 計算 Hash 值

  int hash = hash(key.hashCode()); 

  // 搜索指定 hash 值在對應 table 中的索引

   int i = indexFor(hash, table.length);

  // 若是 i 索引處的 Entry 不爲 null,經過循環不斷遍歷 e 元素的下一個元素

  for (Entry<K,V> e = table[i]; e != null; e = e.next) 

  { 

   Object k; 

   // 找到指定 key 與須要放入的 key 相等(hash 值相同

   // 經過 equals 比較放回 true)

   if (e.hash == hash && ((k = e.key) == key 

    || key.equals(k))) 

   { 

    V oldValue = e.value; 

    e.value = value; 

    e.recordAccess(this); 

    return oldValue; 

   } 

  } 

  // 若是 i 索引處的 Entry 爲 null,代表此處尚未 Entry 

  modCount++; 

  // 將 key、value 添加到 i 索引處

  addEntry(hash, key, value, i); 

  return null; 

 }

計算Hash碼的方法hash是一個純粹的數學計算,其方法以下:

static int hash(int h) 

{ 

    h ^= (h >>> 20) ^ (h >>> 12); 

    return h ^ (h >>> 7) ^ (h >>> 4); 

}

對於任意給定的對象,只要它的 hashCode() 返回值相同,那麼程序調用 hash(int h) 方法所計算獲得的 Hash 碼值老是相同的。

接下來程序會調用 indexFor(int h, int length) 方法來計算該對象應該保存在 table 數組的哪一個索引處。

indexFor(int h, int length) 方法的代碼以下:

static int indexFor(int h, int length) 

{ 

    return h & (length-1); 

}

這個方法很是巧妙,它老是經過 h &(table.length -1) 來獲得該對象的保存位置——而 HashMap 底層數組的長度老是 2 的 n 次方

當 length 老是 2 的倍數時,h & (length-1) 將是一個很是巧妙的設計:

假設 h=5,length=16, 那麼 h & length - 1 將獲得 5;

若是 h=6,length=16, 那麼 h & length - 1 將獲得 6 ……若是 h=15,length=16, 那麼 h & length - 1 將獲得 15;

可是當 h=16 時 , length=16 時,那麼 h & length - 1 將獲得 0 了;

當 h=17 時 , length=16 時,那麼 h & length - 1 將獲得 1 了……

這樣保證計算獲得的索引值老是位於 table 數組的索引以內

其實看到這裏也許你問,爲何不直接將hash碼除以length而後取餘數來得到索引值呢,這樣不也是也能夠實現索引值老是位於table數組的索引以內麼?

緣由我以爲應該是:與運算比除運算來的更有效率

 

當向 HashMap 中添加 key-value 對,由其 key 的 hashCode() 返回值決定該 key-value 對(就是 Entry 對象)的存儲位置。

當兩個 Entry 對象的 key 的 hashCode() 返回值相同時,將由 key 經過 eqauls() 比較值決定是採用覆蓋行爲(返回 true),仍是產生 Entry 鏈(返回 false)。

上面程序中還調用了 addEntry(hash, key, value, i); 代碼,其中 addEntry 是 HashMap 提供的一個包訪問權限的方法,該方法僅用於添加一個 key-value 對。

下面是該方法的代碼:

void addEntry(int hash, K key, V value, int bucketIndex) 

{ 

    // 獲取指定 bucketIndex 索引處的 Entry 

    Entry<K,V> e = table[bucketIndex];   // ①

    // 將新建立的 Entry 放入 bucketIndex 索引處,並讓新的 Entry 指向原來的 Entry 

    table[bucketIndex] = new Entry<K,V>(hash, key, value, e); 

    // 若是 Map 中的 key-value 對的數量超過了極限

    if (size++ >= threshold) 

        // 把 table 對象的長度擴充到 2 倍。

        resize(2 * table.length);   // ②

}

size:該變量保存了該 HashMap 中所包含的 key-value 對的數量

threshold是閥值的意思,閥值等於Entry數組長度乘以負載因子,從上面的例子能夠看出當size++ >= threshold 時,HashMap 會自動調用 resize 方法擴充 HashMap 的容量。每擴充一次,HashMap 的容量就增大一倍。

loadFactor即負載因子,默認的負載因子(DEFAULT_INITIAL_CAPACITY )爲0.75負載

增大負載因子能夠減小 Hash 表(就是那個 Entry 數組)所佔用的內存空間,但會增長查詢數據的時間開銷,而查詢是最頻繁的的操做(HashMap 的 get() 與 put() 方法都要用到查詢);

減少負載因子會提升數據查詢的性能,但會增長 Hash 表所佔用的內存空間

掌握了上面知識以後,咱們能夠在建立 HashMap 時根據實際須要適當地調整 load factor 的值;

若是程序比較關心空間開銷、內存比較緊張,能夠適當地增長負載因子;若是程序比較關心時間開銷,內存比較寬裕則能夠適當的減小負載因子。一般狀況下,程序員無需改變負載因子的值。

若是開始就知道 HashMap 會保存多個 key-value 對,能夠在建立時就使用較大的初始化容量,若是 HashMap 中 Entry 的數量一直不會超過極限容量(capacity * load factor),HashMap 就無需調用 resize() 方法從新分配 table 數組,從而保證較好的性能。固然,開始就將初始容量設置過高可能會浪費空間(系統須要建立一個長度爲 capacity 的 Entry 數組),所以建立 HashMap 時初始化容量設置也須要當心對待。

 

接下來咱們來看一下HashMap 的讀取實現

當 HashMap 的每一個 bucket 裏存儲的 Entry 只是單個 Entry ——也就是沒有經過指針產生 Entry 鏈時,此時的 HashMap 具備最好的性能:當程序經過 key 取出對應 value 時,系統只要先計算出該 key 的 hashCode() 返回值,在根據該 hashCode 返回值找出該 key 在 table 數組中的索引,而後取出該索引處的 Entry,最後返回該 key 對應的 value 便可。

HashMap 類的 get(K key) 方法代碼:

 public V get(Object key) 

 { 

  // 若是 key 是 null,調用 getForNullKey 取出對應的 value 

  if (key == null) 

   return getForNullKey(); 

  // 根據該 key 的 hashCode 值計算它的 hash 碼

  int hash = hash(key.hashCode()); 

  // 直接取出 table 數組中指定索引處的值,

  for (Entry<K,V> e = table[indexFor(hash, table.length)]; 

   e != null; 

   // 搜索該 Entry 鏈的下一個 Entr 

   e = e.next)    // ①

  { 

   Object k; 

   // 若是該 Entry 的 key 與被搜索 key 相同

   if (e.hash == hash && ((k = e.key) == key 

    || key.equals(k))) 

    return e.value; 

  } 

  return null; 

 }

對於 HashMap 及其子類而言,它們採用 Hash 算法來決定集合中元素的存儲位置。當系統開始初始化 HashMap 時,系統會建立一個長度爲 capacity 的 Entry 數組,這個數組裏能夠存儲元素的位置被稱爲「桶(bucket)」,每一個 bucket 都有其指定索引,系統能夠根據其索引快速訪問該 bucket 裏存儲的元素。

不管什麼時候,HashMap 的每一個「桶」只存儲一個元素(也就是一個 Entry),因爲 Entry 對象能夠包含一個引用變量(就是 Entry 構造器的的最後一個參數)用於指向下一個 Entry,所以可能出現的狀況是:HashMap 的 bucket 中只有一個 Entry,但這個 Entry 指向另外一個 Entry ——這就造成了一個 Entry 鏈。如圖 1 所示:

圖 1. HashMap 的存儲示意

相關文章
相關標籤/搜索