深入瞭解Map Set

#引言
近幾天面試,期間面試官有多次提到源碼層面的實現,故將自己以前看的東西拿出來加上自己現有的理解作以歸類總結。

#Map
Map提供了一個更通用的元素存儲方法。Map集合類用戶存儲元素對(稱作“鍵”和“值”),其中每個鍵映射到一個值。從概念分析,感覺也可以將數組看作具有數值鍵的Map。
Java當中有很多定義的Map類,首先先分析下Map接口本身,即其所定義的四種類型的方法,每個Map接口的實現都包含這些方法。

##覆蓋的方法

方法名含義
equals(Object o)比較指定對象與此Map的等價性
hashCode()返回此Map的哈希碼

##Map構建
Map定義的幾個用於插入和刪除元素的變換方法
|方法名|含義|
|:–:|:–:|
|clear()|從Map中刪除所有映射|
|remove(Object key)|從Map中刪除鍵和關聯的值|
|put(Object key,Object value)|將指定值與指定鍵相關聯|
|claer()|從Map中刪除所有映射|
|putAll(Map t)|將指定Map中的所有映射覆制到此Map|

##內部哈希:哈希映射技術
幾乎所有通用Map都使用哈希映射。這是一種將元素映射到數組的非常簡單的機制。
哈希映射結構由一個存儲元素的內部數組組成。由於內部採用數組存儲,因此必然存在一個用於確定任意鍵訪問數組的索引機制。實際上,該機制需要提供一個小於數組大小的整數索引值。該機制稱爲哈希函數。在 Java 基於哈希的 Map 中,哈希函數將對象轉換爲一個適合內部數組的整數。您不必爲尋找一個易於使用的哈希函數而大傷腦筋: 每個對象都包含一個返回整數值的 hashCode() 方法。要將該值映射到數組,只需將其轉換爲一個正值,然後在將該值除以數組大小後取餘數即可。

上圖爲哈希工作原理

###HashMap

特點:非線程安全,並允許null值和null鍵。不保證映射順序。
HashMap實際上是一個"鏈表散列“的數據結構,即數組和鏈表的結合體。
其解決hash衝突也主要依靠鏈地址法。
現在來分析下JDK中HashMap源碼

public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)//Capacity的意思爲容量,此處指初始數組長度的意思
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);

        // 表明容量一定爲2^n
        int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;

        this.loadFactor = loadFactor;
        threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];//看到這裏已經可以發現其創建了一個Entry數組,其大小爲capacity entry爲一個static class 其中包含了key和value,Entry 就是數組中的元素,每個 Entry 其實就是一個 key-value 對,它持有一個指向下一個元素的引用,這就構成了鏈表。
        useAltHashing = sun.misc.VM.isBooted() &&
                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        init();
}

綜上,HashMap 在底層將 key-value 當成一個整體進行處理,這個整體就是一個 Entry 對象。HashMap 底層採用一個 Entry[] 數組來保存所有的 key-value 對,當需要存儲一個 Entry 對象時,會根據 hash 算法來決定其在數組中的存儲位置,在根據 equals 方法決定其在該數組位置上的鏈表中的存儲位置;當需要取出一個Entry 時,也會根據 hash 算法找到其在數組中的存儲位置,再根據 equals 方法從該位置上的鏈表中取出該Entry。(這當中比較的都是key)

###TreeMap
與SortedSet接口類似,SortedMap也是一個結構,待排序的Map,其一個比較常用的實現類是TreeMap。
reeMap的put(K key, V value)方法在每添加一個元素時,都會自動排序。
對於 TreeMap 而言,由於它底層採用一棵“紅黑樹”來保存集合中的 Entry,這意味這 TreeMap 添加元素、取出元素的性能都比 HashMap 低:當 TreeMap 添加元素時,需要通過循環找到新增 Entry 的插入位置,因此比較耗性能;當從 TreeMap 中取出元素時,需要通過循環才能找到合適的 Entry,也比較耗性能。但 TreeMap、TreeSet 比 HashMap、HashSet 的優勢在於:TreeMap 中的所有 Entry 總是按 key 根據指定排序規則保持有序狀態,TreeSet 中所有元素總是根據指定排序規則保持有序狀態。

排序二叉樹雖然可以快速檢索,但在最壞的情況下:如果插入的節點集本身就是有序的,要麼是由小到大排列,要麼是由大到小排列,那麼最後得到的排序二叉樹將變成鏈表:所有節點只有左節點(如果插入節點集本身是大到小排列);或所有節點只有右節點(如果插入節點集本身是小到大排列)。在這種情況下,排序二叉樹就變成了普通鏈表,其檢索效率就會很差。

