java 1.8 HashMap的實現原理

java 1.8 HashMap的實現原理

1. hash 表

  • 數組:採用一段連續的存儲單元來存儲數據。對於指定下標的查找,時間複雜度爲O(1);通過給定值進行查找,需要遍歷數組,逐一比對給定關鍵字和數組元素,時間複雜度爲O(n),當然,對於有序數組,則可採用二分查找,插值查找查找等方式,可將查找複雜度提高爲O(logn);對於一般的插入刪除操作,涉及到數組元素的移動,其平均複雜度也爲O(n)

  • 線性鏈表:對於鏈表的新增,刪除等操作(在找到指定操作位置後),僅需處理結點間的引用即可,時間複雜度爲O(1),而查找操作需要遍歷鏈表逐一進行比對,複雜度爲O(n)

  • 二叉樹: 對一棵相對平衡的有序二叉樹,對其進行插入,查找,刪除等操作,平均複雜度均爲O(logn)。

  • 哈希表:相比上述幾種數據結構,在哈希表中進行添加,刪除,查找等操作,性能十分之高,不考慮哈希衝突的情況下,僅需一次定位即可完成,時間複雜度爲O(1)

    在JDK1.6,JDK1.7中,HashMap採用位桶+鏈表實現,即使用鏈表處理衝突,同一hash值的鍵值對會被放在同一個位桶裏,當桶中元素較多時,通過key值查找的效率較低。

    而JDK1.8中,HashMap採用位桶+鏈表+紅黑樹實現,當鏈表長度超過閾值(8),時,將鏈表轉換爲紅黑樹,這樣大大減少了查找時間。

  • HashMap: 由數組+鏈表組成的,數組是HashMap的主體,鏈表則是主要爲了解決哈希衝突而存在的,如果定位到的數組位置不含鏈表(當前entry的next指向null),那麼對於查找,添加等操作很快,僅需一次尋址即可;如果定位到的數組包含鏈表,對於添加操作,其時間複雜度爲O(n),首先遍歷鏈表,存在即覆蓋,否則新增;對於查找操作來講,仍需遍歷鏈表,然後通過key對象的equals方法逐一比對查找。所以,性能考慮,HashMap中的鏈表出現越少,性能纔會越好

2. HashMap幾個關鍵成員變量

  • initialCapacity:初始容量。指的是 HashMap 集合初始化的時候自身的容量。可以在構造方法中指定;如果不指定的話,總容量默認值是 16 。需要注意的是初始容量必須是 2 的冪次方。

  • size:當前 HashMap 中已經存儲着的鍵值對數量,即 HashMap.size()

  • loadFactor:加載因子。所謂的加載因子就是 HashMap (當前的容量/總容量) 到達一定值的時候,HashMap 會實施擴容。加載因子也可以通過構造方法中指定,默認的值是 0.75 。舉個例子,假設有一個 HashMap 的初始容量爲 16 ,那麼擴容的閥值就是 0.75 * 16 = 12 。也就是說,在你打算存入第 13 個值的時候,HashMap 會先執行擴容。

  • threshold:擴容閥值。即 擴容閥值 = HashMap 總容量 * 加載因子。當前 HashMap 的容量大於或等於擴容閥值的時候就會去執行擴容。擴容的容量爲當前 HashMap 總容量的兩倍。比如,當前 HashMap 的總容量爲 16 ,那麼擴容之後爲 32 。

