HashSet如何防止元素重复详解
积累知识,胜过积蓄金银!毕竟在文章开发的过程中,会遇到各种各样的问题,往往都是一些细节知识点还没有掌握好而导致的,因此基础知识点的积累是很重要的。下面本文《Java集合框架如何避免HashSet重复元素》,就带大家讲解一下知识点,若是你对本文感兴趣,或者是想搞懂其中某个知识点,就请你继续往下看吧~
要让Java的HashSet正确避免元素重复,核心在于必须正确重写hashCode()和equals()方法。1. 自定义类必须同时重写hashCode()和equals()方法,否则HashSet无法识别逻辑上相同的对象为重复;2. equals()方法需满足自反性、对称性、传递性、一致性和与null比较返回false的契约;3. hashCode()必须保证:如果两个对象equals为true,则它们的hashCode必须相等;4. 应使用相同的字段参与hashCode()和equals()的计算;5. 用于计算hashCode()和equals()的字段应尽量保持不可变,或在对象加入HashSet后不再修改,否则会导致哈希码变化,使对象“丢失”在原桶中,造成查找失败、幽灵元素或重复添加等问题;6. 正确的做法是:若需修改关键字段,应先从HashSet中移除对象,修改后再重新添加。只有遵循这些原则,HashSet才能正确维护元素唯一性并保证性能。

