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
,通常按照某些字段判断是否重复 - 用辅助集合实现,比如
Set、Map等 - 使用Java 8+的Stream API进行去重
- 手动遍历结合判断逻辑去重
不同方法适合不同场景,下文详细介绍每种方法。
1. 利用Set集合去重
Set集合(如HashSet、LinkedHashSet、TreeSet)本身不允许重复元素。将List元素加入Set,会自动移除重复值。适合基础类型或已正确实现hashCode和equals的方法。
示例:基础类型去重
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()方法简单高效地去重。 适用条件:基础类型和已正确重写equals和hashCode的对象。
示例:基础类型
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对象去重,核心依赖于equals和hashCode方法。 简单理解:
- 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实现字段唯一去重。
常见面试题讲解
- 如何对List
根据某字段(如name)完成去重? 推荐:用Stream+TreeSet结合Comparator,参考前文代码。 - Java 8 Stream的distinct实现原理? distinct方法内部利用了元素的equals方法做判等,所以对象去重的关键是重写equals和hashCode。
- 去重后如何保持元素的原始添加顺序? 用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的实现至关重要。更多复杂场景可自行组合去重策略,以满足性能、功能和代码可读性需求。