HashMap原理初探

- 參考博客

Java中的equals和hashCode方法詳解
鏈表
java提高篇(二三)—–HashMap Java
HashMap的工作原理

算法的時間複雜度和空間複雜度-總結
Hashmap
Hash (散列函數)
第1部分 HashMap介紹
HashCode的作用原理和實例解析
hash碰撞處理
高性能場景下,HashMap的優化使用建議

- 整體思路

     1. 什麼是HashMap?有什麼作用?
     2. 瞭解HashMap原理前,需要知道哪些知識?
     3. 分析HashMap原理。
     4. 應用場景。

- 什麼是HashMap?有什麼作用?

什麼是HashMap: 基於哈希表的 Map 接口的實現。此實現提供所有可選的映射操作,並允許使用 null 值和 null 鍵。(除了非同步和允許使用 null 之外,HashMap 類與 Hashtable 大致相同。)此類不保證映射的順序,特別是它不保證該順序恆久不變。 此實現假定哈希函數將元素適當地分佈在各桶之間,可爲基本操作(get 和 put)提供穩定的性能。迭代 collection 視圖所需的時間與 HashMap 實例的“容量”(桶的數量)及其大小(鍵-值映射關係數)成比例。所以,如果迭代性能很重要,則不要將初始容量設置得太高(或將加載因子設置得太低)。
作用:通過哈希算法,通過鍵值可快速找到自己需要的對象。

- 瞭解HashMap原理前,需要知道哪些知識??

- 原理分析

  • 創建一個測試類,創建一個hashmap隨機插入一個數據,打入斷點查看hashmap的結構這裏寫圖片描述
    1.有一個table數組,長度爲16,隨機存放之前put進去的key,value對象,這個對象的名稱叫Entry
    2.Entry包含了四個對象,hash,key,next,value
    創建一個對象EntryTest,重寫他的hashCode方法,直接寫死讓所有的返回值都是1
    這裏寫圖片描述
    隨機插入幾個數據,查看hashmap結構這裏寫圖片描述
    這個時候,雖然插入3個對象table的數據長度爲1,點開Entry中的next屬性發現每個next都存放着一個Entry對象,這便是鏈表形式的存儲數據,hashmap採用的是哈希函數中拉鍊法存儲數據,數組+鏈表。查看源碼中的addEntry方法:
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);
        //若HashMap中元素的個數超過極限了,則容量擴大兩倍
        if (size++ >= threshold)
            resize(2 * table.length);
    }

瞭解基本的hashmap結構,我們主要從拉鍊法來分析hashmap的put和get方法。查看put源碼:

/**
  * Associates the specified value with the specified key in this map. If the
  * map previously contained a mapping for the key, the old value is
  * replaced.
  *
  * @param key
  *            key with which the specified value is to be associated
  * @param value
  *            value to be associated with the specified key
  * @return the previous value associated with <tt>key</tt>, or <tt>null</tt>
  *         if there was no mapping for <tt>key</tt>. (A <tt>null</tt> return
  *         can also indicate that the map previously associated
  *         <tt>null</tt> with <tt>key</tt>.)
  */
 public V put(K key, V value) {
  if (key == null)
   return putForNullKey(value);
  int hash = hash(key.hashCode());
  int i = indexFor(hash, table.length);
  for (Entry<k , V> e = table[i]; e != null; e = e.next) {
   Object k;
   if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
    V oldValue = e.value;
    e.value = value;
    e.recordAccess(this);
    return oldValue;
   }
  }

  modCount++;
  addEntry(hash, key, value, i);
  return null;
 }

