面试中常问的 List 去重问题,你都答对了吗?
一道被问烂的面试题如果你去面试 Java 开发岗位尤其是初级到中级岗位十有八九会被问到「如何对一个 List 进行去重」很多候选人会脱口而出「用 HashSet 啊把 List 丢进去再拿出来不就好了。」但老练的面试官会继续追问用 HashSet 去重之后顺序还和原来一样吗如果 List 里装的是自定义对象HashSet 还能正确去重吗如果既要保证顺序又要高效去重应该怎么选Java 8 的stream().distinct()底层是怎么实现的如果数据量上百万哪种方式性能最好重写equals的同时为什么必须重写hashCodeList 去重本身只是一行代码的事但它背后牵扯到集合框架、哈希原理、对象相等性、算法复杂度、Java 8 Stream 机制等知识点。一、为什么面试官偏爱「List 去重」这道题题面简单人人都能答上几句但不同层次的候选人回答深度可以天差地别。这道题可以考察集合框架的掌握程度List、Set、Map 的特性HashSet、LinkedHashSet、TreeSet 的区别。对「相等性」的理解equals 和 hashCode 的契约。算法与复杂度的敏感度O(n²) 和 O(n) 的差距空间换时间的取舍。对 JDK 新特性的了解Stream API 和distinct()实现原理。工程实践意识结合数据量、是否要求顺序、是否要求排序等业务场景选型。二、准备工作构造一个带重复元素的 ListjavaListString list new ArrayList(); list.add(Java); list.add(Python); list.add(Java); list.add(Go); list.add(Python); list.add(C); list.add(Java); // 去重前[Java, Python, Java, Go, Python, C, Java]期望结果[Java, Python, Go, C]即去重的同时尽量保留原有顺序。三、方案一双重 for 循环暴力去重javafor (int i 0; i list.size() - 1; i) { for (int j list.size() - 1; j i; j--) { if (list.get(j).equals(list.get(i))) { list.remove(j); } } }为什么内层循环要倒着遍历ArrayList的remove会触发元素搬移删除后后续元素下标前移。如果从前往后遍历会出现「漏删」或「下标越界」。从后往前遍历时删除元素只影响下标更大的元素而这些位置已经处理过不会漏删。复杂度分析时间复杂度O(n²)空间复杂度O(1)原地操作只适合数据量很小、不希望占用额外内存的场景。四、方案二单层 for 循环 contains 判断javaListString result new ArrayList(); for (String item : list) { if (!result.contains(item)) { result.add(item); } }问题List.contains底层是indexOf本质仍是线性扫描时间复杂度依然是O(n²)。优势代码简洁易读能保持顺序。劣势无法应对大数据量场景。五、方案三HashSet 去重最经典的答案javaSetString set new HashSet(list); ListString result new ArrayList(set);原理HashSet 底层基于 HashMap依赖元素的hashCode和equals判断重复add、contains平均时间复杂度O(1)。整体去重时间复杂度从 O(n²) 降到O(n)。致命缺陷HashSet 不保证元素的迭代顺序。输出可能是[Java, C, Go, Python]而且每次运行结果可能不同。本质空间换时间——额外申请哈希表辅助判重。六、方案四LinkedHashSet 保持顺序去重javaSetString set new LinkedHashSet(list); ListString result new ArrayList(set); // 输出[Java, Python, Go, C]原理LinkedHashSet 继承自 HashSet内部维护一个双向链表记录插入顺序。遍历时按链表顺序返回实现「按插入顺序去重」。注意保持的是插入顺序不是排序顺序。时间复杂度仍为O(n)只是比 HashSet 略多一点内存开销。选型对顺序无要求用HashSet。要求保持第一次出现顺序用LinkedHashSet。要求去重后排序用TreeSet。七、方案五Java 8 Stream 的 distinct()javaListString result list.stream() .distinct() .collect(Collectors.toList());底层实现JDK 源码DistinctOps中串行流去重实质是使用LinkedHashSet所以有序串行流中distinct()能保持元素第一次出现的顺序。关键点distinct()是一个有状态中间操作必须保留所有已见过的元素才能判断后续元素是否重复因此会占用O(n)级别的临时内存。依据的同样是对象的equals和hashCode。对大多数「去重并保持原顺序」场景这是语义最清晰、代码最简洁的写法。八、方案六TreeSet 去重顺便排序javaSetString set new TreeSet(list); ListString result new ArrayList(set); // 输出[C, Go, Java, Python]原理TreeSet 底层基于 TreeMap红黑树插入、删除、查找稳定在O(log n)整体去重O(n log n)。自定义排序javaSetString set new TreeSet( Comparator.comparingInt(String::length) .thenComparing(String::compareTo)); set.addAll(list);必须注意的坑TreeSet 判断重复不是通过equals而是通过compareTo或Comparator.compare返回值是否为 0。javaSetUser set new TreeSet(Comparator.comparingInt(u - u.id)); set.add(new User(1, Alice)); set.add(new User(1, Bob)); System.out.println(set.size()); // 输出 1Bob 被丢弃虽然 Alice 和 Bob 是两个不同对象equals返回 false但 Comparator 认为二者「相等」Bob 被丢弃。应尽量保证排序字段与业务上的重复判定字段一致。九、方案七BitSet 对整数去重进阶加分项适用于取值范围可控的非负整数如用户 ID、状态码。javaBitSet bitSet new BitSet(max 1); for (Integer number : numbers) { bitSet.set(number); } ListInteger result new ArrayList(); for (int i 0; i max; i) { if (bitSet.get(i)) { result.add(i); } } // 输出[1, 3, 5, 8, 9]优点时间复杂度接近 O(n)去重后天然升序位图占用内存小。缺点只适用于非负整数取值上限不能太大。十、自定义对象去重equals 和 hashCode 必须一起重写javastatic class User { private int id; private String name; // 只重写 equals不重写 hashCode —— 错误示范 Override public boolean equals(Object obj) { if (this obj) return true; if (!(obj instanceof User)) return false; User other (User) obj; return id other.id name.equals(other.name); } }为什么去重失败HashSet 先根据hashCode定位桶再在同一桶内用equals判断相等。如果不重写hashCode两个业务上相等的对象仍然使用Object.hashCode根据内存地址计算会落到不同的哈希桶中即使equals返回 trueHashSet 也不会拿它们比较最终去重失败。正确做法javaOverride public boolean equals(Object obj) { if (this obj) return true; if (!(obj instanceof User)) return false; User other (User) obj; return id other.id Objects.equals(name, other.name); } Override public int hashCode() { return Objects.hash(id, name); }equals 的契约自反性、对称性、传递性、一致性、非空性。hashCode 的契约两个对象 equals 返回 truehashCode 必须相等。两个对象 equals 返回 falsehashCode 不一定要不同但不同可提升哈希表性能。面试标准回答重写 equals 必须重写 hashCode是为了保证 equals 契约和 hashCode 契约的一致性否则对象在 HashMap、HashSet 等哈希集合中会表现出不可预期的行为。十一、性能实测对比javapublic class DeduplicateBenchmark { public static void main(String[] args) { int size 200_000; ListInteger list new ArrayList(size); Random random new Random(42); for (int i 0; i size; i) { list.add(random.nextInt(size / 2)); } // 分别测试双重 for 循环、contains、HashSet、LinkedHashSet、Stream.distinct() } }典型性能对比数据量 20 万方案时间复杂度是否保序相对性能双重 for 循环O(n²)是极慢contains 判断O(n²)是极慢HashSetO(n)否快LinkedHashSetO(n)是快略慢于 HashSetStream.distinct()O(n)是快TreeSetO(n log n)排序中等BitSetO(n)升序极快限整数十二、面试答题思路总结面试被问到 List 去重时可以按下面的层次回答先给最经典答案用 HashSet 去重时间复杂度 O(n)但不保证顺序。补充顺序要求如果要求保持原顺序用LinkedHashSet或Stream.distinct()。补充排序要求如果要求去重后排序用TreeSet。补充小数据量场景双重 for 循环或 contains 判断虽然 O(n²) 但代码简单、不占额外空间。补充特殊场景非负整数范围可控时用BitSet极致高效。补充关键坑点自定义对象必须同时重写equals和hashCodeTreeSet 判断重复依赖Comparator而不是equals。总结选型需求推荐方案只要去重不关心顺序HashSet去重 保持原顺序LinkedHashSet / Stream.distinct()去重 排序TreeSet数据量小、不想占额外空间双重 for 循环非负整数、范围可控BitSet一句话总结List 去重看似简单但背后涉及的集合框架、哈希原理、equals/hashCode 契约、算法复杂度和 Stream 机制正是面试官用来区分「背答案型」和「理解原理型」候选人的绝佳素材。

