Java集合框架源碼學習筆記

對集合類的分析從以下幾點入手

1 底層數據結構
2 增刪改查方式
3 初始容量,擴容方式,擴容時機。
4 線程安全與否
5 是否允許空,是否允許重複,是否有序

ArrayList

數據結構:動態數組
初始容量

  • 使用無參構造器時,默認數組大小爲10;
  • 使用指定容量大小initialCapacity的構造器時,初始化容量爲initialCapacity的數組

擴容時機:要添加一個元素前判斷(oldsize+1)是否大於數組容量,大於則進行1.5*oldsize+1擴容
擴容方式:新建一個大小爲原數組1.5倍的數組,將原數組元素複製到新數組,原數組引用指向新數組。
增加和刪除元素:都會調用System.arraycopy()方法將舊數組元素複製到新數組中
其他:非線程安全,元素可以爲空,允許重複,插入有序

Vector(線程安全的,可以指定增長因子的ArrayList)

數據結構:動態數組
初始容量

  • 使用無參構造器時,默認數組大小爲10;
  • 使用指定容量大小initialCapacity的構造器時,初始化容量爲initialCapacity的數組

擴容時機:要添加一個元素前判斷(oldsize+1)是否大於數組容量,大於則進行擴容
擴容方式:如果在創建Vector時,指定了capacityIncrement的大小;則,每次當Vector中動態數組容量增加時>,增加的大小都是capacityIncrement。如果容量的增量小於等於零,則每次需要增大容量時,向量的容量將增大一倍。
增加和刪除元素:都會調用System.arraycopy()方法將舊數組元素複製到新數組中
其他:線程安全,元素可以爲空,允許重複,插入有序

LinkedList

用循環雙向鏈表實現,初始化時header結點的前後指針都指向自己
非線程安全,元素可以爲空,允許重複,插入有序
增刪改查沒什麼好說的。

ArrayDeque

數據結構:循環數組
初始容量

  • 使用無參構造器時,默認容量爲16;
  • 使用指定容量大小numElements的構造器時,numElements小於8,則初始化容量爲8的數組;numElements大於8,則初始化>=numElements的最小的2的冪次方大小的數組。

擴容時機:當head下標和tail下標重合時,說明數組已滿,擴充爲原來的2倍大小。
擴容方式:新建一個大小爲原數組兩倍的數組,將原數組元素複製到新數組,原數組引用指向新數組。
元素下標:通過和(數組長度-1)進行‘&’操作來保證下標始終不越界,構成了循環數組。
其他:非線程安全,元素不能爲空,允許重複,插入有序

PriorityQueue

數據結構:使用數組來實現堆的結構
初始容量:11
擴容時機:添加元素前判斷數組是否已滿,如果滿了需要擴容
擴容方式:舊容量n<64則 變爲2n+2;否則變爲1.5n
添加元素:先將元素添加到最後的葉子結點,再自下而上調整
刪除堆頂元素:將最後的葉子結點元素放在堆頂位置,再自上而下調整
刪除非堆頂元素:將最後一個葉子結點元素放在被刪除元素的位置i,從i先自上而下調整,若調整後發現被刪除元素位置i的元素沒有發生變化,再從i自下而上調整
其他:非線程安全,爲了確保線程安全,使用 java.util.concurrent.PriorityBlockingQueue;元素不能爲空,允許重複,插入無序

HashMap(jdk 1.7)

數據結構:數組+鏈表
初始容量:使用無參構造器時數組大小默認爲16,負載因子0.75
擴容閾值:容量*負載因子
允許鍵爲空,但是隻允許一個這樣的鍵值對存在,存放在table[0]中
不允許重複,插入無序,非線程安全
擴容時機:插入元素前判斷元素個數是否>=擴容閾值,滿足則進行2倍擴容

Hash算法

  1. 計算key的hashCode,然後對hashCode進行9次擾動處理:4次移位+5次異或
  2. index=hash&(table.length-1);

put(key,value)操作:

  1. 若哈希表沒有初始化,即table爲空,則利用構造函數設置的擴容閾值(16)對table數組進行初始化 [真正的初始化存儲數組table是在第一次進行put操作時]
  2. 判斷key是否爲null
    2.1 key爲空(hash==0),則將鍵值對存放在table[0]。該桶中最多隻有一個鍵值對,新value會覆蓋舊value
    2.2 若不爲空,由key的hash得到數組索引,在該桶下遍歷
      2.2.1在鏈表中找到相同的key,則新value覆蓋舊value並返回舊value
      2.2.2 在鏈表中沒有找到相同的key,將先判斷數組是否需要擴容,若需要擴容,則新容量爲舊容量的2倍,再將新的Entry插入到鏈表的頭部,並返回null

