一个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有序?

 

 

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