(一)由淺入深java集合--HashMap原理

轉自:

 HashMap所在Java集合的位置如下圖所示


1 、大致介紹一下java的集合體繫結構


       List Set Map是這個集合體系中最主要的三個接口。

 

       List、Set繼承自Collection接口。

       Set不允許元素重複。HashSet和TreeSet是兩個主要的實現類。

       List有序且允許元素重複。ArrayList、LinkedList和Vector是三個主要的實現類。


       Map也屬於集合系統,但和Collection接口不同,AbstractMap實現了Map接口,HashMap繼承AbstractMap。SortedMap繼承Map接口,TreeMap繼承SortedMap。從圖中我們可知。

       Map是key對value的映射集合,其中key是一個集合,key不能重複,但是value可以重複。HashMap、TreeMap和HashTable是三個主要實現類。

 

 

       大概介紹了一下java的集合類,接下來主要介紹的是HashMap,HashMap在java集合類中的位置在圖中能看到。


2、HashMap位置


      HashMap 繼承了AbstractMap,AbstractMap實現了Map接口,LinkedHashMap繼承了HashMap。


3、 什麼時候使用HashMap


      當你需要通過一個名字來獲取數據的時候就可以用Map,並且這個名字(也就是key)是不重複的,且在添加和刪除等情況下不需要線程安全,這時候我們就可以用HashMap。

      比如當把用戶的信息存入list的時候,當你根據用戶id查詢某某學生名字時,可能需要遍歷,這時候用Map,直接通過key來找到value就可以了。

      總之,需要鍵值對的時候,用map就可以了。


4 、HashMap使用code


       code見:https://github.com/summerxhf/j2ee-demo/blob/master/HashMap-demo/src/main/java/HashMapDemo.java

 

5、 HashMap概述


      HashMap 是基於哈希表的Map接口的非同步實現。此實現提供所有可選的映射操作,並允許使用null值和null鍵。此類不保證映射的順序,特別不保證該順序恆久不變。(我們可以運行demo中的方法,插入順序和輸出順序並不是一個順序。)

 

6、 HashMap數據結構


      在java編程語言中,最基本的結構就是兩種,一個是數組,另外一個是模擬指針(引用)。HashMap實際上是一個“鏈表散列”的數據結構,即數組和鏈表的結合體。

      一維數組

       

      鏈表

       

      所以HashMap結構如下圖

       

      所以HashMap底層就是一個數組結構,數組中的每一項又是存放的鏈表的頭結點。當新建一個HashMap的時候,就會初始化一個數組。

 

      當new一個HashMap,內部代碼如下所示:

             對於任何一個數組,在初始化建立的時候,都會涉及到建立數組的大小,數組長度是否夠用?能否自動擴充數組容量呢?這些問題。

      下面是初始化數組時的一下參數定義code

  1.            /** 
  2.  * The defaultinitial capacity - MUST be a power of two. 
  3.  */  
  4. static final int DEFAULT_INITIAL_CAPACITY = 16;// 默認初始容量爲16,必須爲2的冪  
  5.    
  6. /** 
  7.  * The maximumcapacity, used if a higher value is implicitly specified 
  8.  * by either of the constructors with arguments. 
  9.  * MUST be apower of two <= 1<<30. 
  10.  */  
  11. static final int MAXIMUM_CAPACITY = 1 << 30;// 最大容量爲2的30次方  
  12.    
  13. /** 
  14.  * The loadfactor used when none specified in constructor. 
  15.  */  
  16. static final floatDEFAULT_LOAD_FACTOR = 0.75f;// 默認加載因子0.75  
  17.    
  18. /** 
  19.  * The table,resized as necessary. Length MUST Always be a power oftwo. 
  20.  */  
  21. transientEntry<K,V>[] table;// Entry數組,哈希表,長度必須爲2的冪  
  22.    
  23. /** 
  24.  * The number ofkey-value mappings contained in this map. 
  25.  */  
  26. transient int size;// 已存元素的個數  
  27.    
  28. /** 
  29.  * The next sizevalue at which to resize (capacity * load factor). 
  30.  * @serial 
  31.  */  
  32. int threshold;// 下次擴容的臨界值,size>=threshold就會擴容  
  33.    
  34.    
  35. /** 
  36.  * The loadfactor for the hash table. 
  37.  * 
  38.  * @serial 
  39.  */  
  40. final float loadFactor;// 加載因子  

 


       在newHashMap的時候,構造方法如下:


 

構造方法摘要

HashMap()

         構造一個具有默認初始容量 (16) 和默認加載因子 (0.75) 的空 HashMap。

 

HashMap(int initialCapacity)

         構造一個帶指定初始容量和默認加載因子 (0.75)的空 HashMap。

 

HashMap(int initialCapacity, float loadFactor)

         構造一個帶指定初始容量和加載因子的空 HashMap

 

HashMap(Map<?extendsK,? extendsV> m)

         構造一個映射關係與指定 Map 相同的 HashMap。

 

 

      我們常用的沒有參數的構造方法,代碼如下。

       //構造一個具有默認初始容量 (16)和默認加載因子 (0.75) 的空 HashMap。

  1. public HashMap() {  
  2.    this.loadFactor = DEFAULT_LOAD_FACTOR;  
  3.    threshold = (int)(DEFAULT_INITIAL_CAPACITY* DEFAULT_LOAD_FACTOR);  
  4.    table = new Entry[DEFAULT_INITIAL_CAPACITY];  
  5.    init();  
  6. }  


    


       table爲Entry數組,怎麼理解這個Entry?map爲地圖,Entry可以理解爲地圖中的各個節點,登記處。在new一個HashMap的時候,默認會初始化16個Entry,加載因子是,當數組中的個數超出了加載因子與當前容量的乘積時,就會通過調用rehash方法將容量翻倍。例如默認的擴容因子爲0.75, 則擴充的臨界值爲16* 0.75 = 12, 也就是map中存放超過12個key value映射時,就會自動擴容。

 


