HashMap,你瞭解多少?

概述

上一篇文章 Android 中高級面試必知必會 中 JAVA 部分的第一部分就是容器,容器是 JAVA 中很是重要的一個部分,也是面試時考察基礎知識很重要的一環,咱們首先來看下圖,關於容器部分的整體框架html

集合框架主要分爲兩大類: Collection 和 Map。java

Collection 是 List、Set 等集合高度抽象出來的接口,它包含了這些集合的基本操做,它主要又分爲兩大部分:List和Set。node

Map 是一個映射接口,其中的每一個元素都是一個 key-value 鍵值對,一樣抽象類 AbstractMap 經過適配器模式實現了 Map 接口中的大部分函數。像咱們經常使用的 HashMap、LinkedHashMap 都是繼承自 Map 接口。面試

今天咱們主要講解容器中的 HashMap 部分。算法

問題

咱們首先來看下面幾個問題,讀者能夠本身試着回答一下。
一、 HashMap 的實現原理?底層數據結構?
二、 HashMap 的擴容,擴容因子?
三、 什麼是哈希碰撞,如何解決哈希碰撞?
四、 HashMap 是線程安全的嗎?shell

HashMap

要講到 HashMap,咱們主要分爲如下主要模塊進行講解。編程

哈希

Hash ,通常直接音譯爲「哈希」或「散列」,就是把任意長度的輸入,經過散列算法,變化爲固定長度的輸出,輸出值則稱爲散列值。數組

常見的 Hash 函數有一下幾種。安全

  1. 直接定址法:直接以關鍵字 k 或者 k 加上某個常數做爲哈希地址。
  2. 數字分析法:提取關鍵字中比較均勻的數字做爲哈希地址。
  3. 除留餘數法:用關鍵字 k 初一某個不大於哈希表長度 m 的 數 p ,將所得餘數做爲哈希表地址。
  4. 僞隨機數法:採用一個僞隨機數看成哈希函數。

哈希碰撞

兩個不一樣的輸入值,通過同一散列函數計算出的散列值相同的現象叫作哈希碰撞。bash

常見的解決哈希碰撞的方法有以下幾種:

  1. 開放地址法:一旦發生衝突,就去尋找下一個空的散列地址,只要散列表足夠大,空的散列地址就可以找到,並將記錄存入。
  2. 鏈地址法:將哈希表的每一個單元做爲鏈表的頭結點,全部哈希地址爲 i 的元素構成一個同義詞鏈表。即發生衝突時,就把該關鍵字鏈放在以該單位爲頭結點的鏈表的尾部。
  3. 再哈希法:當哈希地址出現衝突後,用其餘函數計算另外一個哈希函數的地址,直到衝突再也不產生爲止。
  4. 創建公共溢出區:將哈希表分爲基本表和溢出表兩部分,發生衝突的元素都放入溢出表中。

實現原理

在 JDK1.6,JDK1.7 中,HashMap 採用數組+鏈表實現,即便用鏈表處理衝突,同一 hash 值的鏈表都存儲在一個鏈表裏。可是當位於一個桶中的元素較多,即 hash 值相等的元素較多時,經過 key 值依次查找的效率較低。

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

擴容

加載因子(默認0.75):爲何須要使用加載因子,爲何須要擴容呢?
由於若是填充比很大,說明利用的空間不少,若是一直不進行擴容的話,鏈表就會愈來愈長,這樣查找的效率很低,由於鏈表的長度很大(固然最新版本使用了紅黑樹後會改進不少),擴容以後,將原來鏈表數組的每個鏈表分紅奇偶兩個子鏈表分別掛在新鏈表數組的散列位置,這樣就減小了每一個鏈表的長度,增長查找效率。

構造 hash 表時,若是不指明初始大小,默認大小爲 16(即 Node 數組大小 16),若是 Node[] 數組中的元素達到(填充比 * Node.length)從新調整 HashMap 大小 變爲原來 2 倍大小,擴容很耗時

/** 
    * Initializes or doubles table size.  If null, allocates in 
    * accord with initial capacity target held in field threshold. 
    * Otherwise, because we are using power-of-two expansion, the 
    * elements from each bin must either stay at same index, or move 
    * with a power of two offset in the new table. 
    * 
    * @return the table 
    */  
   final Node<K,V>[] resize() {  
       Node<K,V>[] oldTab = table;  
       int oldCap = (oldTab == null) ? 0 : oldTab.length;  
       int oldThr = threshold;  
       int newCap, newThr = 0;  
      
/*若是舊錶的長度不是空*/  
       if (oldCap > 0) {  
           if (oldCap >= MAXIMUM_CAPACITY) {  
               threshold = Integer.MAX_VALUE;  
               return oldTab;  
           }  
/*把新表的長度設置爲舊錶長度的兩倍,newCap=2*oldCap*/  
           else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&  
                    oldCap >= DEFAULT_INITIAL_CAPACITY)  
      /*把新表的門限設置爲舊錶門限的兩倍,newThr=oldThr*2*/  
               newThr = oldThr << 1; // double threshold  
       }  
    /*若是舊錶的長度的是0,就是說第一次初始化表*/  
       else if (oldThr > 0) // initial capacity was placed in threshold  
           newCap = oldThr;  
       else {               // zero initial threshold signifies using defaults  
           newCap = DEFAULT_INITIAL_CAPACITY;  
           newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);  
       }  
      
       if (newThr == 0) {  
           float ft = (float)newCap * loadFactor;//新表長度乘以加載因子  
           newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?  
                     (int)ft : Integer.MAX_VALUE);  
       }  
       threshold = newThr;  
       @SuppressWarnings({"rawtypes","unchecked"})  
