Java 数据结构 -- 12.Java 8 数据结构 HashSet

本文深入探讨了 Java 8 中的 HashSet 数据结构,揭示了其实现原理是基于 HashMap,HashSet 作为 HashMap 的装饰器,通过 HashMap 的操作来实现自身的功能。同时,文章也介绍了 LinkedHashSet,它是通过调用特定的构造方法实现有序存储,其内部依赖于 LinkedHashMap。对于 Set 接口的这两种实现,分析了它们各自的特点和用途。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

书接上文,上一篇中对于 Set 接口与 AbstractSet 抽象实现类做了介绍与分析,本篇将对 Set 最终实现类 HashSet 与 LinkedHashSet 做介绍与分析。

先来看下 HashSet 的源码

/**
 * 这个类实现了 Set 接口,有一个 hash table(事实上是一个 HashMap)支持。它对于套的迭代顺序不做保证,此处
 * 特指,它不保证顺序能一直保持一致。这个类允许 null 元素,结社 hash 功能正确地在桶中分散了元素。
 *
 * 这个类为基本操作(add,remove,contains 和 size)提供了与事件一直的表现,迭代这个套锁需要的时间与 
 * HashSet 实例的长度(元素数量)加上支持的 HashMap 实例的“容量”(桶个数)成正比。因此,如果迭代性能很重要
 * 的话,不将初始容量设置的太高(或者加载因子太低)就是很重要的。
 *
 * 注意这个实现类不是线程安全的。如果多线程并发地访问一个 hash set,并且至少有一个线程更改了 set,它必须从
 * 外部实现线程安全。这通常通过对封装了 set 的对象实现线程安全来完成。
 *
 * 如果没有类似的对象存在,set 应该使用 Collections#synchronizedSet 方法“包装”。这最好在创建的时候完
 * 成,来防止意外的对于 set 的非线程安全的访问: <pre>
 *   Set s = Collections.synchronizedSet(new HashSet(...));</pre>
 *
 * 这个类的 iterator 方法返回的迭代器是 fail-fast 的:如果 set 在被创建后的任何时间点被更改了,以除了通过
 * 迭代器自己的 remove 方法的其他任何方式,迭代器会抛出一个 ConcurrentModificationException。因此,在
 * 并发修改操作时,迭代器失败地快速和干净,而不是在未来某个不确定的时间点,冒险做武断的,描述不清的动作。
 * 
 * 注意一个迭代器的 fail-fast 行为是不能被保证的,通常来说,不可能对出现的非线程安全的同时修改操作做任何硬性
 *的保证。基于最佳性能的基础考虑,Fail-fast 迭代器抛出一个 ConcurrentModificationException。因此,建立
 * 在这中异常上写出的程序的正确性将会是不健壮的:迭代器的 fail-fast 行为应当植被用于检查 bugs。
 * 
 * HashSet 继承自 AbstractSet 抽象类,实现了 Set 接口,Cloneable 接口以及 Serializable 接口。
 */

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{
    static final long serialVersionUID = -5024744406713321676L;

		/**不参与序列化的部分**/
    private transient HashMap<E,Object> map;

    /**与一个在支持 map 中的对象相关的壳值**/
    private static final Object PRESENT = new Object();

    /**
     * 构造一个新的,空的 set,支持的 HashMap 实例有模式容量(16)和加载因子(0.75)
     */
    public HashSet() {
        map = new HashMap<>(); //构造一个空 HashMap
    }

    /**
     * 构造一个新的包含指定数据结构中的元素的 set,HashMap 被使用默认加载因子(0.75)和一个足够包含指定数
     * 据结构中元素的初始化容量来构造。
     */
    public HashSet(Collection<? extends E> c) {
        map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16)); //传参构造一个空 HashMap
        addAll(c); //调用 addAll 方法
    }

    /**
     * 构造一个新的,空的 set,支持 HashMap 实例有指定的初始容量和指定的加载因子
     */
    public HashSet(int initialCapacity, float loadFactor) {
        map = new HashMap<>(initialCapacity, loadFactor); //传参构造一个空的 HashMap
    }

    /**
     * 构造一个新的,空的 set,支持 HashMap 实例有指定的初始容量和模式的加载因子(0.75)。
     */
    public HashSet(int initialCapacity) {
        map = new HashMap<>(initialCapacity); //传参构造一个空的 HashMap
    }

    /**
     * 构造一个新的,空的链接 hash set。(这个私有构造器只被 LinkedHashSet 使用。)支持 HashMap 实例是
     * 一个由指定初始容量和指定加载因子构造的 LinkedHashMap。
     */
    HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor); //传参构造一个空的 LinkedHashMap
    }

    /**
     * 返回一个包含这个 set 中所有元素的迭代器。元素不以固定的顺序返回。
     */
    public Iterator<E> iterator() {
        return map.keySet().iterator(); //调用 HashMap#keySet 方法,返回结果的迭代器
    }

    /**
     * 返回 set 中元素的个数(它的基数)。
     */
    public int size() {
        return map.size(); //返回 HashMap 的长度
    }

    /**
     * 如果 set 中不包含元素,返回 true
     */
    public boolean isEmpty() {
        return map.isEmpty(); //返回 HashMap#isEmpty 方法结果
    }

    /**
     * 如果 set 包含特定元素,返回 true。更正规地说,当且仅当这个 set 包含一个元素与指定对象相等时候返回 
     * true
     */
    public boolean contains(Object o) {
        return map.containsKey(o); //调用 HashMap#containsKey,传入指定对象 
    }

    /**
     * 如果当前 set 中不存在指定元素,则加入。更正规地说,如果当前 set 中不包含有与指定对象相等的元素,则加
     * 入。如果 set 已经包含了元素,这个调用将不做任何改动的返回。
     */
    public boolean add(E e) {
        return map.put(e, PRESENT)==null; //调用 HashMap#put 方法,将结果与 null 做比较
    }

    /**
     * 如果当前 set 中存在指定对象,则从中移除。更正规地说,如果 set 中包含有与指定对象相等的对象,则移除。
     * 如果当前 set 中包含指定对象(或者与当前 set 改变后的结果相等)。(一旦调用返回,当前 set 中将不再包
     * 含元素。)
     */
    public boolean remove(Object o) {
        return map.remove(o)==PRESENT; //调用 HashMap#remove() 方法,比较返回值与 PRESENT
    }

    /**
     * 移除当前 set 中的所有元素。方法调用后,set 将变为空
     */
    public void clear() {
        map.clear(); //调用 HashMap#claer 方法
    }

    /**
     * 返回一个 HashSet 实例的浅拷贝:那些元素本身不被克隆。
     */
    @SuppressWarnings("unchecked")
    public Object clone() {
        try {
            HashSet<E> newSet = (HashSet<E>) super.clone(); //调用 Object#clone 方法,缓存给 newSet
            newSet.map = (HashMap<E, Object>) map.clone(); //调用 HashMap#clone 方法,并赋值给 newSet.map
            return newSet; //返回 newSet
        } catch (CloneNotSupportedException e) {
            throw new InternalError(e);
        }
    }

    /**
     * 保存当前 HashSet 实例的状态为一个流(为了序列化写)。
     */
    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {
        s.defaultWriteObject();

        //写出 HashMap 的容量和加载因子
        s.writeInt(map.capacity());
        s.writeFloat(map.loadFactor());

        //写出 HashMap 的长度
        s.writeInt(map.size());

        //以正确的顺序写出所有元素
        for (E e : map.keySet())
            s.writeObject(e);
    }

    /**
     * 从一个流中重构一个 HashSet 实例(为了反序列化读)。
     */
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();

        //读出 HashMap 的容量并验证
        int capacity = s.readInt();
        if (capacity < 0) {
            throw new InvalidObjectException("Illegal capacity: " +
                                             capacity);
        }

        //读出 HashMap 的加载因子并验证
        float loadFactor = s.readFloat();
        if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
            throw new InvalidObjectException("Illegal load factor: " +
                                             loadFactor);
        }

        //读出 HashMap 的长度并验证
        int size = s.readInt();
        if (size < 0) {
            throw new InvalidObjectException("Illegal size: " +
                                             size);
        }
        /**
         * 通过长度和加载因子设计 set 的容量来保证 HashMap 至少 25% 被填满,直到容量最大值
         */
        capacity = (int) Math.min(size * Math.min(1 / loadFactor, 4.0f),
                HashMap.MAXIMUM_CAPACITY);

				/**
				 * 构造支持 map 将在第一个元素添加的时候懒创建一个数组,所以在构造前检查它。调用 
				 * HashMap.tableSizeFor 来计算实际分配的大小。检查 Map.Entry[].classs,既然它是与被创建的类
				 * 型最接近的公共类型。
				 */
        SharedSecrets.getJavaOISAccess()
                     .checkArray(s, Map.Entry[].class, HashMap.tableSizeFor(capacity));

        /**建立支持 HashMap**/
        map = (((HashSet<?>)this) instanceof LinkedHashSet ? //根据当前对象判断是建立新的 LinkedHashMap 还是 HashMap
               new LinkedHashMap<E,Object>(capacity, loadFactor) :
               new HashMap<E,Object>(capacity, loadFactor));

        /**按正确的顺序读如所有元素**/
        for (int i=0; i<size; i++) {
            @SuppressWarnings("unchecked")
                E e = (E) s.readObject();
            map.put(e, PRESENT); //依次加入 map 中
        }
    }

    /**创建一个包含所有这个 set 中的元素的 fail-fast 的并行迭代器。**/
    public Spliterator<E> spliterator() {
        return new HashMap.KeySpliterator<E,Object>(map, 0, -1, 0, 0);
    }
}

