0

0

Java集合框架如何避免HashSet的元素重复问题_Java集合框架哈希集合的使用教程

星夢妙者

星夢妙者

发布时间:2025-08-08 15:35:01

|

602人浏览过

|

来源于php中文网

原创

要让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的元素重复问题_Java集合框架哈希集合的使用教程

要让Java的

HashSet
正确避免元素重复,核心在于你自定义的类必须正确地重写
hashCode()
equals()
方法。没有这两个方法的正确实现,
HashSet
就无法识别两个逻辑上相同的对象是重复的,因为它的唯一性判断机制正是基于它们。

解决方案

HashSet
内部依赖于对象的哈希码(hash code)和相等性(equality)来判断元素是否重复。当你尝试向
HashSet
中添加一个元素时,它会先调用该元素的
hashCode()
方法来确定它在内部数组中的存储位置(即“桶”)。如果这个桶中已经有元素,它会进一步调用
equals()
方法来比较新元素与桶中现有元素是否相等。只有当
hashCode()
equals()
都表明两个对象“相同”时,
HashSet
才会认为它们是重复的,从而拒绝添加。

这意味着,如果你想让

HashSet
正确处理你自定义的对象,比如一个
Person
类,你就必须为
Person
类提供一套符合契约的
hashCode()
equals()
实现。这个契约非常重要:

立即学习Java免费学习笔记(深入)”;

  • 自反性(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 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
行为可能会变得非常诡异。

豆包手机助手
豆包手机助手

豆包推出的手机系统服务级AI助手

下载

一个常见的误区是,只重写了

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()
值也随之改变了。

问题来了:

  1. 查找失败: 当你尝试用这个修改后的
    p
    contains(p)
    或者
    remove(p)
    时,
    HashSet
    会根据
    p
    新的
    hashCode()
    值去查找,它会去新的桶(比如桶B)里找。但是,
    p
    这个对象实例本身还在旧的桶A里待着呢。结果就是,
    HashSet
    找不到它,
    contains()
    会返回
    false
    remove()
    会失败。
  2. “幽灵”元素: 那个被修改的
    p
    对象实际上还存在于旧的桶A中,但你已经无法通过正常方式访问或移除它了。它变成了
    HashSet
    中的一个“幽灵”元素,占据着空间,却无法被正确管理。
  3. 重复添加: 如果你创建了一个新的
    Person
    对象,其
    id
    是2,
    name
    是"Alice"(与修改后的
    p
    逻辑相等),并尝试将其添加到
    HashSet
    中。
    HashSet
    会根据这个新对象的哈希码将其放置到桶B中。由于桶B中并没有原始的
    p
    对象,
    HashSet
    会认为这个新对象是唯一的,并将其添加进去。这样,你的
    HashSet
    中就可能存在逻辑上重复但物理上不同的对象,彻底破坏了
    HashSet
    的唯一性保证。

所以,这是一个非常重要的规则:不要在对象被添加到

HashSet
之后,修改任何会影响其
hashCode()
equals()
结果的字段。
如果你确实需要修改对象的状态,并且这个状态会影响其在
HashSet
中的唯一性判断,那么正确的做法是:先将旧对象从
HashSet
中移除,修改对象状态,然后将修改后的对象重新添加到
HashSet
中。当然,更好的设计是让作为
HashSet
元素的类是不可变的,这样从根本上避免了这类问题。

相关专题

更多
java
java

Java是一个通用术语,用于表示Java软件及其组件,包括“Java运行时环境 (JRE)”、“Java虚拟机 (JVM)”以及“插件”。php中文网还为大家带了Java相关下载资源、相关课程以及相关文章等内容,供大家免费下载使用。

832

2023.06.15

java正则表达式语法
java正则表达式语法

java正则表达式语法是一种模式匹配工具,它非常有用,可以在处理文本和字符串时快速地查找、替换、验证和提取特定的模式和数据。本专题提供java正则表达式语法的相关文章、下载和专题,供大家免费下载体验。

738

2023.07.05

java自学难吗
java自学难吗

Java自学并不难。Java语言相对于其他一些编程语言而言,有着较为简洁和易读的语法,本专题为大家提供java自学难吗相关的文章,大家可以免费体验。

734

2023.07.31

java配置jdk环境变量
java配置jdk环境变量

Java是一种广泛使用的高级编程语言,用于开发各种类型的应用程序。为了能够在计算机上正确运行和编译Java代码,需要正确配置Java Development Kit(JDK)环境变量。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

397

2023.08.01

java保留两位小数
java保留两位小数

Java是一种广泛应用于编程领域的高级编程语言。在Java中,保留两位小数是指在进行数值计算或输出时,限制小数部分只有两位有效数字,并将多余的位数进行四舍五入或截取。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

398

2023.08.02

java基本数据类型
java基本数据类型

java基本数据类型有:1、byte;2、short;3、int;4、long;5、float;6、double;7、char;8、boolean。本专题为大家提供java基本数据类型的相关的文章、下载、课程内容,供大家免费下载体验。

446

2023.08.02

java有什么用
java有什么用

java可以开发应用程序、移动应用、Web应用、企业级应用、嵌入式系统等方面。本专题为大家提供java有什么用的相关的文章、下载、课程内容,供大家免费下载体验。

430

2023.08.02

java在线网站
java在线网站

Java在线网站是指提供Java编程学习、实践和交流平台的网络服务。近年来,随着Java语言在软件开发领域的广泛应用,越来越多的人对Java编程感兴趣,并希望能够通过在线网站来学习和提高自己的Java编程技能。php中文网给大家带来了相关的视频、教程以及文章,欢迎大家前来学习阅读和下载。

16926

2023.08.03

Golang gRPC 服务开发与Protobuf实战
Golang gRPC 服务开发与Protobuf实战

本专题系统讲解 Golang 在 gRPC 服务开发中的完整实践,涵盖 Protobuf 定义与代码生成、gRPC 服务端与客户端实现、流式 RPC(Unary/Server/Client/Bidirectional)、错误处理、拦截器、中间件以及与 HTTP/REST 的对接方案。通过实际案例,帮助学习者掌握 使用 Go 构建高性能、强类型、可扩展的 RPC 服务体系,适用于微服务与内部系统通信场景。

8

2026.01.15

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
PHP自制框架
PHP自制框架

共8课时 | 0.6万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号