3. JDK1.7中的HashMap實現原理

  • HashMap底層維護一個數組,數組中的每一項都是一個Entry,Entry是HashMap中的一個靜態內部類

      static class Entry<K,V> implements Map.Entry<K,V> {
          final K key;
          V value;
          Entry<K,V> next;//存儲指向下一個Entry的引用,單鏈表結構
          int hash;//對key的hashcode值進行hash運算後得到的值,存儲在Entry,避免重複計算
    
  • HashMap的整體結構如下

  • HashMap構造器: HashMap有4個構造器,其他構造器如果用戶沒有傳入initialCapacity 和loadFactor這兩個參數,會使用默認值,initialCapacity默認爲16,loadFactory默認爲0.75。

  • 鏈表解決hash衝突

    1. 如果 table 數組爲空時先創建數組,並且設置擴容閥值;

    2. 如果 key 爲空時,調用 putForNullKey 方法特殊處理;

    3. 計算 key 的哈希值;

    4. 根據第三步計算出來的哈希值和當前數組的長度來計算得到該key在數組中的索引,其實索引最後的值就等於 hash%table.length ;如果數組的長度爲16, 那麼hash值爲1、17、33,它們計算後(1%16、17%16、33%16)得到的數組小標都爲1,這個時候就會產生hash衝突,引入鏈表,把1、17、33的用一個鏈表保存起來,這個時候Entry實體裏面就是一個列表。

    5. 遍歷該數組索引下的整條鏈表,如果之前已經有一樣的 key ,那麼直接覆蓋value

    6. 如果該 key 之前沒有,那麼就進入addEntry 方法。

  • put方法

      public V put(K key, V value) {
              //如果table數組爲空數組{},進行數組填充(爲table分配實際內存空間),入參爲threshold,此時threshold爲initialCapacity 默認是1<<4(24=16)
              if (table == EMPTY_TABLE) {
                  inflateTable(threshold);
              }
             //如果key爲null,存儲位置爲table[0]或table[0]的衝突鏈上
              if (key == null)
                  return putForNullKey(value);
              int hash = hash(key);//對key的hashcode進一步計算,確保散列均勻
              int i = indexFor(hash, table.length);//獲取在table中的實際位置
              for (Entry<K,V> e = table[i]; e != null; e = e.next) {
              //如果該對應數據已存在,執行覆蓋操作。用新value替換舊value,並返回舊value
                  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++;//保證併發訪問時,若HashMap內部結構發生變化,快速響應失敗
              addEntry(hash, key, value, i);//新增一個entry
              return null;
          }
    
    1. 如果 table 數組爲空時先創建數組,並且設置擴容閥值;

    2. 如果 key 爲空時,調用 putForNullKey 方法特殊處理;

    3. 計算 key 的哈希值;

    4. 根據第三步計算出來的哈希值和當前數組的長度來計算得到該key在數組中的索引,其實索引最後的值就等於 hash%table.length ;

    5. 遍歷該數組索引下的整條鏈表,如果之前已經有一樣的 key ,那麼直接覆蓋 value

    6. 如果該 key 之前沒有,那麼就進入 addEntry 方法。

       void addEntry(int hash, K key, V value, int bucketIndex) {
           if ((size >= threshold) && (null != table[bucketIndex])) {
               resize(2 * table.length);//當size超過臨界閾值threshold,並且即將發生哈希衝突時進行擴容
               hash = (null != key) ? hash(key) : 0;
               bucketIndex = indexFor(hash, table.length);
           }
      
           createEntry(hash, key, value, bucketIndex);
       }
      

    當發生哈希衝突並且size大於閾值的時候,需要進行數組擴容,擴容時,需要新建一個長度爲之前數組2倍的新的數組,然後將當前的Entry數組中的元素全部傳輸過去,擴容後的新數組長度爲之前的2倍

  • get方法

      public V get(Object key) {
           //如果key爲null,則直接去table[0]處去檢索即可。
              if (key == null)
                  return getForNullKey();
              Entry<K,V> entry = getEntry(key);
              return null == entry ? null : entry.getValue();
       }
    

    get方法通過key值返回對應value,如果key爲null,直接去table[0]處檢索。我們再看一下getEntry這個方法

      final Entry<K,V> getEntry(Object key) {
              
          if (size == 0) {
              return null;
          }
          //通過key的hashcode值計算hash值
          int hash = (key == null) ? 0 : hash(key);
          //indexFor (hash&length-1) 獲取最終數組索引,然後遍歷鏈表,通過equals方法比對找出對應記錄
          for (Entry<K,V> e = table[indexFor(hash, table.length)];
               e != null;
               e = e.next) {
              Object k;
              if (e.hash == hash && 
                  ((k = e.key) == key || (key != null && key.equals(k))))
                  return e;
          }
          return null;
      }
    

    從HashMap中get元素時,首先計算key的hashCode,找到數組中對應位置的某一元素,然後通過key的equals方法在對應位置的鏈表中找到需要的元素。

4. 重寫了equals(),爲什麼還要重寫hashCode()呢?

hashCode是用於查找使用的,用於定位在數組中的下標,而equals是用於比較兩個對象的是否相等的,我們重寫一個類的equals方法,而它的hashcode不重寫,將導致定位數組下標的時候,定位到不同下標,put進入一個對象,取出來的時候爲null.

public class demo {
    private static class Student{
        int id;
        String userName;

        public Student(int id, String userName) {
            this.id = id;
            this.userName = userName;
        }
        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()){
                return false;
            }
            Student student = (Student) o;
            //兩個對象是否等值,通過id來確定
            return this.id == student.id;
        }

    }
    public static void main(String []args){
        HashMap<Student,String> map = new HashMap<Student, String>();
        Student student = new Student(1,"小明");
        //put到hashmap中去
        map.put(student,"小明");
        //get取出,從邏輯上講應該能輸出“小明”
        System.out.println("結果:"+map.get(new Student(1,"小明")));
    }
}