/*下面開始構造新表,初始化表中的數據*/  
       Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];  
       table = newTab;//把新表賦值給table  
       if (oldTab != null) {//原表不是空要把原表中數據移動到新表中      
           /*遍歷原來的舊錶*/        
           for (int j = 0; j < oldCap; ++j) {  
               Node<K,V> e;  
               if ((e = oldTab[j]) != null) {  
                   oldTab[j] = null;  
                   if (e.next == null)//說明這個node沒有鏈表直接放在新表的e.hash & (newCap - 1)位置  
                       newTab[e.hash & (newCap - 1)] = e;  
                   else if (e instanceof TreeNode)  
                       ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);  
/*若是e後邊有鏈表,到這裏表示e後面帶着個單鏈表,須要遍歷單鏈表,將每一個結點重*/  
                   else { // preserve order保證順序  
                ////新計算在新表的位置,並進行搬運  
                       Node<K,V> loHead = null, loTail = null;  
                       Node<K,V> hiHead = null, hiTail = null;  
                       Node<K,V> next;  
                      
                       do {  
                           next = e.next;//記錄下一個結點  
          //新表是舊錶的兩倍容量,實例上就把單鏈表拆分爲兩隊,  
&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;//e.hash&oldCap爲偶數一隊,e.hash&oldCap爲奇數一對  
                           if ((e.hash & oldCap) == 0) {  
                               if (loTail == null)  
                                   loHead = e;  
                               else  
                                   loTail.next = e;  
                               loTail = e;  
                           }  
                           else {  
                               if (hiTail == null)  
                                   hiHead = e;  
                               else  
                                   hiTail.next = e;  
                               hiTail = e;  
                           }  
                       } while ((e = next) != null);  
                      
                       if (loTail != null) {//lo隊不爲null,放在新表原位置  
                           loTail.next = null;  
                           newTab[j] = loHead;  
                       }  
                       if (hiTail != null) {//hi隊不爲null,放在新表j+oldCap位置  
                           hiTail.next = null;  
                           newTab[j + oldCap] = hiHead;  
                       }  
                   }  
               }  
           }  
       }  
       return newTab;  
   }
複製代碼

HashMap 的 get 方法

/** 
    * Initializes or doubles table size.  If null, allocates in 
    * accord with initial capacity target held in field threshold. 
    * Otherwise, because we are using power-of-two expansion, the 
    * elements from each bin must either stay at same index, or move 
    * with a power of two offset in the new table. 
    * 
    * @return the table 
    */  
   final Node<K,V>[] resize() {  
       Node<K,V>[] oldTab = table;  
       int oldCap = (oldTab == null) ? 0 : oldTab.length;  
       int oldThr = threshold;  
       int newCap, newThr = 0;  
      
/*若是舊錶的長度不是空*/  
       if (oldCap > 0) {  
           if (oldCap >= MAXIMUM_CAPACITY) {  
               threshold = Integer.MAX_VALUE;  
               return oldTab;  
           }  
/*把新表的長度設置爲舊錶長度的兩倍,newCap=2*oldCap*/  
           else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&  
                    oldCap >= DEFAULT_INITIAL_CAPACITY)  
      /*把新表的門限設置爲舊錶門限的兩倍,newThr=oldThr*2*/  
               newThr = oldThr << 1; // double threshold  
       }  
    /*若是舊錶的長度的是0,就是說第一次初始化表*/  
       else if (oldThr > 0) // initial capacity was placed in threshold  
           newCap = oldThr;  
       else {               // zero initial threshold signifies using defaults  
           newCap = DEFAULT_INITIAL_CAPACITY;  
           newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);  
       }  
      
      
      
       if (newThr == 0) {  
           float ft = (float)newCap * loadFactor;//新表長度乘以加載因子  
           newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?  
                     (int)ft : Integer.MAX_VALUE);  
       }  
       threshold = newThr;  
       @SuppressWarnings({"rawtypes","unchecked"})  