相关新闻

Seay源码审计工具实战:从环境搭建到漏洞确认与避坑指南

Seay源码审计工具实战:从环境搭建到漏洞确认与避坑指南

简介:Seay源代码审计系统是一款面向 Web 应用开发者与安全工程师的自动化代码安全审查工具,专注于发现高危函数、注入漏洞和编程错误,并提供一键审计、函数查询、自定义规则、代码高亮与调试、报告生成等核心能力。资源包为Windows环境下常用…

2026/10/9 13:55:48 阅读更多 →
人大金仓KCA/KCP认证备考:从零散题库到可复现的实操路径

人大金仓KCA/KCP认证备考:从零散题库到可复现的实操路径

简介:这份人大金仓KCA、KCP题库整理面向备考金仓数据库认证的考生与数据库运维初学者,聚焦KingbaseES v8核心考点,帮助读者在刷题中快速定位知识盲区、巩固原理细节。内容覆盖系统表存储位置、ORDER BY排序限制、后台进程、模板数据库、索引最…

2026/10/9 13:54:47 阅读更多 →
Java反混淆工具flaming-shame:字节码还原与混淆对抗实战

Java反混淆工具flaming-shame:字节码还原与混淆对抗实战

简介:flaming-shame 是一款面向 Java 逆向工程与安全分析方向的轻量级反混淆工具,适合需要阅读、调试被混淆字节码的开发者与安全研究人员。它通过静态分析手段比较 Java 程序的结构图,尝试自动还原被混淆的类名、方法名与变量名,…

