四、集合框架(J31–J44)
J31 · 知识点:集合体系
【题目】下列哪个类实现了 List 接口?
A. HashSet B. ArrayList C. HashMap D. TreeSet
答案:B
【考点】集合三大体系(List/Set/Map)的继承结构。
【结论】选 B。ArrayList 直接实现 List 接口,而 HashSet、TreeSet 实现 Set,HashMap 实现 Map。
【逐项辨析】
- A 错:HashSet 实现 Set 接口,不实现 List。
- B 正确:ArrayList 是 List 接口最常用的实现类之一,底层为 Object[] 数组。
- C 错:HashMap 实现 Map 接口,存储键值对,与 List 体系无关。
- D 错:TreeSet 实现 Set 接口,基于红黑树实现有序去重,不实现 List。
【知识点】 Java 集合框架分为 Collection 和 Map 两大顶层接口。Collection 下又分 List、Set、Queue 三个子接口。List 体系的核心特征是有序、可重复、支持索引访问。常见实现类包括:ArrayList(数组,查询快)、LinkedList(双向链表,增删快)、Vector(线程安全数组,已过时)。Set 体系的核心特征是无序(或按序)、不可重复,常见实现类有 HashSet(HashMap 支撑)、TreeSet(红黑树)、LinkedHashSet(链表+哈希)。Map 体系存储键值对,常见实现有 HashMap、TreeMap、Hashtable、ConcurrentHashMap。List 与 Set 均继承 Collection,Map 独立于 Collection 体系之外。
【记忆锚点】 「List 有序可重复,Set 无序不重复,Map 存键值对;ArrayList 是 List 的儿子,HashSet 是 Set 的亲戚」。
【易混对比】
| 接口 | 有序性 | 可重复性 | 典型实现 | 底层结构 |
|---|---|---|---|---|
| List | 有序 | 可重复 | ArrayList、LinkedList、Vector | 数组 / 链表 |
| Set | 无序 / 有序 | 不可重复 | HashSet、TreeSet、LinkedHashSet | 哈希表 / 红黑树 / 链表+哈希 |
| Map | 键无序 / 有序 | 键不可重复 | HashMap、TreeMap、ConcurrentHashMap | 数组+链表+红黑树 / 红黑树 |
换问法:若题目问「哪个类实现了 Map 接口?」则应选 HashMap。
【自测】 以下哪个类同时与 List 和 Set 无任何实现关系? A. ArrayList B. LinkedHashSet C. HashMap D. Vector
答:C。HashMap 仅实现 Map 接口,与 List、Set 均无继承或实现关系。与第 J31 题连考。
【知识关联】
- 同库关联:与 J32–J44 整个集合题群;与 J71(泛型)、J10(数组)对照。
- 实现层:Collection 接口下 List/Set/Queue;Map 不继承 Collection。迭代器模式统一遍历。
- 面试追问:① List/Set/Queue/Map 如何选型?② fail-fast 是什么(J39)?
【拓展延伸】
- 变式问法:哪些是接口哪些是实现;同步包装类位置。
- 版本差异:JDK 8 增加 Stream;JDK 9 List.of/Set.of 不可变工厂。
- 工程注意点:优先接口编程;不可变集合优先;并发场景用 JUC 集合(J42)。
J32 · 知识点:ArrayList 扩容
【题目】关于 ArrayList 的说法,正确的是?
A. 底层是链表结构 B. 默认初始容量 10,每次扩容为原容量的 1.5 倍 C. 每次扩容为原容量的 2 倍 D. 线程安全
答案:B
【考点】ArrayList 底层数组与扩容机制(新容量 = 旧容量 + 旧容量 >> 1)。
【结论】选 B。ArrayList 底层是 Object[] 数组,无参构造首次 add 扩容为 10,后续扩容为原容量的 1.5 倍。
【逐项辨析】
- A错:ArrayList 底层是数组,不是链表;LinkedList 才是链表。
- B正确:默认初始容量 10(首次 add 时从空数组扩容),后续扩容公式 newCapacity = oldCapacity + (oldCapacity >> 1),即 1.5 倍。
- C错:2 倍扩容是 Vector 的默认策略,ArrayList 是 1.5 倍。
- D错:ArrayList 无任何同步机制,非线程安全。
【知识点】 ArrayList 底层基于 Object[] elementData 存储元素。无参构造器在 JDK8 中初始化为空数组 {},第一次调用 add() 时才扩容为默认容量 10;带参构造器 new ArrayList(int initialCapacity) 直接分配指定长度数组。扩容时先计算新容量:oldCapacity + (oldCapacity >> 1),即原容量的 1.5 倍(向下取整),若仍不足则取所需最小容量,最大不超过 Integer.MAX_VALUE - 8。扩容操作涉及创建新数组和 System.arraycopy 拷贝,时间复杂度 O(n),因此预估数据量时应通过构造器指定初始容量,避免频繁扩容。线程安全替代方案:Collections.synchronizedList、CopyOnWriteArrayList。
【记忆锚点】 「ArrayList 是数组,默认扩容一点五;首次 add 才变十,构造指定省开销」。
【易混对比】
| 特性 | ArrayList | Vector |
|---|---|---|
| 底层 | Object[] | Object[] |
| 线程安全 | 否 | 是(synchronized) |
| 默认容量 | 10(首次 add) | 10 |
| 扩容倍数 | 1.5 倍 | 2 倍 |
| 性能 | 高(无锁) | 低(加锁) |
| 推荐使用 | 是 | 否(已过时) |
换问法:若已知需存储 1000 个元素,如何创建 ArrayList 最优?——new ArrayList<>(1000),避免扩容开销。
【自测】new ArrayList<>() 执行后,其内部 elementData 数组长度是多少?
答:0。无参构造初始为空数组,第一次 add 时才扩容为 10。与第 J32 题连考。
【知识关联】
- 同库关联:与 J33(链表)、J39(迭代删除)、J31 体系结构。
- 实现层:默认容量 10,扩容为原来的 1.5 倍(old + old>>1);elementData 数组;modCount 记录结构修改。
- 面试追问:① 如何减少扩容?② 删除中间元素复杂度?
【拓展延伸】
- 变式问法:初始容量/扩容因子;add 与 get 复杂度。
- 版本差异:实现细节各版本小优化,1.5 倍扩容长期稳定。
- 工程注意点:预设容量;随机读多用 ArrayList;频繁头插删考虑 LinkedList 或 ArrayDeque。
J33 · 知识点:LinkedList 特点
【题目】与 ArrayList 相比,LinkedList 的优势是? A. 随机访问更快 B. 头部和中间位置的插入删除更快(双向链表,无需移动元素) C. 占用内存更少 D. 线程安全
答案:B
【考点】数组 vs 链表的复杂度对比。
【结论】选 B。LinkedList 基于双向链表,头尾及中间插入删除只需修改指针,无需搬移元素。
【逐项辨析】
- A错:LinkedList 随机访问需要从头或尾遍历,时间复杂度 O(n);ArrayList 基于数组,按下标寻址 O(1)。
- B正确:LinkedList 底层是双向链表,头部/尾部增删 O(1),中间增删只需修改前后节点指针,无需像数组那样移动大量元素。
- C错:链表每个节点额外存储前驱、后继两个指针引用,内存开销比数组更大。
- D错:LinkedList 非线程安全。
【知识点】 LinkedList 底层实现为双向链表(Node<E> 包含 item、next、prev),同时实现了 List 和 Deque 接口,可作为队列(FIFO)或栈(LIFO)使用。其优势在于插入和删除操作的时间复杂度:头尾操作 O(1),中间操作 O(n) 但常数项极小(仅修改指针)。劣势在于随机访问必须顺序遍历,时间复杂度 O(n)。ArrayList 则相反:随机访问 O(1),中间插入删除 O(n)(需移动后续元素)。选择原则:读多写少、频繁随机访问选 ArrayList;频繁增删、需要队列语义选 LinkedList。两者均为非线程安全。
【记忆锚点】 「链表增删改指针,数组查询按下标;读多用 ArrayList,删多用 LinkedList」。
【易混对比】
| 操作 | ArrayList | LinkedList |
|---|---|---|
| 随机访问 get(i) | O(1) | O(n) |
| 尾部添加 add(E) | O(1) 均摊 | O(1) |
| 头部添加 add(0,E) | O(n) | O(1) |
| 中间插入 add(i,E) | O(n) | O(n),遍历+改指针 |
| 中间删除 remove(i) | O(n) | O(n),遍历+改指针 |
| 内存占用 | 较少(仅数据+少量空闲) | 较多(每个节点两个指针) |
换问法:需要频繁在头部插入元素,应选哪个?——LinkedList。
【自测】 以下代码中,哪种操作在 LinkedList 上效率最高? A. get(500) B. add(0, "A") C. remove(500) D. set(500, "B")
答:B。
add(0, "A")是头部插入,LinkedList 只需修改头指针,O(1);其余均需遍历到指定位置,O(n)。与第 J33 题连考。
【知识关联】
- 同库关联:与 J32、J44(Iterator)、Deque 相关;与 J32 对比时间复杂度表。
- 实现层:双向链表节点 Node;无随机访问索引加速。
- 面试追问:① 为何很少用 LinkedList 做队列(ArrayDeque 更优)?② 插入复杂度?
【拓展延伸】
- 变式问法:get(0) vs remove(0) 复杂度。
- 版本差异:语义稳定。
- 工程注意点:缓存不友好,现代场景多用 ArrayList/ArrayDeque;并发队列用 ConcurrentLinkedQueue。
J34 · 知识点:HashMap 底层结构
【题目】关于 JDK8 中 HashMap 的说法,错误的是?
A. 底层结构是数组 + 链表 + 红黑树 B. 默认初始容量 16,加载因子 0.75 C. 链表长度超过 8 且数组长度达到 64 时,链表转为红黑树 D. 线程安全
答案:D
【考点】HashMap 底层结构、默认参数、树化条件、线程安全性。
【结论】选 D。JDK8 HashMap 基于数组+链表+红黑树实现,但始终非线程安全。
【逐项辨析】
- A 说法正确(非答案):JDK8 HashMap 确实是数组 + 链表 + 红黑树的三合一结构。
- B 说法正确(非答案):默认初始容量 16,加载因子 0.75,扩容阈值 12。
- C 说法正确(非答案):链表长度超过 8 且数组长度达到 64 时,链表转为红黑树,查询从 O(n) 降为 O(log n)。
- D 正确(应选):HashMap 无任何同步机制,多线程并发 put 可能丢数据甚至死循环(JDK7),线程不安全。
【知识点】 JDK8 HashMap 底层为 Node<K,V>[] 数组,每个数组位置称为桶(bucket)。哈希冲突时采用链表法:同一桶内用链表存储多个节点。当链表长度超过 TREEIFY_THRESHOLD(8)且数组长度达到 MIN_TREEIFY_CAPACITY(64)时,链表转为红黑树,将最坏查询复杂度从 O(n) 优化为 O(log n)。若数组长度不足 64,则优先扩容而非树化。默认参数:初始容量 16,加载因子 0.75,扩容阈值为 容量 × 加载因子。扩容时容量翻倍(2 的幂次),所有元素重新散列到新数组。线程安全问题:JDK7 并发扩容时头插法可能导致环形链表死循环;JDK8 改为尾插法避免死循环,但并发 put 仍可能覆盖数据。并发场景应使用 ConcurrentHashMap 或 Collections.synchronizedMap。
【记忆锚点】 「HashMap 数组加链表,超过八条变红黑;默认十六因子零点七五,线程不安全要记牢」。
【易混对比】
| 特性 | HashMap | Hashtable | ConcurrentHashMap |
|---|---|---|---|
| 线程安全 | 否 | 是(synchronized) | 是(CAS + 桶锁) |
| null 键 / 值 | 允许 | 不允许 | 不允许 |
| 底层结构 | 数组+链表+红黑树 | 数组+链表 | 数组+链表+红黑树 |
| 扩容 | 2 倍 | 2 倍 + 1 | 渐进式扩容 |
| 性能 | 高(无锁) | 低(全局锁) | 高(细粒度锁) |
换问法:ConcurrentHashMap 是否允许 null 键?——不允许,无法区分「键不存在」与「键值为 null」。
【自测】 JDK8 HashMap 中,链表转为红黑树需要同时满足哪两个条件?
答:① 链表长度 > 8;② 数组长度 ≥ 64。若数组长度不足 64,优先扩容而非树化。与第 J34 题连考。
【知识关联】
- 同库关联:与 J35(插入演进)、J36(null)、J37(HashSet 基于 HashMap)、J22(key 的 hashCode/equals)核心连考。
- 实现层:数组+链表;JDK8 链表长度≥8 且容量≥64 转红黑树;扰动函数
(h = key.hashCode()) ^ (h >>> 16)。 - 面试追问:① 为何容量是 2 的幂?② 树化阈值为何是 8?
【拓展延伸】
- 变式问法:负载因子 0.75 含义;扩容死循环(JDK7 头插并发)历史问题。
- 版本差异:JDK7 数组+链表头插;JDK8 尾插+树化。
- 工程注意点:预估大小减少 resize;key 保持不可变;并发用 ConcurrentHashMap(J42)。
J35 · 知识点:HashMap 插入方式演进
【题目】关于 HashMap 链表插入方式,下列说法正确的是? A. 一直采用尾插法 B. 一直采用头插法 C. JDK8 之前采用头插法,JDK8 之后改为尾插法 D. 随机插入
答案:C
【考点】JDK 版本演进中的插入方式变化与并发安全隐患。
【结论】选 C。JDK7 及以前 HashMap 采用头插法,JDK8 起改为尾插法,以消除并发扩容死循环隐患。
【逐项辨析】
- C正确:JDK7 头插法,JDK8 尾插法。
- B错:并非一直头插法,JDK8 已改为尾插法。
- A错:JDK7 之前是头插法,不是一直尾插法。
- D错:HashMap 的插入位置由哈希值和桶内结构决定,不存在随机插入。
【知识点】 HashMap 的链表插入方式在 JDK 版本间发生了重要变更。JDK7 及以前采用头插法:新节点插入到链表头部,作者认为后插入的数据更可能被访问(局部性原理)。但头插法在并发扩容时存在致命缺陷:多个线程同时扩容并迁移节点时,可能将同一链表的节点顺序颠倒,形成环形链表,导致 get 操作死循环(CPU 100% 的经典线上事故)。JDK8 改为尾插法:新节点追加到链表尾部,保持了节点在原链表中的相对顺序,从根本上避免了并发扩容导致的环形链表问题。需注意:尾插法仅消除了死循环,HashMap 本身仍非线程安全,并发 put 仍可能丢失数据,线程安全仍需 ConcurrentHashMap。
【记忆锚点】 「七版头插八版尾,头插并发会成环;尾插顺序不死循,线程安全仍不靠」。
【易混对比】
- 头插法 vs 尾插法的并发差异:头插法在扩容迁移时反转链表顺序,多线程下易形成环;尾插法保持原顺序,不会成环。
- 换问法:JDK8 HashMap 尾插法是否意味着可以安全地多线程使用?——不可以,put 仍可能覆盖数据,只是不会死循环。
【自测】 JDK7 HashMap 并发扩容时,头插法导致死循环的根本原因是什么?
答:多线程同时迁移同一链表,头插法反转节点顺序,使 next 指针形成环形引用,get 遍历无法终止。与第 J35 题连考。
【知识关联】
- 同库关联:与 J34 同题深化;与 J39 并发修改。
- 实现层:JDK7 头插导致并发扩容环形链表;JDK8 尾插降低该风险但仍非线程安全。
- 面试追问:① 头插/尾插对并发的影响?② 现在 HashMap 线程安全吗?
【拓展延伸】
- 变式问法:问 JDK7 与 8 插入位置差异。
- 版本差异:JDK 8 明确改为尾插。
- 工程注意点:多线程环境绝不共享未同步 HashMap;用 ConcurrentHashMap 或 ThreadLocal。
J36 · 知识点:null 键值支持
【题目】关于 HashMap 和 Hashtable 对 null 的支持,正确的是?
A. HashMap 允许 null 键和 null 值,Hashtable 不允许 B. 两者都不允许 C. 两者都允许 null 键和 null 值 D. HashMap 不允许 null,Hashtable 允许
答案:A
【考点】HashMap/Hashtable/ConcurrentHashMap 对 null 的容忍度对比。
【结论】选 A。HashMap 允许一个 null 键和多个 null 值;Hashtable 不允许任何 null 键或 null 值。
【逐项辨析】
- C错:Hashtable 不允许 null 键和 null 值,任一情况都会抛出 NullPointerException。
- B错:HashMap 明确允许 null 键和 null 值。
- A正确:HashMap 允许 null 键(哈希到 0 号桶)和 null 值;Hashtable 的 put 方法直接对 key 和 value 调用 hashCode/equals,null 会抛 NPE。
- D错:完全说反了。
【知识点】 HashMap 对 null 的特殊处理:null 键的哈希值被定义为 0,直接落入数组 0 号桶;null 值作为普通值存储。Hashtable 从设计之初就拒绝 null,其 put 方法源码直接检查 if (value == null) 和 key.hashCode(),任一为 null 都抛 NullPointerException。ConcurrentHashMap 同样不允许 null 键和 null 值,原因在于并发环境下无法区分「键不存在(key 未映射)」与「键存在但值为 null」,这会导致 containsKey/get 等操作产生二义性,违背并发集合的语义一致性。因此三个 Map 的 null 容忍度形成明确梯度:HashMap 最宽松,Hashtable 和 ConcurrentHashMap 均禁止。
【记忆锚点】 「HashMap 容 null,Hashtable 不容;ConcurrentHashMap 也跟着不容」。
【易混对比】
| Map 实现 | null 键 | null 值 | 原因 / 机制 |
|---|---|---|---|
| HashMap | 允许(1 个) | 允许 | null 键哈希为 0,落入 0 号桶 |
| Hashtable | 不允许 | 不允许 | put 直接检查,抛 NullPointerException |
| ConcurrentHashMap | 不允许 | 不允许 | 避免并发下「不存在」与「值为 null」的二义性 |
| TreeMap | 不允许(自然排序时) | 允许 | null 键无法 compareTo |
换问法:ConcurrentHashMap 为什么不允许 null 值?——多线程下无法区分 key 不存在和 key 的值为 null。
【自测】 以下代码哪行会抛异常?
HashMap<String, String> map1 = new HashMap<>();
map1.put(null, null);
Hashtable<String, String> map2 = new Hashtable<>();
map2.put(null, null);答:第 4 行抛 NullPointerException。HashMap 允许 null 键和 null 值;Hashtable 不允许。与第 J36 题连考。
【知识关联】
- 同库关联:与 J34、J37、J38(TreeMap 不允许 null key 若无比较器)对照。
- 实现层:HashMap 允许一个 null key(hash 为 0);Hashtable/ConcurrentHashMap 不允许。
- 面试追问:① 为什么 ConcurrentHashMap 不允许 null?② null key 的 hash?
【拓展延伸】
- 变式问法:哪些 Map 实现允许 null。
- 版本差异:长期稳定。
- 工程注意点:业务上少用 null key;Optional/哨兵对象更清晰。
J37 · 知识点:HashSet 底层
【题目】HashSet 的底层实现是? A. 数组 B. HashMap C. LinkedList D. TreeMap
答案:B
【考点】HashSet 与 HashMap 的组成关系。
【结论】选 B。HashSet 内部封装了一个 HashMap 实例,利用 HashMap 的 key 实现去重。
【逐项辨析】
- A错:HashSet 不是直接基于数组,而是基于 HashMap。
- B正确:HashSet 内部持有 HashMap<E, Object>,元素作为 key 存入,value 是一个统一的静态常量 PRESENT。
- C错:LinkedList 是 List 接口的双向链表实现,本身不是任何 Set 的底层;HashSet 的底层是 HashMap。
- D错:TreeMap 是 TreeSet 的底层,不是 HashSet 的。
【知识点】 HashSet 的源码实现极其简洁:它内部维护一个 HashMap 实例,所有 add 操作转化为 map.put(e, PRESENT),其中 PRESENT 是一个静态 final Object 占位对象。由于 HashMap 的 key 具有唯一性(通过 hashCode + equals 判定),HashSet 天然继承了「去重」能力。同理,LinkedHashSet 继承 HashSet 并基于 LinkedHashMap 实现,保证插入顺序;TreeSet 基于 TreeMap 实现,保证元素有序。这一设计体现了 Java 集合框架的复用思想:Set 的去重语义完全复用 Map 的 key 机制,避免重复实现。
【记忆锚点】 「HashSet 就是 HashMap 的皮,元素当 key,PRESENT 当 value;去重靠 hashCode,等于也要跟上」。
【易混对比】
| Set 实现 | 底层 Map | 有序性 | 去重依据 |
|---|---|---|---|
| HashSet | HashMap | 无序 | hashCode + equals |
| LinkedHashSet | LinkedHashMap | 插入有序 | hashCode + equals |
| TreeSet | TreeMap | 排序有序 | Comparable / Comparator |
换问法:向 HashSet 中添加自定义对象时,为什么必须重写 hashCode 和 equals?——因为底层 HashMap 的 key 去重依赖这两个方法。
【自测】 HashSet 的 add() 方法返回值是什么含义?
答:返回 boolean。若元素已存在(key 重复)则返回 false,新增成功返回 true。底层调用 HashMap.put(),根据返回的旧 value 是否为 null 判断。与第 J37 题连考。
【知识关联】
- 同库关联:与 J34(底层 HashMap)、J31、去重语义。
- 实现层:HashSet 包装 HashMap,元素作 key,value 为共享 PRESENT 哨兵。
- 面试追问:① add 返回值含义?② 与 List 去重性能?
【拓展延伸】
- 变式问法:自定义对象进 HashSet 需重写什么。
- 版本差异:JDK 9 Set.of。
- 工程注意点:可变字段勿在入 Set 后修改;去重大数据用 HashSet。
J38 · 知识点:TreeSet 排序机制
【题目】TreeSet 中存放元素,要求元素? A. 必须是基本类型 B. 必须重写 hashCode 方法 C. 实现 Comparable 接口,或在创建 TreeSet 时传入 Comparator 比较器 D. 必须实现 Cloneable 接口
答案:C
【考点】红黑树集合的自然排序与定制排序。
【结论】选 C。TreeSet 基于红黑树,必须能比较元素大小,因此要求元素实现 Comparable 或传入 Comparator。
【逐项辨析】
- C正确:TreeSet 要求元素具备可比性,要么元素类实现 Comparable(自然排序),要么构造 TreeSet 时传入 Comparator(定制排序)。
- A错:TreeSet 存放的是对象,基本类型会自动装箱为包装类(如 Integer、String),且包装类均已实现 Comparable,并不要求「必须是基本类型」。
- B错:hashCode 是 HashSet/HashMap 的需求,TreeSet 不依赖哈希,依赖比较。
- D错:Cloneable 与排序无关,TreeSet 不要求元素可复制。
【知识点】 TreeSet 底层是 TreeMap,而 TreeMap 基于红黑树(自平衡二叉搜索树)。红黑树要求节点之间可以比较大小,以维持有序性和平衡性。因此 TreeSet/TreeMap 对元素有两类要求:① 自然排序——元素类实现 Comparable<T> 接口,重写 compareTo 方法;② 定制排序——构造集合时传入 Comparator<? super E> 比较器。若两者皆无,首次 add 会抛出 ClassCastException。重要陷阱:compareTo/compare 返回 0 时,TreeSet 视为「相等」而拒绝插入,这与 equals 返回 false 可能冲突,导致集合行为异常(如逻辑上不等的对象被去重)。因此规范要求 compareTo 的相等性与 equals 保持一致。
【记忆锚点】 「TreeSet 红黑树,要比大小才能住;Comparable 是天赋,Comparator 是外挂」。
【易混对比】
| 排序方式 | 实现位置 | 方法 | 适用场景 |
|---|---|---|---|
| 自然排序(Comparable) | 元素类内部 | compareTo | 类本身具备唯一自然顺序 |
| 定制排序(Comparator) | 外部比较器 | compare | 不改变原类,或多维度排序 |
换问法:若 TreeSet 中两个元素 compareTo 返回 0 但 equals 返回 false,会发生什么?——TreeSet 认为它们相等,只保留一个,与 equals 语义冲突。
【自测】 以下代码能否正常运行?
class Person {
String name;
Person(String name) { this.name = name; }
}
Set<Person> set = new TreeSet<>();
set.add(new Person("A"));答:不能。Person 未实现 Comparable,且构造 TreeSet 时未传入 Comparator,add 会抛 ClassCastException。与第 J38 题连考。
【知识关联】
- 同库关联:与 J43(Comparable/Comparator)、J31;与自然排序稳定性。
- 实现层:基于 TreeMap 红黑树;无比较器时要求元素实现 Comparable,否则 ClassCastException。
- 面试追问:① 排序依据如何指定?② equals 与 compareTo 不一致的后果?
【拓展延伸】
- 变式问法:字符串 TreeSet 自然序;自定义比较器。
- 版本差异:语义稳定。
- 工程注意点:compareTo 与 equals 保持一致;需要插入序用 LinkedHashSet。
J39 · 知识点:fail-fast 机制
【题目】使用 foreach 遍历 ArrayList 时在循环体内调用 list.remove(),会发生?
A. 正常删除元素 B. 抛出 ConcurrentModificationException C. 死循环 D. 编译错误
答案:B
【考点】快速失败(fail-fast)机制:结构性修改触发 modCount 检查。
【结论】选 B。foreach 遍历本质使用迭代器,循环体内直接调用 list.remove() 会修改 modCount,导致迭代器检查失败而抛 ConcurrentModificationException。
【逐项辨析】
- A错:不会正常删除,因为结构性修改触发了 fail-fast 机制。
- B正确:foreach 底层是 Iterator,迭代器在 next() 时检查 modCount,发现与期望值不一致,抛出 ConcurrentModificationException。
- C错:不会死循环,而是直接抛异常终止。
- D错:代码可以编译通过,异常在运行期抛出。
【知识点】 fail-fast(快速失败)是 Java 集合的一种容错机制:集合在结构上被修改(add、remove、clear 等改变元素个数的行为)时,会递增 modCount 字段。Iterator 创建时记录当前 modCount 为期望值,每次 next() 前检查 modCount 是否变化,若变化则立即抛出 ConcurrentModificationException,避免数据不一致导致的不可预期行为。注意:Iterator 自身的 remove() 方法会同步更新期望值,因此是安全的。安全删除方式还包括:① 使用索引 for 循环倒序删除;② 先收集待删元素,遍历结束后统一用 removeAll 删除;③ 使用 CopyOnWriteArrayList(写时复制,迭代的是原数组快照)。
边界提醒: CME 只在下一次 next() 的 modCount 检查时抛,所以「删完后 cursor 已不小于 size」的极端情况会静默结束(实测:删 3 元素列表的倒数第二个 → 不抛)。本题选 B 依据的是一般结论,不能推成"只要 remove 就必抛"。
【记忆锚点】 「foreach 里别 remove,modCount 一变就崩溃;要删就用迭代器,期望计数跟着对」。
【易混对比】
- fail-fast vs fail-safe:fail-fast(ArrayList、HashMap)遇到修改立即抛异常;fail-safe(CopyOnWriteArrayList、ConcurrentHashMap)迭代的是集合快照或分段视图,允许并发修改但不保证读到最新数据。
- 换问法:以下代码是否正确?
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
if (s.equals("x")) it.remove();
}——正确,Iterator.remove() 会同步更新 expectedModCount。
【自测】 以下代码会抛异常吗?
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
for (String s : list) {
if (s.equals("B")) list.remove(s);
}答:不会抛异常,循环正常结束,list 变成
[A, C](OpenJDK 17 实测)。这是 fail-fast 的一个真实边界:删除恰好发生在倒数第二个元素时,remove把 size 减到与迭代器 cursor 相等(此处 cursor=2、size 由 3 变 2),hasNext()返回 false,next()再也不会被调用,modCount 检查因此没有机会执行。把判断条件换成"A"或"C"就抛 CME 了(实测:remove("A")→ CME,remove("C")→ CME)。与第 J39 题连考:J39 问的是一般结论(选 B),本自测考的是"结论不总成立"的那个特例,两者口径不同,别记成"foreach 里删最后两个之一就不抛"——真正的判据是删完后 cursor 是否还 < size。
【知识关联】
- 同库关联:与 J44(Iterator.remove)、J32/J34 modCount;与 J42(弱一致迭代器)对照。
- 实现层:modCount 期望值不一致抛 ConcurrentModificationException。
- 面试追问:① 如何安全边遍历边删?② 为何单线程也可能触发?
【拓展延伸】
- 变式问法:foreach 中 list.remove;Iterator.remove 合法性。
- 版本差异:语义稳定。
- 工程注意点:用 Iterator.remove 或 removeIf;并发集合用 CME-free 迭代器。
J40 · 知识点:Vector
【题目】关于 Vector 的说法,正确的是?
A. 底层是链表 B. 线程安全(方法用 synchronized 修饰),但性能较低,目前已不推荐使用 C. 不允许 null 元素 D. 每次扩容为原容量的 1.5 倍
答案:B
【考点】Vector 的历史定位、线程安全与扩容策略。
【结论】选 B。Vector 是 JDK1.0 遗留类,方法用 synchronized 修饰保证线程安全,但加锁开销大,已不推荐使用。
【逐项辨析】
- A错:Vector 底层是 Object[] 数组,不是链表。
- B正确:Vector 所有修改方法都加了 synchronized 同步锁,线程安全但性能低下,官方已标记为遗留类。
- C错:Vector 允许 null 元素。
- D错:Vector 默认扩容为 2 倍(可指定 capacityIncrement),1.5 倍是 ArrayList 的策略。
【知识点】 Vector 是 Java 最早的动态数组实现(JDK1.0),与 ArrayList 类似但所有方法都用 synchronized 修饰,保证线程安全。其扩容策略为:若未指定容量增量(capacityIncrement),则扩容为原来的 2 倍;若指定了增量,则新容量 = 旧容量 + 增量。由于全局方法级同步锁导致并发性能极差,且功能被 ArrayList + Collections.synchronizedList / CopyOnWriteArrayList 完全覆盖,Vector 已被标记为遗留类(Legacy)。Stack 继承自 Vector,同样已过时,推荐使用 Deque(如 ArrayDeque)替代栈操作。
【记忆锚点】 「Vector 老古董,全身 synchronized;扩容直接翻一倍,性能太差已过时」。
【易混对比】
| 特性 | Vector | ArrayList | Collections.synchronizedList |
|---|---|---|---|
| 线程安全 | 是(synchronized) | 否 | 是(包装器同步) |
| 扩容倍数 | 2 倍 | 1.5 倍 | 依赖底层 List |
| 性能 | 低 | 高 | 中(迭代需手动同步) |
| 推荐度 | 不推荐 | 单线程首选 | 需要同步时的备选 |
| 迭代安全性 | 需外部同步 | 非安全 | 需外部同步 |
换问法:需要线程安全的 List,除了 Vector 还可以用什么?——Collections.synchronizedList(new ArrayList<>()) 或 new CopyOnWriteArrayList<>()。
【自测】 Vector 与 ArrayList 的扩容倍数分别是多少?
答:Vector 默认 2 倍,ArrayList 1.5 倍。与第 J40 题连考。
【知识关联】
- 同库关联:与 J32(ArrayList)、J41(同步包装)、J42(分段/CAS 演进)历史线。
- 实现层:方法级 synchronized,粒度粗性能差;历史遗留同步容器。
- 面试追问:① Vector 与 ArrayList 区别?② 为何被 ConcurrentHashMap 取代思路?
【拓展延伸】
- 变式问法:Stack 继承 Vector 的设计问题。
- 版本差异:长期不推荐新代码使用。
- 工程注意点:新代码用 ArrayList + 显式同步或 JUC;遗留代码迁移注意行为差异。
J41 · 知识点:Collections 工具类
【题目】下列哪个方法可以将 ArrayList 包装为线程安全?
A. Collections.synchronizedList(list) B. Arrays.asList(list) C. list.synchronize() D. new Thread(list)
答案:A
【考点】Collections 同步包装方法。
【结论】选 A。Collections.synchronizedList(list) 返回线程安全的同步包装列表。
【逐项辨析】
- A 正确:Collections.synchronizedList 返回 SynchronizedList 包装器,所有方法通过 synchronized 同步块保证线程安全。
- B 错:Arrays.asList() 将数组转为固定大小的 List(内部类 Arrays$ArrayList),不支持 add/remove,与线程安全无关。
- C 错:List 接口及其实现类均没有 synchronize() 方法。
- D 错:new Thread(list) 语法不合法,List 不是 Runnable,无法传入 Thread 构造器。
【知识点】 Collections 工具类提供一系列同步包装方法:synchronizedCollection、synchronizedList、synchronizedSet、synchronizedMap 等。它们通过装饰器模式在原集合外包一层同步锁,所有访问方法都通过 synchronized (mutex) 块保护。需注意:虽然单个操作线程安全,但复合操作(如「先判断再添加」if (!list.contains(x)) list.add(x))仍需额外同步,因为两个原子操作之间可能被其他线程打断。迭代时也必须手动同步:synchronized (list) { for (E e : list) ... }。此外,Collections 还提供 unmodifiableXxx(不可修改视图)、singletonXxx(单例集合)、emptyXxx(空集合)等实用工厂方法。
【记忆锚点】 「Collections 工具箱,synchronized 包安全;单操作没问题,复合迭代要加锁」。
【易混对比】
| 线程安全 List 方案 | 实现机制 | 迭代一致性 | 适用场景 |
|---|---|---|---|
| Vector | 方法级 synchronized | 强一致 | 已过时 |
| Collections.synchronizedList | 包装器 synchronized | 强一致(需外部同步迭代) | 读多写少,兼容旧代码 |
| CopyOnWriteArrayList | 写时复制(ReentrantLock) | 弱一致(迭代快照) | 读极多写极少 |
换问法:Collections.synchronizedList 返回的 List,其迭代器是否线程安全?——不安全,迭代时必须在外部对列表对象加 synchronized。
【自测】 以下代码是否正确?
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
for (String s : syncList) {
System.out.println(s);
}答:语法正确但不安全。迭代时其他线程修改 syncList 会抛 ConcurrentModificationException。正确做法是将迭代代码放入
synchronized (syncList)块中。与第 J41 题连考。
【知识关联】
- 同库关联:与 J32/J33 排序、不可变包装、synchronizedList(与 J40/J42 对照)。
- 实现层:sort 底层 TimSort;unmodifiable* 返回只读视图(不是拷贝)。
- 面试追问:① unmodifiable 与 copyOf 区别?② 如何获得真正不可变集合?
【拓展延伸】
- 变式问法:
Collections.unmodifiableList后原 list 改动是否可见(可见)。 - 版本差异:JDK 9+ List.of 深不可变。
- 工程注意点:对外暴露 API 优先不可变;只读包装防误改但注意别名。
J42 · 知识点:ConcurrentHashMap 演进
【题目】JDK8 中 ConcurrentHashMap 的并发安全实现是?
A. 分段锁(Segment,继承 ReentrantLock) B. 读写锁 C. 全局单锁 D. CAS + synchronized 对数组每个桶(节点)加锁
答案:D
【考点】ConcurrentHashMap 从分段锁到 CAS + 桶锁的演进。
【结论】选 D。JDK8 ConcurrentHashMap 取消分段锁,采用 CAS + synchronized 对单个桶头节点加锁,锁粒度更细。
【逐项辨析】
- A错:分段锁(Segment)是 JDK7 ConcurrentHashMap 的实现,JDK8 已取消 Segment。
- D正确:JDK8 定位到桶后,对桶头节点加 synchronized 锁;空桶用 CAS 无锁插入。
- C错:全局单锁是 Hashtable 的做法,ConcurrentHashMap 无论哪个版本都不是全局锁。
- B错:未使用读写锁(ReadWriteLock),而是 CAS + 细粒度 synchronized。
【知识点】 ConcurrentHashMap 的并发实现经历了重大演进。JDK7 采用分段锁:将数据划分为 16 个 Segment(继承 ReentrantLock),每个 Segment 管理一段桶数组,锁粒度是「段」。JDK8 彻底重构:取消 Segment,直接用 Node[] 数组,对每个桶的头节点加 synchronized 锁。具体策略:① 桶为空(null)时,用 CAS 无锁插入新节点;② 桶非空时,对桶头节点加 synchronized 锁,再执行链表/红黑树的插入或更新;③ 扩容时支持多线程协助迁移(stride 分段)。JDK8 方案的优势:锁粒度细化到单个桶,并发度理论上等于桶数量;synchronized 在 JDK6+ 经过锁优化(偏向锁、轻量级锁、自旋),性能不亚于 ReentrantLock;红黑树优化了高冲突场景下的查询性能。
【记忆锚点】 「七版分段十六锁,八版 CAS 加桶锁;段锁变桶锁,粒度更细并发多」。
【易混对比】
| 版本 | 锁机制 | 锁粒度 | 并发度 | 数据结构 |
|---|---|---|---|---|
| JDK7 | Segment(ReentrantLock) | 段(默认 16 段) | 16 | 数组 + 链表 |
| JDK8 | CAS + synchronized(桶头) | 单个桶 | 桶数量级 | 数组 + 链表 + 红黑树 |
换问法:JDK8 ConcurrentHashMap 的 size() 方法如何实现?——使用 CAS 累加的 baseCount 和 CounterCell 数组分片计数,最终求和。
【自测】 JDK8 ConcurrentHashMap 中,为什么空桶用 CAS 而不用 synchronized?
答:空桶插入是最常见场景,CAS 无锁操作可避免线程阻塞和上下文切换,性能最高;只有冲突桶才需要 synchronized 保证多线程安全。与第 J42 题连考。
【知识关联】
- 同库关联:与 J34(HashMap)、J53/J60(锁与 CAS)、J70(可见性)并发题群衔接。
- 实现层:JDK7 分段锁 Segment;JDK8 synchronized+CAS 锁头节点,粒度更细。
- 面试追问:① JDK8 如何降低锁竞争?② size() 强一致吗?
【拓展延伸】
- 变式问法:与 Hashtable 区别;computeIfAbsent 原子性。
- 版本差异:JDK7 vs 8 结构大改;JDK 9+ 进一步优化。
- 工程注意点:高并发 map 首选;复合操作用原子方法;遍历弱一致属预期。
J43 · 知识点:排序与 Comparable/Comparator
【题目】Collections.sort(list) 要求 List 中的元素? A. 实现 Comparable 接口 B. 实现 Comparator 接口 C. 实现 Serializable 接口 D. 没有任何要求
答案:A
【考点】Comparable(自然排序)与 Comparator(定制排序)的使用场景。
【结论】选 A。Collections.sort(list) 使用自然排序,要求元素实现 Comparable 接口。
【逐项辨析】
- A正确:Collections.sort(List<T> list) 的泛型约束为 T extends Comparable<? super T>,元素必须实现 Comparable。
- B错:Comparator 是外部比较器,用于 Collections.sort(list, comparator) 的重载形式。
- C错:Serializable 是序列化标记接口,与排序无关。
- D错:没有要求则无法比较大小,排序逻辑无法成立。
【知识点】 Java 提供两种排序机制:Comparable(自然排序)和 Comparator(定制排序)。Comparable 是元素类内部实现的接口,定义 int compareTo(T o),表示「本对象与另一个对象比较」,一个类只能有一种自然排序。Comparator 是外部比较器,定义 int compare(T o1, T o2),表示「两个对象之间的比较」,可在不修改原类的情况下提供多种排序规则。Collections.sort(list) 和 Arrays.sort(array) 默认使用自然排序;带 Comparator 参数的重载方法使用定制排序。TreeSet/TreeMap 同样支持这两种排序方式。String、Integer 等包装类均已实现 Comparable。
【记忆锚点】 「Comparable 是天赋(类自带),Comparator 是外挂(外部定);一个参数自然排,两个参数定制来」。
【易混对比】
| 特性 | Comparable | Comparator |
|---|---|---|
| 接口位置 | java.lang | java.util |
| 方法 | compareTo(T o) | compare(T o1, T o2) |
| 实现位置 | 元素类内部 | 外部类 / Lambda / 匿名类 |
| 排序规则数量 | 一个类只能有一种 | 可定义多种 |
| 典型使用 | Collections.sort(list) | Collections.sort(list, cmp) |
| 典型类 | String、Integer、Date | 自定义多维度排序 |
换问法:如果元素类已实现 Comparable,但想临时换一种排序规则怎么办?——使用 Comparator 传入 sort 方法,Comparator 优先级高于 Comparable。
【自测】 以下代码使用哪种排序?
List<String> list = Arrays.asList("b", "a", "c");
Collections.sort(list, (a, b) -> b.compareTo(a));答:定制排序(Comparator)。Lambda 表达式实现 Comparator,按字符串降序排列,结果 ["c", "b", "a"]。与第 J43 题连考。
【知识关联】
- 同库关联:与 J38(TreeSet)、J41(Collections.sort);与 Python 的 key/sorted(P15)对照。
- 实现层:Comparable 自然序 compareTo;Comparator 策略对象,可组合 thenComparing。
- 面试追问:① 二者优先级?② 如何实现多字段排序?
【拓展延伸】
- 变式问法:
list.sort(Comparator.comparing(...));null 处理 Comparator.nullsFirst。 - 版本差异:JDK 8 Stream.sorted 与 Comparator 工厂方法丰富。
- 工程注意点:领域对象优先 Comparable 定义默认序;展示层临时排序用 Comparator。
J44 · 知识点:Iterator 接口
【题目】Iterator 中用于删除当前元素的正确方法是? A. remove() B. delete() C. next() D. hasNext()
答案:A
【考点】Iterator 的三个核心方法及其调用约束。
【结论】选 A。Iterator.remove() 删除刚刚由 next() 返回的元素,是迭代过程中唯一安全的删除方式。
【逐项辨析】
- A正确:Iterator 接口定义了 remove() 方法,用于删除当前迭代到的元素。
- B错:Iterator 接口没有 delete() 方法。
- C错:next() 用于获取下一个元素并移动指针,不是删除。
- D错:hasNext() 用于判断是否还有剩余元素,与删除无关。
【知识点】 Iterator 接口定义三个核心方法:hasNext()(检查是否还有下一个元素)、next()(返回下一个元素并将指针后移)、remove()(删除最近一次 next() 返回的元素)。remove() 的调用有严格约束:① 必须在 next() 之后调用,即先迭代到元素才能删除;② 不能连续调用两次 remove(),每次 remove() 后必须再调用 next() 才能再次删除。remove() 是唯一安全的迭代中删除方式,因为它会同步更新迭代器的 expectedModCount,不会触发 fail-fast。直接在集合上调用 remove()(如 ArrayList.remove())会修改 modCount 而迭代器不知情,导致 ConcurrentModificationException。
【记忆锚点】 「next 先指后移,remove 紧跟其后;想删只能 remove(),集合 remove 会崩溃」。
【易混对比】
| 方法 | 作用 | 调用约束 | 是否安全 |
|---|---|---|---|
| hasNext() | 判断是否有下一个 | 任意时刻 | 安全 |
| next() | 返回元素并后移指针 | 需先 hasNext 为 true | 安全 |
| remove() | 删除最近一次 next 的元素 | 必须在 next 之后,不能连续两次 | 安全(更新 expectedModCount) |
| 集合 remove() | 按索引或对象删除 | 迭代时调用 | 不安全(触发 fail-fast) |
换问法:ListIterator 与 Iterator 的区别是什么?——ListIterator 继承 Iterator,支持双向遍历(hasPrevious/previous)、指定位置开始遍历、以及 add/set 操作。
【自测】 以下代码执行结果是什么?
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
Iterator<String> it = list.iterator();
it.next();
it.remove();
it.remove();答:第二句 it.remove() 抛 IllegalStateException。remove() 不能连续调用两次,每次必须先 next()。与第 J44 题连考。
【知识关联】
- 同库关联:与 J39(fail-fast)、J31;foreach 语法糖背后是 Iterator。
- 实现层:hasNext/next/remove 三件套;JDK8+ 可默认方法 forEachRemaining。
- 面试追问:① 如何实现自定义集合可迭代?② ListIterator 有何增强?
【拓展延伸】
- 变式问法:手写 Iterator;fail-fast 与 remove。
- 版本差异:JDK 8 default methods;Spliterator 用于并行流。
- 工程注意点:遍历中修改必须走 Iterator.remove;不要在增强 for 里调用集合 remove。