目录
[模块二:List 体系(有序、可重复)](#模块二:List 体系(有序、可重复))
[1. ArrayList(项目最高频)](#1. ArrayList(项目最高频))
[2. LinkedList](#2. LinkedList)
[3. Vector 与 CopyOnWriteArrayList(线程安全 List)](#3. Vector 与 CopyOnWriteArrayList(线程安全 List))
[模块三:Set 体系(无序、不可重复)](#模块三:Set 体系(无序、不可重复))
[1. HashSet](#1. HashSet)
[2. LinkedHashSet](#2. LinkedHashSet)
[3. TreeSet](#3. TreeSet)
[模块四:Map 体系(面试 100% 必问,重中之重)](#模块四:Map 体系(面试 100% 必问,重中之重))
[1. HashMap(面试核心中的核心)](#1. HashMap(面试核心中的核心))
[2. ConcurrentHashMap(线程安全 Map,面试必问)](#2. ConcurrentHashMap(线程安全 Map,面试必问))
[3. LinkedHashMap 与 TreeMap](#3. LinkedHashMap 与 TreeMap)
[Java 集合框架 面试模拟题(实习面试专属,附标准答案)](#Java 集合框架 面试模拟题(实习面试专属,附标准答案))
[一、基础必考题(8 道,中小厂实习 100% 覆盖)](#一、基础必考题(8 道,中小厂实习 100% 覆盖))
[1. ArrayList 和 LinkedList 的核心区别、性能对比、适用场景分别是什么?](#1. ArrayList 和 LinkedList 的核心区别、性能对比、适用场景分别是什么?)
[2. JDK7 和 JDK8 的 HashMap 有哪些核心区别?](#2. JDK7 和 JDK8 的 HashMap 有哪些核心区别?)
[3. ConcurrentHashMap 和 HashTable 的核心区别是什么?](#3. ConcurrentHashMap 和 HashTable 的核心区别是什么?)
[4. HashSet 的去重原理是什么?](#4. HashSet 的去重原理是什么?)
[5. fail-fast(快速失败)和 fail-safe(安全失败)的核心区别是什么?](#5. fail-fast(快速失败)和 fail-safe(安全失败)的核心区别是什么?)
[6. ArrayList 的扩容机制是什么?](#6. ArrayList 的扩容机制是什么?)
[7. 简述 HashMap 的 put () 方法完整执行流程(JDK8)](#7. 简述 HashMap 的 put () 方法完整执行流程(JDK8))
[8. Comparable 和 Comparator 的核心区别是什么?](#8. Comparable 和 Comparator 的核心区别是什么?)
[二、场景坑点题(6 道,大厂实习高频代码题 / 业务题)](#二、场景坑点题(6 道,大厂实习高频代码题 / 业务题))
[1. 以下是校园二手平台删除下架商品的代码,运行会报什么错?为什么?正确写法是什么?](#1. 以下是校园二手平台删除下架商品的代码,运行会报什么错?为什么?正确写法是什么?)
[2. 以下代码用 HashSet 存储商品对象,运行结果是什么?为什么?](#2. 以下代码用 HashSet 存储商品对象,运行结果是什么?为什么?)
[3. 你的校园二手平台用 HashMap 做商品本地缓存,多线程环境下会出现哪些问题?为什么?应该用什么替代?](#3. 你的校园二手平台用 HashMap 做商品本地缓存,多线程环境下会出现哪些问题?为什么?应该用什么替代?)
[4. 以下场景分别应该用什么 List 集合?为什么?](#4. 以下场景分别应该用什么 List 集合?为什么?)
[5. 以下 HashMap 初始化代码有什么问题?正确写法是什么?](#5. 以下 HashMap 初始化代码有什么问题?正确写法是什么?)
[6. 怎么用 LinkedHashMap 实现校园二手平台的商品 LRU(最近最少使用)缓存?要求缓存最多存 100 个商品,超过容量自动删除最久未访问的商品。](#6. 怎么用 LinkedHashMap 实现校园二手平台的商品 LRU(最近最少使用)缓存?要求缓存最多存 100 个商品,超过容量自动删除最久未访问的商品。)
[三、进阶原理题(4 道,中大厂实习拔高题)](#三、进阶原理题(4 道,中大厂实习拔高题))
[1. HashMap 的容量为什么必须是 2 的 n 次方?](#1. HashMap 的容量为什么必须是 2 的 n 次方?)
[2. JDK8 的 ConcurrentHashMap 为什么用 synchronized 代替 JDK7 的 ReentrantLock?](#2. JDK8 的 ConcurrentHashMap 为什么用 synchronized 代替 JDK7 的 ReentrantLock?)
[3. HashMap 的链表转红黑树的阈值为什么是 8?](#3. HashMap 的链表转红黑树的阈值为什么是 8?)
[4. JDK7 的 HashMap 多线程扩容为什么会出现死循环?](#4. JDK7 的 HashMap 多线程扩容为什么会出现死循环?)
模块一:集合整体架构
核心定义
Java 集合分为两大顶层分支,所有集合都在java.util包下:
1. 两大顶层接口
| 顶层接口 | 特点 | 子分支 |
|---|---|---|
Collection |
单列集合,存储单个元素 | List(有序、可重复)、Set(无序、不可重复)、Queue(队列,先进先出) |
Map |
双列集合,存储 Key-Value 键值对,Key 不可重复 | HashMap、ConcurrentHashMap、TreeMap、LinkedHashMap |
2. 集合与数组的核心区别
| 对比维度 | 数组 | 集合 |
|---|---|---|
| 长度 | 固定,初始化必须指定长度 | 动态扩容,长度可变 |
| 存储类型 | 可以存基本类型、引用类型 | 只能存引用类型(基本类型需要自动装箱为包装类) |
| 方法 | 只有 length 属性,无内置方法 | 封装了大量增删改查、遍历工具方法 |
| 性能 | 连续内存,性能极高 | 基于不同数据结构,性能各有差异 |
3. 迭代器 Iterator
是集合遍历的统一接口,核心设计是统一所有集合的遍历方式 ,屏蔽不同集合的底层实现差异,核心方法:hasNext()、next()、remove()。
4. fail-fast(快速失败)机制
是集合的错误检测机制,遍历集合时,如果其他线程修改了集合的结构(add/remove),迭代器会立即抛出ConcurrentModificationException,核心实现是modCount计数:集合每次修改都会更新modCount,迭代器遍历时会对比expectedModCount和modCount,不一致就抛异常。
个人理解
集合本质是 Java 对常用数据结构的封装,不同集合对应不同的数据结构(数组、链表、红黑树、哈希表),开发中核心是根据业务场景选择合适的集合,而不是所有场景都用 ArrayList;Iterator 是迭代器模式的经典实现,也是 Java 面向抽象编程的体现。
项目实际使用场景
结合校园二手平台开发实践:
- 商品列表、订单列表 :用
List存储,有序、可重复,支持随机查询; - 用户浏览历史去重、用户角色权限去重 :用
Set存储,自动去重; - 商品缓存、用户 Session 缓存、接口限流计数 :用
Map存储,Key-Value 结构,O (1) 时间复杂度查询; - 遍历删除下架商品 :统一用
Iterator遍历,避免并发修改异常。
面试考点标注
✅ 必问:Collection 和 Collections 的区别(Collection 是集合顶层接口,Collections 是集合工具类,提供排序、反转等静态方法);
✅ 原理:fail-fast 机制的实现原理;
【
-
fail-fast = 集合遍历安全检测机制
-
依靠 modCount(集合修改次数)实现
-
迭代器保存 expectedModCount 做对比
-
不一致 → 抛 ConcurrentModificationException
-
集合.remove () 报错,迭代器.remove () 不报错
】
✅ 细节:集合为什么不能存基本类型?(泛型擦除后都是 Object 类型,基本类型不是 Object 的子类)。
模块二:List 体系(有序、可重复)
1. ArrayList(项目最高频)
核心定义
- 底层结构 :JDK8 及之后,底层是
transient Object[] elementData动态数组,默认初始容量 10; - 扩容机制 :容量不足时,自动扩容为原容量的1.5 倍 (位运算
oldCapacity + (oldCapacity >> 1)),扩容时会创建新数组,把原数组元素拷贝到新数组; - 核心特点:随机查询 O (1),尾部增删 O (1),中间增删 O (n)(需要移动元素),非线程安全。
个人理解
ArrayList 是开发中 90% 场景的首选,本质是对数组的封装,解决了数组长度固定的问题,核心优势是随机查询性能极高,适合读多写少、需要随机访问的场景;缺点是中间增删性能差,非线程安全。
项目实际使用场景
- 商品列表、订单列表、用户列表的查询返回:都是读多写少,需要随机分页查询,用 ArrayList 性能最高;
- 方法内的临时数据存储:比如批量导入商品时的临时数据存储,用 ArrayList;
- 避坑实践 :初始化 ArrayList 时如果知道数据量,指定初始容量,比如
new ArrayList<>(1000),避免频繁扩容拷贝数组,提升性能。
面试考点标注
✅ 必问:ArrayList 的扩容机制(初始容量、扩容倍数、扩容流程);
✅ 必问:ArrayList 和数组的性能对比,什么时候用数组什么时候用 ArrayList;
✅ 坑点:ArrayList 的remove()方法,按索引删除和按对象删除的区别,遍历删除的并发修改异常。
2. LinkedList
核心定义
- 底层结构 :双向链表,每个节点有
prev、item、next三个属性,没有初始容量,不需要扩容; - 核心特点:头尾增删 O (1),中间增删 O (n)(需要遍历找节点),随机查询 O (n),非线程安全;
- 实现了
Deque接口,可以作为队列、栈使用。
个人理解
LinkedList 的优势是头尾增删性能极高,适合频繁在头尾操作的场景;缺点是随机查询性能极差,因为要从头部 / 尾部遍历找节点,开发中绝大多数场景都不需要用 LinkedList,ArrayList 足够。
项目实际使用场景
- 订单操作日志、用户操作记录:频繁在尾部新增日志,不需要随机查询,用 LinkedList;
- 接口限流的请求队列:作为 FIFO 队列,频繁在尾部加请求,头部取请求,用 LinkedList;
- 避坑实践 :绝对不要用 LinkedList 做随机查询,比如
for (int i=0; i<linkedList.size(); i++),性能是 ArrayList 的几十倍。
面试考点标注
✅ 必问:ArrayList 和 LinkedList 的核心区别、性能对比、适用场景;
✅ 细节:LinkedList 的底层是双向链表,不是单向链表,所以可以从头尾双向遍历。
3. Vector 与 CopyOnWriteArrayList(线程安全 List)
核心定义
(1)Vector
- 底层和 ArrayList 一样是动态数组,所有方法都加了
synchronized锁,锁整个数组,线程安全,性能极差,已经废弃,不推荐使用。
(2)CopyOnWriteArrayList(写时复制)
- 核心原理:写操作(add/remove)时,复制一份新的数组,在新数组上修改,修改完成后把原数组的引用指向新数组;读操作直接读原数组,不需要加锁;
- 核心特点:线程安全,读性能极高,写性能差(需要拷贝数组),适合读多写少的场景,是 fail-safe 机制(不会抛并发修改异常)。
个人理解
Vector 是粗暴的全方法加锁,性能太差被淘汰;CopyOnWriteArrayList 是读写分离的思想,读无锁,写加锁,适合读多写少的并发场景,缺点是有数据一致性问题(写操作完成前,读的是旧数据),只能保证最终一致性。
项目实际使用场景
- 商品分类全局缓存、字典缓存:读多写少,服务启动时加载一次,后续几乎不修改,多线程并发读,用 CopyOnWriteArrayList,不需要加锁,性能极高;
- 用户黑名单、IP 白名单:读多写少,偶尔新增,用 CopyOnWriteArrayList;
- 避坑实践:绝对不要在写多的场景用 CopyOnWriteArrayList,每次写都要拷贝数组,内存和性能开销极大。
面试考点标注
✅ 必问:fail-fast 和 fail-safe 的核心区别;
✅ 必问:CopyOnWriteArrayList 的实现原理、优缺点、适用场景;
✅ 坑点:CopyOnWriteArrayList 的内存拷贝开销和数据一致性问题。
模块三:Set 体系(无序、不可重复)
1. HashSet
核心定义
- 底层结构 :基于 HashMap 实现,所有元素存在 HashMap 的 Key 中,Value 是一个固定的
private static final Object PRESENT = new Object()常量; - 去重原理 :添加元素时,先计算元素的
hashCode(),找到数组下标;如果下标位置没有元素,直接添加;如果有元素,再调用equals()对比内容,内容相同就不添加,不同就挂在链表上; - 核心特点:无序、去重、非线程安全,允许存 null。
个人理解
HashSet 本质是 HashMap 的马甲,所有逻辑都是 HashMap 实现的,核心价值就是利用 HashMap 的 Key 不可重复的特性实现去重,开发中用来做数据去重。
项目实际使用场景
- 用户浏览商品 ID 去重:用户多次浏览同一个商品,只保留一条记录,用 HashSet;
- 用户角色权限去重:一个用户多个角色,权限重复,用 HashSet 自动去重;
- 批量导入商品的重复校验:批量导入商品时,用 HashSet 校验商品编号是否重复。
面试考点标注
✅ 必问:HashSet 的去重原理;
✅ 坑点:自定义对象要实现hashCode()和equals()才能正确去重,否则会出现重复元素。
2. LinkedHashSet
核心定义
- 底层基于 LinkedHashMap 实现,继承 HashSet,在 HashMap 的基础上增加了双向链表,记录元素的插入顺序;
- 核心特点:有序(按插入顺序)、去重、非线程安全,性能比 HashSet 略低。
项目实际使用场景
- 用户浏览历史:需要按用户浏览顺序保存,同时去重,用 LinkedHashSet;
- 商品搜索关键词历史:按搜索顺序保存,去重,用 LinkedHashSet。
面试考点标注
✅ 对比:HashSet 和 LinkedHashSet 的区别(无序 vs 有序)。
3. TreeSet
核心定义
- 底层基于 TreeMap 实现,元素自动排序,支持自然排序和自定义排序;
- 排序规则 :
- 自然排序:元素实现
Comparable接口,重写compareTo()方法; - 自定义排序:创建 TreeSet 时传入
Comparator比较器;
- 自然排序:元素实现
- 核心特点:有序(按排序规则)、去重、非线程安全,不允许存 null。
项目实际使用场景
- 商品按价格 / 销量排序的去重列表:需要排序 + 去重,用 TreeSet,传入 Comparator 按价格排序;
- 用户积分排行榜去重:按积分排序,去重,用 TreeSet。
面试考点标注
✅ 必问:Comparable 和 Comparator 的核心区别:
| 对比维度 | Comparable | Comparator |
|---|---|---|
| 位置 | 元素类自身实现,在java.lang包 |
单独的比较器类,在java.util包 |
| 方法 | 重写compareTo(T o) |
重写compare(T o1, T o2) |
| 耦合 | 和元素类耦合,只能有一种排序规则 | 解耦,可以定义多种排序规则 |
| 场景 | 固定的自然排序 | 灵活的自定义排序 |
模块四:Map 体系(面试 100% 必问,重中之重)
1. HashMap(面试核心中的核心)
核心定义
(1)底层结构
- JDK7:数组 + 单向链表,头插法,扩容时会出现死链问题;
- JDK8 :数组 + 链表 + 红黑树,尾插法,解决死链问题,核心优化:
- 链表长度≥8 且 数组长度≥64 时,链表转为红黑树,查询复杂度从 O (n) 降到 O (logn);
- 红黑树节点数≤6 时,转回链表。
(2)核心参数
- 默认初始容量:16,必须是 2 的 n 次方;
- 默认负载因子:0.75;
- 扩容阈值:容量 × 负载因子,达到阈值就扩容为原容量的 2 倍;
- 红黑树转换阈值:链表长度 8,树转链表阈值 6。
(3)Hash 扰动函数
JDK8 的 hash 算法:(h = key.hashCode()) ^ (h >>> 16),把 hashCode 的高 16 位和低 16 位异或,让高位也参与下标计算,减少哈希冲突。
(4)扩容流程
- 数组容量达到阈值,创建新数组,容量为原数组的 2 倍;
- 遍历原数组的每个节点,重新计算节点在新数组的下标;
- JDK8 优化:不需要重新计算 hash,通过
hash & oldCap判断,节点要么在原下标,要么在原下标 + 原容量的位置; - 把节点迁移到新数组,完成扩容。
个人理解
HashMap 是哈希表的 Java 实现,核心是用数组做桶,链表 / 红黑树解决哈希冲突,JDK8 的红黑树优化解决了链表过长的查询性能问题,尾插法解决了 JDK7 扩容的死链问题;容量必须是 2 的 n 次方,是为了用hash & (length-1)代替取模运算,性能更高。
项目实际使用场景
- 商品 ID 到商品对象的本地缓存:Key 是商品 ID,Value 是 Goods 对象,O (1) 查询,性能极高;
- 接口参数封装、前端返回数据组装:用 HashMap 封装动态参数,灵活方便;
- 避坑实践 :
- 初始化 HashMap 时指定初始容量,比如
new HashMap<>(16),避免频繁扩容; - Key 推荐用 String、Integer 等不可变类,避免 Key 修改后 hashCode 变化,找不到数据;
- 多线程环境下绝对不要用 HashMap,会出现数据丢失、死循环等问题。
- 初始化 HashMap 时指定初始容量,比如
面试考点标注
✅ 必问:JDK7 和 JDK8 HashMap 的核心区别:
| 对比维度 | JDK7 HashMap | JDK8 HashMap |
|---|---|---|
| 底层结构 | 数组 + 单向链表 | 数组 + 链表 + 红黑树 |
| 插入方式 | 头插法 | 尾插法 |
| 扩容问题 | 多线程扩容会出现死链 | 尾插法解决死链问题 |
| Hash 算法 | 4 次扰动 | 1 次扰动(高 16 位异或) |
| ✅ 必问:HashMap 的完整扩容流程; | ||
| ✅ 必问:HashMap 的容量为什么必须是 2 的 n 次方?(用位运算代替取模,性能更高,哈希分布更均匀); | ||
| ✅ 必问:HashMap 的 Key 为什么推荐用不可变类?(hashCode 稳定,不会出现 Key 修改后找不到的问题); | ||
| ✅ 细节:红黑树转换阈值为什么是 8?(泊松分布,链表长度到 8 的概率极低,平衡链表和红黑树的性能开销)。 |
2. ConcurrentHashMap(线程安全 Map,面试必问)
核心定义
(1)JDK7 实现:分段锁
- 底层是 Segment 数组,每个 Segment 继承 ReentrantLock,是一个独立的小 HashMap;
- 锁的粒度是 Segment,多个线程访问不同的 Segment 不会冲突,默认 16 个 Segment,并发度 16;
- 缺点:锁粒度还是太大,同一个 Segment 的操作还是会竞争锁,性能不够高。
(2)JDK8 实现:CAS + synchronized
- 底层结构和 JDK8 HashMap 一致:数组 + 链表 + 红黑树;
- 锁的粒度是链表的头节点,只有哈希冲突的节点才会加锁,没有哈希冲突的节点用 CAS 无锁更新,并发度大幅提升;
- 空节点用 CAS 插入,有节点的链表 / 红黑树用 synchronized 锁头节点;
- 支持并发扩容,多线程一起迁移节点,提升扩容速度。
个人理解
ConcurrentHashMap 是 Java 中性能最高的线程安全 Map,JDK8 把锁粒度从 Segment 降到了单个节点,性能比 JDK7 提升了数倍,是多线程环境下的首选 Map;HashTable 是锁整个 Map,性能极差,已经完全被淘汰。
项目实际使用场景
- 多线程环境下的用户在线缓存、接口限流计数:多个线程同时读写,用 ConcurrentHashMap,线程安全,性能高;
- 商品库存的本地缓存:多线程同时扣减库存,用 ConcurrentHashMap 保证线程安全;
- 全局用户 Session 缓存:微服务网关多线程并发访问,用 ConcurrentHashMap。
面试考点标注
✅ 必问:JDK7 和 JDK8 ConcurrentHashMap 的线程安全实现区别;
✅ 必问:ConcurrentHashMap 和 HashTable 的区别;
✅ 原理:ConcurrentHashMap 为什么不用 ReentrantLock,改用 synchronized?(JDK6 之后 synchronized 做了大量优化,锁升级,性能不比 ReentrantLock 差,还节省内存);
✅ 细节:ConcurrentHashMap 的 Key 和 Value 为什么不能为 null?(避免多线程下的二义性,无法区分是 key 不存在还是 value 为 null)。
3. LinkedHashMap 与 TreeMap
核心定义
(1)LinkedHashMap
- 继承 HashMap,底层 HashMap + 双向链表,记录元素的插入顺序 / 访问顺序;
- 可以实现 LRU(最近最少使用)缓存,开启
accessOrder=true,访问元素后会把元素移到链表尾部。
(2)TreeMap
- 底层红黑树,Key 自动排序,支持自然排序和自定义排序,和 TreeSet 一致。
项目实际使用场景
- 购物车商品列表:按用户加入购物车的顺序保存,用 LinkedHashMap;
- 商品 LRU 本地缓存:缓存 1000 个热门商品,超过容量自动删除最久未访问的商品,用 LinkedHashMap 实现;
- 商品分类按 ID 排序:Key 是分类 ID,自动排序,用 TreeMap。
面试考点标注
✅ 场景题:怎么用 LinkedHashMap 实现 LRU 缓存;
【
-
LRU :最近最少使用,淘汰最久未访问的数据。
-
LinkedHashMap 核心特性
-
继承
HashMap,底层哈希表 + 双向链表,有序。 -
构造方法
LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)-
accessOrder = true:访问顺序(get/put 后元素移到链表尾部),LRU 核心依赖这个参数。 -
accessOrder = false:插入顺序(默认)。
-
-
-
内置回调方法
removeEldestEntry(Map.Entry<K,V> eldest):返回true时,自动删除最久未使用的头部元素。
】
✅ 对比:HashMap、LinkedHashMap、TreeMap 的区别(无序、插入有序、排序有序)。
当日验收清单
- 不看资料口述:HashMap JDK7 和 JDK8 的核心差异、完整扩容流程;
- 不看资料口述:ConcurrentHashMap JDK8 的线程安全实现原理;
- 结合项目口述:你的项目中不同场景分别用了什么集合,为什么选这个;
- 避坑确认:遍历集合删除元素的并发修改异常怎么解决,多线程环境下用什么集合。
Java 集合框架 面试模拟题(实习面试专属,附标准答案)
一、基础必考题(8 道,中小厂实习 100% 覆盖)
1. ArrayList 和 LinkedList 的核心区别、性能对比、适用场景分别是什么?
【标准答案】
| 对比维度 | ArrayList | LinkedList |
|---|---|---|
| 底层结构 | 动态数组 | 双向链表 |
| 随机查询 | O (1),性能极高 | O (n),需要遍历链表,性能极差 |
| 头尾增删 | O(1) | O(1) |
| 中间增删 | O (n),需要移动数组元素 | O (n),需要遍历找节点 |
| 内存占用 | 连续内存,预留扩容空间 | 每个节点需要存 prev/next 指针,内存开销更大 |
| 适用场景 | 读多写少、需要随机访问的场景(90% 业务场景首选) | 频繁头尾增删、不需要随机查询的场景(队列 / 栈) |
【考点对应】模块二:List 体系核心对比
2. JDK7 和 JDK8 的 HashMap 有哪些核心区别?
【标准答案】
| 对比维度 | JDK7 HashMap | JDK8 HashMap |
|---|---|---|
| 底层结构 | 数组 + 单向链表 | 数组 + 链表 + 红黑树(链表长度≥8 且数组≥64 时树化) |
| 插入方式 | 头插法 | 尾插法 |
| 扩容问题 | 多线程扩容会出现链表死循环(死链) | 尾插法解决死链问题 |
| Hash 算法 | 4 次扰动运算 | 1 次扰动(hashCode 高 16 位异或低 16 位) |
| 扩容优化 | 每个节点重新计算 hash 下标 | 无需重新计算 hash,节点要么在原下标,要么在原下标 + 原容量 |
【考点对应】模块四:HashMap 核心特性
3. ConcurrentHashMap 和 HashTable 的核心区别是什么?
【标准答案】两者都是线程安全的 Map,核心差异在锁的实现和性能:
- 锁粒度不同 :
- HashTable:锁整个 Map 对象,所有方法都加
synchronized,所有操作都竞争同一把锁,性能极差; - ConcurrentHashMap:JDK7 是分段锁(锁 Segment),JDK8 是锁单个链表头节点,无冲突时用 CAS 无锁更新,锁粒度极小,并发性能极高。
- HashTable:锁整个 Map 对象,所有方法都加
- 底层结构不同:HashTable 是数组 + 链表;ConcurrentHashMap JDK8 是数组 + 链表 + 红黑树,和 HashMap 一致。
- null 值支持:HashTable 允许 Key/Value 为 null;ConcurrentHashMap 不允许 Key/Value 为 null(避免多线程下的二义性)。
【考点对应】模块四:ConcurrentHashMap 线程安全实现
4. HashSet 的去重原理是什么?
【标准答案】HashSet 底层基于 HashMap 实现,所有元素存在 HashMap 的 Key 中,Value 是一个固定的 Object 常量,去重逻辑和 HashMap 的 Key 去重完全一致:
- 添加元素时,先调用元素的
hashCode()计算哈希值,找到数组对应的下标位置; - 如果下标位置没有元素,直接添加成功;
- 如果下标位置有元素,再调用
equals()对比两个元素的内容:- 内容相同:判定为重复元素,添加失败;
- 内容不同:挂在链表 / 红黑树上,添加成功。
坑点:自定义对象必须重写
hashCode()和equals(),否则无法正确去重。
【考点对应】模块三:HashSet 去重原理
5. fail-fast(快速失败)和 fail-safe(安全失败)的核心区别是什么?
【标准答案】
| 对比维度 | fail-fast | fail-safe |
|---|---|---|
| 代表集合 | ArrayList、HashMap、HashSet 等普通集合 | CopyOnWriteArrayList、ConcurrentHashMap 等并发集合 |
| 触发场景 | 遍历集合时,其他线程修改了集合结构(add/remove),立即抛出ConcurrentModificationException |
遍历的是集合的快照,不会抛出异常 |
| 实现原理 | 基于modCount计数,遍历对比expectedModCount和modCount,不一致就抛异常 |
写时复制,遍历的是旧数组,修改操作在新数组上执行,互不影响 |
| 数据一致性 | 强一致性,遍历到的数据是最新的 | 最终一致性,遍历到的数据是旧快照,不保证实时一致 |
【考点对应】模块一:集合错误检测机制
6. ArrayList 的扩容机制是什么?
【标准答案】
- 初始容量:JDK8 及之后,默认初始容量为 0,第一次添加元素时才初始化为 10(懒加载,节省内存);
- 扩容触发:元素个数达到数组容量时,触发扩容;
- 扩容规则 :新容量 = 旧容量的 1.5 倍(位运算
oldCapacity + (oldCapacity >> 1)); - 扩容流程:创建新容量的数组,将原数组的元素拷贝到新数组,更新数组引用。
优化技巧:初始化 ArrayList 时如果知道数据量,指定初始容量,避免频繁扩容拷贝数组。
【考点对应】模块二:ArrayList 核心原理
7. 简述 HashMap 的 put () 方法完整执行流程(JDK8)
【标准答案】
- 计算 Key 的 hash 值:
hash = (h = key.hashCode()) ^ (h >>> 16); - 判断数组是否为空,为空则初始化数组(默认容量 16,负载因子 0.75);
- 计算数组下标:
i = (n - 1) & hash; - 如果下标位置为空,用 CAS 直接插入新节点,结束;
- 如果下标位置有节点:
- 头节点的 Key 和当前 Key 相同,直接覆盖 Value;
- 节点是红黑树节点,调用红黑树的插入方法;
- 节点是链表节点,遍历链表,找到相同 Key 则覆盖,找不到则在链表尾部插入新节点;插入后判断链表长度是否≥8,是则触发树化;
- 插入完成后,判断元素个数是否达到扩容阈值,达到则触发扩容。
【考点对应】模块四:HashMap 核心流程
8. Comparable 和 Comparator 的核心区别是什么?
【标准答案】
| 对比维度 | Comparable | Comparator |
|---|---|---|
| 定义位置 | 元素类自身实现,属于java.lang包 |
独立的比较器类,属于java.util包 |
| 核心方法 | 重写int compareTo(T o) |
重写int compare(T o1, T o2) |
| 耦合性 | 和元素类强耦合,一个类只能有一种排序规则 | 和元素类解耦,可以定义多种排序规则 |
| 适用场景 | 元素的固定自然排序(比如 ID、价格排序) | 灵活的自定义排序(比如按销量、浏览量动态排序) |
【考点对应】模块三:TreeSet/TreeMap 排序规则
二、场景坑点题(6 道,大厂实习高频代码题 / 业务题)
1. 以下是校园二手平台删除下架商品的代码,运行会报什么错?为什么?正确写法是什么?
java
List<Goods> goodsList = new ArrayList<>();
goodsList.add(new Goods(1, "手机", true));
goodsList.add(new Goods(2, "电脑", false));
goodsList.add(new Goods(3, "平板", false));
// 遍历删除下架商品
for (Goods goods : goodsList) {
if (!goods.isOnSale()) {
goodsList.remove(goods);
}
}
【标准答案】报错:ConcurrentModificationException(并发修改异常)原因:foreach 循环底层是 Iterator 迭代器,直接调用list.remove()会修改集合的modCount计数,迭代器检测到计数和预期不一致,触发 fail-fast 机制抛出异常。正确写法:使用 Iterator 的 remove () 方
java
Iterator<Goods> iterator = goodsList.iterator();
while (iterator.hasNext()) {
Goods goods = iterator.next();
if (!goods.isOnSale()) {
iterator.remove();
}
}
【考点对应】模块一:fail-fast 机制、集合遍历删除坑点
2. 以下代码用 HashSet 存储商品对象,运行结果是什么?为什么?
java
public class Goods {
private Long id;
private String title;
// 构造方法、getter/setter,未重写hashCode和equals
}
public class Test {
public static void main(String[] args) {
Set<Goods> goodsSet = new HashSet<>();
goodsSet.add(new Goods(1L, "手机"));
goodsSet.add(new Goods(1L, "手机"));
System.out.println(goodsSet.size());
}
}
【标准答案】输出结果:2原因:HashSet 去重依赖hashCode()和equals()方法,Goods 类没有重写这两个方法,默认使用 Object 类的实现:
hashCode()默认返回对象的内存地址,两个 new 出来的对象地址不同,hash 值不同;equals()默认比较对象地址,两个对象地址不同,判定为不重复,因此添加了两个元素。修复:重写 Goods 类的hashCode()和equals(),按 id 判断是否为同一个商品。
【考点对应】模块三:HashSet 去重坑点
3. 你的校园二手平台用 HashMap 做商品本地缓存,多线程环境下会出现哪些问题?为什么?应该用什么替代?
【标准答案】HashMap 是非线程安全的,多线程环境下会出现 3 个核心问题:
- 数据丢失:多个线程同时 put 同一个下标位置,会出现节点覆盖,导致数据丢失;
- 死循环(JDK7):多线程扩容时,头插法会导致链表形成环,get 元素时死循环,CPU 占满 100%;
- 数据不一致 :一个线程 put 元素,另一个线程 get 不到最新数据。替代方案:多线程环境下必须用
ConcurrentHashMap,线程安全且性能极高。
【考点对应】模块四:HashMap 线程安全问题、ConcurrentHashMap 适用场景
4. 以下场景分别应该用什么 List 集合?为什么?
场景 1:商品分类全局缓存,服务启动时加载一次,后续几乎不修改,多线程高并发读;
场景 2:订单操作日志,频繁在尾部新增,不需要随机查询;
场景 3:商品列表分页查询,读多写少,需要随机访问。
【标准答案】
- 场景 1:用
CopyOnWriteArrayList原因:读多写少,CopyOnWriteArrayList 读无锁,性能极高,适合并发读的缓存场景; - 场景 2:用
LinkedList原因:频繁尾部增删,不需要随机查询,LinkedList 尾部增删 O (1),性能更高; - 场景 3:用
ArrayList原因:读多写少,需要随机分页查询,ArrayList 随机访问 O (1),性能最高。
【考点对应】模块二:List 集合选型、CopyOnWriteArrayList 适用场景
5. 以下 HashMap 初始化代码有什么问题?正确写法是什么?
java
// 要存10个商品,初始化容量为10
Map<Long, Goods> goodsMap = new HashMap<>(10);
【标准答案】问题:HashMap 的扩容阈值 = 容量 × 负载因子(默认 0.75),容量 10 的阈值是 7,存入第 8 个元素就会触发扩容,造成不必要的性能开销。正确写法:初始容量 = 预期元素数 / 负载因子 + 1,存 10 个元素的话,初始容量设为 16(10 / 0.75 + 1 ≈ 14,向上取 2 的 n 次方为 16)。
【考点对应】模块四:HashMap 初始容量优化
6. 怎么用 LinkedHashMap 实现校园二手平台的商品 LRU(最近最少使用)缓存?要求缓存最多存 100 个商品,超过容量自动删除最久未访问的商品。
【标准答案】LinkedHashMap 天然支持 LRU,核心实现:
- 开启
accessOrder = true,访问元素后会把元素移到链表尾部; - 重写
removeEldestEntry()方法,当元素个数超过容量时,删除链表头部的最久未访问元素。代码实现:
java
public class GoodsLruCache extends LinkedHashMap<Long, Goods> {
private static final int MAX_CAPACITY = 100;
// 第三个参数accessOrder设为true,开启访问顺序
public GoodsLruCache() {
super(MAX_CAPACITY, 0.75f, true);
}
// 重写删除策略,超过容量删除最久未访问的元素
@Override
protected boolean removeEldestEntry(Map.Entry<Long, Goods> eldest) {
return size() > MAX_CAPACITY;
}
}
【考点对应】模块四:LinkedHashMap 特性、LRU 缓存实现
三、进阶原理题(4 道,中大厂实习拔高题)
1. HashMap 的容量为什么必须是 2 的 n 次方?
【标准答案】核心原因有两个:
- 性能优化 :计算数组下标的时候,用
(n - 1) & hash位运算代替hash % n取模运算,位运算的性能远高于取模运算,只有当 n 是 2 的 n 次方时,(n-1) & hash才等价于取模; - 哈希分布均匀:2 的 n 次方时,n-1 的二进制全是 1,哈希值的每一位都能参与下标计算,减少哈希冲突,让元素均匀分布在数组中。
【考点对应】模块四:HashMap 底层设计原理
2. JDK8 的 ConcurrentHashMap 为什么用 synchronized 代替 JDK7 的 ReentrantLock?
【标准答案】核心原因有三个:
- 性能优化:JDK6 之后 synchronized 做了大量优化(偏向锁、轻量级锁、锁升级),无竞争场景下性能比 ReentrantLock 更高;
- 锁粒度更细:JDK8 锁的是单个链表头节点,锁竞争的概率极低,不需要 ReentrantLock 的 Condition、中断等高级功能,synchronized 足够;
- 节省内存:ReentrantLock 需要基于 AQS 实现,每个锁对象都要维护大量额外信息,内存开销远大于 synchronized。
【考点对应】模块四:ConcurrentHashMap 锁优化
3. HashMap 的链表转红黑树的阈值为什么是 8?
【标准答案】基于泊松分布的概率统计:
- 理想哈希算法下,链表长度达到 8 的概率是
0.00000006(千万分之六),几乎不可能出现,用 8 作为阈值可以避免频繁的链表和红黑树转换,平衡性能开销; - 红黑树的查询性能是 O (logn),链表是 O (n),链表长度小于 8 时,遍历性能和红黑树差距不大,不需要树化;
- 树转链表的阈值是 6,留 2 个差值,避免频繁的树化和退化。
【考点对应】模块四:HashMap 红黑树设计原理
4. JDK7 的 HashMap 多线程扩容为什么会出现死循环?
【标准答案】核心原因是 JDK7 用头插法 + 单链表,多线程扩容时会导致链表形成环:
- 线程 1 和线程 2 同时触发扩容,都拿到了原链表的头节点;
- 线程 1 先完成扩容,用头插法把链表顺序反转了;
- 线程 2 继续执行扩容,按照原顺序遍历节点,头插法会把节点的 next 指针指向已经反转的节点,最终形成环形链表;
- 后续 get 元素遍历环形链表时,会无限循环,CPU 占满 100%。JDK8 用尾插法,保持链表的原有顺序,解决了死循环问题。
【考点对应】模块四:HashMap JDK7 死链问题
实习面试答题加分技巧(绑定你的校园二手平台项目)
- 所有集合题都绑定项目选型 :
- 问 ConcurrentHashMap:主动说「我做校园二手平台的接口限流、用户在线状态缓存,就是用的 ConcurrentHashMap,多线程并发读写,线程安全性能高,之前踩过 HashMap 多线程数据丢失的坑」;
- 问 ArrayList 优化:主动说「我做商品列表批量查询的时候,会根据分页大小指定 ArrayList 的初始容量,避免频繁扩容,性能提升很明显」;
- 主动说踩坑经验:比如问遍历删除,就说「我之前做商品下架功能的时候,用 foreach 删除元素踩过并发修改异常的坑,后来改成 Iterator 的 remove () 方法解决了」;
- 答题逻辑固定为「结论→原理→我的项目实践」,比单纯背知识点的候选人认可度高很多。