get(key)操作:

1.判斷key是否爲空,如果爲空則從table[0]中查詢有沒有key爲null的鍵值對
2.如果不爲空,則利用key的hashCode得到數組索引,在該桶中遍歷Entry去查詢對應的key是否存在

resize(newCapcity)操作:

1.先判斷舊容量是否以及達到2^30,如果達到,則將擴容閾值直接設置爲整數的最大值,返回。
2.否則,對容器中的每個元素重新計算hash值,重新計算對應的新數組索引位置,然後採用鏈表頭插法:將舊數組中各桶中鏈表的元素逆序存放在新的數組中,即擴容前 = 1->2->3,擴容後 = 3->2->1

HashMap(jdk 1.8)

數據結構:數組+鏈表+紅黑樹
初始容量:使用無參構造器時數組大小默認爲16
負載因子:0.75
擴容閾值:容量負載 * 因子
桶的樹化閾值: 默認爲8
桶的鏈表還原閾值: 默認爲6
最小樹形化閾值:默認64

最小樹形化閾值,即當數組長度>=該值時,才允許樹形化鏈表,否則,哈希表元素過多時選擇進行擴容而不是樹形化。
最小樹形化閾值不能小於:4 * 桶的樹形化閾值

允許鍵爲空,但是隻允許一個這樣的鍵值對存在,存放在table[0]中
不允許重複,插入無序
非線程安全

擴容時機:插入元素後判斷元素個數是否>=擴容閾值,滿足則進行2倍擴容

Hash算法

  1. 計算key的hashCode,然後對hashCode進行2次擾動處理:1次移位+1次異或。相當於hashCode的高16位不變,低16位與高16位做異或處理
  2. index=hash&(table.length-1);

put(key,value)操作:

  1. 若哈希表沒有初始化,即table爲空,則利用構造函數設置的擴容閾值(16)對table數組進行初始化 [真正的初始化存儲數組table是在第一次進行put操作時]
  2. 由hash值定位到的桶爲空,則直接插入新結點
  3. 桶不爲空,說明hash衝突
    3.1 首先判斷待插入結點的key與桶中的第一個結點key是否相同
      相等則直接新value替換舊value
    3.2 不相同則由該桶中的第一個結點類型判斷該桶中的結點類型是否爲紅黑樹的結點
      是紅黑樹的結點,在紅黑樹中插入或者更新結點
    3.3 不是紅黑樹結點則說明桶中節點爲鏈表的結點
      在鏈表中插入或者更新結點,注意是尾插法;插入後再判斷鏈表長度是否大於8,大於則進行鏈表的樹化。在樹化方法中會首先判斷數組長度是否小於最小樹形化閾值,小於則進行擴容處理而不是紅黑樹化;大於則將鏈表轉化爲紅黑樹。
  1. 插入新的結點成功後,再判斷鍵值對個數是否大於擴容閾值,進行擴容處理。

get(key)操作:

  1. 由key的hash定位到桶的索引
  2. 當桶非空時,總是先檢查桶中的第一個元素
  3. 若桶中的第一個元素不是我們要找的key,那麼判斷結點是不是樹的結點,是則從紅黑樹中查找元素。
  4. 不是樹結點,則說明爲鏈表結點,依次遍歷

resize()操作:

  1. 先判斷舊容量是否以及達到2^30,如果達到,則將擴容閾值直接設置爲整數的最大值,返回。
  2. 若舊容量大於默認的初始容量16,並且擴爲舊容量2倍的新容量小於容量允許最大值2^30,則將閾值變爲原來的2倍
  3. 對原數組鏈表中的每個鍵值對進行重新定位,
    (1)hash值新增參與運算的位爲0的結點以原來的相對順序構成鏈表,其在新桶集中的索引與舊桶集中的桶索引相同,即元素擴容後的桶索引=原始索引
    (2) hash值新增參與運算的位爲1的結點以原來的相對順序構成鏈表,保留在新增容量的高位的桶中,即元素擴容後的桶索引=原始位置+擴容前的舊容量

HashTable

數據結構:數組+鏈表
初始容量:使用無參構造器時數組大小默認爲11,負載因子0.75
擴容閾值:容量*負載因子
擴容時機:插入元素前判斷元素個數是否>=擴容閾值,滿足則進行(2倍+1)擴容
鍵和值都不能爲空
不允許重複,插入無序
線程安全(synchronized修飾)

Hash算法

  1. hash=hashCode & 0x7fffffff (&0x7FFFFFFF的目的是爲了將負的hash值轉化爲正值,只改變hashCode的符號位)
  2. index=hash % table.length;

