Java 集合
Java 集合框架围绕两大接口展开:List(有序可重复)和 Map(键值映射)。选型的核心依据是线程安全需求和操作模式(读多写少 vs 频繁增删)。
速查结论
- 非并发场景:
HashMap+ArrayList - 并发场景:
ConcurrentHashMap+CopyOnWriteArrayList - 已过时:
Hashtable、Vector、Stack
集合框架选型决策树:
HashMap vs Hashtable
HashMap 是首选,Hashtable 已过时。核心差异在于 null 支持和锁粒度。
| 特性 | HashMap | Hashtable |
|---|---|---|
| 线程安全 | ❌ 非同步 | ✅ synchronized |
| 性能 | 更高 | 较低(全表锁) |
| null 支持 | 允许 null 键和值 | ❌ 不允许 |
| 迭代顺序 | 无序 | 无序 |
| 推荐度 | ✅ 首选 | ❌ 已过时 |
// 非多线程
Map<String, Object> map = new HashMap<>();
// 多线程(推荐)
Map<String, Object> map = new ConcurrentHashMap<>();LinkedHashMap
HashMap 的子类 LinkedHashMap 可以保持插入顺序,适合需要有序遍历的场景。
ArrayList vs LinkedList
ArrayList 在绝大多数场景优于 LinkedList——现代 JVM 对数组操作有深度优化,内存占用更少。
| 操作 | ArrayList | LinkedList |
|---|---|---|
get(index) | O(1) | O(n) |
add(E) | 均摊 O(1) | O(1) |
add(index, E) | O(n) | O(1) |
remove(index) | O(n) | O(1) |
Iterator.remove() | O(n) | O(1) |
结论
大多数场景用 ArrayList。LinkedList 仅在频繁中间插入/删除且主要通过迭代器访问时有优势。
Vector 为什么过时
Vector 对每个操作都加 synchronized,但单操作加锁不等于线程安全——遍历时仍可能抛 ConcurrentModificationException。
Vector 的三个问题
- 单操作加锁 ≠ 线程安全(遍历仍需外部同步)
- 不必要的锁开销导致性能差
- 将"可变数组"和"同步"混在一个类里,违反单一职责
替代方案:
// 线程安全 List
List<String> list = Collections.synchronizedList(new ArrayList<>());
// 或更好的选择
List<String> list = new CopyOnWriteArrayList<>();
// Stack 替代
Deque<Integer> stack = new ArrayDeque<>();StringBuilder vs StringBuffer
StringBuffer 每个方法都加 synchronized,StringBuilder 没有。单线程场景下 StringBuilder 性能约为 StringBuffer 的 3 倍。
| 情况 | 推荐 |
|---|---|
| 单线程 | StringBuilder |
| 多线程 | StringBuffer 或手动同步 |
| Java 5+ 简单拼接 | 直接用 +(编译器优化为 StringBuilder) |
// 循环中拼接必须手动用 StringBuilder
StringBuilder sb = new StringBuilder();
for (String s : list) {
sb.append(s);
}ConcurrentHashMap 核心规则
禁用 null 键值
map.put(null, 1); // 抛 NPE
map.put("key", null); // 抛 NPE注意
为什么禁止 null ConcurrentHashMap 为高并发设计,null 值导致无法区分「键不存在」和「值为 null」,与并发语义冲突。
复合操作非原子性
ConcurrentHashMap 保证单操作(put/get/remove)线程安全,但不保证复合操作的原子性。
// ❌ 非原子!两个线程可能同时通过检查
if (!map.containsKey(key)) {
map.put(key, value);
}
// ✅ 使用原子方法
map.putIfAbsent(key, value);
map.computeIfAbsent(key, k -> calculateValue());| 原子方法 | 说明 |
|---|---|
putIfAbsent(key, value) | 仅当 key 不存在时插入 |
compute(key, remappingFunction) | 根据旧值计算新值 |
computeIfAbsent(key, mappingFunction) | key 不存在时计算并插入 |
computeIfPresent(key, remappingFunction) | key 存在时计算并更新 |
merge(key, value, remappingFunction) | 合并操作 |
集合转数组
使用 toArray(T[] array) 传入长度为 0 的空数组:
List<String> list = Arrays.asList("a", "b", "c");
// ❌ 返回 Object[]
Object[] arr = list.toArray();
// ✅ 类型安全,JDK 优化后性能更佳
String[] arr = list.toArray(new String[0]);为什么传长度 0 的数组
JDK 内部优化后,new String[0] 反而比 new String[size] 更快——因为 JVM 可以直接分配正确大小的数组,避免多余拷贝。
Map 按值排序
::: code-group
Map<String, Double> sorted = map.entrySet().stream()
.sorted(Map.Entry.<String, Double>comparingByValue().reversed())
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(e1, e2) -> e1,
LinkedHashMap::new
));List<Map.Entry<String, Double>> entries =
new LinkedList<>(map.entrySet());
Collections.sort(entries, (a, b) -> b.getValue().compareTo(a.getValue()));
Map<String, Double> result = new LinkedHashMap<>();
for (Map.Entry<String, Double> entry : entries) {
result.put(entry.getKey(), entry.getValue());
}:::
扩展阅读:集合的陷阱和注意事项详见 集合使用注意事项。