java面试中经常会问到的集合问题有哪些(基础版)
文章目录
- 一、集合框架整体架构
- 二、List 接口
- 三、Set 接口
- 四、Map 接口
- 五、集合共性问题
- 总结
Java 集合框架是面试中的基础且高频考点,问题主要围绕 核心接口、实现类特性、底层数据结构、线程安全、性能对比 等展开。以下是常见问题及解析:
一、集合框架整体架构
- Java 集合框架的整体结构是什么?主要接口有哪些?
- 集合框架分为 Collection(存储单元素)和 Map(存储键值对)两大体系:
- Collection:
List
(有序、可重复):ArrayList
、LinkedList
、Vector
。Set
(无序、不可重复):HashSet
、LinkedHashSet
、TreeSet
。Queue
(队列,FIFO):LinkedList
(实现了Queue
)、PriorityQueue
。- Map(键值对映射):
HashMap
、LinkedHashMap
、TreeMap
、Hashtable
、ConcurrentHashMap
。- 核心接口关系:
Collection
和Map
是顶层接口,无继承关系;List
、Set
继承Collection
;Queue
继承Collection
并扩展队列操作。
- Collection 和 Collections 的区别?
Collection
:接口,定义了集合的基本操作(如add()
、remove()
、iterator()
),是List
、Set
等的父接口。Collections
:工具类,提供静态方法操作集合(如排序sort()
、线程安全包装synchronizedList()
、求最大值max()
)。
二、List 接口
-
ArrayList 和 LinkedList 的区别?
维度 ArrayList LinkedList 底层结构 动态数组(连续内存) 双向链表(非连续内存) 随机访问 快( get(index)
时间 O(1))慢(需遍历链表,O(n)) 增删操作 中间增删慢(需移动元素,O(n)) 中间增删快(只需修改指针,O(1)) 内存占用 少(仅存元素) 多(需存储前后指针) 适用场景 读多写少、随机访问频繁 写多(中间增删)、队列/栈操作 -
ArrayList 的扩容机制?
- 初始容量:默认 10(JDK 1.8 后延迟初始化,首次
add
时才分配容量)。- 扩容触发:当元素数量(
size
)超过当前容量时,触发扩容。- 扩容规则:
- 新容量 = 旧容量 × 1.5(如 10 → 15 → 22…)。
- 若扩容后仍不足(如添加大量元素),直接扩容到所需容量。
- 扩容过程:创建新数组,复制旧数组元素(
Arrays.copyOf()
),耗时且消耗内存,建议初始化时指定容量(如new ArrayList(1000)
)。
- Vector 和 ArrayList 的区别?
- 线程安全:
Vector
所有方法加synchronized
(线程安全),ArrayList
非线程安全(性能更高)。- 扩容机制:
Vector
默认扩容为原容量的 2 倍(可指定增长因子),ArrayList
为 1.5 倍。- 替代方案:多线程场景用
CopyOnWriteArrayList
(比Vector
高效),单线程用ArrayList
。
- CopyOnWriteArrayList 的原理?适合什么场景?
- 原理:写时复制,修改元素时复制一份新数组,修改完成后替换旧数组,读操作无锁(直接读旧数组)。
- 优点:读操作高效(无锁),线程安全。
- 缺点:内存占用高(双数组),写操作耗时(复制数组),不保证实时性(读可能看到旧数据)。
- 适用场景:读多写少(如配置缓存、黑名单)。
三、Set 接口
-
HashSet、LinkedHashSet、TreeSet 的区别?
特性 HashSet LinkedHashSet TreeSet 底层结构 哈希表(HashMap 实现) 哈希表 + 双向链表 红黑树(自平衡二叉查找树) 有序性 无序 插入顺序(链表维护) 自然排序或定制排序( Comparator
)去重依据 hashCode()
+equals()
同 HashSet compareTo()
或compare()
性能 读写 O(1) 读写 O(1)(略低于 HashSet) 读写 O(logn) -
HashSet 如何保证元素不重复?
- HashSet 底层依赖
HashMap
实现(元素存在 HashMap 的key
中,value
为固定对象)。- 去重逻辑:添加元素时,先计算
hashCode()
找到哈希桶,再通过equals()
比较桶内元素,若两者都相同则视为重复,拒绝添加。- 关键:自定义对象需重写
hashCode()
和equals()
,且保证“相等的对象必须有相等的哈希码”。
- TreeSet 如何实现排序?
- 两种排序方式:
- 自然排序:元素实现
Comparable
接口,重写compareTo()
方法(如Integer
、String
默认排序)。- 定制排序:创建
TreeSet
时传入Comparator
接口实现类(如new TreeSet<>(Comparator.reverseOrder())
)。- 底层红黑树通过比较结果(
compareTo()
或compare()
的返回值)维护有序性,返回 0 时视为重复元素。
四、Map 接口
-
HashMap 和 Hashtable 的区别?
特性 HashMap Hashtable 线程安全 非线程安全 线程安全(方法加 synchronized
)存储 null 允许 key 和 value 为 null(key 仅一个 null) 不允许 key 或 value 为 null 底层结构 JDK 1.8 前:数组 + 链表;1.8 后:数组 + 链表/红黑树 数组 + 链表 扩容机制 初始容量 16,扩容为 2 倍,负载因子 0.75 初始容量 11,扩容为 2 倍 + 1,负载因子 0.75 迭代器 快速失败( fail-fast
)安全失败( fail-safe
) -
HashMap 的底层数据结构?JDK 1.8 有哪些优化?
- JDK 1.7:数组(哈希桶) + 链表(解决哈希冲突)。
- JDK 1.8:数组 + 链表/红黑树(当链表长度 ≥ 8 且数组容量 ≥ 64 时,链表转为红黑树;当长度 ≤ 6 时,红黑树退化为链表)。
- 优化点:
- 红黑树提升查询性能(链表 O(n) → 红黑树 O(logn))。
- 取消
Entry
类,改用Node
类。- 扩容时计算新索引的方式简化(
e.hash & (newCap - 1)
替代重新计算哈希)。
- HashMap 的扩容机制?
- 触发条件:当元素数量(
size
)> 容量 × 负载因子(默认 16 × 0.75 = 12)时,触发扩容。- 扩容过程:
- 新容量 = 旧容量 × 2(保证容量为 2 的幂,便于哈希计算)。
- 创建新数组,将旧数组元素重新哈希到新数组(JDK 1.8 优化:通过高位哈希值判断索引是否需要 + 旧容量,避免重新计算哈希)。
- 红黑树在扩容时可能拆分为两个链表或红黑树。
- LinkedHashMap 如何保证插入顺序或访问顺序?
- 底层:哈希表 + 双向链表(继承 HashMap,在
Node
中增加before
和after
指针)。- 顺序控制:
- 插入顺序:默认模式,链表按元素插入顺序排列。
- 访问顺序:构造时指定
accessOrder = true
,每次get()
或put()
操作后,将元素移到链表尾部(最近访问的元素在末尾)。- 应用:实现 LRU 缓存(通过重写
removeEldestEntry()
方法,移除最久未访问元素)。
- TreeMap 的底层结构和排序方式?
- 底层:红黑树(根据 key 排序,保证
keySet()
遍历有序)。- 排序方式:同 TreeSet,支持自然排序(
Comparable
)和定制排序(Comparator
)。- 适用场景:需要按 key 排序的映射(如排行榜、区间查询)。
- ConcurrentHashMap 和 Hashtable 的区别?JDK 1.7 和 1.8 的 ConcurrentHashMap 实现有何不同?
与 Hashtable 的区别:
- 线程安全实现:
ConcurrentHashMap
用分段锁(1.7)或 CAS + synchronized(1.8),支持并发读写;Hashtable
用全局锁,性能低。- 扩展性:
ConcurrentHashMap
并发度更高(支持多线程同时操作不同段/桶)。JDK 1.7 vs 1.8 实现:
- 1.7:分段锁(
Segment
数组,每个Segment
是一个小 HashMap,锁只锁定单个Segment
)。- 1.8:取消
Segment
,直接用数组 + 链表/红黑树,通过 CAS 操作和synchronized
锁定链表头/红黑树根节点,粒度更细,性能更好。
五、集合共性问题
- 什么是快速失败(fail-fast)和安全失败(fail-safe)?
- fail-fast:迭代器创建时记录集合的
modCount
(修改次数),迭代过程中若集合被修改(如add
/remove
),modCount
变化,迭代器抛出ConcurrentModificationException
(如ArrayList
、HashMap
的迭代器)。- fail-safe:迭代器遍历的是集合的副本,修改原集合不影响迭代,无异常(如
CopyOnWriteArrayList
、ConcurrentHashMap
),但可能读取到旧数据,且内存开销大。
- 如何实现集合的线程安全?
- 方案 1:使用线程安全集合(
Vector
、Hashtable
、ConcurrentHashMap
、CopyOnWriteArrayList
)。- 方案 2:用
Collections.synchronizedXxx()
包装非线程安全集合(如Collections.synchronizedList(new ArrayList<>())
),底层通过包装类加锁实现。- 方案 3:手动加锁(
synchronized
或Lock
),控制集合的并发访问。
- ArrayList 和 HashMap 的初始化容量和负载因子?
ArrayList
:默认初始容量 10,无负载因子(扩容仅依赖元素数量)。HashMap
:默认初始容量 16,负载因子 0.75(容量 × 负载因子 = 扩容阈值)。- 负载因子作用:平衡空间和时间效率,0.75 表示允许 75% 的哈希桶被使用,减少哈希冲突。
- 为什么 HashMap 的容量必须是 2 的幂?
- 为了通过 位运算 高效计算元素在数组中的索引:
index = hash & (capacity - 1)
。- 当容量是 2 的幂时,
capacity - 1
的二进制为全 1(如 16-1=15 → 1111),hash & (capacity - 1)
等价于hash % capacity
,但位运算比取模更快。
总结
集合面试重点考察 底层数据结构(数组/链表/红黑树)、核心实现类的特性对比(如 ArrayList vs LinkedList)、线程安全方案 及 性能优化点(如 HashMap 扩容、红黑树转换)。回答时需结合具体场景(如读多写少选 ArrayList,并发场景选 ConcurrentHashMap),体现对集合设计思想的理解。