爲了改變排序二叉樹存在的不足,Rudolf Bayer 與 1972 年發明了另一種改進後的排序二叉樹:紅黑樹,他將這種排序二叉樹稱爲“對稱二叉 B 樹”,而紅黑樹這個名字則由 Leo J. Guibas 和 Robert Sedgewick 於 1978 年首次提出。

紅黑樹是一個更高效的檢索二叉樹,因此常常用來實現關聯數組。典型地,JDK 提供的集合類 TreeMap 本身就是一個紅黑樹的實現。

紅黑樹在原有的排序二叉樹增加了如下幾個要求:

  • 性質 1:每個節點要麼是紅色,要麼是黑色。
  • 性質 2:根節點永遠是黑色的。
  • 性質 3:所有的葉節點都是空節點(即 null),並且是黑色的。
  • 性質 4:每個紅色節點的兩個子節點都是黑色。(從每個葉子到根的路徑上不會有兩個連續的紅色節點)
  • 性質 5:從任一節點到其子樹中每個葉子節點的路徑都包含相同數量的黑色節點

根據性質 5:紅黑樹從根節點到每個葉子節點的路徑都包含相同數量的黑色節點,因此從根節點到葉子節點的路徑中包含的黑色節點數被稱爲樹的“黑色高度(black-height)”。

性質 4 則保證了從根節點到葉子節點的最長路徑的長度不會超過任何其他路徑的兩倍。假如有一棵黑色高度爲 3 的紅黑樹:從根節點到葉節點的最短路徑長度是 2,該路徑上全是黑色節點(黑節點 - 黑節點 - 黑節點)。最長路徑也只可能爲 4,在每個黑色節點之間插入一個紅色節點(黑節點 - 紅節點 - 黑節點 - 紅節點 - 黑節點),性質 4 保證絕不可能插入更多的紅色節點。由此可見,紅黑樹中最長路徑就是一條紅黑交替的路徑。

由此我們可以得出結論:對於給定的黑色高度爲 N 的紅黑樹,從根到葉子節點的最短路徑長度爲 N-1,最長路徑長度爲 2 * (N-1)。

#Set
Set 集合不能包含重複的元素的集合。
Set接口只包含繼承自Collection的方法,並增加了重複的元素被禁止約束性

方法名含義
add( )將對象添加到集合
clear()從集合中移除所有對象
contains()如果指定的對象是集合中的元素返回true
isEmpty()如果集合不包含任何元素,則返回true
iterator()返回一個Iterator對象,可用於檢索對象的集合
remove()從集合中刪除指定的對象
size()返回元素集合中的數

##HashSet的實現原理
對於HashSet而言,它是基於HashMap實現的,底層採用HashMap來保存元素。

對於 HashSet 而言,它是基於 HashMap 實現的,HashSet 底層使用 HashMap 來保存所有元素,因此 HashSet 的實現比較簡單,相關 HashSet 的操作,基本上都是直接調用底層 HashMap 的相關方法來完成,我們應該爲保存到 HashSet 中的對象覆蓋 hashCode() 和 equals()。
HashSet保證唯一性:
由於 HashMap 的 put() 方法添加 key-value 對時,當新放入 HashMap 的 Entry 中 key 與集合中原有 Entry 的 key 相同(hashCode()返回值相等,通過 equals 比較也返回 true),新添加的 Entry 的 value 會將覆蓋原來 Entry 的 value(HashSet 中的 value 都是PRESENT),但 key 不會有任何改變,因此如果向 HashSet 中添加一個已經存在的元素時,新添加的集合元素將不會被放入 HashMap中,原來的元素也不會有任何改變,這也就滿足了 Set 中元素不重複的特性。

該方法如果添加的是在 HashSet 中不存在的,則返回 true;如果添加的元素已經存在,返回 false。其原因在於我們之前提到的關於 HashMap 的 put 方法。該方法在添加 key 不重複的鍵值對的時候,會返回 null。

##LinkedHashSet
LinkedHashSet跟HashSet類似,不過LInkedHashSet會維護一個鏈表來加入元素的次序,因爲性能比hashSet略差,不過在遍歷集合元素的時候具有較好的性能,根據加入集合的次序來遍歷

##TreeSet
TreeSet是依靠TreeMap來實現的。
TreeSet是一個有序集合,TreeSet中的元素將按照升序排列,缺省是按照自然排序進行排列,意味着TreeSet中的元素要實現Comparable接口。或者有一個自定義的比較器。
我們可以在構造TreeSet對象時,傳遞實現Comparator接口的比較器對象。

##EnumSet
EnumSet 是一個與枚舉類型一起使用的專用 Set 實現。枚舉set中所有元素都必須來自單個枚舉類型(即必須是同類型,且該類型是Enum的子類)。
枚舉類型在創建 set 時顯式或隱式地指定。枚舉 set 在內部表示爲位向量。

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