1. 程式人生 > >(一)由淺入深java集合--HashMap原理

(一)由淺入深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

5、 HashMap概述

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

6、 HashMap資料結構

在java程式語言中,最基本的結構就是兩種,一個是陣列,另外一個是模擬指標(引用)。HashMap實際上是一個“連結串列雜湊”的

資料結構,即陣列和連結串列的結合體。

一維陣列

       

連結串列

       

所以HashMap結構如下圖

       

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

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

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

下面是初始化陣列時的一下引數定義code

[java] view plain copy print?在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.  * The maximumcapacity, used if a higher value is implicitly specified 
  7.  * by either of the constructors with arguments. 
  8.  * MUST be apower of two <= 1<<30. 
  9.  */  
  10. static final int MAXIMUM_CAPACITY = 1 << 30;// 最大容量為2的30次方  
  11. /** 
  12.  * The loadfactor used when none specified in constructor. 
  13.  */  
  14. static final floatDEFAULT_LOAD_FACTOR = 0.75f;// 預設載入因子0.75  
  15. /** 
  16.  * The table,resized as necessary. Length MUST Always be a power oftwo. 
  17.  */  
  18. transientEntry<K,V>[] table;// Entry陣列,雜湊表,長度必須為2的冪  
  19. /** 
  20.  * The number ofkey-value mappings contained in this map. 
  21.  */  
  22. transient int size;// 已存元素的個數  
  23. /** 
  24.  * The next sizevalue at which to resize (capacity * load factor). 
  25.  * @serial 
  26.  */  
  27. int threshold;// 下次擴容的臨界值,size>=threshold就會擴容  
  28. /** 
  29.  * The loadfactor for the hash table. 
  30.  * 
  31.  * @serial 
  32.  */  
  33. 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。

[java] view plain copy print?在CODE上檢視程式碼片派生到我的程式碼片
  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的程式碼如下:

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

[java] view plain copy print?在CODE上檢視程式碼片派生到我的程式碼片
  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.       modCount++;  
  24.       addEntry(hash, key, value, i);  
  25.       return null;  
  26.    }  

    // key為null怎麼放value

[java] view plain copy print?在CODE上檢視程式碼片派生到我的程式碼片
  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陣列大小。

[java] view plain copy print?在CODE上檢視程式碼片派生到我的程式碼片
  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其他建構函式的原始碼

我們可以看到,我們在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的原理上,一維陣列+單鏈表結構,大概瞭解了他,總之,追本溯源,很好的瞭解他,才能很好的控制他。