hashMap 底層原理+LinkedHashMap 底層原理+常見面試題

1.源碼  java

 

 

java1.7    hashMap 底層實現是數組+鏈表node

java1.8 對上面進行優化  數組+鏈表+紅黑樹面試

2.hashmap  是怎麼保存數據的。segmentfault

    在hashmap 中有這樣一個結構數組

        Node implenets Map.entity{安全

          hash數據結構

          key多線程

          valueide

          next函數

        } 

當咱們像hashMap 中放入數據時,其實就是一個

Enity{

  key

  vaue

}

在存以前會把這個Entity  轉成Node 

    怎麼轉的以下:

  根據Entity 的key   經過hash  算出一個值  當成Node 的 hash  ,key vlaue  ,複製到Node 中,對於沒有產生hash 衝突前,Node 的next 是null.

複製完成後,還須要經過Entity 對象的hash  算出  該 Entiry對象 具體應該放在 hashMap 的那個位置。計算以下  Hash&(lenth-1) 獲得的值就是hashMap 對應具體的位置。(lentth是當前hashMap 的長度)。‘

 解決hash 衝突

    就是不一樣的元素經過   Hash&(lenth-1) 公式算出的位置相同,如今就啓動了鏈表(單項鍊表),掛在了當前位置的下面,而鏈表的元素怎麼關聯呢,就用到了Node  的next  ,next的值就是該鏈表下一個元素在內存中的地址。

    jdk1.7  就是這樣處理的,而到了 1.8  之後,就引用了紅黑樹,1.8之後這個鏈表只讓掛7個元素,超過七個就會轉成一個紅黑樹進行處理(最可能是64,超多64 就會從新拆分)。

    當紅黑樹下掛的節點小於等於6的時候,系統會把紅黑樹轉成鏈表。  Node 在jdk1.8以前是插入l鏈表頭部的,在jdk1.8中是插入鏈表的尾部的。

 hashMap 擴容:

    hashMap  會根據  當前的hashMap 的存儲量達到 16*0.75=12 的時候,就是擴容  16*2=32  依次類推下去。2倍擴容。

    擴容後元素是如何作到均勻分的。

      

 對上面的總結:

 

LinkedHashMap 源碼詳細分析(JDK1.8)

 這位大哥寫的很好,能夠看一下    https://segmentfault.com/a/1190000012964859

  我針對LinkedHashMap 的總結有一下幾點

  1.LinkedHashMap 繼承自 HashMap,因此它的底層仍然是基於拉鍊式散列結構。該結構由數組和鏈表+紅黑樹 在此基礎上LinkedHashMap 增長了一條雙向鏈表,保持遍歷順序和插入順序一致的問題。

  2. 在實現上,LinkedHashMap 不少方法直接繼承自 HashMap(好比put  remove方法就是直接用的父類的),僅爲維護雙向鏈表覆寫了部分方法(get()方法是重寫的)。

  3.LinkedHashMap使用的鍵值對節點是Entity 他繼承了hashMap 的Node,並新增了兩個引用,分別是 before 和 after。這兩個引用的用途不難理解,也就是用於維護雙向鏈表.

  4.鏈表的創建過程是在插入鍵值對節點時開始的,初始狀況下,讓 LinkedHashMap 的 head 和 tail 引用同時指向新節點,鏈表就算創建起來了。隨後不斷有新節點插入,經過將新節點接在 tail 引用指向節點的後面,便可實現鏈表的更新

  5.LinkedHashMap 容許使用null值和null鍵, 線程是不安全的,雖然底層使用了雙線鏈表,可是增刪相快了。由於他底層的Entity 保留了hashMap  node 的next 屬性。

  6.如何實現迭代有序?

  從新定義了數組中保存的元素Entry(繼承於HashMap.node),該Entry除了保存當前對象的引用外,還保存了其上一個元素before和下一個元素after的引用,從而在哈希表的基礎上又構成了雙向連接列表。仍然保留next屬性,因此既可像HashMap同樣快速查找,

  用next獲取該鏈表下一個Entry,也能夠經過雙向連接,經過after完成全部數據的有序迭代.

  7.居然inkHashMap 的put 方法是直接調用父類hashMap的,但在 HashMap 中,put 方法插入的是 HashMap 內部類 Node 類型的節點,該類型的節點並不具有與 LinkedHashMap 內部類 Entry 及其子類型節點組成鏈表的能力。那麼,LinkedHashMap 是怎樣創建鏈表的呢?

   雖然linkHashMap 調用的是hashMap中的put 方法,可是linkHashMap 重寫了,了一部分方法,其中就有  newNode(int hash, K key, V value, Node<K,V> e)linkNodeLast(LinkedHashMap.Entry<K,V> p)

   這兩個方法就是 第一個方法就是新建一個 linkHasnMap 的Entity 方法,而 linkNodeLast 方法就是爲了把Entity 接在鏈表的尾部。

  8.鏈表節點的刪除過程

   與插入操做同樣,LinkedHashMap 刪除操做相關的代碼也是直接用父類的實現,可是LinkHashMap 重寫了removeNode()方法 afterNodeRemoval()方法,該removeNode方法在hashMap 刪除的基礎上有調用了afterNodeRemoval 回調方法。完成刪除。

  刪除的過程並不複雜,上面這麼多代碼其實就作了三件事:

  1. 根據 hash 定位到桶位置
  2. 遍歷鏈表或調用紅黑樹相關的刪除方法
  3. 從 LinkedHashMap 維護的雙鏈表中移除要刪除的節點

