OpenJDK1.8.0 源碼解析————HashMap的實現(一)


    HashMap是Java Collection Framework 的重要成員之一。HashMap是基於哈希表的 Map 接口的實現,此實現提供所有可選的映射操作,映射是以鍵值對的形式映射:key-value。key——此映射所維護的鍵的類型,value——映射值的類型,並且允許使用 null 鍵和 null 值。而且HashMap不保證映射的順序。

    簡單的介紹一下HashMap,就開始HashMap的源碼分析。

    首先簡單的介紹一下HashMap里都包含的數據結構。覺得還是先貼一張圖比較好,結合圖文會比較好理解一些。

  

    現在就可以開說一下這三種數據結構。

    第一個就是Node<K,V>類型的節點。

      備注:static class HashMap.Node<K,V> implements Map.Entry<K,V>

    第二個就是由Node<K,V>類型組成的一個Node<K,V>[] table數組。

    第三個就是一個TreeNode<K,V>類型的紅黑樹。

      備注:static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V>
         static class LinkedHashMap.Entry<K,V> extends HashMap.Node<K,V>
         static class HashMap.Node<K,V> implements Map.Entry<K,V>

    現在結合代碼看一看這三種數據結構(其實Node和Node[] 是同一類,所以就是兩種)。
    

    第一種:Node<K,V>

    看這個之前先看看它實現的父類接口Map.Entry<K,V>的源碼(Entry是Map接口里的一個內部接口)
      

      interface Entry<K,V> {
          K getKey();
          V getValue();
          V setValue(V value);
          boolean equals(Object o);
          int hashCode();
      }

 

 

 


    再看Node的源碼(Node是HashMap類的一個靜態內部類,實現了Map接口里的內部接口Entry)
     

 static class Node<K,V> implements Map.Entry<K,V> {
        //這四個成員變量就是一個Node節點所包含的四個變量域
        //其中hash的計算方法是由一個hash方法得到的,該方法的是實現就是HashMap里,這里為了看的清楚一些,就寫到下面:
        // hash = (h = key.hashCode()) ^ (h >>> 16);
          final int hash;
          final K key;
          V value;
          Node<K,V> next;
        
    
        //構造方法         Node(int hash, K key, V value, Node<K,V> next) {           this.hash = hash;           this.key = key;           this.value = value;           this.next = next;         }
        
//拿到該Node 的key         public final K getKey() { return key; }
        
//拿到該Node 的value         public final V getValue() { return value; }
        
//重寫toString方法         public final String toString() { return key + "=" + value; }

        //修改當前Entry對象的value為傳進入newvalue,返回值為之前的oldvalue值      

         public final V setValue(V newValue) {      

             V oldValue = value;       

              value = newValue;        

             return oldValue;   

         }       

            /*       

            判斷兩個Entry是否相等     

            若兩個Entry的“key”和“value”都相等,則返回true;否則返回false     

            */      

           public final boolean equals(Object o) {        

                 if (o == this)    

                   return true;       

                 if (o instanceof Map.Entry) {         

                    Map.Entry<?,?> e = (Map.Entry<?,?>)o;

                   if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue()))       

                        return true;       

                 }       

                  return false;

          }      

           //重寫了equals,因此重新實現hashCode()       

            public final int hashCode() {     

                return Objects.hashCode(key) ^ Objects.hashCode(value);  

            }   

        } 

  

    第二種:TreeNode(這里只附上TreeNode類的成員變量和一個獲取樹根的方法,其他的省略。因為TreeNode里實現了很多關於紅黑樹的操作方法,如果全部放到這里不僅看不懂而且顯得特別亂)

 

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; 
     //構造函數
     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;
     }    }
//其余部分省略,有興趣的可以自行查看源代碼。 }

 

    然后在說一下TreeNode構造函數所做的事情。

    先了解一下TreeNode的繼承體系:TreeNode繼承了LinkedHashMap.Entry
                    LinkedHashMap.Entry繼承了HashMap.Node
                    HashMap.Node實現了Map.Entry

    了解這個就能清楚的知道TreeNode的構造函數到底做了什么事情。底下的圖顯示了TreeNode利用構造初始化的流程。
      


    其實最終還是調到了Node的構造,初始化TreeNode了從Node繼承而來的四個數據域(int hash,K key,V value,Node<K,V> next)。
  

    接着我們說一下HashMap里幾個重要的成員變量和常量(省略了一部分)。
     

     //通過HahMap實現的接口可知,其支持所有映射操作,能被克隆,支持序列化
      public class HashMap<K,V> extends AbstractMap<K,V> 
            implements Map<K,V>, Cloneable, Serializable {

      private static final long serialVersionUID = 362498820763181265L;

      //默認Node<K,V> table的初始容量16,2的4次方。
      static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 

      //table數組的最大容量為1073 741 824,2的30次方。
      static final int MAXIMUM_CAPACITY = 1 << 30;

      /*
        默認加載因子
        該值和table的長度的乘積為是否對table進行擴容的標志。
        例如當該值默認為0.75,table的長度為16時,就是說當table填充至 12*0.75 =12 之后,這個table就要進行擴容
      */
      static final float DEFAULT_LOAD_FACTOR = 0.75f;


      //哈希表的定義,這個數據結構在前面已經介紹過
      transient Node<K,V>[] table;

      /*
        HashMap的大小,即保存的鍵值對的數量
        當該值大於等於HashMap的閾值時,數組就會擴充
      */
      transient int size;

      /*
        HashMap的閾值.
        用於判斷是否需要調整HashMap的容量
        該值的計算方法為: 加載因子 * (table.length)
        當table.size>=threshold就會擴容,就是如果hashMap中存放的鍵值對大於這個閾值,就進行擴容
        如果加載因子沒有被分配,默認為0.75
        如果table數組沒有被分配,默認為初始容量值(16);
        或若threshold值為0,也表明該值為初始容量值。
      */
      int threshold;
      
/*         加載因子         如果加載因子沒有被分配,默認為0.75       */       final float loadFactor;
      
//當添加到tab[i]位置下的鏈表長度達到8時將鏈表轉換為紅黑樹      static final int TREEIFY_THRESHOLD = 8;         ......     }

 


    介紹完數據結構再來說一下我們平時經常寫的代碼:Map<String,Object> map = new HashMap<String,Object>()

    (這樣的方式獲得的map對象,調用的是默認無參的構造,實際上還有其他三個有參的構造函數)

    要知道寫完這句代碼之后到底發生了什么。我們首先就得看一下HashMap構造器(共4個構造器),源碼如下    

    /*  
        默認無參構     
      構造一個具有默認初始容量 (16) 和默認加載因子 (0.75) 的空 HashMap。
  
*/ public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; } //構造一個帶指定初始容量和加載因子的空 HashMap。 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);
    
