【Java集合】HashSet源码分析

news2024/9/20 20:23:37

目录

一、Set简介

二、HashSet简介

2.1 简介

2.2 HashSet继承关系

三、源码分析

3.1 成员属性

3.2 构造方法

3.3 添加元素

3.3.1 add()方法

3.3.2 addAll()方法

3.4 删除元素

3.4.1 remove()方法

3.4.2 removeAll()方法

3.5 查询元素

3.5.1 contains()方法

3.5.2 containsAll方法

3.6 容量检查

四、HashSet的遍历方法

4.1 通过Iterator遍历HashSet

4.2 通过for-each遍历HashSet

五、HashSet示例

六、总结


一、Set简介

集合(Collection)和集合(Set)有什么区别?

集合,这个概念有点模糊。

  • 广义上来讲,Java中的集合是指java.util包下面的容器类,包括和Collection及Map相关的所有类。
  • 中义上来讲,我们一般说集合特指Java集合中的Collection相关的类,不包含Map相关的类。
  • 狭义上来讲,数学上的集合是指不包含重复元素的容器,即集合中不存在两个相同的元素,在Java里面对应Set。

具体怎么来理解还是要看上下文环境。

比如,面试别人让你说下Java中的集合,这时候肯定是广义上的。

再比如,下面我们讲的把另一个集合中的元素全部添加到Set中,这时候就是中义上的。

HashSet是Set的一种实现方式,底层主要使用HashMap来确保元素不重复(所有的Set的实现类都是基于Map实现的)

二、HashSet简介

2.1 简介

对于HashSet而言,它是基于HashMap来实现的,底层采用HashMap来保存元素。所以如果对HashMap比较熟悉,那么学习HashSet就会非常容易。

  • HashSet 是一个 没有重复元素的集合 。
  • 它是由HashMap实现的, 不保证元素的顺序 ,而且 HashSet允许使用 null 元素 。
  • HashSet是 非同步的 。如果多个线程同时访问一个哈希 set,而其中至少一个线程修改了该 set,那么它必须 保持外部同步。这通常是通过对自然封装该 set 的对象执行同步操作来完成的。如果不存在这样的对象,则应该使用 Collections.synchronizedSet 方法来“包装” set。最好在创建时完成这一操作,以防止对该 set 进行意外的不同步访问:
Set s = Collections.synchronizedSet(new HashSet(...));

 HashSet通过iterator()返回的 迭代器是fail-fast的

2.2 HashSet继承关系

HashSet定义:

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

针对类:HashSet继承AbstractSet抽象Set类。 针对接口:implements Set, Cloneable, java.io.Serializable,实现了Set Clonable Serializable接口。

继承图:

三、源码分析

3.1 成员属性

// 底层使用HashMap来保存HashSet的元素
private transient HashMap<E,Object> map;
// 由于Set只使用到了HashMap的key,所以此处定义一个静态的常量Object类,来充当HashMap的value
private static final Object PRESENT = new Object();

看到这里就明白了,和我们前面说的一样,HashSet是用HashMap来保存数据,而主要使用到的就是HashMap的key。

看到 private static final Object PRESENT = new Object();不知道你有没有一点疑问呢。

这里使用一个静态的常量Object类来充当HashMap的value,既然这里map的value是没有意义的,为什么不直接使用null值来充当value呢?

比如写成这样子 private final Object PRESENT = null ;

我们都知道的是,Java首先将变量PRESENT分配在栈空间,而将new出来的Object分配到堆空间,这里的new Object()是占用堆内存的(一个空的Object对象占用8byte),而null值我们知道,是不会在堆空间分配内存的。

那么想一想这里为什么不使用null值。想到什么吗,看一个异常类 java.lang.NullPointerException,这绝对是Java程序员的一个噩梦,这是所有Java程序猿都会遇到的一个异常,你看到这个异常你以为很好解决,但是有些时候也不是那么容易解决,Java号称没有指针,但是处处碰到NullPointerException。所以啊,为了从根源上避免NullPointerException的出现,浪费8个byte又怎么样,在下面的代码中我再也不会写这样的代码了if (xxx == null) { … } else {….}。