結果爲:NULL

5. JDK1.8中的HashMap

5.1 概述

如果說成百上千個節點在hash時發生碰撞,存儲一個鏈表中,那麼如果要查找其中一個節點,那就不可避免的花費O(N)的查找時間,這將是多麼大的性能損失。這個問題終於在JDK8中得到了解決。再最壞的情況下,鏈表查找的時間複雜度爲O(n),而紅黑樹一直是O(logn),這樣會提高HashMap的效率。

JDK7中HashMap採用的是位桶+鏈表的方式,即我們常說的散列鏈表的方式,而JDK8中採用的是位桶+鏈表/紅黑樹(有關紅黑樹請查看紅黑樹)的方式,也是非線程安全的。當某個位桶的鏈表的長度達到某個閥值的時候,這個鏈表就將轉換成紅黑樹。

JDK8中,當同一個hash值的節點數大於8時,將不再以單鏈表的形式存儲了,會被調整成一顆紅黑樹。這就是JDK7與JDK8中HashMap實現的最大區別。

前面產生衝突的那些KEY對應的記錄只是簡單的追加到一個鏈表後面,這些記錄只能通過遍歷來進行查找,但是超過這個閾值後HashMap開始將列表升級成一個二叉樹,使用哈希值作爲樹的分支變量,如果兩個哈希值不等,但指向同一個桶的話,較大的那個會插入到右子樹裏。

5.2 數組元素Node

JDK中Entry的名字變成了Node,原因是和紅黑樹的實現TreeNode相關聯。

transient Node<k,v>[] table;//存儲(位桶)的數組</k,v>  

//Node是單向鏈表,它實現了Map.Entry接口  
static class Node<k,v> implements Map.Entry<k,v> {  
    final int hash;  
    final K key;  
    V value;  
    Node<k,v> next;  
    //構造函數Hash值 鍵 值 下一個節點  
    Node(int hash, K key, V value, Node<k,v> next) {  
        this.hash = hash;  
        this.key = key;  
        this.value = value;  
        this.next = next;  
    }  
}

5.3 紅黑樹

//紅黑樹  
static final class TreeNode<k,v> extends LinkedHashMap.Entry<k,v> {  
    TreeNode<k,v> parent;  // 父節點  
    TreeNode<k,v> left; //左子樹  
    TreeNode<k,v> right;//右子樹  
    TreeNode<k,v> prev;    // needed to unlink next upon deletion  
    boolean red;    //顏色屬性  
    TreeNode(int hash, K key, V val, Node<k,v> next) {  
        super(hash, key, val, next);  
    }  

    //返回當前節點的根節點  
    final TreeNode<k,v> root() {  
        for (TreeNode<k,v> r = this, p;;) {  
            if ((p = r.parent) == null)  
                return r;  
            r = p;  
        }  
    }  

參考博客:https://www.cnblogs.com/chengxiao/p/6059914.html

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