百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT知识 > 正文

Java对象列表去重:Stream distinct()与equals/hashCode完美搭配

liuian 2025-09-29 07:21 62 浏览

在日常开发中,我们经常遇到需要对对象列表去重的场景——比如从数据库查询出重复数据、批量导入时过滤重复记录,或者合并多个数据源时剔除重复对象。Java 8的Stream API提供了distinct()方法,一句代码就能实现去重,但很多小伙伴在处理自定义对象时却频频踩坑:明明属性相同的对象,用distinct()就是去不掉!今天就带大家彻底搞懂自定义对象去重的底层逻辑,结合equals()hashCode()的最佳实践,让你写出既简洁又高效的去重代码!

一、为什么distinct()对自定义对象"失效"?

先看一个经典反例:定义一个Person类,包含idname属性,当我们用Stream.distinct()List<Person>去重时,明明id相同的对象却被当成不同元素保留了下来——这是为什么?

// 错误示例:未重写equals和hashCode
class Person {
    private String id;
    private String name;

    // 省略构造器和getter/setter
}

public class Test {
    public static void main(String[] args) {
        List<Person> list = Arrays.asList(
            new Person("1", "张三"),
            new Person("1", "张三"), // id相同,理应去重
            new Person("2", "李四")
        );

        // 去重后依然有3个元素!
        long count = list.stream().distinct().count();
        System.out.println(count); // 输出结果:3 
    }
}

关键原因:distinct()的底层依赖