3.2 构造方法

/**
 * 使用HashMap的默认容量大小16和默认加载因子0.75初始化map,构造一个空HashSet
 */
public HashSet() {
    map = new HashMap<E,Object>();
}

/**
 * 构造一个指定Collection参数的HashSet,这里不仅仅是Set,只要实现Collection接口的容器都可以
 */
public HashSet(Collection<? extends E> c) {
    map = new HashMap<E,Object>(Math. max((int) (c.size()/.75f) + 1, 16));
    // 使用Collection实现的Iterator迭代器,将集合c的元素一个个加入HashSet中
    addAll(c);
}

/**
 * 使用指定的初始容量大小和加载因子初始化map,构造一个HashSet
 */
public HashSet(int initialCapacity, float loadFactor) {
    map = new HashMap<E,Object>(initialCapacity, loadFactor);
}

/**
 * 使用指定的初始容量大小和默认的加载因子0.75初始化map,构造一个HashSet
 */
public HashSet(int initialCapacity) {
    map = new HashMap<E,Object>(initialCapacity);
}

/**
 * 非public,主要是给LinkedHashSet使用的
 * 不对外公开的一个构造方法(默认default修饰),底层构造的是LinkedHashMap,dummy只是一个标示参数,无具体意义
 */
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
    map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor);
}

从构造方法可以很轻松的看出,HashSet的底层是一个HashMap,理解了HashMap后,这里没什么可说的。只有最后一个构造方法有写区别,这里构造的是LinkedHashMap,该方法不对外公开,意味着它只能被同一个包或者子类调用,实际上是提供给LinkedHashSet使用的,而第三个参数dummy是无意义的,只是为了区分其他构造方法。

3.3 添加元素

3.3.1 add()方法

直接调用HashMap的put()方法,把元素本身作为key,把PRESENT作为value,也就是这个map中所有的value都是一样的。

/**
 * 利用HashMap的put方法实现add方法
 */
public boolean add(E e) {
    return map.put(e, PRESENT)== null;
}

其实HashSet就是利用HashMap来检查重复的,因为向HashSet中add元素的时候,实际就是调用的HashMap的put(),会将要加入Set的对象作为Key加入到HashMap中,而HashMap限制了Key不能有重复值。

3.3.2 addAll()方法

/**
 * 添加一个集合到HashSet中,该方法在AbstractCollection中
 */
public boolean addAll(Collection<? extends E> c) {
    boolean modified = false;
    // 取得集合c迭代器Iterator
    Iterator<? extends E> e = c.iterator();
    // 遍历迭代器
    while (e.hasNext()) {
        // 将集合c的每个元素加入到HashSet中
        if (add(e.next()))
            modified = true;
    }
    return modified;
}

3.4 删除元素

3.4.1 remove()方法

直接调用HashMap的remove()方法,注意map的remove返回是删除元素的value,而Set的remove返回的是boolean类型。

这里要检查一下,如果是null的话说明没有该元素,如果不是null肯定等于PRESENT。

/**
 * 利用HashMap的remove方法实现remove方法
 */
public boolean remove(Object o) {
    return map.remove(o)==PRESENT;
}

3.4.2 removeAll()方法

/**
 * 删除指定集合c中的所有元素,该方法在AbstractSet中
 */
public boolean removeAll(Collection<?> c) {
    boolean modified = false;
    // 判断当前HashSet元素个数和指定集合c的元素个数,目的是减少遍历次数
    if (size() > c.size()) {
        // 如果当前HashSet元素多,则遍历集合c,将集合c中的元素一个个删除
        for (Iterator<?> i = c.iterator(); i.hasNext(); )
            modified |= remove(i.next());
    } else {
        // 如果集合c元素多,则遍历当前HashSet,将集合c中包含的元素一个个删除
        for (Iterator<?> i = iterator(); i.hasNext(); ) {
            if (c.contains(i.next())) {
                i.remove();
                modified = true;
            }
        }
    }
    return modified;
}

3.5 查询元素

Set没有get()方法,因为Set的get()似乎没有意义,不像List那样可以按index获取元素。

Set只有查询一个元素是否存在的方法。

3.5.1 contains()方法

这里只要一个检查元素是否存在的方法contains(),直接调用map的containsKey()方法。

/**
 * 利用HashMap的containsKey方法实现contains方法
 */
public boolean contains(Object o) {
    return map.containsKey(o);
}

由于HashMap基于Hash表实现,Hash表实现的容器最重要的一点就是可以快速存取,那么HashSet对于contains方法,利用HashMap的containsKey方法,效率是非常之快的。这个方法也是HashSet最核心的卖点方法之一。

3.5.2 containsAll方法

/**
 * 检查是否包含指定集合中所有元素,该方法在AbstractCollection中
 */
public boolean containsAll(Collection<?> c) {
    // 取得集合c的迭代器Iterator
    Iterator<?> e = c.iterator();
    // 遍历迭代器,只要集合c中有一个元素不属于当前HashSet,则返回false
    while (e.hasNext())
    if (!contains(e.next()))
        return false;
    return true;
}

3.6 容量检查

// 返回容器中数据的数量
public int size() {
    return map.size();
}

// 容器是否为空
public boolean isEmpty() {
    return map.isEmpty();
}

四、HashSet的遍历方法

4.1 通过Iterator遍历HashSet

第一步: 根据iterator()获取HashSet的迭代器。

第二步: 遍历迭代器获取各个元素 。

// 直接调用map的keySet的迭代器。返回的元素没有特定的顺序
public Iterator<E> iterator() {
    return map.keySet().iterator();
}

// 假设set是HashSet对象
for(Iterator iterator = set.iterator(); iterator.hasNext(); ) { 
    iterator.next();
}

4.2 通过for-each遍历HashSet

第一步: 根据toArray()获取HashSet的元素集合对应的数组。

第二步: 遍历数组,获取各个元素。

// 假设set是HashSet对象,并且set中元素是String类型
String[] arr = (String[])set.toArray(new String[0]);
for (String str : arr)
    System.out.printf("for each : %s\n", str);

五、HashSet示例

下面我们通过实例学习如何使用HashSet。

代码:

import java.util.Iterator;
import java.util.HashSet;
/*
 * @desc HashSet常用API的使用。
 *
 * @author skywang
 */
public class HashSetTest {
    public static void main(String[] args) {
        // HashSet常用API
        testHashSetAPIs() ;
    }
    /*
     * HashSet除了iterator()和add()之外的其它常用API
     */
    private static void testHashSetAPIs() {
        // 新建HashSet
        HashSet set = new HashSet();
        // 将元素添加到Set中
        set.add("a");
        set.add("b");
        set.add("c");
        set.add("d");
        set.add("e");
        // 打印HashSet的实际大小
        System.out.printf("size : %d\n", set.size());
        // 判断HashSet是否包含某个值
        System.out.printf("HashSet contains a :%s\n", set.contains("a"));
        System.out.printf("HashSet contains g :%s\n", set.contains("g"));
        // 删除HashSet中的“e”
        set.remove("e");
        // 将Set转换为数组
        String[] arr = (String[])set.toArray(new String[0]);
        for (String str:arr)
            System.out.printf("for each : %s\n", str);
        // 新建一个包含b、c、f的HashSet
        HashSet otherset = new HashSet();
        otherset.add("b");
        otherset.add("c");
        otherset.add("f");
        // 克隆一个removeset,内容和set一模一样
        HashSet removeset = (HashSet)set.clone();
        // 删除“removeset中,属于otherSet的元素”
        removeset.removeAll(otherset);
        // 打印removeset
        System.out.printf("removeset : %s\n", removeset);
        // 克隆一个retainset,内容和set一模一样
        HashSet retainset = (HashSet)set.clone();
        // 保留“retainset中,属于otherSet的元素”
        retainset.retainAll(otherset);
        // 打印retainset
        System.out.printf("retainset : %s\n", retainset);
        // 遍历HashSet
        for(Iterator iterator = set.iterator();iterator.hasNext();) 
            System.out.printf("iterator : %s\n", iterator.next());
        // 清空HashSet
        set.clear();
        
        // 输出HashSet是否为空
        System.out.printf("%s\n", set.isEmpty()?"set is empty":"set is not empty");
    }
}

