爲什麼面試官那麼喜歡問HashMap呀

      很多人在面試的時候總會被問到HashMap原理,卻很少有人能夠回答的好,或者說回答的太粗淺,被人一下看出大概的技術水平自己卻毫不知情。今天我們來研究一下爲什麼面試官這麼喜歡問HashMap吧?

1.HashMap put()相關知識

首先我們先了解hashmap的大致原理,hashmap是由數組+鏈表的方式組成的,如下圖:

爲什麼面試官那麼喜歡問HashMap

上圖左邊的數組長度是16(初始大小),每個數組位置上放置的是一個鏈表(Node),put數據時,當鏈表中的key的hash值相同時而value值不同(Java 7存入鏈表頭部,Java 8存入鏈表尾部),如果key值也相同則覆蓋之前的鏈表節點。鏈表長度大於8則轉換爲紅黑樹(Java 8),爲什麼要用紅黑樹後面再進行講解,這裏要詳細介紹一下hash表中的有關紅黑樹的3個關鍵參數:

  • TREEIFY_THRESHOLD = 8

    一個桶的樹化閾值,桶中元素超過這個值才使用紅黑樹節點替換鏈表節點,這個值必須爲8,要不然頻繁轉換效率也不高。

  • static final int UNTREEIFY_THRESHOLD = 6

    一個樹的鏈表還原閾值,當擴容時,會進行重新hash(Java 7 全部重新hash,Java 8部分重新hash)桶中元素個數小於這個值時,就會把樹形的桶元素還原爲鏈表結構。這個值至多爲6。

  • static final int MIN_TREEIFY_CAPACITY = 64

    /**
    * The smallest table capacity for which bins may be treeified.
    * (Otherwise the table is resized if too many nodes in a bin.)
    * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
    * between resizing and treeification thresholds.
    */

對於第三點尚有疑問,上面是從源碼中拷貝的註釋,我個人理解是哈希表的最小的被樹形化的值是64,然後後面什麼防止樹形化和擴容衝突至少是32,我的疑問是鏈表長度超過8不就轉化爲紅黑樹了嗎,這個值具體含義也不是很理解,希望大家指點。

2.爲什麼要引入紅黑樹?

JDK 1.8以前是數組+鏈表,還未引入紅黑樹,這就導致了鏈表過長時查找的時間複雜度是O(n), 完全失去了設計HashMap時的設計初衷,針對這種情況JDK 1.8引入了紅黑樹(查找的時間複雜度爲O(logN),什麼是紅黑樹呢,紅黑樹是一種自平衡的二叉查找樹,不是一種絕對平衡的二叉樹,它放棄了追求絕對平衡,追求大致平衡,在與平衡二叉樹的時間複雜度相差不大的情況下,保證每次插入最多隻需要三次旋轉就能達到平衡,實現起來也更爲簡單。從而獲得更高的查找性能。

3.爲什麼初始值大小爲2的n次方

hashmap的初始值是16,即2的4次方,之後的每次擴容都是兩倍擴容,爲什麼要這麼設計呢?我覺得有一下幾點:

  • 如果往hashmap中存放數據,我們首先得保證它能夠儘量均勻分佈,爲了保證能夠均勻分佈,我們可能會想到用取模的方式去實現,如果用傳統的‘%’方式來實現效率不高,當大小(length)總爲2的n次方時,h&(length-1)運算等價於對length取模,也就是h%lenth,但是&比%具有更高的效率,同時也減少了hash碰撞。

  • 和h&(length-1)相關,當容量大小(n)爲2的n次方時,n-1 的二進制的後幾位全是1,在h爲隨機數的情況下,與(n-1)進行與操作時,會分佈的更均勻,想一想,如果n-1的二進制數是1110,當尾數爲0時,與出來的值尾數永遠爲0,那麼0001,1001,1101等尾數爲1的位置就永遠不可能被entry佔用,就造成了空間浪費。

在hashmap源碼中,put方法會調用indexFor方法,這個方法主要是根據key的hash值找到這個entry在table中的位置,最後 return 的是 h&(length-1)

4.HashMap和其他數據結構的關聯

  • 與Hashtable的關係,Hashtable是線程安全的,比如put,get等很多使用了synchronized修飾,和ConcurrentHashMap相比,在Hashtable的大小增加到一定的時候,效率會急劇下降,因爲迭代時需要被鎖定很長的時間,ConcurrentHashMap引入了分割,僅僅需要鎖定map的某個部分,所以其實效率並不高。再看看Hashtable的put方法

    public synchronized V put(K key, V value) {
       // Make sure the value is not null
       if (value == null) {
           throw new NullPointerException();
      }
    
       // Makes sure the key is not already in the hashtable.
       HashtableEntry<?,?> tab[] = table;
       int hash = key.hashCode();
       int index = (hash & 0x7FFFFFFF) % tab.length;
       @SuppressWarnings("unchecked")
       HashtableEntry<K,V> entry = (HashtableEntry<K,V>)tab[index];
       for(; entry != null ; entry = entry.next) {
           if ((entry.hash == hash) && entry.key.equals(key)) {
               V old = entry.value;
               entry.value = value;
               return old;
          }
      }
    
       addEntry(hash, key, value, index);
       return null;
    }

可以看到,裏面的index是用%的方式進行取下標,看起來效率也不行啊,最後從命名上看HashMap和Hashtable,Hashtable明顯沒遵循駝峯式命名規則,這可能也是它被棄用原因之一(哈哈哈)。

  • 與HashSet的關係,我們先來看HashSet的add方法

    public boolean add(E e) {
       return map.put(e, PRESENT)==null;
    }

這裏面的map是一個HashMap,e是放入的元素,value此時有一個統一的值:

private static final Object PRESENT = new Object();

可以看出,其實HashSet就是一個限制功能了的HashMap,如果你瞭解了HashMap,HashSet你自然就懂了。雖然說Set對於重複的元素不放入,倒不如直接說是底層的Map直接把原值替代了。HashSet沒有提供get方法,原因同HashMap一樣,Set內部是無序的,只能通過迭代的方式獲取。

  • 與LinkedHashMap的關係,LinkedHashMap是一個數組+雙向鏈表與HashMap不同,可以保證元素的迭代順序,該迭代順序可以是插入順序或者是訪問順序(LRU原理)。

  • 與LinkedHashSet的關係,LinkedHashSet是繼承自HashSet,底層實現是LinkedHashMap。

  • TreeSet中存放的元素是有序的(不是插入時的順序,是有按關鍵字大小排序的),且元素不能重複。

5. HashMap在 Java7 和 Java8 的變化

擴容時是否全部重新hash和引入紅黑樹

總結

通過上面的分析,你應該明白爲什麼面試官那麼喜歡問HashMap了,因爲HashMap設計到的點太多了,可問的東西太多了,比較具有代表性,如果能夠充分了解HashMap,也能夠漸漸做到一通百通了。高質量編程視頻shangyepingtai.xin

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