一個Demo手撕HashSet

老規矩了,先看示例demo,再聊聊源碼

import java.util.HashSet;
import java.util.Iterator;

public class HashSetTest {


    public static void main(String[] args)  {

        HashSet hashset = new HashSet();

        System.out.println("HashSet是否爲空\t "+hashset.isEmpty());

        hashset.add("hello world");
        System.out.println("添加重複元素\t"+hashset.add("hello world"));
        System.out.println("添加元素\t "+hashset.add("world hello"));
        System.out.println("添加元素null\t"+hashset.add(null));

        System.out.println("HashSet是否爲空\t "+hashset.isEmpty());
        System.out.println("HashSet大小"+hashset.size());

        System.out.println("HashSet是否含有‘hello world’\t"+hashset.contains("hello world"));
        System.out.println("HashSet是否含有‘world’\t"+hashset.contains("world"));

        System.out.println("刪除元素'hello world'\t"+hashset.remove("hello world"));
        System.out.println("刪除本不存在元素'world'\t"+hashset.remove("world"));

        System.out.println("迭代器遍歷");
        Iterator iterator=hashset.iterator();
        int i=1;
        while(iterator.hasNext())
        {
            System.out.println("這是第"+i+"個元素\t"+iterator.next());
            i++;
        }

        System.out.println("清空HashSet");
        hashset.clear();
        System.out.println("HashSet是否爲空\t "+hashset.isEmpty());
        System.out.println("HashSet大小"+hashset.size());

    }
}

其結果如圖

代碼運行結果

從運行的結果可以看出,HashSet有這麼幾個特點

1. 其集合元素都是唯一的,不存在重複的元素

2. 其集合元素中可以存在null,但有且只能有一個

3. 可以從迭代器遍歷的結果中可以看出,HashSet是不會維護數據插入的順序的,也不會對數據進行排序

好了,說完了特性,我們來聊聊其實現的源碼吧!

1. 類繼承結構

HashSet的類圖

可以看出,HashSet主要繼承了AbstractSet類(Set的抽象類),實現了Serializable(序列化接口)、Cloneable(克隆接口)、Set的接口

需要注意的是,HashSet其實現的常用方法都是覆蓋了AbstractSet類中的方法,其實現都是調用了HashMap中的方法(這點下文有說明),不太明白的則是,爲什麼AbstractSet繼承實現了Set接口,而HashSet也繼承了Set接口(大概原因是類中的實現不能被繼承下去吧???)(有讀者知道這個答案的,請一定告知一聲,鄙人感謝不盡!)

2. 類屬性

   static final long serialVersionUID = -5024744406713321676L;    //序列化ID

    private transient HashMap<E,Object> map;                    //底層數據結構是HashMap實現

    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();         //定義一個虛擬的object對象作爲 HashMap 的value

其屬性值以及其應用意義在後面註釋中有說明了,因此不再另外闡述

注1:由於HashSet繼承實現了Set接口,而Set規定了其集合中元素的唯一性,且也有需要有null值的需求,因此這才採用了HashMap作爲其底層的數據結構。在這點上就體現了一個道理即“先有需求,後有其架構、實現“,望各位讀者能明白,先分析需求才能完成後面的環節

注2:PRESENT的object對象是作爲hashmap中的value而存在的,由於Set是一個單值集合,且具有唯一性,因此其map中的key都對應到該object上,即所有的key對應一個共有的object上,因此其前的修飾符爲static

3. 構造函數

HashSet的構造函數比較多,且適應不同的用途


    public HashSet() {
        map = new HashMap<>();              
    }


  
    public HashSet(Collection<? extends E> c) {         
        map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
        addAll(c);
    }


   
    public HashSet(int initialCapacity, float loadFactor) {
        map = new HashMap<>(initialCapacity, loadFactor);
    }

    

    public HashSet(int initialCapacity) {
        map = new HashMap<>(initialCapacity);
    }

      

    HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }

1. 無參構造函數

無參構造函數也比較簡單,直接初始化new一個空的容量爲16,裝載因子爲0.75的HashMap對象實例,並將其引用複製給map作爲其實現的底層數據結構

2. 有參構造函數

2.1 初始容量的有參構造函數(int initialCapacity)

根據指定的容量值初始化一個HashMap實例,並將其引用複製給map作爲其實現的底層數據結構

2.2 集合對象的有參構造函數(Collection <? extends E> c)

將集合存在的元素大小(而非集合的容量)除以0.75然後加1得到的數字和16進行比較,取其最大值,作爲指定容量,初始化一個HashMap實例,調用addAll函數將集合元素添加進去

2.3 初始容量和裝載因子有參構造函數(int initialCapacity,float loadFactor)

根據指定的容量大小和裝載因子大小來初始化一個HashMap實例,並將其引用複製給map作爲其實現的底層數據結構

2.4 初始容量和裝載因子和標記的有參構造函數(int initialCapacity,float loadFactor,boolean dummy)

根據指定的容量大小和裝載因子大小來初始化一個LinkedHashMap實例,並將其引用複製給map作爲其實現的底層數據結構

注1:值得注意的是,其方法前沒有任何修飾符,表示其爲包權限的,對其不訪問公開

Q1:爲什麼HashMap的容量爲16,裝載因子爲0.75?

Q2:當以集合爲形參調用有參構造方法時,爲什麼需要以集合大小除以0.75+1後和16比較,取其大值爲容量初始化一個HashMap?

Q3:爲什麼有一個不對外公開的構造函數,且其底層結構爲LinkedHashMap?僅僅是爲了支持LinkedHashMap麼?如果是爲了支持LinkedHashMap,函數訪問權限又爲何不對外公開?其dummy參數又是什麼意思?

4. 常用方法


    public int size() {
        return map.size();
    }

   

    public boolean isEmpty() {
        return map.isEmpty();
    }

   

    public boolean contains(Object o) {
        return map.containsKey(o);
    }



    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

  

    public boolean remove(Object o) {
        return map.remove(o)==PRESENT;
    }


    public void clear() {
        map.clear();
    }

從源碼可以看出,由於採用了HashMap作爲其底層數據結構,所以常用的方法中都直接調用了map中的常用方法,這樣一來,代碼精簡,功能也實現了,不得不佩服其作者高超編碼能力和架構設計能力

4.1 size函數

調用了map中的size方法,返回其大小

4.2 isEmpty

調用了map中的isEmpty方法,返回其是否爲空的結果

4.3 contains

傳入object對象引用,調用了map中的containsKey方法,返回其是否包含有該object元素

4.4 clear

調用了map的clear方法,清除其所有元素

需要注意的是,add方法和remove方法

4.5 add方法

通過傳入的E元素值作爲key值,其PRESENT作爲value值,通過調用了map的put方法,完成元素的添加,返回其值和null值對比後的結果

4.6 remove方法

傳入object值,調用map的remove方法,和其PRESENT對比,返回其真假值

5. 常見的問題

5.1 HashSet有何特性?

5.2 HashSet的唯一性和null值是如何確保的?

5.3 爲什麼採用HashMap作爲其底層數據結構?

5.4 HashSet是不是線程安全的?如果不是?Set線程安全的有哪些?

5.5 HashSet是不是維持其輸入順序的?如果不是?可採用哪個類維持其Set的輸入順序?

5.6 HashSet是不是有序的?如果不是?可採用哪個Set有序?

 

 

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