distinct()方法的去重逻辑并非比较对象内容,而是依赖equals()hashCode()方法。默认情况下,这两个方法继承自Object类:

  • equals():比较对象内存地址(类似==
  • hashCode():返回对象内存地址的哈希值

因此,即使两个Person对象的idname完全相同,只要是不同的对象实例,distinct()就会认为它们是不同元素,导致去重失败。

二、核心解决方案:重写equals()和hashCode()

要让distinct()正确识别自定义对象的重复,必须同时重写equals()hashCode(),并遵循以下契约:

  1. 一致性:对象属性不变时,多次调用hashCode()返回值相同
  2. 等价性:若a.equals(b) == true,则a.hashCode() == b.hashCode()
  3. 非等价性:若a.equals(b) == falsehashCode()尽量不同(提升哈希表性能)

步骤1:定义实体类并重写方法

Person类为例,假设我们需要根据id去重(id是唯一标识),代码如下:

import java.util.Objects;

class Person {
    private String id; // 唯一标识,用于去重
    private String name;

    // 构造器、getter/setter省略

    // 重写equals:根据id判断是否相等
    @Override
    public boolean equals(Object o) {
        if (this == o) return true; // 同一对象直接返回true
        if (o == null || getClass() != o.getClass()) return false; // 类型不同返回false
        Person person = (Person) o;
        return Objects.equals(id, person.id); // 比较id是否相同
    }

    // 重写hashCode:基于id生成哈希值
    @Override
    public int hashCode() {
        return Objects.hash(id); // 使用Objects.hash简化哈希计算
    }
}

步骤2:使用Stream.distinct()去重

重写后,distinct()就能正确识别重复对象:

public class Test {
    public static void main(String[] args) {
        List<Person> list = Arrays.asList(
            new Person("1", "张三"),
            new Person("1", "张三"), // 重复对象,会被去重
            new Person("2", "李四")
        );

        // 去重后只剩2个元素
        List<Person> uniqueList = list.stream()
                .distinct()
                .collect(Collectors.toList());

        System.out.println(uniqueList.size()); // 输出结果:2 
    }
}


代码运行效果:通过重写equals和hashCode,distinct()成功去除重复对象

三、3种进阶去重方法:灵活应对复杂场景

除了distinct(),实际开发中还会遇到无法修改实体类(如第三方库对象)或需要按部分属性去重的场景,这时可以用以下3种方法:

方法1:基于单个属性去重(Collectors.toMap)

适用于根据某个唯一属性(如id)去重,且需保留首个出现元素的场景:

List<Person> list = Arrays.asList(
    new Person("1", "张三"),
    new Person("1", "张三"),
    new Person("2", "李四")
);

// 以id为key,冲突时保留已有元素
List<Person> uniqueList = list.stream()
    .collect(Collectors.toMap(
        Person::getId,  // key:id(去重依据)
        p -> p,         // value:对象本身
        (existing, replacement) -> existing // 冲突处理:保留前者
    ))
    .values() // 提取value集合
    .stream()
    .collect(Collectors.toList());

方法2:基于多属性去重(TreeSet自定义比较器)

当需要根据多个属性(如id+name)去重时,可使用TreeSet搭配比较器:

List<Person> list = Arrays.asList(
    new Person("1", "张三"),
    new Person("1", "张三"), // id+name相同,去重
    new Person("1", "张小三") // id相同但name不同,保留
);

// 按id+name组合属性去重
List<Person> uniqueList = list.stream()
    .collect(Collectors.toCollection(
        () -> new TreeSet<>(
            Comparator.comparing(p -> p.getId() + p.getName()) // 组合属性
        )
    ))
    .stream()
    .collect(Collectors.toList());

方法3:动态条件去重(filter+ConcurrentHashMap)

动态指定去重规则(如保留最后出现的元素)时,用filter配合线程安全的集合:

List<Person> list = Arrays.asList(
    new Person("1", "张三"),
    new Person("1", "张三"), // 重复,保留后者
    new Person("2", "李四")
);

// 线程安全的Set记录已出现的id
Set<String> seenIds = ConcurrentHashMap.newKeySet();

// 保留最后出现的重复元素
List<Person> uniqueList = list.stream()
    .filter(p -> seenIds.add(p.getId())) // add返回false表示重复
    .collect(Collectors.toList());

四、避坑指南:常见问题与解决方案

问题场景

错误原因

解决方案

只重写equals不重写hashCode

导致hashCode不同,distinct()认为对象不同

必须同时重写两个方法,确保逻辑一致

去重后元素顺序改变

HashSet/HashMap不保证顺序

使用LinkedHashSettoMap指定LinkedHashMap

并行流去重出现重复

多线程操作共享集合导致线程不安全

使用
ConcurrentHashMap.newKeySet()
替代普通Set

复杂对象哈希冲突

hashCode实现不合理(如固定返回1)

Objects.hash(field1, field2)组合多字段哈希


HashSet去重流程图:先通过hashCode定位,再用equals比较,两者缺一不可

五、实战案例:电商订单数据去重

场景:批量导入订单数据时,需根据orderId去重,避免重复入库。
实现:定义Order类并重写equalshashCode,使用Stream.distinct()去重。

class Order {
    private String orderId; // 订单唯一标识
    private String product;
    private BigDecimal amount;

    // 重写equals和hashCode(基于orderId)
    @Override
    public boolean equals(Object o) { /* 实现略 */ }
    @Override
    public int hashCode() { /* 实现略 */ }
}

// 批量导入去重
List<Order> orders = importFromExcel(); // 从Excel读取订单
List<Order> uniqueOrders = orders.stream()
    .distinct()
    .collect(Collectors.toList());
db.batchInsert(uniqueOrders); // 入库去重后的订单

案例来源:CSDN博客《java使用Stream流对自定义对象数组去重的实现》

六、最佳实践

  1. 优先使用distinct():简单场景下,重写equalshashCode后直接调用,代码最简洁。
  2. 灵活选择工具方法:无法修改实体类用toMap/TreeSet,动态规则用filter+Set
  3. 重视性能优化:大数据集去重时,预排序减少哈希冲突,并行流用ConcurrentHashMap

掌握这些技巧,不仅能解决日常开发中的去重问题,更能深入理解Java集合框架的底层逻辑。快去试试用Stream API优化你的去重代码吧!如果觉得有用,记得点赞收藏哦~

相关推荐

驱动网卡(怎么从新驱动网卡)
驱动网卡(怎么从新驱动网卡)

网卡一般是指为电脑主机提供有线无线网络功能的适配器。而网卡驱动指的就是电脑连接识别这些网卡型号的桥梁。网卡只有打上了网卡驱动才能正常使用。并不是说所有的网卡一插到电脑上面就能进行数据传输了,他都需要里面芯片组的驱动文件才能支持他进行数据传输...

2026-01-30 00:37 liuian

win10更新助手装系统(微软win10更新助手)

1、点击首页“系统升级”的按钮,给出弹框,告诉用户需要上传IMEI码才能使用升级服务。同时给出同意和取消按钮。华为手机助手2、点击同意,则进入到“系统升级”功能华为手机助手华为手机助手3、在检测界面,...

windows11专业版密钥最新(windows11专业版激活码永久)

 Windows11专业版的正版密钥,我们是对windows的激活所必备的工具。该密钥我们可以通过微软商城或者通过计算机的硬件供应商去购买获得。获得了windows11专业版的正版密钥后,我...

手机删过的软件恢复(手机删除过的软件怎么恢复)
手机删过的软件恢复(手机删除过的软件怎么恢复)

操作步骤:1、首先,我们需要先打开手机。然后在许多图标中找到带有[文件管理]文本的图标,然后单击“文件管理”进入页面。2、进入页面后,我们将在顶部看到一行文本:手机,最新信息,文档,视频,图片,音乐,收藏,最后是我们正在寻找的[更多],单击...

2026-01-29 23:55 liuian

一键ghost手动备份系统步骤(一键ghost 备份)

  步骤1、首先把装有一键GHOST装系统的U盘插在电脑上,然后打开电脑马上按F2或DEL键入BIOS界面,然后就选择BOOT打USDHDD模式选择好,然后按F10键保存,电脑就会马上重启。  步骤...

怎么创建局域网(怎么创建局域网打游戏)

  1、购买路由器一台。进入路由器把dhcp功能打开  2、购买一台交换机。从路由器lan端口拉出一条网线查到交换机的任意一个端口上。  3、两台以上电脑。从交换机任意端口拉出网线插到电脑上(电脑设置...

精灵驱动器官方下载(精灵驱动手机版下载)

是的。驱动精灵是一款集驱动管理和硬件检测于一体的、专业级的驱动管理和维护工具。驱动精灵为用户提供驱动备份、恢复、安装、删除、在线更新等实用功能。1、全新驱动精灵2012引擎,大幅提升硬件和驱动辨识能力...

一键还原系统步骤(一键还原系统有哪些)

1、首先需要下载安装一下Windows一键还原程序,在安装程序窗口中,点击“下一步”,弹出“用户许可协议”窗口,选择“我同意该许可协议的条款”,并点击“下一步”。  2、在弹出的“准备安装”窗口中,可...

电脑加速器哪个好(电脑加速器哪款好)

我认为pp加速器最好用,飞速土豆太懒,急速酷六根本不工作。pp加速器什么网页都加速,太任劳任怨了!以上是个人观点,具体性能请自己试。ps:我家电脑性能很好。迅游加速盒子是可以加速电脑的。因为有过之...

任何u盘都可以做启动盘吗(u盘必须做成启动盘才能装系统吗)

是的,需要注意,U盘的大小要在4G以上,最好是8G以上,因为启动盘里面需要装系统,内存小的话,不能用来安装系统。内存卡或者U盘或者移动硬盘都可以用来做启动盘安装系统。普通的U盘就可以,不过最好U盘...

u盘怎么恢复文件(u盘文件恢复的方法)

开360安全卫士,点击上面的“功能大全”。点击文件恢复然后点击“数据”下的“文件恢复”功能。选择驱动接着选择需要恢复的驱动,选择接入的U盘。点击开始扫描选好就点击中间的“开始扫描”,开始扫描U盘数据。...

系统虚拟内存太低怎么办(系统虚拟内存占用过高什么原因)

1.检查系统虚拟内存使用情况,如果发现有大量的空闲内存,可以尝试释放一些不必要的进程,以释放内存空间。2.如果系统虚拟内存使用率较高,可以尝试增加系统虚拟内存的大小,以便更多的应用程序可以使用更多...

剪贴板权限设置方法(剪贴板访问权限)
剪贴板权限设置方法(剪贴板访问权限)

1、首先打开iphone手机,触碰并按住单词或图像直到显示选择选项。2、其次,然后选取“拷贝”或“剪贴板”。3、勾选需要的“权限”,最后选择开启,即可完成苹果剪贴板权限设置。仅参考1.打开苹果手机设置按钮,点击【通用】。2.点击【键盘】,再...

2026-01-29 21:37 liuian

平板系统重装大师(平板重装win系统)

如果你的平板开不了机,但可以连接上电脑,那就能好办,楼主下载安装个平板刷机王到你的个人电脑上,然后连接你的平板,平板刷机王会自动识别你的平板,平板刷机王上有你平板的我刷机包,楼主点击下载一个,下载完成...

联想官网售后服务网点(联想官网售后服务热线)

联想3c服务中心是联想旗下的官方售后,是基于互联网O2O模式开发的全新服务平台。可以为终端用户提供多品牌手机、电脑以及其他3C类产品的维修、保养和保险服务。根据客户需求层次,联想服务针对个人及家庭客户...