HashSet是去重首选,因其基于HashMap实现,通过hashCode()和equals()自动判重,平均时间复杂度O(1);需确保自定义类正确重写二者,否则去重失效。

为什么HashSet是去重的首选
因为HashSet底层基于HashMap实现,插入时自动用hashCode()和equals()判断是否已存在——这是Java标准库中开箱即用、性能最优的去重方案。
常见错误是直接用ArrayList手动遍历去重,不仅代码冗长,时间复杂度还是O(n²);而HashSet.add()平均是O(1)。
- 确保自定义类正确重写
hashCode()和equals(),否则即使逻辑相同也会被当作不同元素 - 如果需要保持插入顺序,改用
LinkedHashSet,它比HashSet稍慢但迭代顺序确定 - 不要用
TreeSet单纯为了去重——它强制排序且要求元素可比较,额外开销大,除非你确实需要有序结果
从List转Set去重的三步实操
最常见场景:已有List或List,要快速去重并返回新集合。
推荐写法:
立即学习“Java免费学习笔记(深入)”;
Listlist = Arrays.asList("a", "b", "a", "c"); Set unique = new HashSet<>(list); // 一行构造完成
注意点:
- 构造时传入原
List,内部会逐个调用add(),自动跳过重复项 - 不建议用
stream().distinct()再收集为Set——多一层封装,无实际收益 - 若原
List极大且只读,可考虑用Collections.unmodifiableSet(new HashSet(list))防止后续误改
Set去重失效的典型原因
现象:new HashSet(list)后仍有重复对象,或set.contains(obj)返回false明明刚加过。
根本原因几乎总是:
- 自定义类没重写
hashCode()和equals(),或只重写了一个 - 重写的
equals()逻辑和业务判断不一致(例如忽略大小写比较字符串,但hashCode()没同步处理) - 对象在加入
Set后修改了影响hashCode()的字段——这会导致HashSet内部桶位置错乱,后续contains()可能找不到
验证方法:对疑似重复的两个对象,手动执行obj1.equals(obj2)和obj1.hashCode() == obj2.hashCode(),必须同时为true。
线程安全场景下怎么安全去重
单线程用HashSet没问题,但多线程并发往同一个Set里add(),结果不可靠。
别用Collections.synchronizedSet(new HashSet())——它只同步单个方法,if (!set.contains(x)) set.add(x)这种检查+添加仍存在竞态。
- 用
ConcurrentHashMap.newKeySet()(Java 8+),它是真正线程安全的Set视图,性能优于synchronizedSet - 如果只是批量去重且能接受最终一致性,先用普通
HashSet在各线程内局部去重,再合并到一个线程安全容器 - 避免在
Set上做复杂操作(如遍历时修改),ConcurrentHashMap.newKeySet()也不支持Iterator.remove()
去重本身看似简单,但hashCode契约、可变性、并发这几处,随便踩一个就让结果出人意料。










