java怎么去除重复数据
- 后端开发
- 2025-08-14
- 1
Set
集合自动去重,将原数据存入
HashSet
(无序)或
LinkedHashSet
(保序),再转回
List
在Java编程中,去除重复数据是开发过程中频繁遇到的需求,尤其在数据处理、统计分析或业务逻辑优化等场景下尤为常见,本文将从基础原理、核心实现方式、典型应用场景及性能对比四个维度展开详解,并提供可落地的代码示例与实用技巧。
核心问题定位:什么是「重复数据」?
这里的「重复」需根据具体业务定义:
简单类型:整型/浮点型的数值相等即视为重复;
对象类型:默认调用equals()
方法判断内容一致性(注意需重写该方法);
复合条件:可能需要结合多个属性进行联合判重(如用户ID+操作时间)。
️ 重要前提:若未正确实现
hashCode()
和equals()
方法,基于哈希结构的去重机制将失效!
主流解决方案及实现细节
方案1:借助Set集合特性(推荐)
子方案 | 特点 | 适用场景 | 注意事项 |
---|---|---|---|
HashSet |
无序,O(1)插入/查询 | 不关心顺序的快速去重 | 会打乱原始顺序 |
LinkedHashSet |
维护插入顺序,O(1)操作 | 需保留顺序且允许重复出现 | 内存占用略高于普通HashSet |
TreeSet |
自动排序(自然序/定制Comparator) | 需要排序后的无重复集合 | 字符串排序区分大小写 |
代码示例:
// 原始列表含重复元素 List<String> list = Arrays.asList("A", "B", "A", "C"); // 转为HashSet去重(丢失顺序) Set<String> set = new HashSet<>(list); // ["A","B","C"] // 保留顺序的去重方案 LinkedHashSet<String> linkedSet = new LinkedHashSet<>(list); // ["A","B","C"] // 转换为排序后的集合 TreeSet<String> treeSet = new TreeSet<>(list); // ["A","B","C"]
进阶用法:配合Stream API
List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 4, 4, 5); List<Integer> distinctList = numbers.stream() .distinct() // 内部使用LinkedHashSet实现 .collect(Collectors.toList()); // [1,2,3,4,5]
方案2:手动遍历+辅助容器(可控性强)
适用于需要记录重复次数或执行复杂逻辑的场景:
public static <T> List<T> removeDuplicates(List<T> originalList) { Map<T, Integer> countMap = new LinkedHashMap<>(); // 保留顺序 for (T item : originalList) { countMap.put(item, countMap.getOrDefault(item, 0) + 1); } return new ArrayList<>(countMap.keySet()); }
此方法优势在于:
可扩展统计每个元素的出现次数;
通过修改Map的值类型可实现更多功能(如标记首次出现的索引);
完全控制元素的筛选逻辑。
️ 方案3:SQL层面去重(数据库交互场景)
当数据来源于数据库时,优先在SQL语句中处理:
SELECT DISTINCT column_name FROM table_name; -单字段去重 SELECT column1, column2 FROM table_name GROUP BY column1, column2; -多字段组合去重
这种方式能显著减少数据传输量,提升整体性能。
️ 方案4:第三方工具类库
Apache Commons Lang提供便捷方法:
import org.apache.commons.collections4.CollectionUtils; List<String> uniqueList = CollectionUtils.removeDuplicates(originalList);
优点:一行代码解决问题;缺点:依赖外部库。
特殊场景处理指南
场景1:对象深度去重(非简单值比较)
当实体类包含多个属性时,需自定义比较逻辑:
class Person { private String name; private int age; // 必须同时重写hashCode()和equals() @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Person)) return false; Person person = (Person) o; return age == person.age && Objects.equals(name, person.name); } @Override public int hashCode() { return Objects.hash(name, age); } }
此时可直接将Person对象放入HashSet进行去重。
场景2:大数据量下的内存优化
对于百万级数据:
使用BitSet
代替布尔数组(仅限整型范围);
采用分块处理+磁盘缓存;
考虑并行流处理(Parallel Stream):
List<Integer> largeList = ...; // 海量数据 List<Integer> distinctList = largeList.parallelStream() .distinct() .collect(Collectors.toList());
性能对比测试(关键指标参考)
方法 | 时间复杂度 | 空间复杂度 | 是否保持顺序 | 备注 |
---|---|---|---|---|
HashSet | O(n) | O(n) | 最快但无序 | |
LinkedHashSet | O(n) | O(n) | 中等速度,保序 | |
TreeSet | O(nlogn) | O(n) | 排序耗时较长 | |
手动遍历+HashMap | O(n) | O(n) | 灵活度高 | |
SQL DISTINCT | O(n) | O(1) | 最适合数据库场景 | |
Stream.distinct() | O(n) | O(n) | Java8+推荐方式 |
常见误区警示
误用new Object().equals(obj):未重写equals的对象永远不相等;
忽略null值处理:多数集合允许单个null元素,多个null仍会被视为重复;
过度依赖简单==运算符:仅适用于基本数据类型比较;
忽视线程安全问题:ConcurrentHashMap可用于并发环境去重。
相关问答FAQs
Q1: 如果既要去重又要统计每个元素的出现次数怎么办?
A: 推荐使用Map<T, Integer>
结构,遍历原集合时更新计数器,最后取Map的keySet即为去重结果,value即为出现次数,示例:
Map<String, Integer> frequencyMap = new HashMap<>(); for (String str : list) { frequencyMap.merge(str, 1, Integer::sum); } System.out.println("出现次数:" + frequencyMap);
Q2: 如何对自定义对象的多个字段进行联合去重?
A: 两种方案:①创建包含目标字段的新键对象(如复合主键类);②使用Collectors.groupingBy
分组后取首条记录,推荐第一种方式,示例:
class CompositeKey { private String field1; private int field2; // 完整实现hashCode()和equals() } Set<CompositeKey> keys = new HashSet<>(); // 遍历时生成CompositeKey并加入集合