Java 容器:别只盯着接口,看实现

Java 的 java.util 集合框架(JCF)是每个 Java 程序员的必修课。但如果你只是机械地记忆 ListSetMap,那你根本不懂容器。容器的本质是数据结构在内存中的组织方式

1. 核心架构:从混乱到有序

Java 集合框架主要分为两大支系:

  • Collection:存储单值元素。
  • Map:存储键值对(Key-Value)。

在 Java 21 之前,Java 的有序集合接口设计得一塌糊涂。你想访问第一个元素,Listget(0)DequegetFirst(),而 SortedSet 却要用 first()。Java 21 引入了 Sequenced Collections,终于把这套烂摊子统一了。

  • SequencedCollection:提供了 addFirstaddLastgetFirstgetLastreversed 等统一方法。
  • SequencedMap:同理,支持有序的键值对操作。

2. List:连续内存才是王道

ArrayList (默认首选)

  • 本质:动态数组。
  • 优势CPU 缓存友好。由于元素在内存中是连续存储的,预取机制能极大地提高遍历性能。随机访问是 O(1)。
  • 代价:中间插入/删除需要移动数组,O(n)。
  • 建议:除非你有极其特殊的理由,否则永远使用 ArrayList

LinkedList (谨慎使用)

  • 本质:双向链表。
  • 缺陷内存碎片化。每个节点都是一个独立的对象,遍历时 CPU 缓存命中率极低。
  • 误区:很多人认为 LinkedList 插入快。实际上,除非你已经拿到了指向某个节点的指针,否则你得先花 O(n) 的时间找到它。
  • 唯一用途:作为 QueueDeque 的实现,或者在极高频的头部/尾部插入场景。

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. 总结:如何选择?

  1. 需要索引访问? -> ArrayList
  2. 需要去重? -> HashSet
  3. 需要排序? -> TreeSetTreeMap
  4. 需要键值对? -> HashMap
  5. 多线程环境?
    • 读写都频繁 -> ConcurrentHashMap
    • 读多写极少 -> CopyOnWriteArrayList
    • 任务传递 -> ArrayBlockingQueue
  6. Java 21+ 需要首尾操作? -> 认准 Sequenced 接口。

记住:代码的品味在于选择最简单、最有效的工具,而不是最复杂的。