//經過一些過濾和處理,現在的loadFactor和initialCapacity都為合法的值     //初始化加載因子     this.loadFactor = loadFactor;
   
 /*       初始化HashMap的閾值       調用tableSizeFor(initialCapacity)方法       該方法根據數組的初始容量的大小求出HashMapde的閾值threshold 
    */     this.threshold = tableSizeFor(initialCapacity);   }   /*     第三個構造方法     構造一個帶指定初始容量和默認加載因子 (0.75) 的空 HashMap。   */   public HashMap(int initialCapacity) {     this(initialCapacity, DEFAULT_LOAD_FACTOR);   }   // 構造一個映射關系與指定 Map 相同的新 HashMap。   public HashMap(Map<? extends K, ? extends V> m) {       this.loadFactor = DEFAULT_LOAD_FACTOR;       putMapEntries(m, false);   }

 



    
上面提到了根據數組初始容量得出HashMap閾值的一個方法:tableSizeFor(int cap)。該方法的源碼如下:

      

      static final int tableSizeFor(int cap) {
          int n = cap - 1;
          n |= n >>> 1;
          n |= n >>> 2;
          n |= n >>> 4;
          n |= n >>> 8;
          n |= n >>> 16;
          return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
      }

 

 

 

  
      簡單的測試了一下這個方法

      

    public class TestHashMap{
        
        static final int MAXIMUM_CAPACITY = 1 << 30;
        
public static void main(String[] args){           System.out.println("---------------------");           System.out.println(" 第一次測試,cap = 0,返回的閾值threshold = "+tableSizeFor(0));           System.out.println("---------------------");           System.out.println(" 第二次測試,cap = 3,返回的閾值threshold = "+tableSizeFor(3));           System.out.println("---------------------");           System.out.println(" 第三次測試,cap = 16,返回的閾值threshold = "+tableSizeFor(16));           System.out.println("---------------------");           System.out.println(" 第四次測試,cap = 100,返回的閾值threshold = "+tableSizeFor(100));           System.out.println("---------------------");           System.out.println(" 第五次測試,cap = 1000,返回的閾值threshold = "+tableSizeFor(1000));           System.out.println("---------------------");           System.out.println(" 第六次測試,cap = 10000,返回的閾值threshold = "+tableSizeFor(10000));           System.out.println("---------------------");         }         static final int tableSizeFor(int cap) {           int n = cap - 1;                     n |= n >>> 1;                    n |= n >>> 2;                     n |= n >>> 4;                  n |= n >>> 8;                    n |= n >>> 16;                    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;         }       }

 

 


   
 輸出結果:
    ---------------------

    第一次測試,cap = 0,返回的閾值threshold = 1
    --------------------
    第二次測試,cap = 3,返回的閾值threshold = 4
    ---------------------
    第三次測試,cap = 16,返回的閾值threshold = 16
    ---------------------
    第四次測試,cap = 100,返回的閾值threshold = 128
    ---------------------
    第五次測試,cap = 1000,返回的閾值threshold = 1024
    ---------------------
    第六次測試,cap = 10000,返回的閾值threshold = 16384
    ---------------------

    總之,tableSizeFor方法是根據table數組的容量計算出hashMap可以維護出的鍵值對。從結果可以看到閾值至少是1,而且是剛好比cap大一點的2的冪。

    至於為什么閾值要做成這樣,看了很多文章,都是說為了使hashMap散列均勻。但是實際上tab[i]中i的選擇是 hash & lenth-1 是和容量在做按位或。

    因此對這里有一些疑惑。

    到此對HashMap一部分介紹完了。哪里有不對的地方,還望指出。還有就是如果能解答我的疑惑,還請指導,十分感謝 ^_^。

    

 
        

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM