面试官:HashSet是如何保证元素不重复的?

f7179a64e31003788958e16bcfb8005c.png

作者 | 磊哥

来源 | Java面试真题解析(ID:aimianshi666)

转载请联系授权(微信ID:GG_Stone)

本文已收录《Java常见面试题》系列,开源地址:https://gitee.com/mydb/interview

HashSet 实现了 Set 接口,由哈希表(实际是 HashMap)提供支持。HashSet 不保证集合的迭代顺序,但允许插入 null 值。也就是说 HashSet 不能保证元素插入顺序和迭代顺序相同。HashSet 具备去重的特性,也就是说它可以将集合中的重复元素自动过滤掉,保存存储在 HashSet 中的元素都是唯一的。

1.HashSet 基本用法

HashSet 基本操作方法有:add(添加)、remove(删除)、contains(判断某个元素是否存在)和 size(集合数量)。这些方法的性能都是固定操作时间,如果哈希函数是将元素分散在桶中的正确位置。HashSet 基本使用如下:

// 创建 HashSet 集合
HashSet<String> strSet = new HashSet<>();
// 给 HashSet 添加数据
strSet.add("Java");
strSet.add("MySQL");
strSet.add("Redis");
// 循环打印 HashSet 中的所有元素
strSet.forEach(s -> System.out.println(s));

2.HashSet 无序性

HashSet 不能保证插入元素的顺序和循环输出元素的顺序一定相同,也就是说 HashSet 其实是无序的集合,具体代码示例如下:

HashSet<String> mapSet = new HashSet<>();
mapSet.add("深圳");
mapSet.add("北京");
mapSet.add("西安");
// 循环打印 HashSet 中的所有元素
mapSet.forEach(m -> System.out.println(m));

以上程序的执行结果如下:8f9e2d2c632a4fb558d99fae71b74c01.png从上述代码和执行结果可以看出,HashSet 插入的顺序是:深圳 -> 北京 -> 西安,而循环打印的顺序却是:西安 -> 深圳 -> 北京,所以 HashSet 是无序的,不能保证插入和迭代的顺序一致

PS:如果要保证插入顺序和迭代顺序一致,可使用 LinkedHashSet 来替换 HashSet。

3.HashSet 错误用法

有人说 HashSet 只能保证基础数据类型不重复,却不能保证自定义对象不重复?这样说对吗?我们通过以下示例来说明此问题。

3.1 HashSet 与基本数据类型

使用 HashSet 存储基本数据类型,实现代码如下:

HashSet<Long> longSet = new HashSet<>();
longSet.add(666l);
longSet.add(777l);
longSet.add(999l);
longSet.add(666l);
// 循环打印 HashSet 中的所有元素
longSet.forEach(l -> System.out.println(l));

以上程序的执行结果如下:33d39304be8185cf37193bbbb6a1450a.png从上述结果可以看出,使用 HashSet 可以保证基础数据类型不重复。

3.2 HashSet 与自定义对象类型

接下来,将自定义对象存储到 HashSet 中,实现代码如下:

public class HashSetExample {public static void main(String[] args) {HashSet<Person> personSet = new HashSet<>();personSet.add(new Person("曹操", "123"));personSet.add(new Person("孙权", "123"));personSet.add(new Person("曹操", "123"));// 循环打印 HashSet 中的所有元素personSet.forEach(p -> System.out.println(p));}
}
@Getter
@Setter
@ToString
class Person {private String name;private String password;public Person(String name, String password) {this.name = name;this.password = password;}
}

以上程序的执行结果如下:403708218b1aa46d61c6ea1fa7643a1f.png从上述结果可以看出,自定义对象类型确实没有被去重,那也就是说 HashSet 不能实现自定义对象类型的去重咯?其实并不是,HashSet 去重功能是依赖元素的 hashCode 和 equals 方法判断的,通过这两个方法返回的都是 true 那就是相同对象,否则就是不同对象。而前面的 Long 类型元素之所以能实现去重,正是因为 Long 类型中已经重写了 hashCode 和 equals 方法,具体实现源码如下:

@Override
public int hashCode() {return Long.hashCode(value);
}
public boolean equals(Object obj) {if (obj instanceof Long) {return value == ((Long)obj).longValue();}return false;
}
//省略其他源码......

更多关于 hashCode 和 equals 的内容,详见:https://mp.weixin.qq.com/s/40zaEJEkQYM3Awk2EwIrWA

那么,想让 HashSet 支持自定义对象去重,只需要在自定义对象中重写 hashCode 和 equals 方法即可,具体实现代码如下:

@Setter
@Getter
@ToString
class Person {private String name;private String password;public Person(String name, String password) {this.name = name;this.password = password;}@Overridepublic boolean equals(Object o) {if (this == o) return true; // 引用相等返回 true// 如果等于 null,或者对象类型不同返回 falseif (o == null || getClass() != o.getClass()) return false;// 强转为自定义 Person 类型Person persion = (Person) o;// 如果 name 和 password 都相等,就返回 truereturn Objects.equals(name, persion.name) &&Objects.equals(password, persion.password);}@Overridepublic int hashCode() {// 对比 name 和 password 是否相等return Objects.hash(name, password);}
}

重新运行以上代码,执行结果如下图所示:af706c4886e9e75885f4bb1fd764500e.png从上述结果可以看出,之前的重复项“曹操”已经被去重了。

4.HashSet 如何保证元素不重复?

我们只要了解了 HashSet 执行添加元素的流程,就能知道为什么 HashSet 能保证元素不重复了?HashSet 添加元素的执行流程是:当把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现,会将对象插入到相应的位置中。但是如果发现有相同 hashcode 值的对象,这时会调用对象的 equals() 方法来检查对象是否真的相同,如果相同,则 HashSet 就不会让重复的对象加入到 HashSet 中,这样就保证了元素的不重复。

为了更清楚的了解 HashSet 的添加流程,我们可以尝试阅读 HashSet 的具体实现源码,HashSet 添加方法的实现源码如下(以下源码基于 JDK 8):

// hashmap 中 put() 返回 null 时,表示操作成功
public boolean add(E e) {return map.put(e, PRESENT)==null;
}

从上述源码可以看出 HashSet 中的 add 方法,实际调用的是 HashMap 中的 put,那么我们继续看 HashMap 中的 put 实现:

// 返回值:如果插入位置没有元素则返回 null,否则返回上一个元素
public V put(K key, V value) {return putVal(hash(key), key, value, false, true);
}

从上述源码可以看出,HashMap 中的 put() 方法又调用了 putVal() 方法,putVal() 的源码如下:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K, V>[] tab;Node<K, V> p;int n, i;//如果哈希表为空,调用 resize() 创建一个哈希表,并用变量 n 记录哈希表长度if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;/*** 如果指定参数 hash 在表中没有对应的桶,即为没有碰撞* Hash函数,(n - 1) & hash 计算 key 将被放置的槽位* (n - 1) & hash 本质上是 hash % n 位运算更快*/if ((p = tab[i = (n - 1) & hash]) == null)// 直接将键值对插入到 map 中即可tab[i] = newNode(hash, key, value, null);else {// 桶中已经存在元素Node<K, V> e;K k;// 比较桶中第一个元素(数组中的结点)的 hash 值相等,key 相等if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))// 将第一个元素赋值给 e,用 e 来记录e = p;// 当前桶中无该键值对,且桶是红黑树结构,按照红黑树结构插入else if (p instanceof TreeNode)e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);// 当前桶中无该键值对,且桶是链表结构,按照链表结构插入到尾部else {for (int binCount = 0; ; ++binCount) {// 遍历到链表尾部if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);// 检查链表长度是否达到阈值,达到将该槽位节点组织形式转为红黑树if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}// 链表节点的<key, value>与 put 操作<key, value>// 相同时,不做重复操作,跳出循环if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}// 找到或新建一个 key 和 hashCode 与插入元素相等的键值对,进行 put 操作if (e != null) { // existing mapping for key// 记录 e 的 valueV oldValue = e.value;/*** onlyIfAbsent 为 false 或旧值为 null 时,允许替换旧值* 否则无需替换*/if (!onlyIfAbsent || oldValue == null)e.value = value;// 访问后回调afterNodeAccess(e);// 返回旧值return oldValue;}}// 更新结构化修改信息++modCount;// 键值对数目超过阈值时,进行 rehashif (++size > threshold)resize();// 插入后回调afterNodeInsertion(evict);return null;}

从上述源码可以看出,当将一个键值对放入 HashMap 时,首先根据 key 的 hashCode() 返回值决定该 Entry 的存储位置。如果有两个 key 的 hash 值相同,则会判断这两个元素 key 的 equals() 是否相同,如果相同就返回 true,说明是重复键值对,那么 HashSet 中 add() 方法的返回值会是 false,表示 HashSet 添加元素失败。因此,如果向 HashSet 中添加一个已经存在的元素,新添加的集合元素不会覆盖已有元素,从而保证了元素的不重复。如果不是重复元素,put 方法最终会返回 null,传递到 HashSet 的 add 方法就是添加成功。

总结

HashSet 底层是由 HashMap 实现的,它可以实现重复元素的去重功能,如果存储的是自定义对象必须重写 hashCode 和 equals 方法。HashSet 保证元素不重复是利用 HashMap 的 put 方法实现的,在存储之前先根据 key 的 hashCode 和 equals 判断是否已存在,如果存在就不在重复插入了,这样就保证了元素的不重复。

0ea13b27960f512cd6b481f5f5583687.gif

往期推荐

532ed2e047d85a929a9b60b68e6879ed.png

面试官:如何实现 List 集合去重?


29156ec600b6081821590d4ca5ea680d.png

面试官:元素排序Comparable和Comparator有什么区别?


d27e3f25c3c724401019c650bad1994f.png

面试官:HashMap有几种遍历方法?推荐使用哪种?

卒然临之而不惊,无故加之而不怒。

博主:80 后程序员。爱好:读书、写作和慢跑。

公众号:Java面试真题解析

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

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

相关文章

【Ubuntu】Ubuntu 20.04无法识别网口/以太网/有线网卡

这里写自定义目录标题0.症状1.查看网卡类型2.下载网卡驱动3.安装网卡驱动0.症状 \qquad表现为插入以太网网口后右上角没有显示网络&#xff0c;即没有下图的音量左侧的标志 打开设置的【网络】选项没有以太网接入&#xff0c;然而以太网口信号灯仍然正常闪烁。这种情况基本可以…

小心Lombok用法中的坑

刚才写完了代码&#xff0c;自测的时候&#xff0c;出现了NPE问题。排查的时候发现是Lombok的坑&#xff0c;以前也遇到过&#xff0c;所以觉得有必要过来记录一下。我先描述一下现象&#xff0c;我的代码里面订单服务A 需要调用缓存服务B&#xff0c;服务B就是一个Bean&#x…

【VSCode】VSCode使用conda环境时找不到python包/找不到Module

这里写自定义目录标题0.问题描述1.原因2.解决方法0.问题描述 \qquad首先需要排除是否是VSCode未配置conda环境的问题&#xff0c;当然&#xff0c;相信VSCode的老粉都不会犯这个低级错误&#xff0c;请CtrlP&#xff0c;在搜索框>select interpreter检查一下python环境。 …

PS如何对JPG文件直接抠图

如何JPG文件直接抠图 先转为智能对象&#xff1a; 再栅格化图层 此进即可直接进行抠图&#xff01;

更快的Maven来了,我的天,速度提升了8倍!

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;周末被 maven-mvnd 刷屏了&#xff0c;于是我也下载了一个 mvnd 体验了一把。虽然测试的数据都是基于我本地项目&#xff0c…

Java 中接口和抽象类竟然有 7 点不同?

作者 | 磊哥来源 | Java面试真题解析&#xff08;ID&#xff1a;aimianshi666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;本文已收录《Java常见面试题》系列&#xff1a;https://gitee.com/mydb/interviewJava 是一门面向对象的编程语言&am…

粉丝不在于多,在于够残

李善友&#xff1a;所有可能被互联网取代的组织一定会被取代 2015-07-30 格局视野 格局视野 格局视野 微信号 geju365 功能介绍 格局生涯学院官方自媒体。面向互联网人的在线商学院。推送互联网行业知识&#xff0c;培养互联网实操人才。聚焦新行业、新模式、新公司、新人物。…

保姆级教学:缓存穿透、缓存击穿和缓存雪崩!

前言对于从事后端开发的同学来说&#xff0c;缓存已经变成的项目中必不可少的技术之一。没错&#xff0c;缓存能给我们系统显著的提升性能。但如果你使用不好&#xff0c;或者缺乏相关经验&#xff0c;它也会带来很多意想不到的问题。今天我们一起聊聊如果在项目中引入了缓存&a…

Fast Global Registration (ECCV 2016) 论文解析

目录0.友情链接1. 论文核心思想1.1. 点云特征匹配1.2. 两个校验1.3. 鲁棒函数与BR对偶1.4.1. Black-Rangarjan Duality (BR对偶性&#xff09;1.4.2.Derivation of Φρ\Phi_\rhoΦρ​1.4.3.E(T,L)E(\bm{T},L)E(T,L)的优化求解方法4.写在后面0.友情链接 FGR基本介绍 CSDN博客…

系统盘压缩卷小于可用空间_操作系统中的可用空间管理

系统盘压缩卷小于可用空间可用空间管理 (Free space management) As we know that the memory space in the disk is limited. So we need to use the space of the deleted files for the allocation of the new file. one optical disk allows only one write at a time in t…

关于头文件是否参与编译的讨论

一、文章来由 写项目的时候发现了这个问题&#xff0c;又是一个比较底层的问题&#xff0c;首先说明&#xff0c;这篇文章只是我根据查阅的资料和做的实验提出的一个讨论&#xff0c;并不一定就是正确答案。因为这个问题网上众说纷纭&#xff0c;我很欢迎大家参与这个讨论&…

Log4j漏洞?一行代码都不改就能永久修复?

△Hollis, 一个对Coding有着独特追求的人△作者 l Hollis来源 l Hollis&#xff08;ID&#xff1a;hollischuang&#xff09;这篇文章我周一发过&#xff0c;但是因为一些"人在江湖、身不由己"的原因&#xff0c;原文删除了&#xff0c;但是很多人找我还是想看看内容…

ubuntu安装eclipse

2019独角兽企业重金招聘Python工程师标准>>> 在Ubuntu 13.04下的安装eclipse 一、eclipse安装过程 首先确保在安装eclipse之前已经安装好Java虚拟机 1. eclipse官网下载压缩包 下载地址&#xff1a;http://www.eclipse.org/downloads/download.php?file/technology…

github果然强大

github果然强大&#xff0c;在idea里写好&#xff0c;可以直接提交到github&#xff0c;在哪台电脑都可以看源码了&#xff0c;手机也可以看 https://github.com/gaojinhua 转载于:https://www.cnblogs.com/gaojinhua/p/4705992.html

保姆级教程,终于搞懂脏读、幻读和不可重复读了!

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;我的文章合集&#xff1a;https://gitee.com/mydb/interview在 MySQL 中事务的隔离级别有以下 4 种&#xff1a;读未提交&am…

保姆级教程,终于搞懂脏读、幻读和不可重复读了!(经典回顾)

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;我的文章合集&#xff1a;https://gitee.com/mydb/interview在 MySQL 中事务的隔离级别有以下 4 种&#xff1a;读未提交&am…

WPF入门教程系列十五——WPF中的数据绑定(一)

使用Windows Presentation Foundation (WPF) 可以很方便的设计出强大的用户界面&#xff0c;同时 WPF提供了数据绑定功能。WPF的数据绑定跟Winform与ASP.NET中的数据绑定功能类似&#xff0c;但也有所不同&#xff0c;在 WPF中以通过后台代码绑定、前台XAML中进行绑定&#xff…

实战,实现幂等的8种方案!

前言 大家好&#xff0c;我是程序员田螺。今天我们一起来聊聊幂等设计。什么是幂等为什么需要幂等接口超时&#xff0c;如何处理呢&#xff1f;如何设计幂等&#xff1f;实现幂等的8种方案HTTP的幂等1. 什么是幂等? 幂等是一个数学与计算机科学概念。在数学中&#xff0c;幂等…

灰度共生矩阵及其数字特征_数字系统及其表示

灰度共生矩阵及其数字特征Any number system has a set of symbols known as Digits with some rules performing arithmetic operations. A collection of these makes a number has two parts. They are integer portion and fraction portion. These portions are separated…

绝绝子,画框架图就用这个工具

前言看过我以往文章的小伙伴可能会发现&#xff0c;我的大部分文章都有很多配图。我的文章风格是图文相结合&#xff0c;更便于大家理解。最近有很多小伙伴发私信问我&#xff1a;文章中的图是用什么工具画的。他们觉得我画的图风格挺小清新的&#xff0c;能够让人眼前一亮。先…