TreeMap 和SortMap

  1.TreeMap實現了SortedMap接口,保證了有序性。默認的排序是根據key值進行升序排序,也能夠重寫comparator方法來根據value進行排序具體取決於使用的構造方法。不容許有null值null鍵。TreeMap是線程不安全的。

   2.TreeMap基於紅黑樹(Red-Black tree)實現TreeMap的基本操做 containsKey、get、put 和 remove 的時間複雜度是 log(n) 。

public class SortedMapTest {

public static void main(String[] args) {

SortedMap<String,String> sortedMap = new TreeMap<String,String>();
sortedMap.put("1", "a");
sortedMap.put("5", "b");
sortedMap.put("2", "c");
sortedMap.put("4", "d");
sortedMap.put("3", "e");
Set<Entry<String, String>> entry2 = sortedMap.entrySet();
for(Entry<String, String> temp : entry2){
System.out.println("修改前 :sortedMap:"+temp.getKey()+" 值"+temp.getValue());
}
System.out.println("\n");

//這裏將map.entrySet()轉換成list
List<Map.Entry<String,String>> list =
new ArrayList<Map.Entry<String,String>>(entry2);

Collections.sort(list, new Comparator<Map.Entry<String,String>>(){

@Override
public int compare(Entry<String, String> o1, Entry<String, String> o2) {
// TODO Auto-generated method stub
return o1.getValue().compareTo(o2.getValue());
}

});

for(Map.Entry<String,String> temp :list){
System.out.println("修改後 :sortedMap:"+temp.getKey()+" 值"+temp.getValue());
}
}

}

 附加點上面沒有講到的面試題:

1 HashMap特性?
  HashMap的特性:HashMap存儲鍵值對,實現快速存取數據;容許null鍵/值;線程不安全;不保證有序(好比插入的順序)。

2 HashMap中hash函數怎麼是是實現的?還有哪些 hash 的實現方式?
  1. 對key的hashCode作hash操做(高16bit不變,低16bit和高16bit作了一個異或); 
  2. h & (length-1); //經過位操做獲得下標index。

3. 擴展問題1:當兩個對象的hashcode相同會發生什麼?
  由於兩個對象的Hashcode相同,因此它們的bucket位置相同,會發生「碰撞」。HashMap使用鏈表存儲對象,這個Entry(包含有鍵值對的Map.Entry對象)會存儲在鏈表中。

4 擴展問題2:拋開 HashMap,hash 衝突有那些解決辦法?
  開放定址法、鏈地址法、再哈希法。

5若是兩個鍵的hashcode相同,你如何獲取值對象?
  重點在於理解hashCode()與equals()。 
  經過對key的hashCode()進行hashing,並計算下標( n-1 & hash),從而得到buckets的位置。兩個鍵的hashcode相同會產生碰撞,則利用key.equals()方法去鏈表或樹(java1.8)中去查找對應的節點。

6 針對 HashMap 中某個 Entry 鏈太長,查找的時間複雜度可能達到 O(n),怎麼優化?
  將鏈表轉爲紅黑樹,實現 O(logn) 時間複雜度內查找。JDK1.8 已經實現了。

