3 Java集合框架(List、Set、Map)
问题背景
Java集合框架是面试中最基础、最高频的知识块之一。面试官通常从“ArrayList与LinkedList区别”切入,考察对数据结构、线程安全、扩容机制的理解,并延伸到HashMap原理、红黑树转换、ConcurrentHashMap分段锁等。本章梳理核心接口与实现类的选型对比、底层原理、实战陷阱,帮助你在面试中建立体系化认知。
核心概念
- Collection:集合根接口,提供
add()、remove()、iterator()等通用方法。 - List:有序可重复,支持索引访问。典型实现:
ArrayList(数组)、LinkedList(双向链表)、Vector(线程安全,已很少用)。 - Set:无序不可重复,基于
equals()和hashCode()去重。典型实现:HashSet(哈希表)、TreeSet(红黑树)、LinkedHashSet(哈希表+链表保序)。 - Map:键值对,键唯一,值可重复。典型实现:
HashMap(数组+链表/红黑树)、TreeMap(红黑树)、ConcurrentHashMap(分段锁/CAS)。
原理机制
3.1 ArrayList 扩容机制
- 底层
Object[] elementData,默认初始容量 10(懒加载,第一次 add 时创建)。 - 扩容因子 1.5 倍(
oldCapacity + (oldCapacity >> 1)),使用Arrays.copyOf()拷贝数组。 - 如果一次性 addAll 大量元素,会直接扩容至所需最小容量。
面试必问: ArrayList 的
ensureCapacity() 可提前触发扩容,减少后续 add 时的复制开销。
3.2 HashMap 数据结构与哈希冲突
- JDK 8 中:数组 + 链表 + 红黑树。默认初始容量 16,负载因子 0.75。
- 链表长度 ≥ 8 且数组长度 ≥ 64 时,链表转红黑树;树节点数 < 6 时退化为链表。
- 哈希冲突解决:先用
key.hashCode()扰动(高16位异或),再取模((n-1) & hash)。
// 扰动函数 (JDK 8)
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
3.3 线程安全方案对比
| 方案 | 原理 | 适用场景 |
|---|---|---|
Hashtable | 全表 synchronized | 遗留系统,不推荐 |
Collections.synchronizedMap() | 对象锁包装 | 低并发,简单封装 |
ConcurrentHashMap (JDK 8) | CAS + synchronized + 红黑树 | 高并发首选 |
流程步骤:集合遍历最佳实践
- List 遍历:普通 for 循环(随机访问快)或 Iterator(删除安全)。
- Set 遍历:只能用
forEach或 Iterator,不能按索引。 - Map 遍历:用
entrySet()同时获取 key 和 value,避免重复查get(key)。
技巧: 使用
Iterator.remove() 删除元素,不要用 list.remove() 在 for-each 中操作,会引发 ConcurrentModificationException。
案例说明
案例1: 使用 HashMap 缓存用户信息,key= userId,value=User对象。注意重写 equals() 和 hashCode(),否则重复 key 无法正常覆盖。
public class User {
private Long id;
private String name;
// 必须重写 equals 和 hashCode(使用 id 字段)
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(id, user.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
案例2: 需要保持插入顺序的 Set → 使用 LinkedHashSet(维护双向链表);需要按自然排序的 Set → 使用 TreeSet。
表格总结:List、Set、Map 核心实现对比
| 接口 | 实现类 | 底层结构 | 有序性 | 线程安全 | 适用场景 |
|---|---|---|---|---|---|
| List | ArrayList | Object[] | 是 | 否 | 随机访问、尾部频繁插入 |
| List | LinkedList | 双向链表 | 是 | 否 | 头部/中间频繁增删 |
| Set | HashSet | HashMap(key) | 否 | 否 | 快速去重,无顺序要求 |
| Set | TreeSet | 红黑树 | 按比较器排序 | 否 | 需要自动排序 |
| Map | HashMap | 数组+链表/红黑树 | 否 | 否 | 通用键值存储 |
| Map | ConcurrentHashMap | 分段数组+CAS | 否 | 是 | 高并发无锁读 |
| Map | TreeMap | 红黑树 | 按键排序 | 否 | 需要有序遍历 |
误区提醒
常见错误:
- 误以为
Set就是无序——LinkedHashSet维护插入顺序,TreeSet按排序规则有序。 - 在
HashMap中修改key对象(可变字段)导致哈希值改变,造成无法通过原 key 找到 entry。 - 混淆
Vector和ArrayList—— Vector 的同步方法有性能开销,现在已基本被CopyOnWriteArrayList替代。 iterator()遍历时删除元素必须使用iterator.remove(),否则触发快速失败机制。
实践清单
- ☐ 能用 ArrayList 时不用 LinkedList(尤其随机访问场景)。
- ☐ 自定义对象放入 HashSet/HashMap 前,重写
equals()和hashCode()。 - ☐ 多线程环境使用
ConcurrentHashMap而不是Hashtable或synchronizedMap。 - ☐ 初始化时预估集合大小,调用
new ArrayList<>(expectedSize)或new HashMap<>(expectedSize/0.75f+1)避免频繁扩容。 - ☐ 使用
Collections.unmodifiableList()等返回只读视图,防止集合被意外修改。