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()方法判断的。
  • 线程不安全




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

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

相关文章
|
小程序 测试技术 uml
电商小程序01需求分析
电商小程序01需求分析
|
Prometheus Kubernetes 监控
Kubernetes 的 NameSpace 无法删除应该怎么办?
Kubernetes 的 NameSpace 无法删除应该怎么办?
|
消息中间件 存储 中间件
图解 kafka 架构与工作原理
面试官提问:什么是 Kafka ?用来干嘛的?
1749 2
图解 kafka 架构与工作原理
|
存储 缓存 JSON
实战干货 | 分布式多级缓存设计方案
分布式多级缓存设计方案,解决海量数据读取的性能问题,包含多级缓存的存储设计,流程设计;利用多数据副本保证数据的可用性,同时通过不同数据源特点提供更高性能、更多场景数据差异化的支持
1877 0
实战干货 | 分布式多级缓存设计方案
|
11月前
|
机器学习/深度学习 存储 并行计算
Ascend上的PageAttention
PageAttention旨在解决大型语言模型(LLM)服务中的内存管理低效问题,如内存碎片化、利用率低及缺乏灵活的内存共享机制。通过借鉴操作系统中的虚拟内存和分页技术,PageAttention实现了块级别的内存管理和灵活的KV cache共享机制,显著提高内存利用率,降低延迟,提升模型处理速度和性能。相比传统注意力机制,PageAttention通过分段处理序列,有效解决了长序列处理时的计算效率低下和内存过度使用问题。
|
8月前
|
设计模式 存储 SQL
【Java并发】【volatile】适合初学者体质的volatile
当你阅读dalao的框架源码的时候,你是否会见到这样一个关键字 - - - volatie,诶,你是否会好奇,为什么要加它?加了它有什么作用?
218 14
【Java并发】【volatile】适合初学者体质的volatile
|
Dubbo Java 应用服务中间件
开源微服务如何选型?Spring Cloud、Dubbo、gRPC、Istio 详细对比
开源微服务如何选型?Spring Cloud、Dubbo、gRPC、Istio 详细对比
1518 112
|
人工智能 Cloud Native Java
从云原生视角看 AI 原生应用架构的实践
本文核心观点: • 基于大模型的 AI 原生应用将越来越多,容器和微服务为代表的云原生技术将加速渗透传统业务。 • API 是 AI 原生应用的一等公民,并引入了更多流量,催生企业新的生命力和想象空间。 • AI 原生应用对网关的需求超越了传统的路由和负载均衡功能,承载了更大的 AI 工程化使命。 • AI Infra 的一致性架构至关重要,API 网关、消息队列、可观测是 AI Infra 的重要组成。
53333 115
|
存储 网络架构
Next.js 实战 (四):i18n 国际化的最优方案实践
这篇文章介绍了Next.js国际化方案,作者对比了网上常见的方案并提出了自己的需求:不破坏应用程序的目录结构和路由。文章推荐使用next-intl库来实现国际化,并提供了详细的安装步骤和代码示例。作者实现了国际化切换时不改变路由,并把当前语言的key存储到浏览器cookie中,使得刷新浏览器后语言不会失效。最后,文章总结了这种国际化方案的优势,并提供Github仓库链接供读者参考。
639 0
Next.js 实战 (四):i18n 国际化的最优方案实践
|
开发工具 git
git怎么设置http代理服务器
git怎么设置http代理服务器
429 12