Java Containers
Java 容器:别只盯着接口,看实现
Java 的 java.util 集合框架(JCF)是每个 Java 程序员的必修课。但如果你只是机械地记忆 List、Set、Map,那你根本不懂容器。容器的本质是数据结构在内存中的组织方式。
1. 核心架构:从混乱到有序
Java 集合框架主要分为两大支系:
- Collection:存储单值元素。
- Map:存储键值对(Key-Value)。
在 Java 21 之前,Java 的有序集合接口设计得一塌糊涂。你想访问第一个元素,List 用 get(0),Deque 用 getFirst(),而 SortedSet 却要用 first()。Java 21 引入了 Sequenced Collections,终于把这套烂摊子统一了。

- SequencedCollection:提供了
addFirst、addLast、getFirst、getLast、reversed等统一方法。 - SequencedMap:同理,支持有序的键值对操作。
2. List:连续内存才是王道
ArrayList (默认首选)
- 本质:动态数组。
- 优势:CPU 缓存友好。由于元素在内存中是连续存储的,预取机制能极大地提高遍历性能。随机访问是 O(1)。
- 代价:中间插入/删除需要移动数组,O(n)。
- 建议:除非你有极其特殊的理由,否则永远使用
ArrayList。
LinkedList (谨慎使用)
- 本质:双向链表。
- 缺陷:内存碎片化。每个节点都是一个独立的对象,遍历时 CPU 缓存命中率极低。
- 误区:很多人认为
LinkedList插入快。实际上,除非你已经拿到了指向某个节点的指针,否则你得先花 O(n) 的时间找到它。 - 唯一用途:作为
Queue或Deque的实现,或者在极高频的头部/尾部插入场景。
Vector (垃圾堆里的东西)
- 评价:过时的设计。每个方法都加了
synchronized,性能低下。如果你需要线程安全,去java.util.concurrent找,别碰这个。
3. Set:去重的本质是 Map
Set 的大多数实现其实就是 Map 的包装(把 Value 设为一个 Dummy Object)。
- HashSet:底层是
HashMap。无序,查找 O(1)。 - LinkedHashSet:维护了插入顺序的
HashSet。 - TreeSet:底层是红黑树。有序,查找 O(log n)。
- ConcurrentSkipListSet:并发场景下的有序集合,基于跳表(Skip List)实现。
4. Map:性能的核心
HashMap
- 实现:数组 + 链表 + 红黑树(JDK 8+)。
- 优化:当链表长度超过 8 且数组容量超过 64 时,链表转为红黑树。这是为了防止恶意 Hash 碰撞导致的 O(n) 拒绝服务攻击。
- 性能:理想情况下 O(1)。
Map 的原子操作 (Java 8+)
别再写 if (map.get(key) == null) put(...) 这种蠢代码了,这涉及两次哈希定位。
compute 家族方法让一切原子化,单次哈希定位完成操作:
| 方法 | 适用场景 | 键不存在 | 键存在 | 返回值 |
|---|---|---|---|---|
computeIfAbsent | 缓存/初始化 | 计算并插入 | 不操作 | 关联的值(新计算或已存在) |
computeIfPresent | 更新已有值 | 不操作 | 计算并替换 | 新值(若键不存在返回 null) |
compute | 强制计算/删除 | 计算并插入 | 计算并替换 | 新值(若返回 null 则删除键) |
merge | 合并/累加 | 用默认值 | 计算并替换 | 合并后的值 |
// 1. computeIfAbsent:缓存加载,键不存在时才计算
// 返回值:关联的值(新计算或已存在),支持链式调用
map.computeIfAbsent(key, k -> new ArrayList<>()).add(value);
// 等价于:
// if (!map.containsKey(key)) {
// map.put(key, new ArrayList<>());
// }
// map.get(key).add(value);
// 但 computeIfAbsent 只做一次哈希定位,性能更好
// 2. computeIfPresent:更新已存在的值
// 返回值:新值(若键不存在返回 null)
map.computeIfPresent(key, (k, v) -> v + 1);
// 3. compute:强制计算(也可用于删除,返回 null 则删除键)
// 返回值:新值(若返回 null 则删除该键)
map.compute(key, (k, v) -> v == null ? initialValue : transform(v));
// 4. merge:优雅的累加器
// 返回值:合并后的值
map.merge(key, 1, Integer::sum);
// 批量操作示例:合并多个 map
otherMap.forEach((k, v) -> map.merge(k, v, (a, b) -> a + b));关键优势:这些方法都是原子性的,在 ConcurrentHashMap 中尤其重要,避免了 check-then-act 的竞态条件。
// 优雅的计数器实现
map.merge(key, 1, Integer::sum);5. 并发容器:别再用全局锁了
如果你还在用 Collections.synchronizedMap(),那你还在石器时代。它用一把全局锁把整个容器锁住,并发性能极差。
ConcurrentHashMap (工业级杰作)
- 原理:JDK 8 放弃了分段锁,改用 CAS + synchronized 桶锁。
- 优势:锁的粒度细化到了每个哈希桶。读操作完全无锁(利用
volatile),写操作只锁住受影响的桶。
CopyOnWriteArrayList
- 原理:写时复制。修改时拷贝整个数组。
- 场景:读极多、写极少(如配置信息)。如果写操作频繁,内存开销和 GC 压力会让你怀疑人生。
BlockingQueue
- 用途:生产者-消费者模式的基石。
- 实现:
ArrayBlockingQueue(有界)、LinkedBlockingQueue(可选有界)、PriorityBlockingQueue(优先级)。
跳表 (Skip List) vs 红黑树
在 ConcurrentSkipListMap 中使用跳表而不是红黑树,是因为跳表在并发修改时只需要局部调整指针,比红黑树的全局重平衡更容易实现无锁/低锁化。
6. 总结:如何选择?
- 需要索引访问? ->
ArrayList - 需要去重? ->
HashSet - 需要排序? ->
TreeSet或TreeMap - 需要键值对? ->
HashMap - 多线程环境?
- 读写都频繁 ->
ConcurrentHashMap - 读多写极少 ->
CopyOnWriteArrayList - 任务传递 ->
ArrayBlockingQueue
- 读写都频繁 ->
- Java 21+ 需要首尾操作? -> 认准
Sequenced接口。
记住:代码的品味在于选择最简单、最有效的工具,而不是最复杂的。



