Java集合(四):迭代器与最佳实践
Java集合(四):迭代器与最佳实践
导语:本篇收尾:迭代器与 fail-fast / fail-safe 机制、辅助工具类与最佳实践,以及 RandomAccess、红黑树等高频补充,共 16 题。
一、迭代器、fail-fast 与 fail-safe
1. 什么是 Iterator?如何使用?有什么特点?
答: Iterator 是集合的统一遍历接口,提供 hasNext()、next()、remove()。特点:
- 遍历与集合实现解耦;
- 通过
fail-fast机制(见下)检测并发修改; - 边遍历边删除必须用
iterator.remove(),不能用集合自身的remove()。
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
if ("x".equals(s)) it.remove(); // 正确
}2. 什么是 fail-fast(快速失败)?哪些集合是?
答: fail-fast 指:在迭代过程中,若集合发生了结构性修改(增/删元素)且不是通过该迭代器自身操作,则立刻抛出 ConcurrentModificationException。原理:集合维护 modCount(修改次数),迭代器持有一个 expectedModCount,每次 next() 时比对,不一致即抛异常。ArrayList、HashMap、HashSet 等普通集合的迭代器都是 fail-fast。
这是在"单线程误改"或"多线程并发改"时的善意告警,而非并发安全的保证。
3. 什么是 fail-safe(安全失败)?哪些集合是?
答: fail-safe 指:迭代在集合的副本/快照上进行,或修改被弱一致性隔离,因此不会抛 ConcurrentModificationException。CopyOnWriteArrayList、ConcurrentHashMap 的迭代器属于此类(弱一致)。代价:可能读不到遍历期间的最新修改,且副本有额外内存开销。
4. Iterator 和 ListIterator 的区别?
答:
Iterator:所有Collection可用,只能单向遍历,只能remove;ListIterator:仅List可用,支持双向遍历(hasPrevious/previous),可在遍历中add/set,可获取索引。
5. 如何边遍历边删除 Collection 中的元素?
答: 必须用迭代器的 remove():
// 正确
for (Iterator<String> it = list.iterator(); it.hasNext();) {
if (cond) it.remove();
}
// 错误(for-each 中 list.remove 会抛 ConcurrentModificationException)
for (String s : list) { if (cond) list.remove(s); }Java 8+ 也可用 list.removeIf(cond) 更简洁。
6. 遍历 List 有哪些方式?最佳实践是什么?
答: ① 普通 for + 下标(ArrayList 快,LinkedList 慢 O(n²));② 增强 for-each(基于 Iterator,最通用);③ Iterator 显式(需要边遍历边删除时);④ list.forEach/Stream(Java 8+)。最佳实践:随机访问集合用 for-each 或下标皆可;LinkedList 避免下标遍历;需要删除用 Iterator 或 removeIf。
7. 遍历 Map 的最佳实践?
答: 优先用 entrySet() 遍历 KV(只遍历一次),避免 keySet() 遍历两次(先取 key 再 get 查 value)。Java 8+ 用 map.forEach((k,v) -> {...}) 更简洁:
for (Map.Entry<String,Integer> e : map.entrySet()) {
String k = e.getKey(); Integer v = e.getValue();
}二、辅助工具类与最佳实践
8. Comparable 和 Comparator 的区别?
答:
Comparable(内部比较器):类自身实现compareTo(T o),定义"自然排序",强制耦合在类里,只有一个排序规则。Comparator(外部比较器):独立类/lambda 实现compare(T o1, T o2),作为"策略"传入sort、TreeSet构造器等,可定义多个不同排序规则,更灵活。
// Comparator 示例(按年龄倒序)
Collections.sort(list, (a,b) -> b.getAge() - a.getAge());9. TreeMap/TreeSet 对 key 有什么要求?
答: key 必须可比较:要么 key 类实现 Comparable,要么在构造 TreeMap/TreeSet 时传入 Comparator;否则运行时抛 ClassCastException。且 compare 返回 0 被视为"相同 key"(去重),所以要保证与 equals 语义一致。
10. 如何创建一个不可修改的集合?
答: 两种方式:
- 旧:
Collections.unmodifiableList/Set/Map(...)(返回视图,原集合仍可改,且试图修改会抛UnsupportedOperationException); - Java 9+:
List.of(...)/Set.of(...)/Map.of(...)直接创建真正不可变的集合(更推荐,且of创建的集合不允许 null)。
11. Queue 和 BlockingQueue 了解吗?
答: Queue 是队列接口,常用 LinkedList、ArrayDeque、PriorityQueue(优先队列,堆实现)。BlockingQueue 位于 java.util.concurrent,在 Queue 基础上支持阻塞的 put/take(队列满/空时阻塞线程),是生产者-消费者模型的核心,实现有 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue 等。
12. HashMap 排序如何实现?(经典上机题)
答: HashMap 本身无序,需借助 LinkedHashMap(保序)配合 Collections.sort:
entrySet()转List;Collections.sort(list, comparator)按 value/key 排序;- 依次
put进LinkedHashMap,返回之(保留排序结果)。
List<Map.Entry<Integer,User>> l = new ArrayList<>(map.entrySet());
l.sort((a,b) -> b.getValue().getAge() - a.getValue().getAge());
Map<Integer,User> sorted = new LinkedHashMap<>();
l.forEach(e -> sorted.put(e.getKey(), e.getValue()));13. 高并发下有哪些线程安全的集合可选?
答:
ConcurrentHashMap:高并发 Map;CopyOnWriteArrayList/CopyOnWriteArraySet:读多写少场景;ConcurrentSkipListMap/ConcurrentSkipListSet:并发有序;BlockingQueue系列:并发队列;Collections.synchronizedXXX:粗粒度同步,性能一般,作备选。
经验:读远大于写 →
CopyOnWriteArrayList;高并发 KV →ConcurrentHashMap;并发有序 →ConcurrentSkipListMap;阻塞生产消费 →BlockingQueue。
三、其他高频补充
14. 什么是 RandomAccess 接口?为什么 ArrayList 实现了它而 LinkedList 没有?
答: RandomAccess 是 java.util 包下的一个标记接口(空接口),没有任何方法,仅作为"支持快速(随机)访问"的语义标识。
ArrayList底层是数组,按索引get(i)是 O(1),实现了RandomAccess;LinkedList底层是双向链表,必须从头/尾遍历到指定位置,随机访问是 O(n),没有实现RandomAccess。
Collections.binarySearch 等算法会据此选择不同实现(indexedBinarySearch 或 iteratorBinarySearch):对实现了 RandomAccess 或规模小的列表走下标访问,否则走迭代器。
关键点:
RandomAccess只是标识,ArrayList 并不是"因为实现了它才快",而是因为它底层数组天然支持 O(1) 随机访问,才去实现该接口。遍历LinkedList用下标for循环会导致 O(n²),应避免。
15. ConcurrentHashMap 能保证复合操作的原子性吗?如何保证?
答: 不能保证所有复合操作的原子性。 复合操作指由多个基本操作(put/get/containsKey/remove)组合而成的操作,例如"若 key 不存在则插入":
if (!map.containsKey(key)) {
map.put(key, value); // 这两步之间可能被其他线程插入,导致覆盖
}多线程下 containsKey 与 put 之间可被其他线程打断,产生竞态。
保证原子性的方式:使用 ConcurrentHashMap 提供的原子复合方法:
putIfAbsent(key, value):不存在才放入;compute/computeIfAbsent/computeIfPresent:根据 key 计算并原子更新 value;merge(key, value, remapping):合并旧值与新值。
map.putIfAbsent(key, value); // 等价于上面的复合逻辑且线程安全结论:
ConcurrentHashMap保证单个基本操作的线程安全,但多步复合逻辑仍需使用上述原子方法或加锁,不能误以为"用了它就万事大吉"。
16. 什么是红黑树?它有哪些性质?
答: 红黑树是一种自平衡的二叉查找树,JDK 1.8 的 HashMap(链表过长时树化)、TreeMap、TreeSet 均基于红黑树实现,能在近似 O(log n) 时间完成插入/删除/查找。
它满足以下性质:
- 每个节点非红即黑;
- 根节点为黑色;
- 叶子节点(NIL 哨兵节点)为黑色;
- 红色节点的两个子节点必须为黑色(即不存在连续的红色节点);
- 从任一节点到其所有后代 NIL 节点的路径上,包含相同数量的黑色节点(黑高一致)。
这些约束保证最长路径不超过最短路径的 2 倍,从而近似平衡。当 HashMap 链表长度 ≥ 8 且数组容量 ≥ 64 时转为红黑树,把查询从 O(n) 降到 O(log n)。
红黑树的插入/删除通过变色 + 旋转(左旋/右旋)维持平衡,逻辑较复杂,理解"为何能保证 O(log n)"比死记旋转步骤更重要。
