Java 开发面试题(集合框架模块)
问题 1:请你梳理一下 Java 集合框架的整体结构,说说核心接口和主要实现类的关系?
考察点
- 对 Java 集合框架体系的全局认知,是否清晰核心划分维度(
Collection与Map)。 - 核心接口(
Collection、List、Set、Queue、Map)的继承 / 实现关系,以及各接口的核心特性(如是否有序、是否可重复)。 - 能否区分 “接口定义规范” 与 “实现类落地逻辑”,避免混淆概念。
参考答案
Java 集合框架主要分为 Collection 和 Map 两大核心体系,二者无直接继承关系,共同作用于数据存储与操作,具体结构如下:
-
Collection体系:存储 “单个元素的集合”,定义了元素的添加、删除、遍历等基础操作,核心子接口及实现类如下:List接口:特性是有序(元素插入顺序可保留)、可重复,支持通过索引(index)访问元素。- 实现类 1:
ArrayList:底层基于动态数组实现,查询效率高(get(index)时间复杂度O(1)),增删(尤其是中间位置)效率低(需数组拷贝,O(n))。 - 实现类 2:
LinkedList:底层基于双向链表实现,增删效率高(中间位置操作O(1),仅需修改指针),查询效率低(需遍历链表,O(n)),同时实现Deque接口可作为队列 / 栈使用。 - 实现类 3:
Vector:线程安全版ArrayList,底层也是数组,但方法加了synchronized锁,效率低,现在基本被CopyOnWriteArrayList替代。
- 实现类 1:
Set接口:特性是无序(元素插入顺序不保留,Hash 实现类)、不可重复,不支持索引访问。- 实现类 1:
HashSet:底层依赖HashMap(用HashMap的key存储元素,value存空对象),基于哈希表实现,判断重复依赖equals()和hashCode()方法。 - 实现类 2:
TreeSet:底层依赖TreeMap(key存储元素),基于红黑树实现,支持元素排序(自然排序或自定义Comparator),判断重复基于排序结果(而非hashCode)。
- 实现类 1:
Queue接口:特性是先进先出(FIFO) ,专注于 “队列” 数据结构的操作(如offer()入队、poll()出队)。- 实现类:
ArrayDeque(基于循环数组,效率高于LinkedList)、PriorityQueue(基于优先级堆,元素按优先级出队,非 FIFO)。
- 实现类:
-
Map体系:存储 “键值对(key-value)” 集合,key唯一(不可重复),value可重复,核心实现类如下:HashMap:底层基于数组 + 链表 + 红黑树(JDK1.8 及以后),key无序,允许key为null(仅 1 个)、value为null(多个),非线程安全。HashTable:线程安全版HashMap,方法加synchronized锁,key和value均不允许为null,底层无红黑树优化,效率低。TreeMap:底层基于红黑树,key按自然排序或自定义Comparator排序,key不允许为null。ConcurrentHashMap:线程安全且高效的HashMap替代类,JDK1.8 用 “CAS+synchronized节点锁” 实现,锁粒度细,并发效率高。
指导
- 回答时避免只罗列类名,需先讲 “
Collection与Map的核心区别(单元素 vs 键值对)”,再分体系展开,体现 “从整体到局部” 的逻辑。 - 易混淆点:
List的 “有序” 是 “插入顺序可追溯”,而非 “排序后的顺序”;Set的 “无序” 仅针对HashSet,TreeSet是有序(排序后的顺序),需特别说明。
问题 2:ArrayList 和 LinkedList 的核心区别有哪些?分别适用于什么场景?
考察点
- 对两种 List 实现类底层数据结构的理解(数组 vs 链表)。
- 基于底层结构推导的时间复杂度差异(查询、增删操作)。
- 内存占用特点及适用场景的匹配能力,避免 “只知区别,不懂应用”。
参考答案
ArrayList 和 LinkedList 的核心区别源于底层数据结构,具体差异可从 5 个维度对比:
| 对比维度 | ArrayList | LinkedList |
|---|---|---|
| 底层数据结构 | 动态数组(transient Object[] elementData) | 双向链表(每个节点含prev、next指针) |
查询效率(get(index)) | O(1)(直接通过索引定位数组元素) | O(n)(需从表头 / 表尾遍历到目标索引) |
| 增删效率 | 1. 末尾增删:O(1)(无数组拷贝);2. 中间增删:O(n)(需拷贝数组,移动元素) | 1. 任意位置增删(已知节点):O(1)(仅改指针);2. 未知节点(需定位):O(n)(遍历定位) |
| 内存占用 | 存在 “扩容冗余”:数组容量超过实际元素个数时,冗余空间会浪费内存 | 每个节点需存储prev/next指针,单个元素内存开销比 ArrayList 大 |
| 线程安全性 | 非线程安全(并发修改会抛ConcurrentModificationException) | 非线程安全(同上) |
适用场景:
- ArrayList:适合查询操作频繁、增删主要在末尾的场景,例如 “商品列表展示”(用户频繁浏览,很少在中间插入 / 删除商品)。
- LinkedList:适合增删操作频繁(尤其是中间位置)、查询少的场景,例如 “消息队列”(频繁在队列头部出队、尾部入队)、“链表式任务调度”(中间插入 / 删除任务)。
指导
- 回答时需 “先讲底层原因,再讲表现差异”,例如:“ArrayList 查询快是因为底层是数组,索引可直接定位;LinkedList 增删快是因为链表只需改指针,无需移动元素”,避免只说 “查询快” 而不解释根源。
- 易踩坑点:LinkedList 的 “增删快” 是有前提的 ——已知目标节点(如通过
iterator遍历到节点),若需先通过索引定位节点(如get(5)),则定位过程已耗时O(n),整体效率可能不如 ArrayList,需特别提醒。
问题 3:JDK1.8 之后,HashMap 的底层实现原理是什么?哈希冲突是如何解决的?
考察点
- 对 HashMap 底层结构演进的掌握(JDK1.7 vs JDK1.8 的区别)。
- 哈希冲突的解决机制(链表 + 红黑树),以及 “树化”“退化” 的触发条件。
- 扩容机制(初始容量、负载因子、扩容逻辑)的细节,尤其是 JDK1.8 的优化(高位运算计算新索引)。
参考答案
JDK1.8 对 HashMap 做了重大优化,底层结构从 “数组 + 链表” 升级为数组 + 链表 + 红黑树,核心原理如下:
1. 底层核心结构
- 数组(
Node[] table):称为 “哈希桶数组”,每个元素是Node对象(存储key、value、hash、next指针),数组的索引由key的hash值计算得出(index = (table.length - 1) & hash),确保index在数组范围内。 - 链表:当多个
key计算出相同的index(即 “哈希冲突”)时,这些Node会以链表形式存储(next指针串联)。 - 红黑树:当链表长度 >=8,且数组长度 >=64时,链表会转化为红黑树(
TreeNode继承Node),目的是将查询时间复杂度从链表的O(n)降为红黑树的O(logn);当红黑树节点数<=6** 时,会退化回链表(红黑树维护成本高于链表)。
2. 哈希冲突解决机制
HashMap 通过 “链地址法” 解决哈希冲突(即冲突的Node组成链表 / 红黑树),为减少冲突,做了两层优化:
-
- 哈希值计算优化:
key的hashCode()返回int值(32 位),HashMap 会对其做 “高位扰动”:hash = (h = key.hashCode()) ^ (h >>> 16),将高位 16 位与低位 16 位异或,让高位信息参与index计算,减少哈希冲突概率。
- 哈希值计算优化:
-
- 索引计算优化:
index = (table.length - 1) & hash,由于table.length始终是 2 的幂(初始 16,扩容后翻倍),table.length - 1的二进制是 “全 1”(如 16-1=15→1111),此时&运算等价于 “取模”,但效率更高。
- 索引计算优化:
3. 扩容机制(核心参数与逻辑)
- 核心参数:
- 初始容量:默认 16(
DEFAULT_INITIAL_CAPACITY),可通过构造函数指定(需是 2 的幂,否则会自动调整为最近的 2 的幂)。 - 负载因子:默认 0.75(
DEFAULT_LOAD_FACTOR),用于判断何时扩容(当前元素个数size > 数组长度table.length * 负载因子时触发扩容)。
- 初始容量:默认 16(
- 扩容逻辑:
- 扩容后新数组长度为原长度的 2 倍(
newCap = oldCap << 1)。 - 遍历原数组的每个
Node,重新计算index(JDK1.8 优化:无需重新计算hash,只需判断hash的 “新增高位 bit”—— 若为 0,index不变;若为 1,index = 原index + 旧数组长度),减少计算开销。 - 将原
Node迁移到新数组(链表 / 红黑树结构保留),原数组废弃,完成扩容。
- 扩容后新数组长度为原长度的 2 倍(
指导
- 易混淆点 1:JDK1.7 与 1.8 的区别 ——1.7 是 “数组 + 链表”,头插法(并发扩容可能导致死循环);1.8 是 “数组 + 链表 + 红黑树”,尾插法(避免死循环),且树化需满足 “链表长度 >=8 且数组长度 >=64”(缺一不可,若数组长度 < 64,只会先扩容,不会树化)。
- 易踩坑点:负载因子并非越小越好 —— 负载因子小(如 0.5),冲突少但数组冗余多;负载因子大(如 1.0),内存利用率高但冲突多,默认 0.75 是 “冲突与内存” 的平衡,一般不建议修改。
问题 4:HashMap、HashTable、ConcurrentHashMap 三者的核心区别是什么?为什么现在很少用 HashTable 了?
考察点
- 对 “线程安全性” 的理解(不同实现方式的效率差异)。
- 三者在
key/valuenull 值支持、底层结构、扩容机制上的差异。 - 能否结合实际开发场景,解释 “HashTable 被淘汰” 的原因,体现技术选型思维。
参考答案
三者均属于Map体系,但核心差异集中在线程安全性、null 值支持、底层优化三个维度,具体对比如下:
| 对比维度 | HashMap | HashTable | ConcurrentHashMap(JDK1.8) |
|---|---|---|---|
| 线程安全性 | 非线程安全(并发修改会抛ConcurrentModificationException) | 线程安全(方法加synchronized锁,全局锁) | 线程安全(CAS+synchronized节点锁,细粒度锁) |
| key/value null 支持 | 允许key为 null(仅 1 个,因key唯一)、value为 null(多个) | 不允许key和value为 null(会抛NullPointerException) | 不允许key为 null、value为 null(会抛NPE) |
| 底层结构(JDK1.8) | 数组 + 链表 + 红黑树 | 数组 + 链表(无红黑树优化) | 数组 + 链表 + 红黑树 |
| 初始容量与扩容 | 初始容量 16,扩容为 2 倍(oldCap << 1) | 初始容量 11,扩容为 2 倍 + 1(oldCap * 2 + 1) | 初始容量 16,扩容为 2 倍 |
| 效率 | 高(无锁) | 低(全局synchronized,并发时锁竞争严重) | 高(锁粒度仅为冲突的 Node 节点,并发度高) |
为什么很少用 HashTable?核心原因是其 “线程安全实现方式低效”,具体有两点:
- 全局锁导致并发性能差:HashTable 的所有方法(如
put()、get())都加了synchronized锁,且锁对象是this(即 HashTable 实例本身)—— 意味着多个线程操作 HashTable 的任意方法(哪怕是不同key)都会竞争同一把锁,并发时会严重阻塞,效率远低于 ConcurrentHashMap。 - 功能缺陷与优化缺失:
- 不支持
key/value为 null,灵活性低于 HashMap。 - 底层无红黑树优化,链表过长时查询效率低(
O(n))。 - 扩容机制不合理(初始 11,扩容为 2n+1),导致
index计算时 “取模” 效率低(非 2 的幂,无法用&运算替代取模)。
- 不支持
综上,HashTable 的 “线程安全” 是 “重量级且低效” 的,而 ConcurrentHashMap 既能保证线程安全,又通过细粒度锁和红黑树优化实现了高并发效率,因此 HashTable 基本被 ConcurrentHashMap 替代。
指导
- 回答时需重点突出 “线程安全实现方式” 的差异 —— 这是三者最核心的区别,也是 HashTable 被淘汰的关键。
- 易混淆点:ConcurrentHashMap 不允许
key/value为 null,这点与 HashMap 不同,需特别注意(面试中常被问到 “ConcurrentHashMap 能否存 null”,答案是不能,会抛 NPE)。
问题 5:JDK1.8 的 ConcurrentHashMap 是如何保证线程安全的?和 JDK1.7 的实现有什么区别?
考察点
- 对 ConcurrentHashMap 核心优化(锁粒度降级)的理解。
- JDK1.7(分段锁)与 JDK1.8(CAS + 节点锁)的实现差异,以及优化的原因。
- 对 “CAS”(无锁编程)和 “synchronized 节点锁” 的工作原理的掌握。
参考答案
ConcurrentHashMap 的核心目标是 “在保证线程安全的同时,提升并发效率”,JDK1.7 和 1.8 通过不同的锁机制实现这一目标,具体如下:
一、JDK1.7 ConcurrentHashMap 的实现(分段锁机制)
- 底层结构:
Segment[] + HashEntry[] + 链表——Segment是一个继承ReentrantLock的内部类,每个Segment对应一个 “子哈希表”(HashEntry[]),相当于将整个 ConcurrentHashMap 拆分为多个 “小 HashMap”。 - 线程安全机制:通过 “分段锁(Segment 锁) ” 实现线程安全 —— 每个
Segment是一把独立的锁,线程操作某个key时,只需获取该key所在Segment的锁,无需竞争全局锁。- 例如:线程 A 操作
key1(属于Segment1),线程 B 操作key2(属于Segment2),二者可同时执行,互不阻塞,并发度等于Segment的数量(默认 16)。
- 例如:线程 A 操作
- 缺陷:
- 锁粒度仍不够细:若多个线程操作同一
Segment下的不同key,仍会竞争同一把Segment锁,并发效率受限。 - 结构复杂:
Segment的存在增加了底层结构的复杂度,且扩容时需对每个Segment单独扩容,逻辑繁琐。
- 锁粒度仍不够细:若多个线程操作同一
二、JDK1.8 ConcurrentHashMap 的实现(CAS+Synchronized 节点锁)
JDK1.8 摒弃了分段锁,采用 “CAS 无锁操作 + Synchronized 节点锁” 的组合,锁粒度从 “Segment” 降级到 “Node 节点”,并发效率大幅提升,具体实现如下:
-
底层结构简化:与 HashMap1.8 一致,为 “
Node[]数组 + 链表 + 红黑树”,移除了Segment,结构更简洁。 -
核心线程安全机制:
-
- CAS 无锁操作(用于数组初始化与节点插入):
- 数组初始化:通过
CAS操作保证Node[] table仅被初始化一次(避免多线程重复初始化)。 - 链表头节点插入:当
table[index]为null时,通过CAS尝试将新Node设置为table[index],若 CAS 成功则插入完成;若失败(说明有其他线程已插入节点),则转用synchronized锁。
-
- Synchronized 节点锁(用于链表 / 红黑树操作):
- 当
table[index]已存在节点(发生哈希冲突)时,对该节点(链表头节点或红黑树根节点)加synchronized锁,确保同一时间只有一个线程操作该链表 / 红黑树(如插入、删除节点)。 - 锁粒度仅为 “冲突的 Node 节点”,不同
index的节点操作完全不互斥,并发度理论上等于数组长度,远高于 JDK1.7。
-
-
其他线程安全保障:
- 使用
volatile修饰Node[] table和Node的val、next字段,保证多线程间的内存可见性(修改后立即刷新到主内存,读取时从主内存加载)。 - 红黑树的修改(如旋转、变色)也通过
synchronized锁保护,避免并发修改导致的结构破坏。
- 使用
三、JDK1.7 与 1.8 的核心区别
| 对比维度 | JDK1.7 ConcurrentHashMap | JDK1.8 ConcurrentHashMap |
|---|---|---|
| 锁机制 | 分段锁(Segment 锁,ReentrantLock 实现) | CAS+Synchronized 节点锁 |
| 锁粒度 | 粗粒度(Segment 级别) | 细粒度(Node 节点级别) |
| 底层结构 | Segment [] + HashEntry [] + 链表 | Node [] 数组 + 链表 + 红黑树 |
| 并发度 | 固定(等于 Segment 数量,默认 16) | 动态(理论上等于数组长度) |
| 扩容复杂度 | 高(需对每个 Segment 单独扩容) | 低(与 HashMap 扩容逻辑类似) |
指导
- 回答时需先讲 JDK1.7 的分段锁,再讲 JDK1.8 的优化方向(锁粒度降级),最后对比区别,体现 “演进思维”。
- 易混淆点:JDK1.8 的
synchronized锁并非 “全局锁”,而是 “锁定当前冲突的 Node 节点”,需明确说明锁的范围;CAS 的作用是 “无锁初始化 / 插入头节点”,减少锁竞争,而非替代synchronized。
更多推荐


所有评论(0)