通过 HashSet 的源码可以看到,HashSet 其实是维护了一个 HashMap 变量,并通过操作 HashMap 来完成对 HashSet 的操作,没有自己的迭代器,或者说这些迭代器是在 HashMap 中定义的,可以说 HashSet 本身只是一个 HashMap 的装饰器。

然后来看 LinkedHashSet 的源码

/**
 * Set 接口的 Hash table 和链接链接实现类,伴随一个可以预计的顺序。这个实现类与 HashSet 不同的是它维护了
 * 一个穿过它所有 entries 的双向链接链表。这个链接链表定义了迭代操作顺序,就是以元素加入 set 的顺序(插入-
 * 顺序)。注意插入顺序不被相同对象的重新插入影响。(如果 s.add(e) 在 m.contains(e) 立即返回 true 之前被
 * 调用时,那么一个键 key 是重新加入的。)
 *
 * 这个实现类使得客户端免受不确定的,通常是由 HashSet 提供的乱序,不带来与 TreeSet 相关的额外性能消耗。它
 * 可以被用来产生一个与原始的 set 相同顺序的 set 的拷贝:
 * <pre>
 *     void foo(Set s) {
 *         Set copy = new LinkedHashSet(s);
 *         ...
 *     }
 * </pre>
 * 如果一个模块将 set 作为输入,拷贝它,并且稍后返回一个有明确顺序的拷贝时候,这种技术是尤为有用的(客户端通
 * 常对于以相同顺序返回的东西心存感激。)
 *
 * 这个类提供了所有 Set 的可选操作,并且允许 null 元素。就像 HashSet,在假设 hash 功能正确地分配元素到不
 * 同的桶中时,它对于基础操作(add,contains 和 remove)提供时间相关的性能。性能看上去就比 HashSet 略微
 * 低一些,因为维护链接链表需要额外成本,并伴随一个异常:对于一个 LinkedHashSet 的迭代操作时间与这个 set 
 * 的长度长正比,与它的容量无关。对于一个 HashSset 的迭代操作看上去更昂贵,时间与它的容量成正比。
 *
 * 有两个参数会影响到一个链接 hash set 的性能:初始化容量和加载因子。它们就像对于 HashSet 那样精确定义。注
 * 意,然而,相比于 HashSet ,这个类选择过高的初始化容量值的代价没有这么严重,因为迭代操作时间不会受到容量的
 * 影响。
 *
 * 注意这个实现类不是线程安全的。如果多线程同时并行地访问一个链接 hash set,并且至少有一个线程修改了 set,
 * 它必须从外部实现线程安全。这通常是通过对于自然封装这个 set 的对象实现线程安全来完成的。
 *
 * 如果没有这样的对象,set 应该被使用 Collections#synchronizedSet 方法“包装”。这最好在被创建时完成,来
 * 防止意外的对于这个 set 的非线程安全的访问 <pre>
 *   Set s = Collections.synchronizedSet(new LinkedHashSet(...));</pre>
 *
 * 通过本类的 iterator 方法返回的迭代器是 fail-fast 的:如果在迭代器创建后的任意时间点 set 被结构性的改变
 * 了,只要不是通过迭代器自己的 ListIterator#remove 方法,迭代器将会抛出一个同时修改异常。因此,对于表面上
 * 的同步更改操作,迭代器失败的快速和干净,而不是在未来的某个不确定的时间点冒险做一些不确定的行为。
 *
 * 注意一个迭代器的 fail-fast 行为是不能被保证的,通常来说,不可能对出现的非线程安全的同时修改操作做任何硬性
 * 的保证。基于最佳性能的基础考虑,Fail-fast 迭代器抛出一个 ConcurrentModificationException。因此,建
 * 立在这中异常上写出的程序的正确性将会是不健壮的:迭代器的 fail-fast 行为应当植被用于检查 bugs。
 *
 * LinkedHashSet 继承自 HashSet,实现了 Set 接口,Cloneable 接口以及 Serializable 接口
 */

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

    private static final long serialVersionUID = -2851667679971038690L;

    /**
     * 使用指定初始容量和加载因子构造一个新的,空的链接 hash set
     */
    public LinkedHashSet(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor, true); //调用 HashSet 的构造方法
    }

    /**
     * 使用指定的初始化容量和默认加载因子(0.75)构造一个新的,空的链接 hash set
     */
    public LinkedHashSet(int initialCapacity) {
        super(initialCapacity, .75f, true); //调用 HashSet 的构造方法
    }

    /**
     * 使用默认的容量(16)和默认加载因子(0.75)构造一个新的,空的链接 hash set
     */
    public LinkedHashSet() {
        super(16, .75f, true); //调用 HashSet 的构造方法
    }

    /**
     * 使用指定数据结构中相同的元素构造一个新的链接 hash set。这个链接 hash set 被构造成足够装下指定数据
     * 结构中的元素的大小,加载因子为默认加载因子(0.75)。
     */
    public LinkedHashSet(Collection<? extends E> c) {
        super(Math.max(2*c.size(), 11), .75f, true); //调用 HashSet 的构造方法
        addAll(c); //调用 AbstractCollection 中的 addAll 方法
    }

    /**
     * 构造一个包含这个 set 中所有元素的延迟绑定并且 fail-fast 的并行迭代器。
     */
    @Override
    public Spliterator<E> spliterator() {
        return Spliterators.spliterator(this, Spliterator.DISTINCT | Spliterator.ORDERED);
    }
}

通过 LinkedHashSet 的源码可以看到,它基本上只是调用了 HashSet 父类中为 LinkedHashSet 特别预留的构造方法 HashSet(int initialCapacity, float loadFactor, boolean dummy),而这个方法的具体实现还是在 LinkedHashMap 中。纵观 Set 到 LinkedHashSet,可以认为 Set 提供的能力是一个没有下标操作的,元素不能重复的数据结构,HashSet 中看到这些能力的最终实现在 Map 实现类,而 LinkedHashSet 中看到实现有序的方式就是通过一个特殊的 Map 实现类构造器。

以上就是对于 Set 接口最终实现类 HashSet 以及它的子类 LinkedHashSet 的介绍与分析。下一篇将对 TreeSet 进行介绍与分析。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值