/*下面開始構造新表,初始化表中的數據*/  
       Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];  
       table = newTab;//把新表賦值給table  
       if (oldTab != null) {//原表不是空要把原表中數據移動到新表中      
           /*遍歷原來的舊錶*/        
           for (int j = 0; j < oldCap; ++j) {  
               Node<K,V> e;  
               if ((e = oldTab[j]) != null) {  
                   oldTab[j] = null;  
                   if (e.next == null)//說明這個node沒有鏈表直接放在新表的e.hash & (newCap - 1)位置  
                       newTab[e.hash & (newCap - 1)] = e;  
                   else if (e instanceof TreeNode)  
                       ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);  
/*若是e後邊有鏈表,到這裏表示e後面帶着個單鏈表,須要遍歷單鏈表,將每一個結點重*/  
                   else { // preserve order保證順序  
                ////新計算在新表的位置,並進行搬運  
                       Node<K,V> loHead = null, loTail = null;  
                       Node<K,V> hiHead = null, hiTail = null;  
                       Node<K,V> next;  
                      
                       do {  
                           next = e.next;//記錄下一個結點  
          //新表是舊錶的兩倍容量,實例上就把單鏈表拆分爲兩隊,  
&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;//e.hash&oldCap爲偶數一隊,e.hash&oldCap爲奇數一對  
                           if ((e.hash & oldCap) == 0) {  
                               if (loTail == null)  
                                   loHead = e;  
                               else  
                                   loTail.next = e;  
                               loTail = e;  
                           }  
                           else {  
                               if (hiTail == null)  
                                   hiHead = e;  
                               else  
                                   hiTail.next = e;  
                               hiTail = e;  
                           }  
                       } while ((e = next) != null);  
                      
                       if (loTail != null) {//lo隊不爲null,放在新表原位置  
                           loTail.next = null;  
                           newTab[j] = loHead;  
                       }  
                       if (hiTail != null) {//hi隊不爲null,放在新表j+oldCap位置  
                           hiTail.next = null;  
                           newTab[j + oldCap] = hiHead;  
                       }  
                   }  
               }  
           }  
       }  
       return newTab;  
   }
複製代碼

HashMap 中的 put 方法

public V put(K key, V value) {  
        return putVal(hash(key), key, value, false, true);  
    }  
     /** 
     * Implements Map.put and related methods 
     * 
     * @param hash hash for key 
     * @param key the key 
     * @param value the value to put 
     * @param onlyIfAbsent if true, don't change existing value * @param evict if false, the table is in creation mode. * @return previous value, or null if none */ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; /*若是table的在(n-1)&hash的值是空,就新建一個節點插入在該位置*/ if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); /*表示有衝突,開始處理衝突*/ else { Node<K,V> e; K k; /*檢查第一個Node,p是否是要找的值*/ if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { /*指針爲空就掛在後面*/ if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); //若是衝突的節點數已經達到8個,看是否須要改變衝突節點的存儲結構,&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp; &emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;//treeifyBin首先判斷當前hashMap的長度,若是不足64,只進行 //resize,擴容table,若是達到64,那麼將衝突的存儲結構爲紅黑樹 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } /*若是有相同的key值就結束遍歷*/ if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } /*就是鏈表上有相同的key值*/ if (e != null) { // existing mapping for key,就是key的Value存在 V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue;//返回存在的Value值 } } ++modCount; /*若是當前大小大於門限,門限本來是初始容量*0.75*/ if (++size > threshold) resize();//擴容兩倍 afterNodeInsertion(evict); return null; } 複製代碼

HashMap 是否爲線程安全?

關於 HashMap 線程不安全這一點,《Java 併發編程的藝術》一書中是這樣說的

HashMap 在併發執行 put 操做時會引發死循環,致使 CPU 利用率接近 100%。由於多線程會致使 HashMap 的 Node 鏈表造成環形數據結構,一旦造成環形數據結構,Node 的 next 節點永遠不爲空,就會在獲取 Node 時產生死循環。

其實死循環並非發生在put操做時,而是發生在擴容時。詳細的解釋能夠看下面幾篇博客:
Java HashMap 的死循環
HashMap在java併發中如何發生死循環

如何線程安全的使用 HashMap?

  • Hashtable
  • ConcurrentHashMap
  • Synchronized Map
//Hashtable
Map<String, String> hashtable = new Hashtable<>();
  
//synchronizedMap
Map<String, String> synchronizedHashMap = Collections.synchronizedMap(new HashMap<String, String>());
  
//ConcurrentHashMap
Map<String, String> concurrentHashMap = new ConcurrentHashMap<>();
複製代碼

具體實現原理在後續文章中會有所涉獵,請持續關注。

以後會就行更新 Java 中的集合相關內容,我會根據內容多少決定分幾篇文章去講,大體內容如我整理腦圖

爲避免失聯或想第一時間查看個人文章更新,可關注個人微信公衆號 KevenZheng ,以後會陸續更新上述目錄的內容,敬請關注。

如需轉載,請聯繫我或註明出處!

相關文章
相關標籤/搜索