Java List元素去重与对象去重详解

Java开发过程中,经常会遇到需要对List集合中的元素进行去重处理。无论是基本数据类型元素,还是自定义对象,去重的方法和技巧都不完全相同。本篇详细介绍各类Java去重常见写法、原理、注意事项以及性能对比,并提供丰富的实例代码,面向新手逐步讲解每个知识点。


什么是List去重

List去重就是从一个List集合中移除重复的数据,只保留唯一元素。例如: [1][2][2][3][3][3][4]去重后得到[1][2][3][4]

Java中的List(如ArrayList)允许元素重复,而很多场景下我们希望数据唯一,例如去重后的统计和展示。


常见去重方法概述

  • 基础类型去重:如List 、List ,直接比较值
  • 自定义对象去重:如List ,通常按照某些字段判断是否重复
  • 用辅助集合实现,比如SetMap
  • 使用Java 8+的Stream API进行去重
  • 手动遍历结合判断逻辑去重

不同方法适合不同场景,下文详细介绍每种方法。


1. 利用Set集合去重

Set集合(如HashSet、LinkedHashSet、TreeSet)本身不允许重复元素。将List元素加入Set,会自动移除重复值。适合基础类型或已正确实现hashCodeequals的方法。

示例:基础类型去重

List<Integer> list = Arrays.asList(1, 2, 2, 3, 3, 3, 4);
Set<Integer> set = new HashSet<>(list);
// 如需保留原顺序,可用LinkedHashSet
List<Integer> distinctList = new ArrayList<>(set);
System.out.println(distinctList); // [1, 2, 3, 4]

特点和注意事项

  • Set不能保证原始元素顺序(除非用LinkedHashSet)
  • 对自定义对象,必须重写hashCode和equals,否则无法正确去重

2. 利用Stream API去重

Java 8引入Stream流,可以用distinct()方法简单高效地去重。 适用条件:基础类型和已正确重写equalshashCode的对象。

示例:基础类型

List<String> list = Arrays.asList("A", "B", "A", "C", "B");
List<String> distinct = list.stream().distinct().collect(Collectors.toList());
System.out.println(distinct); // [A, B, C]

示例:自定义对象

假设User类已重写hashCode和equals。

class User {
    private String name;
    private int age;

    // 构造方法、getter/setter、equals和hashCode略
}

List<User> users = ...;
List<User> distinctUsers = users.stream().distinct().collect(Collectors.toList());

特点和注意事项

  • 保留集合原有顺序
  • 提供简洁函数式写法
  • 对象必须正确实现equals和hashCode,否则无法按预期去重

3. 自定义字段去重(如根据某字段唯一)

有些场景下,只按照对象的某个字段去重。例如User列表,只保留name唯一的元素。

示例代码(手动遍历+Set辅助)

List<User> users = ...;
Set<String> nameSet = new HashSet<>();
List<User> result = new ArrayList<>();

for (User user : users) {
    if (nameSet.add(user.getName())) { // Set add返回true代表没重复
        result.add(user);
    }
}

示例代码(Stream API + TreeSet)

利用Stream和TreeSet Comparator实现,根据name去重:

List<User> users = ...;

List<User> distinctByName = users.stream()
    .collect(Collectors.collectingAndThen(
        Collectors.toCollection(() ->
            new TreeSet<>(Comparator.comparing(User::getName))),
        ArrayList::new
    ));

特点和注意事项

  • 可指定任意字段去重
  • 不依赖对象的equals和hashCode实现
  • 保留第一个命中元素(如name重复时,第一个元素保留)

4. Map辅助去重(属性为Key)

如果要去重且保留某些元素最后一次出现,可以用Map覆盖旧值。

示例代码:

List<User> users = ...;
Map<String, User> map = new LinkedHashMap<>();
for (User user : users) {
    map.put(user.getName(), user); // 后面出现的同名user会覆盖前面
}
List<User> distinct = new ArrayList<>(map.values());

特点

  • 按字段去重,保留最后一个元素
  • 可以灵活提取任意唯一属性,适合复杂去重需求

5. 手动遍历去重

最基础的做法,适合小集合。 对每个元素检查是否已存在于结果集,不存在则添加。

示例代码:

List<Integer> list = Arrays.asList(1, 2, 2, 3, 4);
List<Integer> result = new ArrayList<>();
for (Integer num : list) {
    if (!result.contains(num)) {
        result.add(num);
    }
}

特点

  • 代码简单
  • 性能较低(每次遍历result,随着数量增长效率变差)
  • 适合小数据量场景

6. 使用Guava工具库去重

Guava提供Sets.newHashSet等便捷方法。

List<Integer> list = Arrays.asList(1,2,2,3,4);
List<Integer> distinct = new ArrayList<>(Sets.newHashSet(list));

也可以用ImmutableSet.copyOf(list),功能类似。 适合已有Guava库项目。


对象去重:equals和hashCode的作用详解

Java对象去重,核心依赖于equalshashCode方法。 简单理解:

  • equals决定了对象在语义上的“同一性”
  • hashCode决定了对象的“哈希值”,用于HashSet、HashMap等集合存储定位
  • 只有同时正确实现了这两个方法,集合类才能正确识别并移除重复元素

示例:重写User的equals和hashCode

public class User {
    private String name;
    private int age;

