一、先按需求选接口
| 需求 | 选择 |
|---|---|
| 有序、可重复、随机访问 | ArrayList |
| 频繁头部插入/删除、当队列用 | ArrayDeque(不是 LinkedList) |
| 去重 | HashSet / LinkedHashSet(保序)/ TreeSet(排序) |
| 键值查询 | HashMap / LinkedHashMap / TreeMap |
| 线程安全 | ConcurrentHashMap、CopyOnWriteArrayList |
面向接口编程:变量声明用 List<String> list = new ArrayList<>();,方便替换实现。
二、ArrayList 与 LinkedList
常见误解是「LinkedList 插入删除快」。真实情况:
ArrayList随机访问 O(1),尾部追加摊还 O(1),中间插入 O(n)(要移动元素)。LinkedList随机访问 O(n)(要遍历),头尾插入 O(1),中间插入需要先找到位置,也是 O(n)。
而且 LinkedList 每个节点有额外对象开销,缓存局部性差。绝大多数场景 ArrayList 更快。需要队列时用 ArrayDeque,它的头尾操作都是 O(1) 且没有额外节点开销。
三、HashMap 的容量与扩容
Map<String, Integer> map = new HashMap<>(16); // 指定初始容量
Map<String, Integer> map2 = new HashMap<>(16, 0.75f); // 容量 + 负载因子
- 默认初始容量 16,负载因子 0.75,元素数超过
容量 × 0.75就扩容为两倍。 - 扩容要重新计算哈希并迁移所有元素,代价大。已知数据量时预设容量能避免多次扩容。
预设容量的算法是「期望元素数 / 0.75 + 1」,再取最近的 2 的幂。比如要放 100 个元素,初始容量给 128 或 256。
Java 8 之后,单个桶的链表长度超过 8 且容量 ≥ 64 时会转成红黑树,避免哈希碰撞导致 O(n) 查找。
四、遍历时删除元素
// 错误:抛 ConcurrentModificationException
for (String s : list) {
if (s.startsWith("a")) list.remove(s);
}
原因:for-each 底层用 Iterator,list.remove 修改了 modCount,迭代器下次 next() 检查到不一致就抛异常。
三种正确写法:
// 1. 显式使用 Iterator(推荐,兼容性最好)
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if (it.next().startsWith("a")) it.remove();
}
// 2. removeIf(Java 8+,最简洁)
list.removeIf(s -> s.startsWith("a"));
// 3. 倒序索引删除
for (int i = list.size() - 1; i >= 0; i--) {
if (list.get(i).startsWith("a")) list.remove(i);
}
Map 同理,用 map.entrySet().iterator() 或 map.entrySet().removeIf(...)。
五、ConcurrentHashMap 不是万能的
它保证单个操作的原子性,但「先检查再写入」的组合操作仍不是原子的:
// 不安全:两个线程可能都通过判断
if (!map.containsKey(k)) map.put(k, init());
// 安全:原子操作
map.computeIfAbsent(k, key -> init());
map.merge(k, 1, Integer::sum);
另外 ConcurrentHashMap 不允许 null 键和 null 值,这与 HashMap 不同,迁移时要注意。
六、几个易错点
equals与hashCode必须同时重写:自定义对象当HashMap的键时,只重写equals会导致查找不到。Arrays.asList返回定长列表:add/remove会抛UnsupportedOperationException。List.of()不可变:Java 9+ 的工厂方法返回不可变集合,要可变得new ArrayList<>(List.of(...))。subList是视图:修改子列表会影响原列表,且原列表结构变化后子列表操作会抛异常。TreeMap的键必须可比较:否则抛ClassCastException,或构造时传Comparator。
七、性能习惯
- 循环里避免
list.size()重复调用(虽然 JIT 通常能优化,但写法上养成习惯)。 - 大量字符串拼接用
StringBuilder。 - 遍历
Map用entrySet(),不要keySet()再get(key)(多一次查找)。 - 集合很大时注意内存,必要时用
int[]或专用库(如 fastutil)。
小结
默认用 ArrayList 和 HashMap;队列用 ArrayDeque;已知容量就预设,避免扩容;遍历删除用 Iterator.remove 或 removeIf;ConcurrentHashMap 的组合操作用 computeIfAbsent。