1.對key做null的檢查,如果爲null,會被存儲在table[0]中,因爲null的hash值總是爲0
2.計算好hash值,hash值即爲存在table數組中的索引,爲了考慮到table數組的均勻分配,最好能保證每個裏面都能分配到一個,增加查詢速度
3.indexFor(hash,table.length)用來計算在table數組中存儲Entry對象的精確的索引,HashMap的底層數組長度總是2的n次方,在構造函數中存在:capacity <<= 1;這樣做總是能夠保證HashMap的底層數組長度爲2的n次方。當length爲2的n次方時,h&(length - 1)就相當於對length取模,而且速度比直接取模快得多,這是HashMap在速度上的一個優化。至於爲什麼是2的n次方點擊這篇博客。
4.通過hash算法可以知道,如果2個key有相同的hash值,此爲hash碰撞,相同的hash值會以鏈表的形式存儲:

如果在剛纔計算出來的索引位置沒有元素,直接把Entry對象放在那個索引上。
如果索引上有元素,然後會進行迭代,一直到Entry->next是null。當前的Entry對象變成鏈表的下一個節點。
如果我們再次放入同樣的key會怎樣呢?邏輯上,它應該替換老的value。事實上,它確實是這麼做的。在迭代的過程中,會調用equals()方法來檢查key的相等性(key.equals(k)),如果這個方法返回true,它就會用當前Entry的value來替換之前的value。

這裏要注意兩個問題:

一是鏈的產生。這是一個非常優雅的設計。系統總是將新的Entry對象添加到bucketIndex處。如果bucketIndex處已經有了對象,那麼新添加的Entry對象將指向原有的Entry對象,形成一條Entry鏈,但是若bucketIndex處沒有Entry對象,也就是e==null,那麼新添加的Entry對象指向null,也就不會產生Entry鏈了。
二、擴容問題。 隨着HashMap中元素的數量越來越多,發生碰撞的概率就越來越大,所產生的鏈表長度就會越來越長,這樣勢必會影響HashMap的速度,爲了保證HashMap的效率,系統必須要在某個臨界點進行擴容處理。該臨界點在當HashMap中元素的數量等於table數組長度*加載因子,默認是0.75也就是說16*0.75=13就開始擴容了。但是擴容是一個非常耗時的過程,因爲它需要重新計算這些數據在新table數組中的位置並進行復制處理。所以如果我們已經預知HashMap中元素的個數,那麼預設元素的個數能夠有效的提高HashMap的性能。

  • bucketIndex相當於緩衝池的作用,避免了每次產生Entry對象是都去操作table數組,對象在產生的時候只需要去關心bucketIndex位置的元素就可以完成整個數組元素的插入和創建
  • 爲了防止鏈表的長度過長,系統會設置臨界點進行擴容,臨界點爲table數組長度*加載因子。但是擴容是一個非常耗時的過程如果我們預知hashmap中的元素進行設計就能有效提高hashmap性能

put方法的實現還是圍繞數組加鏈表的形式,需要注意的是碰撞和擴容的問題。接下來是存儲的問題,查看put方法的實現:

public V get(Object 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; e = e.next) {
            Object k;
            //若搜索的key與查找的key相同,則返回相對應的value
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                return e.value;
        }
        return null;
    }

圍繞數組加鏈表結構,通過key的hash值找到table數組中的Entry ,key是以String的形式存儲在Entry對象中的,只需要使用equals方法匹配到對應鏈表中的key取到value即可獲取到value的值:
這裏寫圖片描述

- 應用場景

  • 如何優化

1.考慮加載因子地設定初始大小
2.減小加載因子
3.String類型的key,不能用==判斷或者可能有哈希衝突時,儘量減少長度
4.使用定製版的EnumMap
5.使用IntObjectHashMap

優化從提高查詢效率和減少系統開銷分析。

加載因子存在的原因,還是因爲減緩哈希衝突,如果初始桶爲16,等到滿16個元素才擴容,某些桶裏可能就有不止一個元素了。所以加載因子默認爲0.75,也就是說大小爲16的HashMap,到了第13個元素,就會擴容成32。

對於使用鏈表法的散列表來說,查找一個元素的平均時間是O(1+a),因此如果負載因子越大,對空間的利用更充分,然而後果是查找效率的降低;
如果負載因子太小,那麼散列表的數據將過於稀疏,對空間造成嚴重浪費。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章