    // 构造器略

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        User user = (User) obj;
        return age == user.age && Objects.equals(name, user.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

只有在正确重写后,Set、distinct流等方法才能有效去重相同用户。


Stream API:去重高级用法

除了基本的distinct,用Stream可以做自定义字段去重、多字段去重或筛选。

根据多个字段去重(如name和age同时唯一)

List<User> users = ...;
List<User> distinct = users.stream()
   .collect(Collectors.collectingAndThen(
        Collectors.toCollection(() ->
            new TreeSet<>(Comparator.comparing(u -> u.getName() + u.getAge()))),
        ArrayList::new
    ));

此处把name+age拼接作为唯一键实现去重。


性能对比与优化建议

方法 性能特点 保持顺序 适合场景
Set去重 高效 否(用LinkedHashSet则是) 大量基础类型、对象
Stream去重 中高效 常规数据、对象去重
Map辅助 高效 指定字段唯一去重
遍历contains 低效 小数据量
Guava工具 高效 Guava项目

List去重常见坑及解决方案

  • 未重写equals/hashCode造成对象去重失效 解决:确保自定义对象正确重写这两个方法。
  • String类型小写/大写不同等价但未去重 解决:去重前统一格式处理,如toLowerCase()
  • 需要原始顺序,误用HashSet 解决:换用LinkedHashSet或Stream API。
  • 字段去重需求未实现 解决:用Map或TreeSet+Comparator实现字段唯一去重。

常见面试题讲解

  1. 如何对List 根据某字段(如name)完成去重? 推荐:用Stream+TreeSet结合Comparator,参考前文代码。
  2. Java 8 Stream的distinct实现原理? distinct方法内部利用了元素的equals方法做判等,所以对象去重的关键是重写equals和hashCode。
  3. 去重后如何保持元素的原始添加顺序? 用LinkedHashSet或Stream API。

场景实战:

场景1:用户注册系统,List 按手机号唯一去重

List<User> userList = new ArrayList<>();
Set<String> phoneSet = new HashSet<>();
List<User> uniqueByPhone = new ArrayList<>();

for (User user : userList) {
    if (phoneSet.add(user.getPhone())) {
        uniqueByPhone.add(user);
    }
}

这种适合场景:手机号重复只保留第一个用户。


场景2:商品去重,保留最新的一次商品信息

List<Product> products = ...;
Map<String, Product> productMap = new LinkedHashMap<>();
for (Product product : products) {
    productMap.put(product.getId(), product);
}
List<Product> distinctProducts = new ArrayList<>(productMap.values());

此方案适合如商品信息有更新时仅保留最新数据。


保证线程安全的去重方法

在多线程环境下,操作集合需要线程安全。

  • Collections.synchronizedList
  • ConcurrentHashMap/ConcurrentSkipListSet
  • Stream API并行流(.parallelStream) 但去重实现和原理类似,核心思路都是利用辅助集合标记唯一性。

如何处理大数据量的List去重

数据量超百万时,优先用HashSet、Map等高效集合结构,不建议用contains遍历。

可分批处理,如分页将部分数据分流分批去重,或者利用数据库层实现(如SQL去重语句)。


代码完整范例:综合演示

下面是一个包含多种去重方式的演示类:

import java.util.*;
import java.util.stream.*;

class User {
    private String name;
    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public String getName() { return name; }
    public int getAge() { return age; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof User)) return false;
        User user = (User) o;
        return age == user.age && Objects.equals(name, user.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

public class ListDistinctDemo {
    public static void main(String[] args) {
        List<User> users = Arrays.asList(
            new User("张三", 20),
            new User("张三", 20),
            new User("李四", 25),
            new User("李四", 30),
            new User("王五", 20)
        );

        // 方法1:Stream.distinct(要求重写equals和hashCode)
        List<User> distinct1 = users.stream().distinct().collect(Collectors.toList());
        System.out.println(distinct1);

        // 方法2:按name字段唯一去重
        List<User> distinct2 = users.stream()
            .collect(Collectors.collectingAndThen(
                Collectors.toCollection(() ->
                    new TreeSet<>(Comparator.comparing(User::getName))),
                ArrayList::new
            ));
        System.out.println(distinct2);

        // 方法3:按name+age字段唯一去重
        List<User> distinct3 = users.stream()
            .collect(Collectors.collectingAndThen(
                Collectors.toCollection(() ->
                    new TreeSet<>(Comparator.comparing(u -> u.getName() + u.getAge()))),
                ArrayList::new
            ));
        System.out.println(distinct3);

        // 方法4:用Map按name唯一,保留最后一次
        Map<String, User> map = new LinkedHashMap<>();
        for (User user : users) {
            map.put(user.getName(), user);
        }
        List<User> distinct4 = new ArrayList<>(map.values());
        System.out.println(distinct4);
    }
}

常见问题与FAQ

  • Q:为什么有时候distinct方法没生效? A:对象未重写equals和hashCode。
  • Q:怎么指定任意字段去重? A:参考TreeSet+Comparator或Map辅助代码。
  • Q:怎么去重还能保留顺序? A:LinkedHashSet或Stream API都会保留原聚集顺序。

总结与扩展思考

Java List的去重方式非常多,每种适合场景不同。最核心原理是用辅助集合的数据结构来标记唯一,或者利用流的distinct方法语法糖。对于自定义对象,正确理解equals和hashCode的实现至关重要。更多复杂场景可自行组合去重策略,以满足性能、功能和代码可读性需求。


正文到此结束
评论插件初始化中...
Loading...