synchronized put(key,value)操作:

  1. 先判斷value是否爲空,爲空則拋出異常
  2. 利用hash算法定位桶索引
    2.1 key和hash值相同,則更新value
    2.2 否則,鏈表頭部插入新Entry。插入前判斷是否需要擴容

synchronized get(key)操作:

1.利用hash算法定位桶索引
2.key和hash值相同,則返回value,否則返回null

resize()操作:

1.先設置新的數組長度爲舊長度的2倍+1
2.判斷數組新長度是否大於允許最大數組長度(Integer.MAX_VALUE - 8),大於則設置新的數組長度爲允許最大數組長度。
3. 將舊數組鏈表的元素複製到新的數組鏈表,正序遍歷原桶中的元素,然後採用頭插法插入到新的桶中,所以相當於逆序複製。

總結: 相當於線程安全版本的jdk1.7HashMap,採用數組鏈表結構,鍵和值都不允許爲空,hash算法比hashMap簡單,不對hashCode做複雜的擾動處理。

LinkedHashMap(jdk1.6)【繼承自HashMap】

數據結構:數組+鏈表+雙向鏈表
初始容量:使用無參構造器時數組大小默認爲16,負載因子0.75
擴容閾值:容量*負載因子
允許鍵爲空,但是隻允許一個這樣的鍵值對存在,存放在table[0]中
不允許重複,非線程安全,有序:訪問順序和插入順序
擴容時機插入元素後判斷元素個數是否>=擴容閾值,滿足則進行2倍擴容

對HashMap中的recordAccess方法進行了重寫:
當AccessOrder爲true時,雙向鏈表的元素按照訪問順序排序,調用get方法時其中的recordAccess方法會將當前訪問的元素添加到雙向鏈表的末尾; 當AccessOrder爲false時,雙向鏈表元素按照插入順序排序,recordAccess方法什麼也不做,進行put操作時新結點默認是加到雙向鏈表末尾的,滿足插入順序。

Hash算法

  1. 計算key的hashCode,然後對hashCode進行9次擾動處理:4次移位+5次異或
  2. index=hash&(table.length-1);

put(key,value)操作:

相比較HashMap的put方法
重寫了其中的addEntry方法,新增了將新結點加入到雙向鏈表的步驟。
重寫了recordAccess方法,涉及到了LRU算法,即header後的第一個結點就是最近最少使用的結點。

get(key)操作:

對hashMap中的get方法中的recordAccess進行了重寫,用於LRU算法。具體元素在雙向鏈表中如何排序取決於AccessOrder標誌位。

resize(newCapcity)操作:

1.先判斷舊容量是否以及達到2^30,如果達到,則將擴容閾值直接設置爲整數的最大值,返回。
2.否則,對容器中的每個元素重新計算hash值,重新計算對應的新數組索引位置【這裏重寫了transfer方法,思路是一樣的,不過LinkedHashMap利用雙向鏈表獨特的結構進行鍵值對的遍歷重哈希】,然後採用鏈表頭插法將舊數組鏈表的元素存放在新的數組鏈表中。

總結:LinkedHashMap的Entry繼承了HashMap.Entry,並且多了before和after兩個指針,用於構建雙向鏈表。通過雙向鏈表,LinkedHashMap能夠將鍵值按照元素插入排序和元素訪問順序排序,具體哪一種排序方式由AccessOrder這個標誌位來決定。按照元素訪問順序排序的算法思想是LRU,將當前訪問的entry移到雙向鏈表的末尾,這樣header後的第一個結點就是最近最少使用的結點,也就是最老的結點。

HashSet(jdk1.8)

由它的構造函數可以看出來HashSet的功能是通過HashMap實現的,HashSet中的元素集合就是HashMap的key集合,實現HashSet的HashMap中的所有value都相等,這就保證了HashSet元素的唯一性。

TreeMap(jdk1.8)

底層數據結構爲紅黑樹,紅黑樹是一個自平衡的二叉查找樹,查找、刪除、增加元素的時間複雜度均爲O(logn). 由於元素之間的比較是key類型數據之間的比較,所以key不能爲空。

TreeSet

由它的構造函數可以看出來TreeSet的功能是通過TreeMap實現的,TreeSet中的元素集合就是TreeMap的key集合,實現TreeSet的HashMap中的所有value都相等,這就保證了TreeSet元素的唯一性。
同樣key不能爲空。

LinkedHashSet

由它的構造函數可以看出來LinkedHashSet的功能是通過LinkedHashMap實現的,LinkedHashSet中的元素集合就是LinkedHashMap的key集合。
同樣key不能爲空。

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