7.若是HashMap的大小超過了負載因子(load factor)定義的容量,怎麼辦?
  擴容。這個過程也叫做rehashing,由於它重建內部數據結構,並調用hash方法找到新的bucket位置。 
  大體分兩步: 
  1.擴容:容量擴充爲原來的兩倍(2 * table.length); 
  2.移動:對每一個節點從新計算哈希值,從新計算每一個元素在數組中的位置,將原來的元素移動到新的哈希表中。 (如何計算上面講的有)
   

8 爲何String, Interger這樣的類適合做爲鍵?
  String, Interger這樣的類做爲HashMap的鍵是再適合不過了,並且String最爲經常使用。 
  由於String對象是不可變的,並且已經重寫了equals()和hashCode()方法了。 
  1.不可變性是必要的,由於爲了要計算hashCode(),就要防止鍵值改變,若是鍵值在放入時和獲取時返回不一樣的hashcode的話,那麼就不能從HashMap中找到你想要的對象。不可變性還有其餘的優勢如線程安全。 
  注:String的不可變性能夠看這篇文章《【java基礎】淺析String》。 
  2.由於獲取對象的時候要用到equals()和hashCode()方法,那麼鍵對象正確的重寫這兩個方法是很是重要的。若是兩個不相等的對象返回不一樣的hashcode的話,那麼碰撞的概率就會小些,這樣就能提升HashMap的性能。

9.hashmap.put 爲何是線程不安全的。(很重要)

正常狀況下  hashmap 在保存數據時,底層是數組+鏈表+紅黑樹  可是 你去源碼中看時,i發現子啊hashMap 底層沒有加任何的多線程中的鎖機制,好比: synchronize修飾  ,因此在多線程的狀況下  hashMap 的單項鍊表,

可能會變成一個環形的鏈表,因此這個鏈表上的Next元素的指向永遠不爲null, 因此在遍歷的時候就是死循環啊。

 9.1HashMap在put的時候,插入的元素超過了容量(由負載因子決定)的範圍就會觸發擴容操做,就是rehash,這個會從新將原數組的內容從新hash到新的擴容數組中,在多線程的環境下,存在同時其餘的元素也在進行put操做,若是hash值相同,可能出現同時在同一數組下用鏈表表示,形成閉環,致使在get時會出現死循環,因此HashMap是線程不安全的

 9.2 HashMap底層是一個Entry數組,當發生hash衝突的時候,hashmap是採用鏈表的方式來解決的,在對應的數組位置存放鏈表的頭結點。對鏈表而言,新加入的節點會從頭結點加入。在hashmap作put操做的時候會調用到以上的方法。如今假如A線程和B線程同時對同一個數組位置調用addEntry,兩個線程會同時獲得如今的頭結點,而後A寫入新的頭結點以後,B也寫入新的頭結點,那B的寫入操做就會覆蓋A的寫入操做形成A的寫入操做丟失

   

10 ,hashmap 初始化時就生了一個長度爲16 的數組。

  1.1 爲何初始化時16   而不是別的數字,

    1.實際上是4或者8 只要是2的N次冪就行,由於hashMap put 元素時,會根據key 進行運算獲得一個位置,運算就是,根據key的hash值&hashMap的長度-1(hash&length-1)  ,

    假如hashMap的長度爲16     補充:&運算時,都是1才爲1,其餘狀況都爲0

    hash值   1010 1010 1000 0000 ....   1010

    &

    lennth-1             0111

    你會發現無論hash值爲多少,只要 length 的長度是2的N次冪, 那麼length-1 的二進制最後一位就是1,因此  hash值&上length-1 最後獲得的二進制數字的末位,多是1 也多是0,

    若是 其長度不是2的n次冪,好比 15 ,那麼15-1 =14 的 二進制 0110,那麼趕上hash  的到二進制末位,永遠就是0了 ,這就側面的代表了經過計算出來的元素位置的分散性。

    爲何不選4,8 這些也是2的N次冪做爲擴容初始化值呢?其實8 也行4 也行,可是 個人java 是c語言寫的,而c語言是由彙編語言,而彙編的語言來源是機器語言,而彙編的語言使用的就是16進制,對於經驗而言,固然就選16嘍。

相關文章
相關標籤/搜索