要让Java的HashSet正确避免元素重复,核心在于你自定义的类必须正确地重写hashCode()和equals()方法。没有这两个方法的正确实现,HashSet就无法识别两个逻辑上相同的对象是重复的,因为它的唯一性判断机制正是基于它们。
解决方案
HashSet内部依赖于对象的哈希码(hash code)和相等性(equality)来判断元素是否重复。当你尝试向HashSet中添加一个元素时,它会先调用该元素的hashCode()方法来确定它在内部数组中的存储位置(即“桶”)。如果这个桶中已经有元素,它会进一步调用equals()方法来比较新元素与桶中现有元素是否相等。只有当hashCode()和equals()都表明两个对象“相同”时,HashSet才会认为它们是重复的,从而拒绝添加。
这意味着,如果你想让HashSet正确处理你自定义的对象,比如一个Person类,你就必须为Person类提供一套符合契约的hashCode()和equals()实现。这个契约非常重要:
- 自反性(Reflexive): 任何非null的
x,x.equals(x)必须返回true。 - 对称性(Symmetric): 任何非null的
x和y,如果x.equals(y)返回true,那么y.equals(x)也必须返回true。 - 传递性(Transitive): 任何非null的
x、y和z,如果x.equals(y)返回true,并且y.equals(z)返回true,那么x.equals(z)也必须返回true。 - 一致性(Consistent): 任何非null的
x和y,只要它们用于equals比较的信息没有被修改,那么多次调用x.equals(y)都应该返回相同的结果。 - 与null的比较: 任何非null的
x,x.equals(null)必须返回false。 hashCode一致性: 如果两个对象通过equals方法比较是相等的,那么它们的hashCode()方法必须产生相同的整数结果。反之不一定成立,即不同的对象可以有相同的哈希码(哈希冲突),但此时equals方法会负责区分它们。
通常,IDE(如IntelliJ IDEA或Eclipse)都提供了自动生成hashCode()和equals()的功能,它们通常基于类的成员变量来生成,这大大简化了我们的工作。但理解背后的原理仍然至关重要。
import java.util.HashSet;
import java.util.Objects; // Java 7+ 提供了Objects.hash()和Objects.equals()简化实现
public class MyObject {
private int id;
private String name;
public MyObject(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
@Override
public boolean equals(Object o) {
// 检查是否是同一个对象引用
if (this == o) return true;
// 检查是否为null或类型不匹配
if (o == null || getClass() != o.getClass()) return false;
// 类型转换
MyObject myObject = (MyObject) o;
// 比较关键字段
return id == myObject.id &&
Objects.equals(name, myObject.name); // 使用Objects.equals处理null安全
}
@Override
public int hashCode() {
// 使用Objects.hash()生成哈希码,它能处理null字段
return Objects.hash(id, name);
}
@Override
public String toString() {
return "MyObject{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
public static void main(String[] args) {
HashSet<MyObject> set = new HashSet<>();
MyObject obj1 = new MyObject(1, "Alice");
MyObject obj2 = new MyObject(2, "Bob");
MyObject obj3 = new MyObject(1, "Alice"); // 逻辑上与obj1相同
set.add(obj1);
System.out.println("添加obj1: " + set.size()); // 1
set.add(obj2);
System.out.println("添加obj2: " + set.size()); // 2
set.add(obj3); // 尝试添加逻辑上重复的obj3
System.out.println("添加obj3: " + set.size()); // 仍然是2,因为obj3被识别为重复
System.out.println("Set内容: " + set);
System.out.println("Set是否包含obj1: " + set.contains(obj1)); // true
System.out.println("Set是否包含obj3: " + set.contains(obj3)); // true
}
}深入理解hashCode()与equals()方法在HashSet中的作用
说起来,HashSet能做到这一点,背后其实藏着一套精妙的机制。它并不是直接把所有元素都拿出来两两比较,那效率也太低了。它利用了哈希表的原理。
当你调用add()方法时,HashSet会先计算你传入对象的hashCode()。这个哈希码会告诉HashSet大概要去哪个“桶”(bucket)里找。想象一下,你有一个大抽屉柜,每个抽屉上都有一个编号,哈希码就是这个编号。HashSet会根据哈希码把对象放到对应的抽屉里。如果两个对象的哈希码不同,那它们肯定不在同一个抽屉里,也就肯定不相等,HashSet压根就不会去比较它们的equals()方法,直接就认为它们是不同的对象。这大大提高了查找效率。
但是,不同的对象可能会计算出相同的哈希码,这叫做“哈希冲突”。就像你可能有两个不同的文件,它们的校验和(哈希值)碰巧一样。当发生哈希冲突时,HashSet会在同一个桶里存储多个对象。这时,equals()方法就派上用场了。HashSet会遍历这个桶里的所有对象,逐一调用它们的equals()方法与新对象进行比较。如果发现有任何一个对象与新对象相等(equals()返回true),那么HashSet就认为新对象是重复的,不会添加到集合中。
所以,hashCode()是第一道关卡,它负责快速定位和初步筛选;而equals()则是第二道、也是最终的关卡,它负责精确判断两个对象是否真的相等。如果hashCode()写得不好,导致大量冲突,那么HashSet的性能会急剧下降,因为它不得不频繁地进行equals()比较,甚至退化成接近ArrayList的线性查找。我记得有一次,调试一个线上问题,就卡在HashSet里明明放了重复数据,却怎么也找不到原因,最后才发现是equals()方法写错了,或者干脆没重写,导致HashSet无法正确识别对象相等性,它只是在比较内存地址了。
自定义类作为HashSet元素时需要注意什么?
当你把自定义类的实例放进HashSet时,除了前面提到的正确重写hashCode()和equals(),还有一些非常实际的坑需要注意,否则你的HashSet行为可能会变得非常诡异。
一个常见的误区是,只重写了equals()而忘记重写hashCode()。在这种情况下,你的equals()方法可能正确地判断了两个对象逻辑上相等,但由于没有重写hashCode(),它们会使用Object类默认的hashCode()方法,这通常是基于对象的内存地址生成的。结果就是,即使两个逻辑上相等的对象,它们的哈希码也可能不同,HashSet会把它们放到不同的桶里,从而认为它们是两个独立的、不重复的元素。这样一来,HashSet的唯一性保证就彻底失效了。
另一个重要的点是,用于计算hashCode()和equals()的字段应该是不可变的。或者说,一旦一个对象被添加到HashSet中,你就不应该再修改那些参与hashCode()和equals()计算的字段。如果修改了,这个对象的哈希码可能就变了。那么,当你想从HashSet中查找或删除这个对象时,HashSet会根据它新的哈希码去寻找,而对象本身还在它旧的哈希码对应的桶里“待着”,这就导致你找不到了,也删不掉,对象就像“失踪”了一样。这其实是一个非常隐蔽且难以调试的问题。
举个例子,如果你的Person类里有age字段,并且hashCode()和equals()都依赖于age。你把一个Person对象加到HashSet里,然后修改了这个Person对象的age。此时,这个Person对象在HashSet内部的位置(桶)是基于它旧的age计算出来的哈希码决定的。当你再次尝试用这个修改后的Person对象去contains()或者remove()时,HashSet会根据新的age计算出新的哈希码,去另一个桶里找,自然就找不到了。
所以,最佳实践是,如果你要将对象放入HashSet,确保那些决定其唯一性的字段是不可变的,或者至少保证这些字段在对象被添加到集合后不再改变。
修改HashSet中已存在元素的哈希值会发生什么?
这绝对是一个会让你头疼的问题,因为它会导致HashSet的内部结构混乱,进而产生各种意想不到的错误行为。
正如前面提到的,HashSet在添加元素时,会根据元素的hashCode()值将其放置到内部哈希表的特定“桶”中。一旦元素被放置,它的位置就固定了,至少在理论上是这样。如果你在元素被添加到HashSet之后,修改了该元素中用于计算hashCode()的字段,那么该元素的hashCode()值就会发生变化。
想象一下这个场景:你有一个Person对象p,它的id是1,name是"Alice"。你把它add到了HashSet中。HashSet根据p当前的哈希码,把它放到了哈希表的某个桶里(比如桶A)。现在,你直接修改了p的id,变成了2。此时,p的hashCode()值也随之改变了。
问题来了:
- 查找失败: 当你尝试用这个修改后的
p去contains(p)或者remove(p)时,HashSet会根据p新的hashCode()值去查找,它会去新的桶(比如桶B)里找。但是,p这个对象实例本身还在旧的桶A里待着呢。结果就是,HashSet找不到它,contains()会返回false,remove()会失败。 - “幽灵”元素: 那个被修改的
p对象实际上还存在于旧的桶A中,但你已经无法通过正常方式访问或移除它了。它变成了HashSet中的一个“幽灵”元素,占据着空间,却无法被正确管理。 - 重复添加: 如果你创建了一个新的
Person对象,其id是2,name是"Alice"(与修改后的p逻辑相等),并尝试将其添加到HashSet中。HashSet会根据这个新对象的哈希码将其放置到桶B中。由于桶B中并没有原始的p对象,HashSet会认为这个新对象是唯一的,并将其添加进去。这样,你的HashSet中就可能存在逻辑上重复但物理上不同的对象,彻底破坏了HashSet的唯一性保证。
所以,这是一个非常重要的规则:不要在对象被添加到HashSet之后,修改任何会影响其hashCode()或equals()结果的字段。 如果你确实需要修改对象的状态,并且这个状态会影响其在HashSet中的唯一性判断,那么正确的做法是:先将旧对象从HashSet中移除,修改对象状态,然后将修改后的对象重新添加到HashSet中。当然,更好的设计是让作为HashSet元素的类是不可变的,这样从根本上避免了这类问题。
理论要掌握,实操不能落!以上关于《HashSet如何防止元素重复详解》的详细介绍,大家都掌握了吧!如果想要继续提升自己的能力,那么就来关注golang学习网公众号吧!
HTML音频添加文字稿的正确方法
- 上一篇
- HTML音频添加文字稿的正确方法
- 下一篇
- 优题宝搜题次数查看教程
-
- 文章 · java教程 | 12分钟前 |
- Java合并两个Map的高效技巧分享
- 294浏览 收藏
-
- 文章 · java教程 | 15分钟前 | java class属性 Class实例 getClass() Class.forName()
- Java获取Class对象的4种方式
- 292浏览 收藏
-
- 文章 · java教程 | 21分钟前 |
- Java正则表达式:字符串匹配与替换技巧
- 183浏览 收藏
-
- 文章 · java教程 | 43分钟前 |
- Java处理外部接口异常的正确方法
- 288浏览 收藏
-
- 文章 · java教程 | 48分钟前 | 多线程 reentrantlock 性能开销 公平锁 FIFO原则
- Java公平锁实现与ReentrantLock使用详解
- 271浏览 收藏
-
- 文章 · java教程 | 52分钟前 |
- Java文件未找到异常排查方法
- 484浏览 收藏
-
- 文章 · java教程 | 1小时前 |
- Java开发图书推荐系统实战教程解析
- 278浏览 收藏
-
- 文章 · java教程 | 1小时前 | codePointAt Unicode编码 Java字符整数转换 补充字符 char类型
- Java字符与整数转换技巧
- 310浏览 收藏
-
- 文章 · java教程 | 1小时前 |
- 卸载旧Java,安装最新版步骤
- 244浏览 收藏
-
- 文章 · java教程 | 1小时前 |
- Java开发记账报表工具教程
- 342浏览 收藏
-
- 文章 · java教程 | 1小时前 |
- Java数组去重i==j逻辑解析
- 486浏览 收藏
-
- 文章 · java教程 | 1小时前 |
- Java处理IOException子类的正确方式
- 288浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- ChatExcel酷表
- ChatExcel酷表是由北京大学团队打造的Excel聊天机器人,用自然语言操控表格,简化数据处理,告别繁琐操作,提升工作效率!适用于学生、上班族及政府人员。
- 3179次使用
-
- Any绘本
- 探索Any绘本(anypicturebook.com/zh),一款开源免费的AI绘本创作工具,基于Google Gemini与Flux AI模型,让您轻松创作个性化绘本。适用于家庭、教育、创作等多种场景,零门槛,高自由度,技术透明,本地可控。
- 3390次使用
-
- 可赞AI
- 可赞AI,AI驱动的办公可视化智能工具,助您轻松实现文本与可视化元素高效转化。无论是智能文档生成、多格式文本解析,还是一键生成专业图表、脑图、知识卡片,可赞AI都能让信息处理更清晰高效。覆盖数据汇报、会议纪要、内容营销等全场景,大幅提升办公效率,降低专业门槛,是您提升工作效率的得力助手。
- 3418次使用
-
- 星月写作
- 星月写作是国内首款聚焦中文网络小说创作的AI辅助工具,解决网文作者从构思到变现的全流程痛点。AI扫榜、专属模板、全链路适配,助力新人快速上手,资深作者效率倍增。
- 4525次使用
-
- MagicLight
- MagicLight.ai是全球首款叙事驱动型AI动画视频创作平台,专注于解决从故事想法到完整动画的全流程痛点。它通过自研AI模型,保障角色、风格、场景高度一致性,让零动画经验者也能高效产出专业级叙事内容。广泛适用于独立创作者、动画工作室、教育机构及企业营销,助您轻松实现创意落地与商业化。
- 3798次使用
-
- 提升Java功能开发效率的有力工具:微服务架构
- 2023-10-06 501浏览
-
- 掌握Java海康SDK二次开发的必备技巧
- 2023-10-01 501浏览
-
- 如何使用java实现桶排序算法
- 2023-10-03 501浏览
-
- Java开发实战经验:如何优化开发逻辑
- 2023-10-31 501浏览
-
- 如何使用Java中的Math.max()方法比较两个数的大小?
- 2023-11-18 501浏览