运行结果:

size : 5
HashSet contains a :true
HashSet contains g :false
for each : d
for each : b
for each : c
for each : a
removeset : [d, a]
retainset : [b, c]
iterator : d
iterator : b
iterator : c
iterator : a
set is empty

六、总结

  1. HashSet内部使用HashMap的key存储元素,以此来保证元素不重复;
  2. HashSet是无序的,因为HashMap的key是无序的;
  3. HashSet中允许有一个null元素,因为HashMap允许key为null;
  4. HashSet是非线程安全的;
  5. HashSet是没有get()方法的;

参考链接:https://www.cnblogs.com/tong-yuan/p/HashSet.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/182692.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

项目管理:如何编写高质量的Makefile?

文章目录背景熟练掌握 Makefile 语法规划 Makefile 要实现的功能设计合理的 Makefile 结构掌握 Makefile 编写技巧技巧 1&#xff1a;善用通配符和自动变量技巧 2&#xff1a;善用函数技巧 3&#xff1a;依赖需要用到的工具技巧 4&#xff1a;把常用功能放在 /Makefile 中&…

nodeJS - 切换使用淘宝镜像【临时切换、 长期切换】

一、文章引导 #mermaid-svg-zWQadgqvTsLhAes4 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-zWQadgqvTsLhAes4 .error-icon{fill:#552222;}#mermaid-svg-zWQadgqvTsLhAes4 .error-text{fill:#552222;stroke:#55222…

自动驾驶感知——视觉感知经典算法

文章目录1. 车道线检测技术1.1 基于规则的车道线检测技术1.1.1 流程框架1.1.2 预处理模块1.1.3 车道线识别感兴趣区域提取1.1.4 灰度图转化1.1.5 灰度图去噪1.1.6 二值化操作1.1.7 鲁棒性参数估计——RANSAC1.1.8 后处理模块1.1.9 输出1.2 车道线检测技术发展路线2. 目标检测技…

10.图和树基础

一、基本介绍 1.图 图描述的是一些个体之间的关系。这些个体之间既不是前驱后继的顺序关系&#xff0c;也不是祖先后代的层次关系&#xff0c;而是错综复杂的网状关系。我们一般用图G(V,E)G(V,E)G(V,E)来表示&#xff0c;VVV表示结点&#xff0c;EEE表示边。 根据边是否有权值…

爱快软路由安装Docker插件

在爱快云 插件应用中开启Docker插件 在爱快web端页面的[系统设置]->[磁盘管理]->[磁盘分区]设置磁盘分区&#xff0c;选择普通存储&#xff0c;挂载路径名可以随便取。 点击[高级应用]->[插件管理] 点击页面的Docker图标。 启用Docker服务 点击中间的[镜像管理]&…

n皇后问题

n皇后问题 题目&#xff1a; 按照国际象棋的规则&#xff0c;皇后可以攻击与之处在同一行 或同一列 或同一斜线上的棋子。 n 皇后问题 研究的是如何将 n 个皇后放置在 nn 的棋盘上&#xff0c;并且使皇后彼此之间不能相互攻击。 给你一个整数 n &#xff0c;返回所有不同的…

基于java的大理旅游系统

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战 主要内容&#xff1a;毕业设计(Javaweb项目|小程序等)、简历模板、学习资料、面试题库、技术咨询 文末联系获取 项目介绍…

professional issue复习

Legal concepts Development of UK law • The Kingdom of England was established in 927. • The Principality of Wales was established in 1216. Common law • Following 1066, a unified system of law (English common law) slowly came into existence. It was “c…

漫谈cgroup

什么是cgroup cgroup 是linux内核的一个功能&#xff0c;用来限制、控制与分离一个进程组的资源&#xff08;如CPU、内存、磁盘I/O等&#xff09;。它是由 Google 的两位工程师进行开发的&#xff0c;自 2008 年 1 月正式发布的 Linux 内核 v2.6.24 开始提供此能力。 cgroup …

代码随想录算法训练营第30天 二叉树 java :39. 组合总和 40.组合总和II 131.分割回文串

