HashSet源码详解

简介: HashSet源码详解

介绍

在往期文章中,我们从源码层面详细分析了java集合框架中List的实现如ArrayListLinkedListVectorQueue的实现如PriorityQueueArrayDequeMap的实现如HashMapTreeMapLinkedHashMapHashTable。他们有各自的底层实现和不同的特点。

今天开始,我们进入java集合框架的新篇——Set,先看一下jdk对Set的定义是什么

A collection that contains no duplicate elements. More formally, sets contain no pair of elements e1 and e2 such that e1.equals(e2), and at most one null element.

翻译:不包含重复元素的集合。更正式地说,集合不包含e1.equals(e2)这样的元素对e1和e2,最多包含一个空元素。

从Set的定义我们了解到

  • Set集合中的元素不允许重复,且元素是否重复是通过元素的equals()方法判断的。
  • Set集合中最多包含一个空元素。这其实是取决于它的底层实现。

再看一下Set有哪些实现类供我们使用,下面是Set的UML类图

Set类图.png

Set的主要实现有三个:TreeSetHashSetLinkedHashSet,今天我们先从HashSet入手,从源码层面详细分析它的实现原理。下面我们把上面Set类图中与HashSet无关的类去掉,把注意力放在HashSet上。而在学习HashSet源码之前,必须对HashMap的源码了如指掌

UML类图.png

类的声明

public class HashSet<E> extends AbstractSet<E>
                        implements Set<E>, Cloneable, java.io.Serializable

从类的声明和上面的UML类图中可以了解到:

  • 继承AbstractSet抽象类,对其进行了扩展。
  • 实现了Set接口,表示它是一个Set集合,满足Set集合的定义
  • 实现了Cloneable接口,提供了对象克隆方法,但请注意,是浅克隆
  • 实现了Serializable接口,支持序列化

成员变量

// HashSet的底层使用哈希表作为存储结构,
// 而HashMap已经实现了哈希表,因此HashSet无需再次对哈希表进行实现,直接通过HashMap对哈希表进行数据的存取即可。
private transient HashMap<E,Object> map;

// 虽然HashSet使用HashMap来操作哈希表,
// 但是HashMap存储的是<key,value>键值对,
// 因此HashSet在保存数据时,key为实际保存的数据,使用一个固定的对象PRESENT作为假的value值
private static final Object PRESENT = new Object();

构造函数

HashSet提供了四个构造函数,由于HashSet的底层为HashMap,因此在HashSet的构造方法中,都是先实例化一个HashMap对象。

  • 无参构造

    直接调用HashMap的无参构造来实例化一个HashMap对象。

    public HashSet() {
         
         
        map = new HashMap<>();
    }
    
  • 通过传入一个集合构造

    先调用HashMap的HashMap(int initialCapacity)构造方法来实例化HashMap对象,再通过putAll()方法将集合中的元素添加到当前实例中。

    public HashSet(Collection<? extends E> c) {
         
         
        // HashMap的默认初始容量为16,
        // (int) (c.size()/.75f) + 1 是通过传入的集合中元素的数量 除以 0.75,将所得的商+1,
        // 从两个数中取最大值是为了避免底层哈希表的频繁扩容。在HashMap中我们有聊过这个逻辑。
        map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
        // 将集合中的元素批量保存
        addAll(c);
    }
    
  • 通过初始容量加载因子实例化

    可以看到,其实还是调用了HashMap对应的构造方法,对HashMap的实例指定了初始容量和加载因子

    public HashSet(int initialCapacity, float loadFactor) {
         
         
        map = new HashMap<>(initialCapacity, loadFactor);
    }
    
  • 通过初始容量实例化

    调用了HashMap对应的构造方法,对HashMap的实例指定了初始容量

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

另外还有一个构造方法,该方法没有被public关键字修饰,即默认的修饰符,因此只能被同一个包内的子类调用。而且该构造方法的第三个boolean参数dummy并没有被使用,只有另外两个参数初始容量加载因子被使用了,因此可以判断该参数的作用是为了与上面public HashSet(int initialCapacity, float loadFactor)构造方法做区分用的。另外我们还发现,该构造方法内部实例化的不是HashMap对象而是LinkedHashMap对象,是因为该构造方法是由Set的另一个实现LinkedHashSet调用的,简单说一下,LinkedHashSet的底层实现为HashSet,和HashSet同包,且为HashSet的子类。所以,该构造函数是为了实例化LinkedHashSet对象而来的。

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

常用方法

因为HashSet是通过HashMap来操作其底层的哈希表的,因此我们对HashSet中数据的存取都是通过调用HashMap提供的方法实现的。

  • size()

    直接调用HashMap的size()方法

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

    直接调用HashMap的isEmpty()方法

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

    直接调用HashMap的contains()方法

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

    直接调用HashMap的add()方法

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

    调用父类AbstractCollectionallAll()方法,就是对集合进行遍历并调用add()方法保存元素

    public boolean addAll(Collection<? extends E> c) {
         
         
        boolean modified = false;
        for (E e : c)
            if (add(e))
                modified = true;
        return modified;
    }
    
  • remove()

    直接调用HashMap的remove()方法

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

    直接调用HashMap的clear()方法

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

总结

  • HashSet的底层实现为HashMap,HashMap的底层实现为哈希表,因此HashSet的底层实现为哈希表
  • 元素不允许重复,且元素是否重复是通过元素的equals()方法判断的。
  • 线程不安全




纸上得来终觉浅,绝知此事要躬行。

————————————————我是万万岁,我们下期再见————————————————

相关文章
|
7月前
|
存储 Java uml
|
6月前
|
安全
HashSet(源码解读)
HashSet(源码解读)
25 0
|
7月前
|
设计模式
LinkedHashSet源码详解
LinkedHashSet源码详解
|
7月前
HashSet 源码解读
HashSet 源码解读
33 1
|
存储 安全 Java
Java集合Map之HashMap常用操作
在我看来 , 链表是为了解决hash碰撞使用的一种方法 : 拉线法 , 而红黑树是为了解决&quot;拉的这个线&quot;(链表存储的元素太多)过长的话元素遍历慢的问题
67 2
|
存储 索引
HashMap、HashTable和HashSet区别源码分析
HashMap、HashTable和HashSet区别源码分析
110 0
HashMap、HashTable和HashSet区别源码分析
|
存储 Java 索引
【JavaDS】HashMap与HashSet的底层原理
【JavaDS】HashMap与HashSet的底层原理
144 0
【JavaDS】HashMap与HashSet的底层原理
|
存储 缓存
LinkedHashMap源码简读
1、LinkedHashMap继承自HashMap,HashMap具有的特性它都具有。 2、实际上,LinkedHashMap是通过双向链表和散列表这两种数据组合实现的。LinkedHashMap中的“Linked”实际上指的是双向链表,并非指“用链表法解决散列冲突”。 3、LinkedHashMap不仅支持按照插入顺序遍历数据,还支持按照访问顺序来遍历数据。通过设置`accessOrder`属性为true即可。也就是说它本身就是一个支持LRU缓存淘汰策略的缓存系统。
|
算法 Java 程序员
【集合框架】JDK1.8源码分析之HashMap & LinkedHashMap迭代器(三)
  在遍历HashMap与LinkedHashMap时,我们通常都会使用到迭代器,而HashMap的迭代器与LinkedHashMap迭代器是如何工作的呢?下面我们来一起分析分析。
120 0
【集合框架】JDK1.8源码分析之HashMap & LinkedHashMap迭代器(三)