2026/10/9 13:54:47 阅读更多 →

最新新闻

MOSS-Transcribe-Diarize Web后端架构解析:任务状态机、作业管理与 FastAPI 实现

MOSS-Transcribe-Diarize Web后端架构解析:任务状态机、作业管理与 FastAPI 实现

MOSS-Transcribe-Diarize Web后端架构解析:任务状态机、作业管理与 FastAPI 实现 【免费下载链接】MOSS-Transcribe-Diarize A 0.9B model for long-form transcription in 50 languages with speaker diarization, timestamps, and acoustic event awareness 项目…

2026/10/9 14:28:53 阅读更多 →
MySQL 8.0 DBA实战沙盒:基于ActivityGuide的GTID复制与InnoDB Cluster实验指南

MySQL 8.0 DBA实战沙盒:基于ActivityGuide的GTID复制与InnoDB Cluster实验指南

简介:本资源是Oracle官方出品的《MySQL 8.0 for Database Administrators Activity Guide》实验手册PDF,专为数据库管理员及进阶运维人员设计,聚焦MySQL 8.0核心管理能力实战训练,覆盖安装配置、安全加固(角色管理与密…

2026/10/9 14:28:53 阅读更多 →
华为OD面试MySQL高频考点:索引优化与事务锁机制实战指南

华为OD面试MySQL高频考点:索引优化与事务锁机制实战指南

1. 华为OD面试里,数据库MySQL到底在考什么先说个扎心的现实:华为OD的技术面,MySQL这块很少会问你“背得滚瓜烂熟”的八股文定义,考官更爱拿真实场景来试探你的底子。比如直接抛一句“这张表数据量到三百万了,查询越来越…

2026/10/9 14:28:53 阅读更多 →
MySQL查询优化实战:慢查询定位、索引设计与SQL改写全攻略

MySQL查询优化实战:慢查询定位、索引设计与SQL改写全攻略

这套MySQL查询优化的东西,我本来是想写一篇"速查清单"式的技术笔记,但回头想想,真正在工作中救人于水火的,往往不是一个孤立技巧,而是一整套排查思路。就比如之前线上有个订单列表接口,上线时明明…

2026/10/9 14:28:53 阅读更多 →
MySQL约束体系详解:从六大约束到生产实践,保障数据完整性

MySQL约束体系详解:从六大约束到生产实践,保障数据完整性

平时在 MySQL 里建表写 SQL,大家关注最多的往往是索引、查询优化、事务隔离,约束(CONSTRAINT)反而成了最容易被忽略的那块。但数据质量一旦出问题,重跑数据、修数、补全、排查重复记录,哪个都比当初多写一行…

2026/10/9 14:28:53 阅读更多 →
WSL更新报错排查:内核升级与Docker/VS Code场景处理

WSL更新报错排查:内核升级与Docker/VS Code场景处理

1. 为什么会出现“WSL needs updating”?——先看版本模型的坑2. 官方推荐方案:把WSL内核和系统组件更新到最新3. 场景化的处理:从docker到VS Code到存储路径4. 遇到其他WSL安装问题的排查清单对了,先说结论:这个报错基…

2026/10/9 14:27:52 阅读更多 →

日新闻

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API这个话题,隔三差五就会在群里被翻出来讨论一次。上周还有个同事线上处理一个订单超时问题,排查到最后发现是ZonedDateTime序列化后时区丢了,用户在下单当天晚上看到的时间整整差了8个小时。这类问题几乎每个做Java开发的人都遇到过…

2026/10/9 0:00:49 阅读更多 →
EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

前几个月我手头有好几台机器需要互相访问:办公室台式机、家里 NAS、还有一台云主机。如果只是偶尔传个文件倒还好,问题是工作场景经常要在几处环境之间来回切换,每次都先登录跳板机再层层代理,实在折腾。我先后试过端口映射、自建…

2026/10/9 0:00:49 阅读更多 →
AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent 这个词在过去一年里被反复提及,但真正动手搭过一套能跑起来的 Agent 系统的人都知道,从"知道它是什么"到"让它稳定干活"之间隔着一整套工程决策。我前后参与过几个 Agent 项目的落地,从最初用现成框架拼装&…

2026/10/9 0:01:50 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:32 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:40 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 10:11:06 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 21:13:17 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 6:17:20 阅读更多 →