文章目录LeetCode 39. 组合总和本题题解思路LeetCode 40.组合总和II本题题解思路LeetCode 131.分割回文串本题题解思路那么在代码里什么是切割线呢&#xff1f;那么在代码里什么是切割线呢&#xff1f;总结LeetCode 39. 组合总和 本题题解 思路 根据递归三部曲来分析 递归函…

单板硬件设计:存储器

在单板设计中&#xff0c;无论是涉及到一个简易的CPU、MCU小系统或者是复杂的单板设计&#xff0c;都离不开存储器设计&#xff1a; 1、存储器介绍 存储器的分类大致可以划分如下&#xff1a; ROM和RAM指的都是半导体存储器&#xff0c;ROM在系统停止供电的时候仍然可以保持…

visudo配置sudo权限

visudo配置sudo权限配置visudo仅允许字符终端登陆(tty)--授权localhost允许图形和tty登陆--授权all用户组提权-示例配置在sudoers.d目录下创建授权文件--推荐五段式配置三段式配置检查sudoers配置是否有误如何在sudo运行的命令中防止使用参数结果验证配置visudo https://blog.…

【数据结构】8.2 插入排序

文章目录前言1. 直接插入排序直接插入排序算法直接插入排序性能分析2. 折半插入排序3. 希尔排序希尔排序算法希尔排序算法分析排序方法比较前言 类似于俺们打牌时的插入&#xff0c;每抓来一张牌的时候&#xff0c;就将它放在合适的位置上&#xff0c;插入一张牌之后手里的牌仍…

MQ相关概念

1) 队列管理器 队列管理器是MQ系统中最上层的一个概念&#xff0c;由它为我们提供基于队列的消息服务。 2) 消息 在MQ中&#xff0c;我们把应用程序交由MQ传输的数据定义为消息&#xff0c;我们可以定义消息的内容并对消息进行广义的理解&#xff0c;比如&#xff1a;用户的各种…

JavaWeb-FilterListener

JavaWeb-Filter&Listener 1&#xff0c;Filter 1.1 Filter概述 Filter 表示过滤器&#xff0c;是 JavaWeb 三大组件(Servlet、Filter、Listener)之一。 过滤器可以把对资源的请求拦截下来&#xff0c;从而实现一些特殊的功能。 如下图所示&#xff0c;浏览器可以访问服…

JAVA性能统计项目

一、项目背景&#xff1a;我们希望设计开发一个小的框架&#xff0c;能够获取接口调用的各种统计信息&#xff0c;比如&#xff0c;响应时间的最大值&#xff08;max&#xff09;、最小值&#xff08;min&#xff09;、平均值&#xff08;avg&#xff09;、百分位值&#xff08…

力扣OJ(2000+)

目录 2032. 至少在两个数组中出现的值 2037. 使每位学生都有座位的最少移动次数 2042. 检查句子中的数字是否递增 2097. 合法重新排列数对 2180. 统计各位数字之和为偶数的整数个数 2185. 统计包含给定前缀的字符串 2283. 判断一个数的数字计数是否等于数位的值 2287. …

基于MBD 的软件品质保证技术

基于MBD的软件是什么&#xff1f; 基于MBD的软件是基于模型开发的软件&#xff0c;主要应用于汽车、电子电气、机器人、航空、航天等行业。 ​​​ 与使用现有代码开发程序的方法不同&#xff0c;MBD 方法包括首先开发模型&#xff0c;将模型转换为代码&#xff0c;然后基于转换…

Ansys Speos | 2023R1 动态仿真助力车灯早期优化

前言 光学仿真是产品设计师应用的关键工具之一&#xff0c;能让用户在制作物理原型之前就通过数字环境体验产品。这对汽车领域来说显得尤为重要&#xff0c;随着汽车照明功能&#xff08;如转向指示灯&#xff09;越来越生动&#xff0c;TIER-1 需要能够在样件前&#xff0c;通…

Mac安装android studio

1. 下载as 下载地址 2. 安装 3. 启动软件 4.创建新项目 选择空白活动 名字为FirstApp&#xff0c;语言选择java 等待项目加载完毕 项目加载完毕 5.创建设备 6. 启动项目