HashSet底層原理詳解
1. 說明
- HashSet實現了Set接口
- HashSet底層實質上是HashMap
- 可以存放null值,但是只能有一個null
- HashSet不保證元素是有序的,取決於hash后,再確定索引的結果,即不保證存放元素的順序和取出順序一致
- 不能有重復元素/對象
2. 底層機制說明
HashSet底層是HashMap,HashMap底層是(數組+鏈表+紅黑樹)
- 先獲取元素的哈希值(hashcode方法)
- 對哈希值進行運算,得出一個索引值即為要存放在哈希表中的位置號
- 如果該位置上沒有其他元素,則直接存放,如果該位置上有其他元素,則需要進行equals判斷,如果相等,則不再添加,如果不相等,則以鏈表的方式添加
- Java8以后,如果一條鏈表中的元素個數到達TREEIFY_THRESHOLD(默認是8),並且table的大小>=MIN_TREEIFY_CAPACITY(默認64),就會進行數化(紅黑樹)
class HashSetSource {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
//第 1 次 add
hashSet.add("java");
//第 2 次 add
hashSet.add("php");
hashSet.add("java");
System.out.println("set=" + hashSet);
/**
* 對 HashSet 的源碼解讀
*
* 1. 執行 HashSet()
* public HashSet() {
* map = new HashMap<>();
* }
*
* 2. 執行 add()
* public boolean add(E e) {//e = "java"
* return map.put(e, PRESENT)==null;
* //(static) PRESENT = new Object();
* }
*
* 3.執行 put() , 該方法會執行 hash(key) 得到 key 對應的 hash 值
* 根據算法 h = key.hashCode()) ^ (h >>> 16)
* public V put(K key, V value) {//key = "java" value = PRESENT 共享
* return putVal(hash(key), key, value, false, true);
* }
*
* 4.執行 putVal
* final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
* Node<K,V>[] tab;
* Node<K,V> p;
* int n, i; //定義了輔助變量
* //table 就是 HashMap 的一個數組,類型是 Node[]
* //if 語句表示如果當前 table 是 null, 或者 大小=0
* //就是第一次擴容,到 16 個空間.
* if ((tab = table) == null || (n = tab.length) == 0)
* n = (tab = resize()).length;
*
* //(1)根據 key,得到 hash 去計算該 key 應該存放到 table 表的哪個索引位置,並把這個位置的對象,賦給 p
* //(2)判斷 p 是否為 null
* //(2.1) 如果 p 為 null, 表示還沒有存放元素, 就創建一個 Node (key="java",value=PRESENT)
* //(2.2) 就放在該位置 tab[i] = newNode(hash, key, value, null)
* if ((p = tab[i = (n - 1) & hash]) == null)
* tab[i] = newNode(hash, key, value, null);
* else {
*
* //一個開發技巧提示: 在需要局部變量(輔助變量)時候,在創建
*
* Node<K,V> e; K k;
*
* //如果當前索引位置對應的鏈表的第一個元素和准備添加的 key 的 hash 值一樣
* //並且滿足 下面兩個條件之一:
*
* //(1) 准備加入的 key 和 p 指向的 Node 結點的 key 是同一個對象
* //(2) p 指向的 Node 結點的 key 的 equals() 和准備加入的 key 比較后相同
* //就不能加入
*
* if (p.hash == hash &&
* ((k = p.key) == key || (key != null && key.equals(k))))
* e = p;
*
* //再判斷 p 是不是一顆紅黑樹,
* //如果是一顆紅黑樹,就調用 putTreeVal , 來進行添加
*
* else if (p instanceof TreeNode)
* e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
* else {
*
* //如果 table 對應索引位置,已經是一個鏈表, 就使用 for 循環比較
*
* //(1) 依次和該鏈表的每一個元素比較后,都不相同, 則加入到該鏈表的最后
* // 注意在把元素添加到鏈表后,立即判斷 該鏈表是否已經達到 8 個結點
* // , 就調用 treeifyBin() 對當前這個鏈表進行樹化(轉成紅黑樹)
* // 注意,在轉成紅黑樹時,要進行判斷, 判斷條件
*
* // if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY(64))
* // resize();
*
* // 如果上面條件成立,先 table 擴容.
* // 只有上面條件不成立時,才進行轉成紅黑樹
*
* //(2) 依次和該鏈表的每一個元素比較過程中,如果有相同情況,就直接 break
*/
}
}
3. 分析HashSet的擴容和轉成紅黑樹機制
- HashSet底層是HashMap,第一次添加時,table的數組擴容到16,臨界值(threshold)是16 * 加載因子(loadFactor是0.75)=12
- 如果table數組使用到了臨界值12,就會擴容到16 * 2 = 32,新的臨界值就是32 * 0.75 = 24,依次類推
- Java8以后,如果一條鏈表中的元素個數到達TREEIFY_THRESHOLD(默認是8),並且table的大小>=MIN_TREEIFY_CAPACITY(默認64),就會進行數化(紅黑樹),否則仍然采用數組擴容機制