当前位置:首页 > 后端开发 > 正文

java怎么去除重复数据

Java可通过 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并加入集合
0