7 、HashMap初始化之後

       new完一個HashMap後,進行put值,put的代碼如下:

         //在此映射中關聯指定值與指定鍵。如果該映射以前包含了一個該鍵的映射關係,則舊值被替換,並返回舊值。

   

  1. public V put(K key, V value) {  
  2.       // 如果key爲null使用putForNullKey來獲取  
  3.       if (key == null)  
  4.           return putForNullKey(value);  
  5.       // 使用hash函數預處理hashCode  
  6.       int hash = hash(key.hashCode());  
  7.       // 獲取對應的索引  
  8.       int i = indexFor(hash, table.length);  
  9.       // 得到對應的hash值的桶,如果這個桶不是,就通過next獲取下一個桶  
  10.       for (Entry<K,V> e = table[i]; e != null;e = e.next) {  
  11.           Object k;  
  12.           // 如果hash相同並且key相同  
  13.           if (e.hash== hash && ((k = e.key) == key || key.equals(k))) {  
  14.               // 獲取當前的value  
  15.               V oldValue = e.value;  
  16.               // 將要存儲的value存進去  
  17.               e.value = value;  
  18.               e.recordAccess(this);  
  19.               // 返回舊的value  
  20.               return oldValue;  
  21.           }  
  22.       }  
  23.   
  24.       modCount++;  
  25.       addEntry(hash, key, value, i);  
  26.       return null;  
  27.    }  

    // key爲null怎麼放value

  

  1. private V putForNullKey(V value) {  
  2.      // 遍歷table[0]的所有桶  
  3.      for (Entry<K,V> e = table[0]; e != null;e = e.next) {  
  4.          // 如果key是null  
  5.          if (e.key== null) {  
  6.              // 取出oldValue,並存入value  
  7.              V oldValue = e.value;  
  8.              e.value = value;  
  9.              e.recordAccess(this);  
  10.              // 返回oldValue  
  11.              return oldValue;  
  12.          }  
  13.      }  
  14.      modCount++;  
  15.      addEntry(0null, value, 0);  
  16.      return null;  
  17.   }  

         //預處理hash值,避免較差的離散hash序列,導致桶沒有充分利用

   static int hash(int h) {

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

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

    }

 

           //返回對應hash值得索引 ,h爲key的hashCode處理後的值,length爲table中Entry數組大小。

   

  1.  static int indexFor(int h, int length) {  
  2.        /***************** 
  3.         * 由於length是2的n次冪,所以h &(length-1)相當於h % length。 
  4.         * 對於length,其2進製表示爲1000...0,那麼length-1爲0111...1。 
  5.         * 那麼對於任何小於length的數h,該式結果都是其本身h。 
  6.         * 對於h = length,該式結果等於0。 
  7.         * 對於大於length的數h,則和0111...1位與運算後, 
  8.         * 比0111...1高或者長度相同的位都變成0, 
  9.         * 相當於減去j個length,該式結果是h-j*length, 
  10.         * 所以相當於h % length。 
  11.         * 其中一個很常用的特例就是h & 1相當於h % 2。 
  12.         * 這也是爲什麼length只能是2的n次冪的原因,爲了優化。 
  13.         */  
  14.        return h & (length-1);  
  15. }  

 

8、瞭解HashMap其他構造函數的源碼

 

       https://github.com/summerxhf/j2ee-demo/blob/master/HashMap-demo/src/main/java/HashMap.java

       

       我們可以看到,我們在put的時候,先根據key的hashCode重新計算hash值,根據hash值得到這個元素在數組中的位置(方法 indexFor),如果數組中的位置上已經有其他元素了,那麼這個元素將以鏈表的形式存放,在鏈表頭中加入新的,最先put的key的value 放在鏈尾。如果該數組位置上沒有元素,則直接將該元素放到改位置上。


9、 一些其他疑問


9.1、對於,put key爲null的值呢?

       如果key爲null的時候,方法putForNullKey告訴我們答案。

       key爲null的時候,value也可以不爲null。不過在 addEntry(0, null, value, 0);的時候,存放的hash值,以及數組的下標值爲0,key值爲null。

 

       如下圖所示,每一行鏈表中,Entry的key是一個。Entry中有key和value ,以及鏈表連接指向。

 



 

9.2、 有人會問到底啥事hashCode


       其實就是經過一系列的數學運算,移位運算得到一個數,就成爲了hashCode。不解釋,看源碼哦.

 


9.3、 Entry的數組的大小,也就是table的大小,爲什麼必須是2的冪次方?

       HashMap的結構是數組+單鏈表結構,我們希望元素是均勻分配的,最理想的效果是,Entry中的每個位置都只有應元素,也就是鏈表的頭結點,就是鏈表的尾節點,這樣查詢效率最高,不需要遍歷鏈表,有而不需要進行equals比較key,而且利用率最大 ,%取模運算 哈希值%table容量=數組下標,而代碼中這樣實現的h & (length-1),當length總是2的n次方時,h &(length-1)運算等價於對length取模,也就是h%length,但是&比%的效率要高。

 


9.4、 HashMap線程不安全,那多線程下使用如何做呢?

       1包裝一下

       2 Map m =Collections.synchronizedMap(newHashMap(...));

       3使用java.util.HashTable,效率最低

       4使用java.util.concurrent.ConcurrentHashMap,相對安全,效率較高

 


       接下來一一說明Map接口的其他實現 。


總結


       從如何使用上,key value映射時,HashMap的原理上,一維數組+單鏈表結構,大概瞭解了他,總之,追本溯源,很好的瞭解他,才能很好的控制他。



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