Java之HashMap的底層原理:面試常考知識點

一、 面試知識點

  1. 隨着18年以來現在互聯網對java面試題也是越問越深,其中hashmap更是java必問問題,那麼我們今天就來總結一下hashmap 的底層原理和麪試常考知識點。
  2. HashMap 是一種存儲高校但是不保證有序的容器,它的數據結構爲"數組+鏈表/紅黑樹"的結構(當鏈表長度到8以後數據結構改爲紅黑樹)
    image.png
  • 底層實現了Map<k,v> 的接口並實現了淺拷貝和序列化,HashMap 默認初始值大小爲16 ,初始值大小必須爲2的冪次,如果用戶輸入的不是2的冪,那麼系統自動更新爲輸入值附近的2的冪次,最大大小爲2的30次冪。HashMap的閾值默認爲 0.75,當存儲節點超過該值,對map進行擴容。
    每次擴容爲原來的1倍。
    image.png
  • HashMap 提供了四種構造方法,分別是默認構造方法;可以指定初始容量構造方法;可以指定初始值和閾值的構造方法;以及基於一個Map的構造方法。一般常用的都是給定初始容量大小的構造方法
    image.png
  • 在一次put(添加操作)的時候,HashMap 會先進行初始化,如果沒有先進行初始化操作,初始化過程會取比用戶指定容量大的最近2的冪次數作爲數組的初始容量,如果設置了擴容的閾值也一併更新。初始化完成以後繼續put 方法
    1. 先判斷有沒有初始化
    2. 在判斷傳入的key是否爲空 就存儲在table(0)位置
    3. key不爲空就對key進行hash,hash的結果在 & 上數組的長度就得到了位置。
    4. 如果存儲位置爲空就創建新節點,不爲空就說存在hash衝突了。
    5. 解決衝突HashMap會遍歷整個鏈表,如果有相同的value值就更新,否則創建節點添加到鏈表頭。
    6. 添加還要判斷存儲節點是否達到閾值,達到閾值要進行擴容。
    7. 擴容兩倍,擴容的時候使用Arrays.copy() 進行擴容。
    8. 擴容過後新插入的節點也要重新進行hash 一遍才能插入。
// jdk8 源碼
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  • 取值的操作和添加差不多。
    1. 先判斷是否爲空,爲空就取table(0)去找
    2. 不爲空也低先hash & 數組長度得到下標位置
    3. 在遍歷找到相同的找到相應的值。
  • 以上這些是一些常用的知識點,但是如果你只是知道以上這些還是不夠的,一般面試官還會問你HashMap 線程安全碼? 當然大家都知道是不安全的,但是要是問你怎麼解決呢? 如果你要是回答加鎖或者回答使用HashTable 基本上就掛了。HashMao是一個線程不安全的容器,在併發操作會出現丟失更新問題,嚴重會導致cpu宕機的,一般報錯爲java.util.ConcurrentModificationException.那我們該怎麼解決呢?
    1. 使用java類庫提供的collections工具包下的Collections.synchronizedMap(new HashMap()),返回一個線程安全的Map
    2. 使用併發包(java.util.concurrent)下的ConcurrentHashMap,ConcurrentHashMap採用分段式鎖機制實現線程安全。
  • 能不能手寫一個HashMap 線程不安全的案例
private static void hashMapNotSafe() {
     // 線程不安全版本
       Map<String, String> hashMap = new HashMap<>();
       //併發版本的hashMap。
  // Map<String, String> hashMap = new ConcurrentHashMap<>() ;
       for (int i = 0; i < 30; i++) {
           new Thread(()-> {
               hashMap.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0, 8));
               System.out.println(hashMap);
           }, String.valueOf(i)).start();
       }
   }
  • java8和java7的區別
    1. Hash1.7 和1.8 最大的不同在於1.8 採用了“數組+鏈表+紅黑樹”的數據結構,在鏈表長度超過8 時,把鏈表轉化成紅黑樹來解決HashMap 因鏈表變長而查詢變慢的問題;
    2. 1.7 的底層節點爲Entry,1.8 爲node ,但是本質一樣,都是Map.Entry 的實現
    3. 還有就是在存取數據時添加了關於樹結構的遍歷更新與添加操作,並採用了尾插法來避免環形鏈表的產生

二、 考點分析

HashMap 作爲最基礎的容器,常用來考1.7和1.8的區別,除了這個要想在面試中脫穎而出還要對HashMap的前因後果要多瞭解。

  1. 考點一:爲什麼初始容量爲2的冪等次?爲什麼負載因子爲0.75f?爲什麼做那麼多擾動處理。
  • 這些問題都要圍繞一個點回答:減少hash 衝突。
  • 容量必須爲2的冪次是爲了增加取值的可能性。
    • 2的n次冪轉換爲二進制爲1後面n個0,在計算下標的是否爲hash&(length-1),也就是&(n-1)個1.所有二進制爲1的好處?
      • 0/1 & 1 都爲它本身
      • 0/1 & 0 都爲 0
    • 可以看出&1保證了取值的平均。如果某一位爲0 ,比如最後一位,那麼它&出來下標就一定是個偶數,減少了HashMap 數組一半的取值,大大增加了衝突的可能。
  • 負載因子爲0.75f是空間與時間的均衡。
    • 如果負載因子小,意味着閾值變小。比如容量爲10 的HashMap,負載因子爲0.5f,那麼存儲5個就會擴容到20,出現哈希衝突的可能性變小,但是空間利用率不高。適用於有足夠內存並要求查詢效率的場景。
    • 相反如果閾值爲1 ,那麼容量爲10,就必須存儲10個元素才進行擴容,出現衝突的概率變大,極端情況下可能會從O(1)退化到O(n)。適用於內存敏感但不要求要求查詢效率的場景
  • hash()的意義在於使hash結果不同hash 算法的好壞直接印象hash結構的效率。1.8 之所以把9 次擾動降到2 次,是出於計算效率的考慮。
  1. 考點二:& 字符串和%字符串 雖然效果一樣,但是操作效果更高。
  2. 考點三:爲什麼int String 更適合做key?
  • int 和 String 的好處在於hash 出來的值不會改變。如果是一個對象,那麼他們可能會因爲內部引用的改變而hashCode 值的改變,會導致存儲重複的數據或找不到數據的情況。

三、 面試時候由於HashMap 引導出來的其他問題?

  • 不僅僅是HashMap 的問題,在面試的時候,面試官會引導出很多其他問題,所以這個地方你在回答問題的時候要設計引導到你熟悉的內容上
  • 說說concurrentHashMap 和 HashMap 的區別以及底層的實現?
  • 說說HashMap 如何實現有序(LinkHashMap 和TreeMap)以及他們的差別
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章