集合框架

共 78 题
📑 题目列表 78 题
#
★★★

1. ArrayDeque 与 LinkedList 作为队列的性能对比

ArrayDeque 与 LinkedList 作为队列的性能如何对比?

  • ArrayDeque 循环数组实现
  • LinkedList 链表实现
  • 时间复杂度与内存开销

ArrayDeque 基于循环数组(resizable array)实现,头尾操作(addFirst/addLast/removeFirst/removeLast)都是 O(1) 且局部性好、内存紧凑;LinkedList 基于双向链表实现,头尾操作也是 O(1),但每个节点含对象头与前后指针,内存开销大、局部性差、缓存不友好。作为队列(FIFO),ArrayDeque 通常性能优于 LinkedList:更少内存分配、更小 GC 压力、缓存友好。LinkedList 的额外能力是作为 List(支持索引访问,但 O(n))与 Deque。ArrayDeque 不允许 null(LinkedList 允许)。队列/栈场景推荐 ArrayDeque,LinkedList 适合需要 List 语义或频繁中间插入的场景。

两者头尾操作都是 O(1),但 ArrayDeque 的数组布局更缓存友好、内存紧凑,性能通常更好;LinkedList 的节点指针开销大。业务队列优先 ArrayDeque。

Deque<String> queue = new ArrayDeque<>();   // 推荐队列
queue.addLast("a"); queue.addLast("b");
queue.removeFirst();   // FIFO
Deque<String> linked = new LinkedList<>();  // 链表,内存开销大
#
★★★

2. ArrayList 的扩容机制、容量与 ensureCapacity 优化

ArrayList 的扩容机制是什么?容量与 ensureCapacity 如何优化?

  • ArrayList 扩容机制(1.5 倍)
  • 容量与 size
  • ensureCapacity 的作用

ArrayList 底层是 Object[],容量(capacity)是数组长度,size 是元素个数。扩容:当 size 达到 capacity 时,扩容为原容量的 1.5 倍(新容量 = 旧容量 + 旧容量 >> 1),并复制到新数组(System.arraycopy),是 O(n) 操作。扩容成本主要在数组复制。优化:预知元素数量时,用构造器指定初始容量(new ArrayList<>(expectedSize))或调用 ensureCapacity(n) 预先扩容,避免多次扩容与多次复制。ensureCapacity 在元素批量加入前调用可减少扩容次数。扩容系数 1.5 是时间与空间折中(避免每次加 1 的 O(n²) 与过多浪费)。

ArrayList 扩容是 1.5 倍 + 复制,均摊 O(1)。批量化插入前用 ensureCapacity/初始容量减少扩容次数,是性能优化关键。

ArrayList<String> list = new ArrayList<>(1000);  // 初始容量
list.ensureCapacity(10000);   // 批量前预扩容,减少复制
for (int i = 0; i < 10000; i++) list.add(String.valueOf(i));
#
★★★

3. ArrayList 迭代过程中并发修改为何抛 ConcurrentModificationException,单线程下边遍历边 remove 怎样避免该异常

ArrayList 迭代过程中并发修改为何抛 ConcurrentModificationException?单线程下边遍历边 remove 如何避免该异常?

  • modCount 与快速失败
  • ConcurrentModificationException 成因
  • 遍历时 remove 的正确方式

ArrayList 的迭代器(Iterator)维护 modCount 快照,任何结构性修改(add/remove/clear)都会使 modCount 变化;迭代器在 hasNext()/next() 时检查 modCount 若与快照不一致,说明集合被并发/结构化修改,抛 ConcurrentModificationException(fail-fast)。因此边遍历边用 list.remove(...) 会抛异常。单线程下避免方式:1)用 Iterator.remove()(它同步更新 modCount 与自身状态);2)用 ListIterator;3)用 removeIf(基于迭代器安全);4)收集待删元素后统一 removeAll。Iterator.remove() 安全因为它内部维护 expectedModCount 更新。

ConcurrentModificationException 是 fail-fast 机制,检测 modCount 变化的非法并发修改。单线程边遍历边删要使用迭代器自身的 remove 或 removeIf,而非集合的 remove。

List<String> list = new ArrayList<>(List.of("a", "b", "c"));
// 错误:list.remove 抛 CME
for (String s : list) { if (s.equals("a")) list.remove(s); }
// 正确:Iterator.remove 或 removeIf
list.removeIf(s -> s.equals("a"));
// 或
Iterator<String> it = list.iterator();
while (it.hasNext()) { if (it.next().equals("a")) it.remove(); }
#
★★★

4. ArrayList.subList 为何只是原列表视图,原列表发生结构修改后访问子列表会出现什么结果

ArrayList.subList 为何只是原列表视图?原列表发生结构修改后访问子列表会出现什么结果?

  • subList 是视图(view)
  • 视图与原列表共享底层数组
  • 结构修改导致的不一致

ArrayList.subList(from, to) 返回的是原列表的"视图"(SubList 对象),它共享底层数组而非复制。因此对子列表的修改反映到原列表,反之亦然。但由于 SubList 维护自己的 modCount 快照,若原列表发生结构性修改(affecting size),子列表的 modCount 与原列表不一致,访问子列表(如 size()、get、迭代)会抛 ConcurrentModificationException。即"原列表结构修改后访问子列表"会抛 CME,因为子列表检测到共享数组被修改。正确使用:不要在原列表结构修改后继续访问子列表;如需独立副本用 new ArrayList<>(list.subList(...))。

subList 是共享底层数组的视图,结构修改破坏视图的 modCount 一致性,导致 CME。需要独立数据时复制而非用视图。

List<String> list = new ArrayList<>(List.of("a","b","c","d"));
List<String> sub = list.subList(1, 3);   // 视图
list.add("e");          // 结构修改原列表
sub.size();             // 抛 ConcurrentModificationException
List<String> copy = new ArrayList<>(list.subList(1, 3));  // 独立副本
#
★★★

5. Collections.synchronizedList 与 CopyOnWriteArrayList 的适用场景

Collections.synchronizedList 与 CopyOnWriteArrayList 的适用场景是什么?

  • synchronizedList 的同步包装
  • CopyOnWriteArrayList 的写时复制
  • 读多写少 vs 读写混合

Collections.synchronizedList 用 synchronized 锁包装普通 List,所有操作(含迭代)线程安全,但迭代时需手动加锁(或遍历容器);适合读多写少但需要低写开销、且无并发读需求的场景。CopyOnWriteArrayList 每次写(add/remove)都复制整个底层数组,读操作不加锁(无锁读),迭代器是快照(不会被并发修改影响,不抛 CME);适合"读多写极少"、迭代频繁且要求迭代不被修改影响的场景(如事件监听器列表、配置缓存)。取舍:synchronizedList 写开销低但读也要锁、迭代需加锁;CopyOnWriteArrayList 读无锁、迭代快照安全,但写开销大(复制)。写多时用 ConcurrentLinkedQueue 或普通线程安全集合。

两者都线程安全但机制不同:synchronizedList 全锁、写便宜;CopyOnWriteArrayList 写时复制、读无锁迭代快照。选型依据"读写比例"与"迭代是否要求快照"。

List<String> sync = Collections.synchronizedList(new ArrayList<>());
synchronized (sync) { for (String s : sync) { ... } }  // 迭代需加锁
List<String> cow = new CopyOnWriteArrayList<>();   // 读无锁,迭代快照
cow.add("a");
for (String s : cow) { ... }   // 迭代不会抛 CME
#
★★★

6. Collections.unmodifiableList 与 List.copyOf 在底层集合继续变化时,观察结果有何不同

Collections.unmodifiableList 与 List.copyOf 在底层集合继续变化时,观察结果有何不同?

  • unmodifiableList 是视图
  • List.copyOf 是独立副本
  • 底层变化的可见性

Collections.unmodifiableList(list) 返回的是原列表的"只读视图",不复制底层数据;若原列表继续变化(add/remove),通过 unmodifiableList 观察到的结果会随之变化(仍是同一底层数组)。List.copyOf(list)(JDK 10+)创建独立的不可变副本,复制底层数据;之后原列表变化不影响 copyOf 的结果(隔离)。差异核心:unmodifiableList 是"结构性只读但底层共享"的视图,copyOf 是"独立不可变快照"。若需保证原列表变化不影响已发布结果,用 List.copyOf;若只是防外部修改但需反映底层变化,用 unmodifiableList。两者都提供只读保护(不能通过它修改),但 copyOf 提供快照隔离。

unmodifiableList=只读视图(共享底层),copyOf=独立不可变副本(隔离)。选择取决于"是否需要快照隔离"。

List<String> src = new ArrayList<>(List.of("a"));
List<String> un = Collections.unmodifiableList(src);  // 视图
List<String> cp = List.copyOf(src);                   // 副本
src.add("b");
un.size();   // 2(反映底层变化)
cp.size();   // 1(独立快照,不变)
#
★★★

7. Comparable 与 Comparator 的职责边界如何划分,自然排序与定制排序在 TreeSet/TreeMap 中如何影响去重与查找

Comparable 与 Comparator 的职责边界如何划分?自然排序与定制排序在 TreeSet/TreeMap 中如何影响去重与查找?

  • Comparable(自然排序)vs Comparator(定制排序)
  • TreeSet/TreeMap 的排序与去重
  • 排序与 equals 的一致性

Comparable 定义类自身的"自然排序"(compareTo),是类固有的排序契约;Comparator 提供外部"定制排序"(compare),可随时变化、不侵入类。TreeSet/TreeMap 基于红黑树,按"比较器"(自然排序用 compareTo,或外部 Comparator)排序,且去重依据"比较结果"而非 equals:两个元素 compare 返回 0 即视为重复(去重);查找(containsKey/contains)也按比较器定位。关键:若 Comparable/Comparator 与 equals 不一致(compare 0 但 equals false),TreeSet 会把"不相等"的元素视为重复,去重与查找结果异常。设计时应让排序与 equals 一致,或明确其语义。

Comparable 是自然排序、Comparator 是定制排序;TreeSet/TreeMap 用比较器排序并据此去重,比较与 equals 不一致会破坏去重与查找语义。

// Comparable 自然排序
class A implements Comparable<A> { int x; public int compareTo(A o) { return Integer.compare(x, o.x); } }
// Comparator 定制排序
Comparator<A> byDesc = (a, b) -> Integer.compare(b.x, a.x);
TreeSet<A> set = new TreeSet<>(byDesc);
set.add(new A(1)); set.add(new A(1));  // compare 0 视为重复,只保留一个
#
★★★

8. ConcurrentHashMap 在 JDK 21+ 对虚拟线程友好的协作机制与桶级别锁粒度的演化

ConcurrentHashMap 在 JDK 21+ 对虚拟线程友好的协作机制与桶级别锁粒度如何演化?

  • ConcurrentHashMap 的桶锁粒度
  • 虚拟线程友好(synchronized 可被虚拟线程挂起)
  • JDK 21+ 演化

ConcurrentHashMap 的桶级别锁粒度:JDK 8 起用 CAS + synchronized 实现桶级锁(每个桶的 put 用 synchronized 锁该桶头节点),锁粒度细(桶级),并发度高。JDK 21+ 对虚拟线程友好:synchronized 在虚拟线程下可被"挂起"而不阻塞载体线程(虚拟线程的 synchronized 走 carrier 挂起机制),因此桶级 synchronized 锁在虚拟线程并发下更友好;同时 JDK 21+ 优化了扩容与协作机制(如 helpTransfer 帮助扩容、计数协作)。演化:从 JDK 7 的 Segment 分段锁(粒度粗)到 JDK 8 的桶级 CAS+synchronized(粒度细),到 JDK 21+ 结合虚拟线程优化并发行为。桶锁粒度细化提升并发度,虚拟线程友好减少阻塞载体线程。

CHM 从分段锁演进到桶级锁(CAS+ synchronized),粒度更细;虚拟线程下 synchronized 可挂起不阻塞载体,JDK 21+ 并发更友好。

// ConcurrentHashMap 桶级锁(JDK 8+)
// 每次 put 只锁对应桶的头节点,粒度为单桶
// JDK 21+ 虚拟线程下 synchronized 可挂起,不阻塞 carrier
ConcurrentHashMap<String, Integer> chm = new ConcurrentHashMap<>();
chm.computeIfAbsent("k", k -> expensive(k));
#
★★★

9. ConcurrentHashMap 的 mappingCount 与 size 在并发修改期间各有什么一致性和溢出差异

ConcurrentHashMap 的 mappingCount 与 size 在并发修改期间各有什么一致性和溢出差异?

  • size() 与 mappingCount()
  • 并发修改下的近似性
  • 溢出差异(int vs long)

ConcurrentHashMap.size() 返回 int,mappingCount() 返回 long。两者在并发修改期间都只是"近似值"(基于计数器的实时快照,不保证精确一致),但映射数超过 Integer.MAX_VALUE 时 size() 会溢出(返回错误值),mappingCount() 用 long 避免溢出。官方建议:映射数可能超过 int 范围时用 mappingCount()。一致性:两者都是"尽力而为"的近似,调用瞬间可能因并发修改而略过/略少于实际值,但不会因并发而崩溃。工程上:需要精确计数应外部同步或累加;用于大致规模判断用 mappingCount/size 均可。

size() 是 int(可能溢出),mappingCount() 是 long(不溢出),都非精确(并发下近似)。大映射用 mappingCount。

ConcurrentHashMap<String, Long> chm = new ConcurrentHashMap<>();
int s = chm.size();            // int,可能溢出
long m = chm.mappingCount();   // long,不溢出
// 两者在并发修改期间都是近似值
#
★★★

10. ConcurrentHashMap 的 size 统计为何不精确

ConcurrentHashMap 的 size 统计为何不精确?

  • 计数器的并发累加
  • 无锁计数
  • 近似性原因

ConcurrentHashMap 的 size 统计使用无锁计数(baseCount + CounterCell 数组),各个线程在修改时累加计数器,但无锁累加在并发下无法保证"绝对精确"——计数器可能漏算或重复(并发更新时的竞态),因此 size() 返回的是"近似值",在并发修改期间可能与实际值略有偏差。这是为换取无锁高并发而牺牲的精确性。官方文档明确说明 size() 是估算值。若需要精确计数,需外部同步或维护自己的计数。近似原因:并发修改无锁累加 + 计数器不保证强一致,但统计成本低、不阻塞。

CHM 用无锁计数器(baseCount+CounterCell)换取高并发,size 是近似值。精确计数需外部同步,CHM 的 size 适合"大致规模"判断。

ConcurrentHashMap<String, Integer> chm = new ConcurrentHashMap<>();
// 无锁计数:baseCount + CounterCell[],并发累加近似
int size = chm.size();   // 并发下近似值
// 精确计数需外部同步或原子累加器
#
★★★

11. ConcurrentHashMap 的实现原理(CAS + synchronized + 树化)

ConcurrentHashMap 的实现原理是什么?包括 CAS、synchronized 与树化?

  • CAS 初始化与空桶插入
  • synchronized 桶级锁
  • 树化(红黑树)

ConcurrentHashMap(JDK 8+)实现:底层是 Node[] 桶数组。写入时:1)空桶用 CAS 直接插入(无锁);2)桶非空时用 synchronized 锁桶头节点,在桶内插入;3)链表元素过多(阈值 8)且数组够大(>=64)时树化为红黑树(TreeBin),降低查找复杂度;4)扩容时 helpTransfer 协助(多线程协作扩容)。头节点用 volatile 保证可见性。树化降低链表长链的查找成本(O(n)→O(log n))。CAS 用于空桶插入与计数,synchronized 用于桶级锁,树化处理长链。相比 JDK 7 的 Segment 分段锁,JDK 8 粒度更细(桶级)。

CHM 用"CAS 空桶 + synchronized 桶锁 + 树化长链 + 协作扩容"实现高并发:空桶无锁、非空桶锁单桶、长链树化。粒度细、并发高。

// 逻辑简示:空桶 CAS 插入,非空桶锁桶头,长链树化
Node<K,V>[] tab = table;
if (tab[i] == null) {          // 空桶
    if (casTabAt(tab, i, null, new Node<>(k, v))) return;  // CAS 无锁插入
} else {
    synchronized (tab[i]) {    // 桶级锁
        // 插入或树化
    }
}
#
★★★

12. ConcurrentHashMap 禁止 null 键值如何消除并发查找中的歧义,缺失值应怎样建模

ConcurrentHashMap 禁止 null 键值如何消除并发查找中的歧义?缺失值应怎样建模?

  • CHM 禁止 null 键值
  • 消除"值不存在"与"值为 null"歧义
  • 缺失值建模

ConcurrentHashMap 禁止 null 键和 null 值,原因:消除并发查找中的歧义——并发下一次 get 返回 null 无法区分"键不存在"或"键存在但值为 null",而并发场景无法通过再次检查消除歧义(值可能已变)。若允许 null,get 返回 null 的语义不明。缺失值建模:用 Optional 包装值,或用"哨兵对象"(如特殊 EMPTY 常量)表示缺失,或用 getOrDefault 提供默认值,或查不到时用 computeIfAbsent 初始化。禁止 null 让 get 返回 null 意味着"键不存在",语义清晰。

禁止 null 是并发安全的语义设计,避免 get 返回 null 的歧义。缺失用 Optional/哨兵/默认值建模,get 返回 null 即"无此键"。

// CHM 禁止 null 键值
chm.put("k", null);   // 抛 NPE
// 缺失值建模:Optional
ConcurrentHashMap<String, Optional<String>> m = new ConcurrentHashMap<>();
String v = m.getOrDefault("k", Optional.empty()).orElse("default");
// 或用哨兵对象
#
★★★

13. ConcurrentHashMap.computeIfAbsent 的映射函数为何应短小且无递归更新,阻塞时会影响哪些桶

ConcurrentHashMap.computeIfAbsent 的映射函数为何应短小且无递归更新?阻塞时会影响哪些桶?

  • computeIfAbsent 的原子性
  • 映射函数持有桶锁
  • 递归更新与死锁

computeIfAbsent 在键不存在时执行映射函数,且该函数在"桶锁"内执行(对涉及的桶锁定),因此映射函数应短小、快速,避免长时间阻塞影响该桶的其他操作;且映射函数内不应再对同一 CHM 做修改(递归更新),否则可能死锁(JDK 8 前会死锁,JDK 8 检测到并抛异常)。阻塞时影响的桶:仅映射函数锁定的那个桶(桶级锁),其他桶不受影响,但所有对该桶的并发操作(put/get/remove 该桶)会被阻塞。因此映射函数应只做简单计算(如 new 对象),不做 I/O、不递归更新。

computeIfAbsent 的映射函数在桶锁内执行,须短小无递归更新,否则阻塞该桶或死锁。它影响的只是被锁的单个桶。

ConcurrentHashMap<String, Heavy> chm = new ConcurrentHashMap<>();
// 映射函数应短小、无副作用、无递归更新
Heavy h = chm.computeIfAbsent("k", k -> new Heavy());   // 简单创建
// 反模式:函数内递归更新同一 CHM 会死锁/抛异常
#
★★★

14. ConcurrentLinkedQueue 的无锁实现(CAS)

ConcurrentLinkedQueue 的无锁实现(CAS)原理是什么?

  • 无锁队列(lock-free)
  • CAS 操作
  • 头尾指针与线程安全

ConcurrentLinkedQueue 是基于 CAS 的无锁(lock-free)队列:用 volatile head 与 tail 指针,入队(add/offer)用 CAS 更新尾指针,出队(poll)用 CAS 更新头指针。无锁意味着不阻塞:CAS 失败则重试,多个线程可并发推进不会死锁(整体 lock-free 保证至少一个线程前进)。实现细节:延迟更新 tail(为减少 CAS 次数,tail 不总是紧跟队尾)、用哨兵/标记处理竞争。优点:无锁、低延迟、高并发(尤其多线程入队出队);缺点:无界(内存可能增长)、size 是 O(n)(遍历计数)、无阻塞背压。适合高并发、无界安全场景。

CLQ 用 volatile 头尾指针 + CAS 实现无锁入队出队,不阻塞、低延迟,但无界、size O(n)。适合高并发生产消费。

ConcurrentLinkedQueue<String> q = new ConcurrentLinkedQueue<>();
q.offer("a");   // CAS 更新尾指针入队
String s = q.poll();   // CAS 更新头指针出队
while (!q.isEmpty()) { ... }
#
★★★

15. ConcurrentSkipListMap 的弱一致迭代器能观察到哪些并发更新,为什么不抛 ConcurrentModificationException

ConcurrentSkipListMap 的弱一致迭代器能观察到哪些并发更新?为什么不抛 ConcurrentModificationException?

  • 弱一致迭代器
  • 并发更新可见性
  • 不抛 CME 的原因

ConcurrentSkipListMap 的迭代器是"弱一致"(weakly consistent)的:迭代器遍历时,可能看到部分已提交的并发更新(如已插入的元素),也可能看不到同时进行的更新(不保证强一致),但不会抛 ConcurrentModificationException。原因:它不维护 modCount 快照,底层是跳表(skip list),迭代器基于链表/跳表结构遍历,并发修改通过 CAS 安全更新,迭代器能在遍历时感知指针变化而不崩溃。弱一致保证:迭代器不会看到"部分写入"的中间状态,它看到的元素在某时刻确实存在;但元素集合可能随并发变化。适用于高并发下需要遍历 Map 的场景。

SkipList 迭代器弱一致:不抛 CME,基于跳表结构安全遍历,可见部分并发更新但不保证强一致。换掉快照机制换取并发可遍历。

ConcurrentSkipListMap<Integer, String> m = new ConcurrentSkipListMap<>();
m.put(1, "a");
for (Map.Entry<Integer, String> e : m.entrySet()) {  // 弱一致迭代
    // 并发插入的元素可能可见也可能不可见,但不抛 CME
}
#
★★★

16. CopyOnWriteArrayList 执行 removeIf 或批量写入时会复制多少数据,适合怎样的读写比例

CopyOnWriteArrayList 执行 removeIf 或批量写入时会复制多少数据?适合怎样的读写比例?

  • 写时复制
  • removeIf/批量写的数据量
  • 读写比例

CopyOnWriteArrayList 每次写(add/remove/removeIf/批量写)都会复制整个底层数组:removeIf 或批量写入时,会创建一个新数组(复制所有不需要删除的元素,或批量写入后复制全部),因此写操作成本与集合大小成正比(O(n) 复制)。适合"读多写极少"的场景:读操作无锁(O(1) 直接访问数组),写操作虽复制但频率低,整体高效。适合读为主、写稀少的场景(如事件监听器列表、配置缓存、只读快照)。若写频繁,复制成本高,不宜使用。读写比例:读远多于写(如 99% 读 1% 写)时最合适。

CopyOnWriteArrayList 写时复制整个数组,写成本 O(n);适合读多写极少的场景,读无锁、迭代快照。写频繁则不宜。

List<String> cow = new CopyOnWriteArrayList<>(List.of("a","b","c","d"));
cow.removeIf(x -> x.equals("a"));   // 复制整个数组,移除 "a" 后生成新数组
cow.add("e");                       // 复制整个数组
// 适合读多写极少:读 O(1) 无锁,写 O(n) 复制
#
★★★

17. CopyOnWriteArrayList 的迭代器为何提供快照语义,长时间持有迭代器会造成什么内存压力

CopyOnWriteArrayList 的迭代器为何提供快照语义?长时间持有迭代器会造成什么内存压力?

  • 迭代器快照语义
  • 写时复制与迭代器
  • 内存压力

CopyOnWriteArrayList 的迭代器直接在"创建迭代器时的底层数组"上遍历(快照语义),因为写操作会复制出新数组,迭代器持有旧数组引用,因此迭代期间并发写不抛 CME、迭代器看到的是创建时的快照。快照语义的好处:迭代安全、无锁。内存压力:若长时间持有迭代器(或迭代器引用),它持有的旧数组不会被 GC 回收(因为迭代器仍引用它),而每次写又创建新数组,导致旧数组滞留,内存压力增大。因此不应长时间持有 COW 迭代器,用完即弃;在写频繁场景下累积旧数组会内存膨胀。

COW 迭代器持有的数组是快照,写时复制新数组,旧数组若被迭代器长期引用则无法回收,造成内存压力。用完迭代器即释放。

List<String> cow = new CopyOnWriteArrayList<>();
Iterator<String> it = cow.iterator();   // 快照:持有当前数组
cow.add("x");                            // 复制新数组
// it 仍引用旧数组,若长期持有 it,旧数组无法回收
#
★★★

18. EnumSet 在枚举常量超过六十四个时如何改变内部表示,其位集合优势是否仍然存在

EnumSet 在枚举常量超过六十四个时如何改变内部表示?其位集合优势是否仍然存在?

  • EnumSet 内部表示(RegularEnumSet vs JumboEnumSet)
  • 64 个常量的分界
  • 位集合优势

EnumSet 根据枚举常量数量选择内部表示:若枚举常量数量 <= 64,用 RegularEnumSet(单个 long 位集,1 位/常量);若 > 64,用 JumboEnumSet(long 数组,每个 long 存 64 个常量)。分界是 64 个常量。位集合优势在 JumboEnumSet 中仍然存在:用 long[] 位集,每个操作仍是 O(1)(按索引定位到对应 long),批量操作(containsAll、retainAll 等)用位运算(AND/OR),仍高效。JumboEnumSet 只是用多个 long 实现位集,性能与内存优势保留(比 HashSet 更紧凑),只是位集分布在多个 long 上。

EnumSet 按 64 个常量的分界选择 RegularEnumSet(单 long)或 JumboEnumSet(long[]),位集合优势(紧凑、位运算 O(1))在 Jumbo 下仍保留。

enum Small { A, B, C }              // <= 64 -> RegularEnumSet(单 long)
enum Big { A, B, ... }              // > 64 -> JumboEnumSet(long[])
EnumSet<Big> set = EnumSet.allOf(Big.class);  // JumboEnumSet,位集仍高效
#
★★★

19. HashMap 在 JDK 17/21 中红黑树化阈值(8/64)与负载因子的演进及取舍

HashMap 在 JDK 17/21 中红黑树化阈值(8/64)与负载因子的演进及取舍是什么?

  • 树化阈值 8 与容量阈值 64
  • 负载因子 0.75
  • 取舍

HashMap 树化条件:一个桶的链表长度达到阈值 8 且数组长度 >= 64 时,链表转为红黑树(TreeBin);若数组长度 < 64,则先扩容(不树化)。负载因子默认 0.75:达到"容量 x 0.75"时扩容 2 倍,是时间与空间的折中(0.75 减少哈希冲突概率,同时避免过度浪费空间)。这些阈值在 JDK 17/21 基本延续(树化 8/64、负载因子 0.75),是工程经验值:树化 8 让链表查找从 O(n) 降到 O(log n),但树化有开销(树节点占用更大),故用 64 容量门槛避免过早树化;负载因子 0.75 平衡冲突率与空间使用。JDK 8 引入树化,JDK 17/21 保持并优化。

树化阈值 8(链表长度)+ 64(数组容量)避免过早树化,负载因子 0.75 平衡时间/空间,是 HashMap 性能的关键工程参数。

// 树化条件:链表长度 >= 8 且 数组长度 >= 64
// 若数组 < 64 则先扩容而非树化
// 负载因子 0.75:达到容量*0.75 扩容 2 倍
HashMap<String, Integer> m = new HashMap<>(16, 0.75f);  // 初始容量、负载因子
#
★★★

20. HashMap 在 JDK 8/17 中数据结构与并发 bug 修复演进

HashMap 在 JDK 8/17 中数据结构与并发 bug 修复如何演进?

  • JDK 8 引入红黑树
  • 并发 bug(死循环)修复
  • 哈希扰动优化

HashMap 演进:JDK 7 用"数组 + 单向链表",哈希算法用 hash & (length-1),存在并发扩容时链表成环导致死循环(get 死循环)的 bug;JDK 8 引入"数组 + 链表 + 红黑树"(长链树化),把哈希扰动改为"低 16 位异或高 16 位"(优化分布),并修复了并发扩容死循环问题(链表头插改为尾插,且树化/扩容更安全,但仍非线程安全——并发下仍可能丢数据,只是不死循环)。JDK 17 延续 JDK 8 结构,继续修复细节并发问题但不保证线程安全。HashMap 非线程安全,并发要用 ConcurrentHashMap;JDK 8 修复了死循环 bug 并引入树化。

JDK 8 引入树化、扰动函数优化并修复并发死循环 bug,但 HashMap 仍非线程安全,并发用 CHM。JDK 17 延续树化结构。

// JDK 8+ 哈希扰动:高 16 位异或低 16 位
int h = key.hashCode();
h ^= (h >>> 16);
// 数据结构:数组 + 链表 + 红黑树(长链树化)
// 并发:HashMap 非线程安全,用 ConcurrentHashMap
#
★★★

21. HashMap 容量为何必须为 2 的幂

HashMap 容量为何必须为 2 的幂?

  • 2 的幂与哈希定位
  • hash & (length-1)
  • 扩容与重新分布

HashMap 容量设计为 2 的幂,原因是:定位桶用 hash & (length - 1) 代替取模(%),当 length 是 2 的幂时,length-1 是低位全 1 的掩码,hash & (length-1) 等价于 hash % length 且更快(位运算)。同时 2 的幂保证扩容(翻倍)后元素只需根据"新增的高位"判断是否移位,无需重新计算完整哈希,便于扩容拆分。若容量非 2 的幂,& 运算会丢失位分布、冲突增加。因此构造时即使指定非 2 的幂容量,HashMap 也会向上取最近的 2 的幂(tableSizeFor)。

2 的幂让 hash & (length-1) 等价取模且高效,扩容翻倍只按高位分组,减少重算。构造时自动取最近的 2 的幂。

// 桶定位:hash & (length-1),length 为 2 的幂时等价 hash % length
int idx = h & (length - 1);
// 扩容翻倍:元素按新增高位判断是否移位(0 留在原桶,1 移到原桶+旧容量)
// tableSizeFor 把非 2 的幂容量向上取 2 的幂
#
★★★

22. HashMap 扩容时如何利用旧容量对应的高位将节点拆成两组,并避免重新计算完整哈希值

HashMap 扩容时如何利用旧容量对应的高位将节点拆成两组,并避免重新计算完整哈希值?

  • 扩容源码机制
  • 高位拆分(hi/lo)
  • 避免重算哈希

HashMap 扩容(翻倍到 2*oldCap)时,利用"旧容量对应的高位"将每条链表的节点拆成两组:判断 (e.hash & oldCap) == 0——若为 0,节点留在原桶(lo 组);若为 1,节点移到"原桶 + oldCap"(hi 组)。因为容量翻倍后,新桶下标 = 原下标 或 原下标+oldCap,取决于新增的高位是否为 1。这样无需重新计算完整哈希(hash 不变,只按新增高位分组),只需 O(1) 判断每个节点。这是 JDK 8 的优化:避免 JDK 7 逐个重算哈希、且减少哈希函数调用。

扩容按 hash & oldCap 拆成 lo/hi 两组,利用高位分组,避免重算哈希,是 JDK 8 扩容优化的核心。

int oldCap = oldTab.length;
for (Node e : bin) {
    if ((e.hash & oldCap) == 0) {   // 低位组:留在原桶
        loTail.next = e;
    } else {                        // 高位组:移到 原桶+oldCap
        hiTail.next = e;
    }
}
// 新桶下标 = 原下标 或 原下标 + oldCap
#
★★★

23. HashMap 桶树化为何同时受链表长度和数组容量约束,过早树化会付出什么代价

HashMap 桶树化为何同时受链表长度和数组容量约束?过早树化会付出什么代价?

  • 树化条件(8 与 64)
  • 过早树化的代价
  • 容量约束的原因

HashMap 树化需同时满足:链表长度 >= 8 且数组长度 >= 64。若数组长度 < 64,即使链表长也先扩容而非树化。容量约束原因:数组过小但链表长,说明哈希冲突严重(可能因容量不足导致),此时应扩容(把元素分散)而非树化——扩容能解决根本问题且成本可控。过早树化的代价:树节点(TreeNode)比普通链表节点占用更多内存(约 2 倍,含父/左右/红黑指针等),且红黑树操作有插入/旋转/染色开销;若数组较小、多数桶短链,树化反而浪费内存与性能。因此用 64 容量门槛避免"小数组下过早就树化",只在链表确实长且数组够大时才树化。

树化需链表>=8 且数组>=64:小数组下冲突先扩容解决,树节点内存大、有旋转开销,过早树化浪费内存与性能。

// 树化逻辑(简化)
if (binCount >= 8) {            // 链表长度 >= 8
    if (tab.length >= 64) treeifyBin(tab);   // 数组 >= 64 才树化
    else resize();             // 否则先扩容
}
#
★★★

24. HashMap 的实现原理(桶数组、链表、红黑树、扰动函数)

HashMap 的实现原理是什么?包括桶数组、链表、红黑树、扰动函数?

  • 桶数组与哈希定位
  • 链表与红黑树
  • 扰动函数

HashMap 底层是哈希桶数组(Node[] table),通过哈希定位桶:hash(key) 扰动后 (length-1) & hash 得到桶下标。同一桶内:若冲突少用链表(JDK 8 起),冲突多(链表>=8 且数组>=64)树化为红黑树(降低查找 O(log n))。扰动函数:h ^ (h >>> 16) 让高 16 位参与低 16 位,改善分布(尤其容量较小时)。扩容:达到 负载因子*容量 时翻倍并重新分布。查找/插入:先定位桶,再在桶内比较(equals + hashCode)。HashMap 非线程安全,允许一个 null 键和多个 null 值。

HashMap = 桶数组 + 链表 + 红黑树,扰动函数优化分布,扩容翻倍。哈希定位 + 桶内比较是核心,非线程安全。

// 扰动函数
static int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
// 桶定位:hash & (length-1)
// 桶内:链表(冲突少)或红黑树(冲突多且数组够大)
HashMap<String, Integer> m = new HashMap<>();
m.put("a", 1);   // 定位桶 + 桶内插入
#
★★★

25. HashMap 的树化机制能降低哪些复杂度,仍需防范哪些资源攻击

HashMap 的树化机制能降低哪些复杂度?仍需防范哪些资源攻击?

  • 树化降低查找复杂度
  • 哈希冲突攻击(HashDoS)
  • 安全防范

HashMap 树化把"长链桶的查找"从 O(n)(链表)降到 O(log n)(红黑树),缓解了哈希过度冲突时的性能退化。但仍需防范资源攻击:1)哈希冲突攻击(HashDoS):攻击者构造大量 hashCode 相同的键,使桶内形成长链/树,即使树化 O(log n) 也比正常 O(1) 慢,可做 CPU 耗尽攻击;2)若攻击者让哈希分布差,树化无法完全避免退化。防范:1)用不易被预测哈希的键(如 String 的哈希相对安全,但可通过精心构造);2)对 web 输入做大小限制、数量限制;3)用随机哈希种子(Java 对 String 哈希有随机化,但局限性);4)选用安全哈希/平衡结构(如并发安全、随机化)。树化缓解但无法根治哈希攻击。

树化把最坏查找从 O(n) 降到 O(log n),但哈希冲突攻击仍可造成性能退化,需限制输入规模、用随机哈希等防御。

// 树化:长链 O(n) -> O(log n)
// 防范 HashDoS:限制键数量、输入大小,避免大量同 hash 键
Map<String, String> m = new HashMap<>();
if (input.size() > MAX) { reject(); }   // 限制输入规模
#
★★

26. HashSet 与 HashMap 的关系(HashSet 底层实现)

HashSet 与 HashMap 的关系是什么?HashSet 的底层实现如何?

  • HashSet 底层用 HashMap
  • 元素作为 key,固定 value
  • 去重与哈希

HashSet 底层是 HashMap:HashSet 把元素作为 HashMap 的 key,固定 value 为一个共享的 PRESENT 对象(Object)。因此 HashSet 的去重、查找、哈希都基于 HashMap 的 key 语义:元素 hashCode/equals 决定存储位置与去重。add 元素实际上是 map.put(e, PRESENT),存在则返回 false(不重复)。HashSet 的迭代顺序不保证(无序)。由于底层是 HashMap,HashSet 也非线程安全、允许 null(HashMap 允许一个 null key)。元素作为 key 要求实现正确的 hashCode/equals。

HashSet 是 HashMap 的"包装":元素作 key、固定 value,复用 HashMap 的哈希与去重逻辑。理解 HashMap 即理解 HashSet。

// HashSet 底层:private transient HashMap<E, Object> map;
// add: map.put(e, PRESENT)  // PRESENT 是共享 Object
Set<String> set = new HashSet<>();
set.add("a");   // map.put("a", PRESENT)
#
★★

27. Hashtable 与 HashMap 的差异

Hashtable 与 HashMap 的差异是什么?

  • 线程安全
  • null 键值
  • 继承与性能

Hashtable 与 HashMap 差异:1)线程安全:Hashtable 全方法 synchronized(线程安全),HashMap 非线程安全;2)null:Hashtable 不允许 null 键和 null 值,HashMap 允许一个 null 键和多个 null 值;3)底层:Hashtable 用数组+链表(无树化),HashMap(JDK 8+)用数组+链表+红黑树;4)继承:Hashtable 继承 Dictionary,HashMap 继承 AbstractMap;5)性能:Hashtable 全锁、性能差,HashMap 无锁、性能好;6)扩容:Hashtable 扩容 2 倍+1,HashMap 扩容 2 倍。Hashtable 是遗留类,推荐用 HashMap 或 ConcurrentHashMap(线程安全)。

Hashtable 是遗留的同步 Hash,HashMap 更现代(允许 null、树化、性能好)。并发用 ConcurrentHashMap 而非 Hashtable。

Hashtable<String, String> ht = new Hashtable<>();   // 线程安全但全锁、禁 null
ht.put("a", "b");   // ht.put("a", null) 抛 NPE
HashMap<String, String> hm = new HashMap<>();       // 非线程安全、允许 null
hm.put(null, "x");
#
★★

28. IdentityHashMap 为什么使用引用相等和开放寻址,它适合处理哪些对象图遍历问题

IdentityHashMap 为什么使用引用相等和开放寻址?它适合处理哪些对象图遍历问题?

  • 引用相等(identity)
  • 开放寻址
  • 对象图遍历与序列化

IdentityHashMap 使用引用相等(==)而非 equals 比较键,因此同样"equals 相等"但不同实例的对象被视为不同键;它用开放寻址(线性探测)作为冲突解决,而非链地址法(HashMap 用链表)。原因:引用相等语义适合"按对象身份"去重/映射,开放寻址在身份比较下更高效、无额外节点分配。适用场景:1)对象图遍历/序列化(如判断对象是否已访问、维护对象身份映射);2)序列化框架(如 Java 序列化用对象引用表);3)需要基于对象身份而非 equals 的映射;4)避免 equals 被重写带来问题。例如序列化时记录"已处理的对象身份"。

IdentityHashMap 用 == 引用相等 + 开放寻址,适合按对象身份做映射(对象图遍历、序列化去重),避免 equals 语义的干扰。

IdentityHashMap<Object, String> m = new IdentityHashMap<>();
String a = new String("x"), b = new String("x");
m.put(a, "1"); m.put(b, "2");   // 引用相等,a!=b,两个键
// 对象图遍历:记录已访问对象引用
Set<Object> visited = Collections.newSetFromMap(new IdentityHashMap<>());
#
★★

29. Iterator 与 ListIterator 的能力差异体现在哪里,哪些集合支持双向遍历与 add/set 操作

Iterator 与 ListIterator 的能力差异体现在哪里?哪些集合支持双向遍历与 add/set 操作?

  • Iterator 与 ListIterator 的方法
  • 双向遍历
  • ListIterator 支持 add/set

Iterator 是通用迭代器,只有 hasNext/next/remove(向后遍历、删除);ListIterator 是 List 专用的迭代器,额外支持:hasPrevious/previous(双向遍历)、add(添加元素)、set(替换元素)、nextIndex/previousIndex(索引)。ListIterator 只能用于 List 集合(ArrayList、LinkedList 等),支持双向遍历与 add/set;Iterator 可用于所有 Collection。Set、Map 无 ListIterator(无序)。ListIterator 的 add/set 是 List 特有的修改能力,且迭代中用 ListIterator 修改安全。

Iterator 向后遍历+删除,ListIterator 双向遍历+add/set+索引,仅 List 支持。ListIterator 是 List 的增强迭代器。

List<String> list = new ArrayList<>(List.of("a","b","c"));
ListIterator<String> it = list.listIterator();
it.next(); it.next();
it.previous();          // 双向
it.set("X");            // 替换
it.add("Y");            // 添加
#
★★

30. JDK 21 List.copyOf 不可变集合在共享场景下的发布安全保证

JDK 21 List.copyOf 不可变集合在共享场景下的发布安全保证是什么?

  • List.copyOf 不可变集合
  • 安全发布
  • 共享场景

List.copyOf 创建不可变集合,其元素不可修改、不可增删。安全发布保证:不可变集合(如 List.of/copyOf 返回的 ImmutableCollections)配合 final 或正确发布后,其内容对其他线程安全可见(不可变对象天然线程安全,无并发写)。List.copyOf 复制输入元素,隔离后续修改。在共享场景(如缓存、配置、常量发布)中,用 List.copyOf 创建不可变集合并正确发布(如作为 final 字段、或通过同步发布),其他线程读取无需同步且安全。注意:copyOf 是浅拷贝,元素自身若可变仍可能被外部修改,需保证元素不可变或深拷贝。不可变集合的安全发布依赖"对象正确发布 + 内容不可变"。

List.copyOf 不可变集合 + 正确发布 = 线程安全共享,无并发写。但浅拷贝仅保证容器不可变,可变元素需另行处理。

public final class Config {
    public final List<String> items = List.copyOf(rawItems);  // final 不可变,安全发布
}
// 其他线程读取 items 无需同步(不可变 + final 发布)
#
★★

31. JDK 21 顺序集合(Sequenced Collections)

JDK 21 的顺序集合(Sequenced Collections)是什么?

  • SequencedCollection/SequencedSet/SequencedMap
  • 首尾访问方法
  • 接口演进

JDK 21 引入 Sequenced Collections(JEP 431):一组新增接口 SequencedCollection、SequencedSet、SequencedMap,统一了"有序集合"的首尾访问与逆序操作。SequencedCollection 提供 getFirst/getLast/addFirst/addLast/removeFirst/removeLast 和 reversed()(逆序视图);SequencedSet 增加带排重的 addFirst/addLast;SequencedMap 提供 firstEntry/lastEntry、putFirst/putLast 等。它解决了过去"有序集合(List、Deque、LinkedHashSet、TreeMap)首尾操作 API 不统一"的问题,为这些集合提供统一的首尾访问语义。List、Deque、LinkedHashSet、SortedSet、TreeMap 等实现这些接口。

Sequenced Collections 统一有序集合的首尾访问与逆序视图,是 JDK 21 的集合接口演进,让有序集合操作标准化。

List<String> list = new ArrayList<>(List.of("a","b","c"));
list.getFirst();   // "a"
list.addLast("d"); // 追加到末尾
SequencedCollection<String> rev = list.reversed();  // 逆序视图
// LinkedHashSet 实现 SequencedSet,支持 getFirst/addLast
#
★★

32. Java 9 集合工厂方法 List.of/Map.of 的特性与限制

Java 9 集合工厂方法 List.of/Map.of 的特性与限制是什么?

  • List.of/Map.of 不可变集合
  • 不可变与 null 限制
  • 无重复

Java 9 的 List.of/Set.of/Map.of 及 copyOf 工厂方法创建不可变集合:1)不可变:不能增删改,否则抛 UnsupportedOperationException;2)不允许 null 元素/键值(否则 NPE);3)Set.of/Map.of 不允许重复元素/键(否则 IllegalArgumentException);4)不保证迭代顺序(依实现);5)内存高效(ImmutableCollections 紧凑存储)。限制:不支持 null、不可修改、元素数量有限(of 有 10 个参数重载,超过用 copyOf 或 List.of(array))。适合定义常量集合、静态配置。与 Arrays.asList 不同(asList 返回可变固定大小列表)。

List.of/Map.of 是"不可变、禁 null、无重复"的工厂,适合常量集合;与可变的 Arrays.asList 有别。

List<String> l = List.of("a", "b");     // 不可变
l.add("c");    // 抛 UnsupportedOperationException
List.of("a", null);   // 抛 NPE
Set.of("a", "a");     // 抛 IllegalArgumentException(重复)
Map.of("k", "v");     // 不可变 Map
#
★★

33. LinkedHashMap 的访问顺序与 LRU 实现原理

LinkedHashMap 的访问顺序与 LRU 实现原理是什么?

  • 插入顺序与访问顺序
  • 访问顺序 LRU
  • removeEldestEntry

LinkedHashMap 继承 HashMap,额外维护双向链表记录顺序:默认"插入顺序"(accessOrder=false),也可设为"访问顺序"(accessOrder=true)。访问顺序模式下,每次 get/put 被访问的元素会移到链表尾部(被访问即"最近使用"),链表头部即最久未使用(LRU)。基于此可实现 LRU 缓存:构造 LinkedHashMap(accessOrder=true),并覆写 removeEldestEntry 方法,当元素数超过容量时返回 true 以删除链表头(最久未用)元素。这是 LRU 的标准实现。

LinkedHashMap 用双向链表维护顺序,accessOrder=true 时按访问重排(尾部最新),配合 removeEldestEntry 实现 LRU 淘汰。

class LRUCache<K,V> extends LinkedHashMap<K,V> {
    private final int cap;
    LRUCache(int cap) { super(cap, 0.75f, true); this.cap = cap; }  // accessOrder=true
    @Override protected boolean removeEldestEntry(Map.Entry<K,V> e) {
        return size() > cap;   // 超过容量删除最久未用
    }
}
#
★★

34. LinkedList 的双向链表结构与随机访问代价

LinkedList 的双向链表结构与随机访问代价是什么?

  • 双向链表结构
  • 随机访问 O(n)
  • 头尾操作 O(1)

LinkedList 用双向链表实现:每个节点(Node)含元素、前驱指针、后继指针。头尾操作(addFirst/addLast/removeFirst/removeLast、peek)是 O(1),因为维护头尾引用;中间插入/删除需遍历到该位置(O(n));随机访问 get(index) 需从头/尾遍历到目标(O(n),LinkedList 会从近端开始遍历一半)。内存开销大(每节点含两个指针),缓存不友好。相比 ArrayList,LinkedList 的随机访问与中间操作更慢,内存更大。适合频繁头尾操作(Deque)或需要 List 语义但插入为主(仍要遍历)。多数场景 ArrayList 更优。

LinkedList 双向链表,头尾 O(1)、随机访问与中间操作 O(n)、内存开销大。适合头尾队列,不适合随机访问。

LinkedList<String> ll = new LinkedList<>();
ll.addFirst("a"); ll.addLast("b");   // O(1)
String s = ll.get(0);    // O(1)(头)
String mid = ll.get(100); // O(n) 遍历
ll.add(50, "x");          // O(n) 遍历到中间
#
★★

35. 手写一个支持随机删除的 LRU 缓存(LinkedHashMap 与双链表+HashMap 两种实现)?

如何手写一个支持随机删除的 LRU 缓存?有 LinkedHashMap 与双链表+HashMap 两种实现?

  • LinkedHashMap 实现
  • 双链表 + HashMap 实现
  • 随机删除

LRU 缓存两种实现:1)LinkedHashMap(accessOrder=true)实现:继承 LinkedHashMap,设置访问顺序,覆写 removeEldestEntry 在超容量时删除最久未用元素;支持 get/put/随机删除(remove)。2)双链表 + HashMap 实现:用 HashMap 存 key→节点的映射,双向链表维护访问顺序(头=最近使用,尾=最久未用);get/put 时把节点移到链表头,超容量时删除链表尾节点并从 HashMap 删除;随机删除(remove(key))从 HashMap 找到节点,从链表摘下并删除。两种都 O(1) 的 get/put/remove。双链表+HashMap 更可控、不依赖继承。

LRU 核心是"HashMap 定位 + 链表维护顺序"。LinkedHashMap 封装了这层(accessOrder 重排 + removeEldestEntry),双链表+HashMap 手写更透明。

class LRUCache<K,V> {
    private final Map<K, Node<K,V>> map = new HashMap<>();
    private final int cap;
    private final Node<K,V> head = new Node<>(null,null), tail = new Node<>(null,null);
    LRUCache(int cap){ this.cap=cap; head.next=tail; tail.prev=head; }
    V get(K k){ Node<K,V> n=map.get(k); if(n==null) return null; moveToHead(n); return n.v; }
    void put(K k,V v){ Node<K,V> n=map.get(k);
        if(n!=null){ n.v=v; moveToHead(n); }
        else { n=new Node<>(k,v); map.put(k,n); addFirst(n);
            if(map.size()>cap){ Node<K,V> last=tail.prev; remove(last); map.remove(last.k);} } }
    // moveToHead/addFirst/remove 维护链表
}
#
★★

36. List.copyOf 创建的是浅不可变集合还是深不可变对象,元素自身可变时怎样防止状态泄漏

List.copyOf 创建的是浅不可变集合还是深不可变对象?元素自身可变时怎样防止状态泄漏?

  • 浅不可变
  • 深不可变与防御性拷贝
  • 状态泄漏

List.copyOf 创建的是"浅不可变"集合:容器本身不可变(不能增删改),但元素引用是复制的,元素对象自身若可变,外部仍可通过元素引用修改其状态,导致"状态泄漏"(容器声称不可变但元素可变)。要防止状态泄漏:1)保证元素自身不可变(用不可变对象作为元素);2)元素可变时做深拷贝(复制元素对象)或返回不可变视图;3)暴露时返回副本/不可变包装。判断"不可变"要区分"容器不可变"与"元素不可变"。copyOf 只保证容器不可变。

List.copyOf 是浅不可变(容器不可变、元素引用共享)。防状态泄漏需元素不可变或深拷贝,不能只依赖容器不可变。

List<MutableObj> src = new ArrayList<>();
src.add(new MutableObj("a"));
List<MutableObj> copy = List.copyOf(src);   // 浅拷贝,元素引用共享
copy.get(0).setName("changed");             // 修改原元素,状态泄漏
// 防泄漏:不可变元素 或 深拷贝
List<MutableObj> safe = src.stream().map(MutableObj::copy).toList();
#
★★

37. Map.computeIfAbsent 的原子性陷阱

Map.computeIfAbsent 的原子性陷阱是什么?

  • computeIfAbsent 的原子性
  • 并发下的映射函数
  • 递归更新

computeIfAbsent 在键不存在时执行映射函数并放入结果。陷阱:1)并发陷阱:HashMap 的 computeIfAbsent 非线程安全(并发下可能重复执行映射函数或丢数据),应使用 ConcurrentHashMap;2)递归更新陷阱:映射函数内对同一 Map 的修改(尤其 JDK 8 的 HashMap 检测到后抛 IllegalStateException,ConcurrentHashMap 可能死锁);3)映射函数副作用:函数可能被调用多次(在并发或冲突时),不应有副作用;4)映射函数返回 null 时不做插入(键仍不存在)。原子性陷阱主要体现在"映射函数应无副作用、无递归更新、并发安全用 CHM"。

computeIfAbsent 原子性陷阱:并发需 CHM、映射函数无副作用无递归更新、返回 null 不插入。理解函数执行时机与并发语义。

// 并发:用 ConcurrentHashMap 保证原子性
ConcurrentHashMap<String, Integer> chm = new ConcurrentHashMap<>();
chm.computeIfAbsent("k", k -> 1);   // 原子,只执行一次
// 陷阱:映射函数内递归更新同一 map 会抛异常/死锁
// 副作用:函数可能被多次调用,不应有副作用
#
★★

38. Map.merge 在旧值不存在或重映射函数返回 null 时分别做什么,如何用于原子计数

Map.merge 在旧值不存在或重映射函数返回 null 时分别做什么?如何用于原子计数?

  • merge 的语义
  • 旧值不存在/重映射返回 null
  • 原子计数

Map.merge(key, value, remappingFunction):若键不存在,则放入 value(不调用 remappingFunction);若键存在,用 remappingFunction 计算 (oldValue, value) 的新值,若新值为 null 则删除该键,否则放入新值。旧值不存在时直接把 value 放入。用于原子计数:merge(key, 1, Integer::sum) 在键不存在时放入 1,存在时累加——配合 ConcurrentHashMap 可安全原子计数(无需先 get 再 put)。remappingFunction 返回 null 表示删除键。这是"原子累加"的惯用法。

merge 是"原子 upsert":不存在放 value,存在用函数合并,返回 null 删除键。merge(key,1,Integer::sum) 是原子计数惯用法。

ConcurrentHashMap<String, Integer> counts = new ConcurrentHashMap<>();
counts.merge("a", 1, Integer::sum);   // 不存在放 1,存在累加 1
counts.merge("a", 1, Integer::sum);   // "a" -> 2
// remappingFunction 返回 null 时删除键
counts.merge("a", 1, (old, v) -> null);  // 删除 "a"
#
★★

39. PriorityBlockingQueue 默认无界意味着什么,消费者变慢时应如何避免堆内存持续增长

PriorityBlockingQueue 默认无界意味着什么?消费者变慢时应如何避免堆内存持续增长?

  • PriorityBlockingQueue 无界
  • 消费者背压
  • 内存增长控制

PriorityBlockingQueue 默认无界(基于数组的二叉堆,自动扩容),即生产者可以无限入队,不受容量限制。因此若消费者变慢,生产者持续入队会导致队列无限增长,堆内存持续上升直至 OOM。避免策略:1)用有界队列(如 ArrayBlockingQueue)强制背压;2)用容量受限的包装(如创建时限制、或自定义 rejection);3)生产者侧做背压(限流、等待消费者、拒绝投递);4)监控队列大小阈值告警;5)若仍用无界队列,则限制生产速率。无界队列+背压控制是避免内存增长的关键,消费者慢时需生产者减速或拒绝。

PriorityBlockingQueue 无界,吞吐大方但无背压,消费者慢会导致内存无限增长。需有界化或生产者背压/限流+监控。

// 无界:消费者慢时内存增长
PriorityBlockingQueue<Task> q = new PriorityBlockingQueue<>();
// 控制:生产者限流/背压
if (q.size() > MAX) { reject(task); }   // 或阻塞等待
// 或使用有界队列(需自定义优先级)
#
★★

40. PriorityQueue 的二叉堆实现与 comparator 失效

PriorityQueue 的二叉堆实现与 comparator 失效是什么?

  • 二叉堆实现
  • comparator 失效
  • 动态修改元素

PriorityQueue 用二叉堆(优先队列)实现:默认最小堆,元素按自然顺序(Comparable)或给定 Comparator 排序,堆顶是最小元素(优先级最高)。poll() 取堆顶 O(log n),插入 O(log n),peek O(1)。comparator 失效:PriorityQueue 的堆序只在插入/删除时维护;若修改已入堆元素的"比较字段"(元素可变),堆序被破坏,下一个 poll 可能返回错误元素。因此元素入堆后不应修改影响比较的字段,或使用不可变元素。PriorityQueue 非线程安全,线程安全用 PriorityBlockingQueue。

PriorityQueue 是二叉堆,堆序在插入/删除时维护;修改已入堆元素会比较失效。需用不可变元素或重新插入。

PriorityQueue<Task> pq = new PriorityQueue<>(Comparator.comparing(Task::priority));
pq.add(new Task(3)); pq.add(new Task(1));
pq.poll().priority();   // 1(最小)
// comparator 失效:修改已入堆元素的 priority 后堆序破坏
Task t = new Task(2); pq.add(t);
t.priority = 5;   // 堆序失效,poll 结果不保证
#
★★

41. SynchronousQueue 的公平与非公平模式如何匹配生产者和消费者,对吞吐与饥饿有何影响

SynchronousQueue 的公平与非公平模式如何匹配生产者和消费者?对吞吐与饥饿有何影响?

  • SynchronousQueue 语义(无缓冲)
  • 公平模式(队列)与非公平模式(栈)
  • 吞吐与饥饿

SynchronousQueue 无缓冲,每个 put 必须等待一个 take 立即配对(反之亦然),容量为 0。公平模式(公平=transferQueue)用队列结构,先到先得(FIFO),匹配顺序公平,但可能吞吐略低;非公平模式(transferStack)用栈结构(LIFO),吞吐更高(栈顶匹配快),但可能饥饿(栈顶的后来者可能抢先匹配)。公平模式减少饥饿、匹配有序;非公平模式吞吐高、但极端下后到者可能偏好。选型:需要公平保证用公平模式,追求吞吐用非公平。SynchronousQueue 常用于传递性(线程池、无缓冲交换)。

SynchronousQueue 无缓冲,公平=队列(FIFO,公平无饥饿)、非公平=栈(LIFO,吞吐高可饥饿)。按公平/吞吐需求选择。

SynchronousQueue<String> q = new SynchronousQueue<>(true);   // 公平模式
SynchronousQueue<String> q2 = new SynchronousQueue<>(false); // 非公平模式
new Thread(() -> { try { q.put("x"); } catch (InterruptedException e) {} }).start();
String s = q.take();   // 必须配对
#
★★

42. TreeMap 与 ConcurrentSkipListMap 的红黑树/跳表实现差异

TreeMap 与 ConcurrentSkipListMap 的红黑树/跳表实现差异是什么?

  • TreeMap 红黑树
  • ConcurrentSkipListMap 跳表
  • 线程安全与性能

TreeMap 用红黑树(自平衡二叉搜索树)实现,按 key 排序,操作 O(log n),但非线程安全;ConcurrentSkipListMap 用跳表(skip list,多层有序链表)实现,操作 O(log n),线程安全(无锁,基于 CAS)。差异:1)数据结构:红黑树 vs 跳表;2)线程安全:TreeMap 非线程安全,CSLM 线程安全;3)并发:CSLM 无锁、并发友好,TreeMap 需外部同步;4)实现复杂度:跳表更易实现无锁并发。两者都提供有序集合与范围操作(subMap、headMap 等)。并发有序场景用 CSLM,单线程有序用 TreeMap。

TreeMap 红黑树(非线程安全)、CSLK 跳表(无锁线程安全),都 O(log n) 有序。并发有序用跳表,单线程用红黑树。

TreeMap<Integer, String> tm = new TreeMap<>();   // 红黑树,非线程安全
ConcurrentSkipListMap<Integer, String> sm = new ConcurrentSkipListMap<>();  // 跳表,无锁线程安全
sm.put(2, "b"); sm.put(1, "a");
sm.firstKey();   // 1
#
★★

43. TreeMap 的 Comparator 若与 equals 不一致,Map 契约、去重结果和 containsKey 会怎样表现

TreeMap 的 Comparator 若与 equals 不一致,Map 契约、去重结果和 containsKey 会怎样表现?

  • TreeMap 用 Comparator 排序
  • 与 equals 不一致
  • 去重与 containsKey

TreeMap 按 Comparator(或自然排序)组织,比较结果决定排序与去重,而非 equals。若 Comparator 与 equals 不一致:1)两个 equals 相等但 compare != 0 的键会被当作不同键(Map 契约中"equals 相等则视为同一键"被违);2)两个 equals 不等但 compare == 0 的键会被当作同一键(去重/覆盖);3)containsKey 用 Comparator 定位(比较路径),可能因比较语义与 equals 不一致而行为异常。结果是 TreeMap 的去重与查找遵循 Comparator 而非 equals,若二者不一致会破坏 Map 契约。设计时应让 Comparator 与 equals 一致,或明确语义。

TreeMap 以 Comparator 为唯一键语义,与 equals 不一致会导致去重/containsKey 按比较器而非相等性,破坏 Map 契约。

Comparator<String> clen = (a, b) -> Integer.compare(a.length(), b.length());
TreeMap<String, String> m = new TreeMap<>(clen);
m.put("ab", "1"); m.put("cd", "2");   // compare 0(长度相同)视为同一键,覆盖
m.containsKey("xy");   // 长度也相同,可能命中
#
★★

44. TreeMap 的 subMap、headMap 和 tailMap 是实时视图,越界写入与原映射修改如何相互影响

TreeMap 的 subMap、headMap 和 tailMap 是实时视图,越界写入与原映射修改如何相互影响?

  • 范围视图(subMap/headMap/tailMap)
  • 实时视图
  • 越界写入

TreeMap 的 subMap(from, to)、headMap(to)、tailMap(from) 返回"视图"(NavigableSubMap),实时反映原映射:对视图的修改反映到原 TreeMap,反之亦然。越界写入:视图有边界,向视图写入超出其范围(如 subMap 的 to 之外)的键会抛 IllegalArgumentException(范围键不合法)。原映射修改:如果原映射在视图范围内增加/删除键,视图能看到变化;原映射在视图范围外修改不影响视图内容。视图与原映射共享底层树,只是加了范围限制。越界写入抛异常,范围内修改双向可见。

范围视图是原 TreeMap 的实时视图,范围内修改双向可见,越界写入抛 IllegalArgumentException。

TreeMap<Integer, String> m = new TreeMap<>();
m.put(1,"a"); m.put(2,"b"); m.put(3,"c");
NavigableMap<Integer,String> sub = m.subMap(1, 3);   // [1,3) 视图
m.put(2, "B");      // 视图可见
sub.put(2, "B2");   // 反映到原映射
sub.put(5, "e");    // 越界 -> IllegalArgumentException
#
★★

45. WeakHashMap 与 IdentityHashMap 的使用场景

WeakHashMap 与 IdentityHashMap 的使用场景是什么?

  • WeakHashMap 弱引用键
  • IdentityHashMap 引用相等
  • 使用场景

WeakHashMap 用弱引用(WeakReference)作键,当键不再被其他地方强引用时被 GC 回收,条目自动删除;适合"缓存键的生命周期与外部引用一致"的场景(如当对象不活跃时缓存条目消失),避免缓存键造成内存泄漏。IdentityHashMap 用引用相等(==)比较键(而非 equals),适合"按对象身份"映射的场景(序列化对象图、记录已访问对象、避免 equals 重写干扰)。两者使用场景不同:WeakHashMap 用于弱引用缓存(键生命周期由外部强引用决定),IdentityHashMap 用于身份映射(序列化、对象图遍历)。

WeakHashMap 用弱引用键做缓存(键无强引用即回收),IdentityHashMap 用引用相等做身份映射。按"生命周期"与"相等语义"选择。

// WeakHashMap:键无强引用时条目被回收
WeakHashMap<Object, String> cache = new WeakHashMap<>();
Object key = new Object(); cache.put(key, "v");
key = null;   // 之后 GC 回收 key,条目被删
// IdentityHashMap:按对象身份
IdentityHashMap<Object, String> id = new IdentityHashMap<>();
id.put(new String("x"), "1"); id.put(new String("x"), "2");  // 两个不同对象
#
★★

46. WeakHashMap 与 SoftReference/WeakReference 在缓存场景下的内存压力差异

WeakHashMap 与 SoftReference/WeakReference 在缓存场景下的内存压力差异是什么?

  • WeakReference 与 SoftReference 区别
  • WeakHashMap 的弱引用
  • 缓存内存压力

WeakReference 与 SoftReference 都是引用类型:WeakReference 弱引用,对象只要无强引用即被 GC 回收(下一次 GC 即清);SoftReference 软引用,对象在内存不足时才被回收(JVM 内存压力大时)。WeakHashMap 用 WeakReference 作键,键无强引用即回收(WeakReference 语义),因此缓存条目"随键生命周期"被回收,内存压力小但可能过早回收(键一失强引用即删)。SoftReference 适合缓存(内存不足才回收,保留可用缓存),内存压力大时回收。差异:WeakHashMap(弱引用)回收激进、条目生命周期短;SoftReference 缓存回收依赖内存压力、更利于保留。缓存场景:若希望"内存不足才回收"用 SoftReference,若"键无强引用即回收"用 WeakHashMap。

WeakReference 无强引用即回收(WeakHashMap 借此),SoftReference 内存不足才回收(适合缓存)。内存压力差异:WeakHashMap 激进、Soft 保守。

// WeakHashMap:键无强引用即回收(WeakReference 语义)
WeakHashMap<Object, V> m = new WeakHashMap<>();
// SoftReference 缓存:内存不足才回收,保留可用缓存
Cache<Object, V> c = new Cache<>();
c.put(key, new SoftReference<>(value));
#
★★

47. WeakHashMap 的值若强引用自己的键,为何可能阻止条目回收,缓存应如何打破引用环

WeakHashMap 的值若强引用自己的键,为何可能阻止条目回收?缓存应如何打破引用环?

  • 值强引用键
  • 引用环与回收
  • 打破引用环

WeakHashMap 的键是弱引用,但值若强引用键,会形成"键(弱)↔ 值(强引用键)"的引用环:虽然键只有弱引用,但值强引用键,使键无法被回收(值的强引用仍指向键),导致条目不被回收、内存泄漏。WeakHashMap 官方文档明确警告:值不应直接或间接强引用键。打破引用环:1)值不引用键;2)用 WeakReference 包装值;3)值通过弱引用引用键;4)用 SoftReference 值;5)改用其他缓存实现(如 Caffeine)。缓存设计要避免"值→键"的强引用,确保键可被回收。

WeakHashMap 值强引用键形成环,使弱引用键无法回收。打破环:值不引用键或值用弱引用/软引用。

// 反模式:值强引用键
WeakHashMap<Key, Value> m = new WeakHashMap<>();
class Value { Key key; }   // 值保存键 -> 键无法回收
m.put(key, new Value(key));  // 泄漏
// 解决:值不保存键,或值用 WeakReference<Key>
class Value { WeakReference<Key> keyRef; }
#
★★

48. equals 与 hashCode 不一致时 HashMap 的查找行为

equals 与 hashCode 不一致时 HashMap 的查找行为是什么?

  • hashCode 确定桶
  • equals 桶内比较
  • 不一致的后果

HashMap 查找先按 key 的 hashCode 定位桶,再在该桶内用 equals 比较寻找匹配项。若 equals 与 hashCode 不一致(如 equals 相等但 hashCode 不同),两个"相等"的对象会落入不同桶,导致查找失败:get(equalKey) 在错误桶中找不到,containsKey 返回 false,去重(put)失效。原因是桶维度依赖 hashCode,桶内依赖 equals,二者缺一不可。若 hashCode 相等但 equals 不等,则落入同一桶,equals 区分(正常,只是冲突)。因此必须保证 equals 相等则 hashCode 相等,否则 HashMap(及 HashSet)行为异常。

hashCode 定位桶、equals 桶内比较。equals 与 hashCode 不一致,相等对象落入不同桶,查找/去重失败。

class Bad {
    String id;
    public boolean equals(Object o) { return o instanceof Bad b && b.id.equals(id); }
    // 未重写 hashCode(集成 Object 的地址哈希)
}
// 相同 id 的两个对象 hashCode 不同 -> 不同桶
m.put(new Bad("1"), "x");
m.get(new Bad("1"));   // 找不到,返回 null
#
★★

49. fail-fast 与 fail-safe 迭代器的实现差异是什么,modCount 检测与 CopyOnWriteArrayList 快照机制各如何工作

fail-fast 与 fail-safe 迭代器的实现差异是什么?modCount 检测与 CopyOnWriteArrayList 快照机制各如何工作?

  • fail-fast(modCount 检测)
  • fail-safe(快照)
  • 迭代器差异

fail-fast 迭代器(ArrayList、HashMap 等)维护 modCount 快照,每次迭代时检查 modCount 是否变化,若被结构修改(并发或迭代中修改)则抛 ConcurrentModificationException,快速失败。fail-safe 迭代器(CopyOnWriteArrayList、ConcurrentSkipList 等)不抛 CME,遍历的是"快照"或弱一致数据:CopyOnWriteArrayList 的迭代器在创建时持有底层数组快照,后续写创建新数组,迭代器仍遍历旧数组,并发修改不影响遍历、不抛 CME。差异:fail-fast 用 modCount 检测并快速失败(可能漏改),fail-safe 用快照/弱一致避免异常(可能不反映最新数据)。两种机制各有取舍。

fail-fast 用 modCount 检测并发修改并抛 CME,fail-safe 用快照/弱一致遍历不抛异常。CopyOnWriteArrayList 迭代器是数组快照。

// fail-fast:modCount 检测
List<String> l = new ArrayList<>();
for (String s : l) { l.add("x"); }   // 抛 CME
// fail-safe:CopyOnWriteArrayList 迭代器快照
CopyOnWriteArrayList<String> c = new CopyOnWriteArrayList<>();
for (String s : c) { c.add("x"); }   // 不抛 CME,遍历旧快照
#
★★

50. 已知批量元素数量时,怎样使用 ArrayList 初始容量或 ensureCapacity 减少扩容与数组复制

已知批量元素数量时,怎样使用 ArrayList 初始容量或 ensureCapacity 减少扩容与数组复制?

  • 初始容量
  • ensureCapacity
  • 减少扩容复制

已知批量元素数量时,用构造器指定初始容量(new ArrayList<>(expectedSize))或加入前调用 ensureCapacity(expectedSize) 预分配容量,避免 ArrayList 在批量加入过程中多次扩容(每次扩容复制数组)。因为扩容是 O(n) 复制,提前一次性分配容量可避免多次复制,提升性能。示例:new ArrayList<>(1000) 直接分配 1000 容量,addAll 或循环 add 时无需扩容。ensureCapacity 动态预扩容也非常有用(在批量前调用)。注意容量是数组长度(可大于 size),预留空间不影响逻辑。

预分配容量(构造器或 ensureCapacity)避免批量加入时的多次扩容复制,是 ArrayList 性能优化的重要技巧。

// 已知数量:构造器指定初始容量
int n = names.size();
List<String> list = new ArrayList<>(n);
// 或批量前 ensureCapacity
ArrayList<String> list2 = new ArrayList<>();
list2.ensureCapacity(10000);
for (...) list2.add(x);   // 不触发扩容
#
★★

51. 把可变对象作为 HashMap 键后再修改参与哈希的字段,会导致哪些查找与删除异常

把可变对象作为 HashMap 键后再修改参与哈希的字段,会导致哪些查找与删除异常?

  • 可变对象作键
  • 修改哈希字段
  • 查找/删除异常

把可变对象作为 HashMap 键后,若修改了参与 hashCode 的字段,会导致该对象的 hashCode 改变,但它在桶中的位置仍是按旧 hashCode 放置的。之后:get 用新 hashCode 定位到错误桶,找不到该键(返回 null);containsKey 返回 false;remove 也无法删除(定位到错误桶);且该键永久留在 HashMap 中(内存泄漏)。因为 HashMap 的桶位置由 hashCode 决定,修改哈希字段后位置不一致。避免:用不可变对象作键,或修改字段后重新 put/remove(且不保证顺序),重写 hashCode 时只用不可变字段。

键的 hashCode 改变后桶位置失效,get/remove 定位错误桶,键泄漏。需用不可变键或保证 hashCode 字段不变。

class MutableKey { int id; public int hashCode() { return id; } }
MutableKey k = new MutableKey(); k.id = 1;
Map<MutableKey, String> m = new HashMap<>();
m.put(k, "v");
k.id = 2;          // 修改哈希字段
m.get(k);          // 定位到错误桶,返回 null
m.containsKey(k);  // false
m.remove(k);       // 无法删除,键泄漏
#
★★

52. 流式集合(Streams)与集合操作的边界

流式集合(Streams)与集合操作的边界是什么?

  • Stream 与 Collection 的区别
  • 惰性 vs 急切
  • 一次性消费

Stream 与 Collection 边界:Collection 是"数据容器"(存储元素,可重复访问、可修改),Stream 是"计算管道"(描述对数据的操作,惰性求值、一次性消费)。Stream 不存储数据,只是数据源(Collection/数组/生成器)上的操作视图;Stream 是惰性的(中间操作延迟到终止操作才执行)、不可重复使用(终端操作后 Stream 关闭,再次使用抛 IllegalStateException)。集合操作是急切的数据结构操作,Stream 是惰性的函数式管道。边界:数据存储用 Collection,数据处理用 Stream,Stream 迭代元素后不可复用。

Collection 存数据、Stream 算数据。Stream 惰性、一次性、无存储,与集合的急切、可反复、有存储相对。

List<String> list = List.of("a","b","c");   // 集合:存储
Stream<String> s = list.stream().filter(x -> x.startsWith("a"));  // Stream:惰性
s.forEach(System.out::println);
s.count();   // 抛 IllegalStateException(已消费)
#
★★

53. 访问顺序 LinkedHashMap 的 get 操作为何会改变结构,作为并发 LRU 缓存时应如何同步

访问顺序 LinkedHashMap 的 get 操作为何会改变结构?作为并发 LRU 缓存时应如何同步?

  • accessOrder 重排
  • get 改变结构
  • 并发同步

访问顺序模式下(accessOrder=true),每次 get 或 put 命中会把该元素移到链表尾部(最近使用),因此 get 是"结构性修改"(改变链表顺序),即使逻辑上只是读取。作为并发 LRU 缓存时,多个线程同时 get/put 会并发修改链表,LinkedHashMap 非线程安全,需同步:用 Collections.synchronizedMap 包装(但迭代需加锁),或把整个 LRU 操作包在 synchronized 块内,或使用线程安全 LRU(如 ConcurrentLinkedHashMap / Caffeine)。由于 get 改变结构,并发下必须保证访问顺序更新与淘汰的原子性。

accessOrder=true 时 get 重排链表(结构性修改),并发 LRU 需同步整个访问+淘汰操作,或用线程安全 LRU 实现。

class LRUCache<K,V> extends LinkedHashMap<K,V> {
    private final int cap;
    LRUCache(int cap){ super(cap, 0.75f, true); this.cap=cap; }
    protected boolean removeEldestEntry(Map.Entry<K,V> e){ return size()>cap; }
}
// 并发同步:所有操作包在 synchronized 内
Map<K,V> cache = Collections.synchronizedMap(new LRUCache<>(cap));
#
★★

54. 跨服务传输集合时为何应优先定义稳定 DTO,而不是依赖具体集合实现的 Java 序列化形式

跨服务传输集合时为何应优先定义稳定 DTO,而不是依赖具体集合实现的 Java 序列化形式?

  • 传输契约与服务边界
  • Java 序列化与集合实现耦合
  • 稳定 DTO 的优势

跨服务传输应用稳定 DTO(定义字段与类型),而非直接序列化具体集合实现,原因:1)具体集合实现(如 ArrayList、HashMap)的 Java 序列化形式与内部结构耦合,跨语言/跨版本不稳定,内部实现变化(如 HashMap 结构演进)破坏传输;2)DTO 定义稳定契约,字段类型明确,便于版本兼容、跨语言(JSON/Protobuf)与校验;3)避免把内部实现细节(集合类型、容量)暴露到外部;4)DTO 可定制序列化(忽略字段、脱敏)。服务间传输应基于字段契约而非集合实现。Java 原生序列化本身也有安全与兼容问题,更应避免。

传输是"契约"而非"实现":DTO 定义稳定字段与类型,具体集合实现与内部结构耦合、跨版本/跨语言不稳定。服务间用 DTO。

// 反模式:直接传输具体集合
class Response { ArrayList<Item> items; }   // 依赖具体实现
// 推荐:稳定 DTO
class Response { List<ItemDto> items; }     // 接口类型 + DTO 字段
// 用 JSON 定义契约,而非 Java 序列化集合实现
#
★★

55. 遍历 Collections.synchronizedList 时为什么仍需对返回列表加锁,流式遍历是否例外

遍历 Collections.synchronizedList 时为什么仍需对返回列表加锁?流式遍历是否例外?

  • synchronizedList 的迭代加锁
  • 流式遍历
  • 例外情况

Collections.synchronizedList 的单个方法(add/get)是同步的,但迭代器(iterator)本身不保证同步,遍历时若其他线程并发修改列表,可能抛 CME 或看到不一致数据。因此遍历(for-each、iterator)时必须手动对返回列表加锁(synchronized(list)),JDK 文档明确要求。流式遍历(stream)同样不是例外:流的遍历基于迭代器/内部迭代,不会自动加锁,仍需外部同步(synchronized 包裹或用线程安全集合)。唯一例外是使用线程安全且快照/弱一致的集合(如 CopyOnWriteArrayList),但 synchronizedList 不具备。所以 synchronizedList 的所有遍历(含 stream)都需手动加锁。

synchronizedList 只同步单方法,迭代器与流不自动加锁,遍历(含 stream)需手动 synchronized,只有快照集合才免。

List<String> list = Collections.synchronizedList(new ArrayList<>());
synchronized (list) {              // 遍历必须加锁
    for (String s : list) { ... }
}
synchronized (list) {              // 流式遍历同样需加锁
    list.stream().forEach(...);
}
#
★★

56. 集合创建 Stream 后再结构性修改源集合属于什么干扰行为,结果为何不能依赖 fail-fast

集合创建 Stream 后再结构性修改源集合属于什么干扰行为?结果为何不能依赖 fail-fast?

  • Stream 与源集合修改
  • 未定义行为
  • fail-fast 不可依赖

集合创建 Stream 后,若在 Stream 管道执行期间(创建后到终端操作)结构性修改源集合,属于"未定义行为"(Javadoc 说明):Stream 的源是集合的当前状态,但中间操作可能已通过迭代器/快照读取部分数据,修改源集合可能导致结果不确定(部分 old 部分 new 数据、抛异常或返回错误结果)。不能依赖 fail-fast:虽然 ArrayList 的 stream 迭代器可能因 modCount 变化抛 CME,但这是"尽力而为"的检测,不是保证——Java 规范明确 stream 的源修改是未定义行为,不保证抛 CME。因此应避免在 Stream 执行期间修改源集合,结果不可依赖。

流管道执行期间修改源集合是未定义行为,fail-fast 的 CME 只是尽力检测非保证,结果不可依赖。应避免修改源。

List<String> list = new ArrayList<>(List.of("a","b"));
Stream<String> s = list.stream().filter(x -> true);
list.add("c");          // 修改源集合(未定义行为)
s.forEach(System.out::println);   // 结果不确定,可能抛 CME 或返回部分数据
#
★★

57. ArrayDeque 通过循环数组实现双端操作时如何处理头尾回绕,为什么不允许存储 null

ArrayDeque 通过循环数组实现双端操作时如何处理头尾回绕?为什么不允许存储 null?

  • 循环数组与回绕
  • 头尾指针
  • 禁止 null 的原因

ArrayDeque 用循环数组(circular array)实现,维护 head 和 tail 索引,入队/出队时指针回绕(取模)到数组另一端,实现高效的 O(1) 双端操作。回绕处理:head 前移时 (head - 1) & (len - 1)(len 是 2 的幂),tail 后移时 (tail + 1) & (len - 1),数组满时扩容。禁止 null:ArrayDeque 不允许存储 null,因为 null 用作"空位标记"——判断队列空/满时用 head==tail,若允许 null 会与空位混淆,无法区分"元素为 null"与"无元素"。因此 Deque 相关操作 add/offer 传入 null 抛 NPE。

循环数组用位运算回绕指针,null 用作空位标记故禁止存储。这是 ArrayDeque 不允许 null 的根本原因。

ArrayDeque<String> dq = new ArrayDeque<>();
dq.addFirst("a");
dq.addLast("b");
dq.add(null);   // 抛 NPE(禁止 null,null 作空位标记)
// 回绕:head = (head - 1) & (elements.length - 1)
#
★★

58. BitSet 的 size、length 和 cardinality 分别表示什么,序列化稀疏高位数据时有何影响

BitSet 的 size、length 和 cardinality 分别表示什么?序列化稀疏高位数据时有何影响?

  • size/length/cardinality 语义
  • 稀疏高位数据
  • 序列化影响

BitSet 三个方法:size() 返回内部存储的位数(容量,words 数组总位数),length() 返回最高置位位 + 1(逻辑最大索引),cardinality() 返回置位(1)的个数。稀疏高位数据(如只设置了 bit 1000000):size() 可能很大(内部数组按最高位分配),length() 也到 1000000+1,但 cardinality 很小。序列化影响:BitSet 序列化按内部 words 存储(含高位空字),稀疏高位数据会序列化大量"空位"(高位的 word 都是 0),导致序列化体积大、浪费空间。处理:稀疏数据考虑用压缩/其他表示,或接受体积。

size 是容量、length 是最高置位+1、cardinality 是置位个数。稀疏高位数据序列化会带上大量空 word,体积大。

BitSet bs = new BitSet();
bs.set(1000000);          // 只设一位
bs.size();        // 大(按 1000000 分配)
bs.length();      // 1000001
bs.cardinality(); // 1(置位个数)
// 序列化会包含空 word,体积大
#
★★

59. BitSet 的位运算加速与序列化

BitSet 的位运算加速与序列化是什么?

  • 位运算(AND/OR/XOR)
  • 集合运算
  • 序列化

BitSet 用 long[] 存储位,支持位运算(and、or、xor、andNot)按 word(long)批量运算,加速集合运算:两个 BitSet 的交集/并集/差集用整字位运算实现,O(n/64)(word 数),比逐位快得多。适合做集合运算、位图、标记。序列化:BitSet 实现 Serializable,序列化按内部 words 存储,包含容量相关的高位;稀疏高位数据序列化体积大(序列化会按整个 words 数组写入,稀疏场景下大量高位空位同样占用空间)。做位运算是 BitSet 的核心价值(集合运算、压缩布尔标记),但序列化需注意稀疏数据体积。

BitSet 用 long[] 支持整字位运算(AND/OR/XOR),集合运算高效;序列化按 words 存储,稀疏高位体积大。

BitSet a = new BitSet(); a.set(1); a.set(3);
BitSet b = new BitSet(); b.set(3); b.set(5);
a.and(b);          // 交集:{3}
a.or(b);           // 并集
a.xor(b);          // 对称差
a.andNot(b);       // 差集
#
★★

60. BlockingQueue 的常见实现(Array/Linked/Priority/Delay)

BlockingQueue 的常见实现有哪些?包括 Array、Linked、Priority、Delay?

  • ArrayBlockingQueue 有界
  • LinkedBlockingQueue 无界/有界
  • PriorityBlockingQueue 优先级

BlockingQueue 常见实现:1)ArrayBlockingQueue:有界数组队列,固定容量,公平/非公平锁,生产者消费者阻塞;2)LinkedBlockingQueue:链表队列,默认无界(可指定容量),put/take 两把锁;3)PriorityBlockingQueue:无界优先队列(二叉堆),元素按优先级出队;4)DelayQueue:无界延迟队列,元素需实现 Delayed,getDelay 到期才可出队(用于定时任务)。用途:ArrayBlockingQueue 用于有界背压,LinkedBlockingQueue 用于无界/有界 FIFO,PriorityBlockingQueue 用于优先级调度,DelayQueue 用于延迟/定时。它们都支持阻塞 put/take(满时阻塞生产者、空时阻塞消费者)。

四种 BlockingQueue 各有侧重:有界背压(Array/Linked)、优先级(Priority)、延迟(Delay)。按背压/顺序/延迟需求选择。

BlockingQueue<Integer> aq = new ArrayBlockingQueue<>(100);   // 有界
BlockingQueue<Integer> lq = new LinkedBlockingQueue<>();     // 无界
BlockingQueue<Task> pq = new PriorityBlockingQueue<>();      // 优先级
DelayQueue<DelayedTask> dq = new DelayQueue<>();             // 延迟
aq.put(1);   // 满则阻塞
#
★★

61. Collection.toArray(IntFunction) 如何避免不安全的数组强转,目标数组元素类型由谁决定

Collection.toArray(IntFunction) 如何避免不安全的数组强转?目标数组元素类型由谁决定?

  • toArray(IntFunction)(JDK 11+)
  • 数组类型安全
  • 目标数组类型

Collection.toArray(IntFunction<T[]>)(JDK 11+)接收一个生成数组的函数,如 toArray(String[]::new),返回类型为 T[](与集合元素类型一致),避免了传统的 toArray(new T[0]) 的强转(Object[] 强转 T[] 可能 ClassCastException)。目标数组元素类型由传给 IntFunction 的数组类型决定:String[]::new 生成 String[],因此返回 String[],类型安全。编译器根据 IntFunction 的泛型推断返回类型,避免强转。相比 toArray()(返回 Object[],需强转)更安全。

toArray(IntFunction) 按函数生成正确类型的数组,返回类型由函数决定,避免 Object[] 强转的 ClassCastException。

List<String> list = List.of("a","b");
String[] arr = list.toArray(String[]::new);   // 返回 String[],类型安全
String[] arr2 = list.toArray(new String[0]);  // 传统方式(也可能)
// list.toArray() 返回 Object[],需强转有风险
#
★★

62. DelayQueue 如何结合 getDelay 与 compareTo 决定出队顺序,延迟相同的元素如何保证确定性

DelayQueue 如何结合 getDelay 与 compareTo 决定出队顺序?延迟相同的元素如何保证确定性?

  • Delayed 接口(getDelay/compareTo)
  • 出队顺序
  • 延迟相同处理

DelayQueue 的元素必须实现 Delayed 接口(getDelay(TimeUnit) + compareTo)。队内用 PriorityQueue 按 compareTo 排序(延迟最小者在前),出队时检查队头 getDelay() 是否到期(<=0),到期才 poll 出队,否则阻塞等待。因此出队顺序由 compareTo 决定(通常按延迟时间),getDelay 决定是否可出队。延迟相同的元素:compareTo 若返回 0,PriorityQueue 不保证稳定顺序(无确定顺序),可能不确定性出队。若要保证确定性,compareTo 需在延迟相同时增加次级排序键(如序号/插入时间)作为 tie-breaker,使延迟相同的元素有确定顺序。

DelayQueue 用 compareTo 排序(PriorityQueue)+ getDelay 判断到期。延迟相同需 compareTo 加次级键保证确定性。

class DelayedTask implements Delayed {
    long deadline; long seq;
    public long getDelay(TimeUnit u) { return u.convert(deadline - System.nanoTime(), NANOSECONDS); }
    public int compareTo(Delayed o) {
        int c = Long.compare(deadline, ((DelayedTask)o).deadline);
        return c != 0 ? c : Long.compare(seq, ((DelayedTask)o).seq);  // 次级键保证确定性
    }
}
#
★★

63. EnumMap 与 EnumSet 的位图实现

EnumMap 与 EnumSet 的位图实现是什么?

  • EnumMap 数组实现
  • EnumSet 位图实现
  • 高效原因

EnumMap 用"数组"实现:内部按枚举常量排序的数组(按 ordinal 索引),put/get 直接按 ordinal 访问数组,O(1) 且无 HashMap 的哈希开销,内存紧凑。EnumSet 用"位图"实现:用 long(或 long[])的位表示每个枚举常量是否存在(位索引对应 ordinal),contains/add/remove 是位操作 O(1),批量操作(containsAll 等)用位运算。两者都因枚举常量数量有限且 ordinal 已知而可用数组/位图,实现高效紧凑。EnumMap 适合枚举值到值的映射,EnumSet 适合枚举值集合/标记。

EnumMap 用数组按 ordinal 索引,EnumSet 用位图按 ordinal 置位,都因枚举有限而高效紧凑。适用枚举映射与集合。

EnumMap<Day, String> m = new EnumMap<>(Day.class);   // 数组按 ordinal 索引
m.put(Day.MON, "mon");
EnumSet<Day> s = EnumSet.of(Day.MON, Day.FRI);       // 位图
s.contains(Day.MON);   // 位操作 O(1)
#
★★

64. ImmutableCollections 的内部表示(NIL/SetN 等)

ImmutableCollections 的内部表示(NIL/SetN 等)是什么?

  • ImmutableCollections
  • ListN/SetN/MapN 等
  • 紧凑存储

ImmutableCollections 是 JDK 9+ 元素工厂方法(List.of、Set.of、Map.of、copyOf)返回的不可变集合的内部实现类集合,命名如 ListN、SetN、MapN、List12、Set12、Map1 等。内部表示:N 变体(ListN/SetN/MapN)用紧凑数组存储元素(SetN/MapN 用数组哈希表,用开放寻址/线性探测),按元素数量选择 1/2/N 变体(Set12 用简单字段)。特点:不可变(无 modCount、无扩容)、内存紧凑、无 null。NIL 表示空集合(如 ListN 空、SetN 的空表示)。这些是内部实现,开发不应依赖具体类名,但理解其紧凑存储有助于了解内存。

ImmutableCollections 是工厂方法返回的不可变集合内部实现,按元素数量用 ListN/SetN/MapN/List12 等紧凑数组/字段存储,NIL 表示空。

List<String> l = List.of("a","b");   // 内部可能是 ListN(数组存储)
Set<String> s = Set.of("a");         // 内部可能是 Set12
Map<String,Integer> m = Map.of("k",1); // Map1
// 内部实现类名(ListN/SetN/MapN 等)是 JDK 内部,不应依赖
#
★★

65. JDK 21 SequencedCollection 的 reversed 返回何种视图,双向修改时首尾语义如何对应

JDK 21 SequencedCollection 的 reversed 返回何种视图?双向修改时首尾语义如何对应?

  • reversed() 逆序视图
  • 双向修改
  • 首尾语义

SequencedCollection.reversed() 返回一个"逆序视图"(reversed view),它是原集合的视图而非副本:对逆序视图的修改反映到原集合(反之亦然)。逆序视图的首尾与原集合相反:视图的 getFirst() 对应原集合的 getLast(),getLast() 对应原集合的 getFirst()。双向修改:在逆序视图上 addFirst 会在原集合尾部添加,addLast 会在原集合头部添加;removeFirst/removeLast 同理。因此首尾语义在逆序视图上镜像。reversed() 是 O(1) 视图(不复制),可用于非破坏性逆序遍历。

reversed() 是逆序视图(非副本),首尾语义镜像,双向修改反映到原集合。适合逆序访问且不改数据。

List<String> list = new ArrayList<>(List.of("a","b","c"));
SequencedCollection<String> rev = list.reversed();  // 逆序视图
rev.getFirst();   // "c"(原 last)
rev.addLast("d"); // 在原集合头部添加 "d"
list.getFirst();  // "d"
#
★★

66. LinkedBlockingQueue 未显式指定容量时有什么风险,容量与生产者背压应怎样协同配置

LinkedBlockingQueue 未显式指定容量时有什么风险?容量与生产者背压应怎样协同配置?

  • LinkedBlockingQueue 默认无界
  • 无界风险
  • 容量与背压

LinkedBlockingQueue 若未显式指定容量,默认是"无界"(Integer.MAX_VALUE),生产者可无限入队,消费者慢时队列无限增长,导致内存 OOM。风险:无背压。配置:应显式指定容量(new LinkedBlockingQueue<>(capacity))形成有界队列,配合生产者背压:队列满时 put 阻塞生产者(天然背压),或 offer 带超时/拒绝策略。容量与背压协同:容量决定缓冲窗口,过大则内存占用高、背压弱;过小则吞吐受限、生产者常阻塞。根据系统吞吐与消费者平均处理速度合理配置容量,并用 put 阻塞或 offer 超时实现背压,监控队列水位。

LinkedBlockingQueue 默认无界有 OOM 风险,应显式设容量并配合 put 阻塞/offer 超时实现背压,按吞吐与消费速度配置容量。

// 无界风险:默认无界 Integer.MAX_VALUE
LinkedBlockingQueue<Task> q = new LinkedBlockingQueue<>();   // 无界
// 有界 + 背压
LinkedBlockingQueue<Task> q2 = new LinkedBlockingQueue<>(1000);  // 有界
q2.put(task);        // 满则阻塞(背压)
q2.offer(task, 5, TimeUnit.SECONDS);  // 超时 offer
#
★★

67. TreeMap 的底层红黑树实现细节(旋转/染色)与一致性遍历?

TreeMap 的底层红黑树实现细节(旋转/染色)与一致性遍历是什么?

  • 红黑树性质
  • 旋转/染色
  • 一致性遍历

TreeMap 底层是红黑树(自平衡二叉搜索树),满足红黑树性质:根黑、红节点的子节点黑、任一节点到叶子的黑色节点数相同(黑高相等)、null 为黑。插入/删除通过"旋转"(左旋/右旋)与"染色"(改变红黑)恢复平衡,保证树高 O(log n),操作 O(log n)。遍历(中序遍历)按 key 升序,得到有序结果。一致性遍历:TreeMap 的迭代器按 key 顺序遍历(中序),遍历期间若结构修改(非迭代器方法)会抛 CME(fail-fast)。红黑树平衡保证查找/插入/删除 O(log n) 且有序遍历。

TreeMap 红黑树用旋转/染色维持平衡(O(log n)),中序遍历有序,迭代器 fail-fast。平衡树保证有序集合高效。

// TreeMap 插入平衡:左旋/右旋 + 染色(红黑)
TreeMap<Integer, String> tm = new TreeMap<>();
tm.put(3,"c"); tm.put(1,"a"); tm.put(2,"b");   // 自动平衡
for (Integer k : tm.keySet()) { ... }   // 中序遍历:1,2,3 有序
#

68. List.of 与 Set.of 为什么拒绝 null,构造 Set 时出现重复元素又会抛出什么异常

List.of 与 Set.of 为什么拒绝 null?构造 Set 时出现重复元素又会抛出什么异常?

  • 拒绝 null 的原因
  • Set.of 重复元素异常
  • 不可变集合约束

List.of/Set.of 拒绝 null:因为这些是不可变集合,null 在不可变集合中语义不明(且与"无元素"混淆),JDK 明确元素工厂方法不允许 null(抛 NPE)。Set.of 构造时若出现重复元素(equals 相等),抛 IllegalArgumentException(重复元素)。原因:Set 语义要求元素唯一,工厂方法在构建时检测重复并报错,避免运行时模糊。List.of 允许重复(List 允许重复元素),只是拒 null。这些约束保证不可变集合的语义清晰。

List.of/Set.of 拒 null(NPE),Set.of 重复元素抛 IllegalArgumentException。不可变集合语义严格。

List.of("a", null);   // 抛 NPE
Set.of("a", "a");     // 抛 IllegalArgumentException(重复)
List.of("a", "a");    // 允许(List 允许重复)
Set.of("a", "b");     // 正常
#

69. List、Set、Map 接口层次与典型实现类的特性对比

List、Set、Map 接口层次与典型实现类的特性如何对比?

  • List/Set/Map 接口
  • 典型实现类
  • 特性对比

List(有序、可重复、可索引访问)— 实现:ArrayList(动态数组,随机访问快)、LinkedList(双向链表,头尾操作快);Set(无序/去重、不可重复)— 实现:HashSet(哈希,O(1))、LinkedHashSet(哈希+链表,保插入顺序)、TreeSet(红黑树,有序);Map(键值对)— 实现:HashMap(哈希)、LinkedHashMap(保序)、TreeMap(有序)、ConcurrentHashMap(线程安全)。对比:List 有序可重复,Set 去重无序(或有序),Map 键值映射。选择依据:是否需要索引/顺序/去重/排序/线程安全。

List 有序可重复、Set 去重、Map 键值。实现类按需求(哈希/链表/树/保序/线程安全)选择。

List<String> list = new ArrayList<>();      // 有序可重复,随机访问
Set<String> set = new HashSet<>();          // 去重无序
Set<String> lhs = new LinkedHashSet<>();    // 去重保插入顺序
Map<String,Integer> map = new HashMap<>();  // 键值
TreeMap<String,Integer> tm = new TreeMap<>(); // 有序
#

70. Map.compute、computeIfAbsent 与 putIfAbsent 对函数调用和 null 值处理有哪些关键差异

Map.compute、computeIfAbsent 与 putIfAbsent 对函数调用和 null 值处理有哪些关键差异?

  • compute/computeIfAbsent/putIfAbsent 语义
  • 函数调用时机
  • null 值处理

三者差异:putIfAbsent(key, value):键不存在才放入 value(不调用函数),不处理 null 特殊化;computeIfAbsent(key, fn):键不存在时调用 fn 计算并放入,fn 返回 null 则不放入(键保持不存在);compute(key, fn):无论键是否存在都调用 fn(oldVal, param) 计算新值,fn 返回 null 则删除键。关键差异:1)函数调用时机:putIfAbsent 无函数,computeIfAbsent 仅键不存在时调用,compute 总是调用;2)null 处理:computeIfAbsent 返回 null 不插入,compute 返回 null 删除键,putIfAbsent 允许 null 值(放入)。用途:putIfAbsent 简单填充,computeIfAbsent 原子惰性初始化,compute 原子更新。

putIfAbsent 无条件填值、computeIfAbsent 缺失才计算(null 不插入)、compute 总是计算(null 删除)。按"是否调用函数、null 语义"选择。

map.putIfAbsent("k", v);            // 键不存在才放值
map.computeIfAbsent("k", k -> f(k)); // 缺失才计算,null 不插入
map.compute("k", (k, old) -> old == null ? 1 : old + 1);  // 总是计算,null 删除键
#

71. PriorityQueue 中比较结果相同的元素是否保持插入顺序,需要稳定优先级时应如何设计键

PriorityQueue 中比较结果相同的元素是否保持插入顺序?需要稳定优先级时应如何设计键?

  • PriorityQueue 不稳定
  • 比较相同不保序
  • 稳定优先级设计

PriorityQueue 不保证稳定顺序:当两个元素比较结果相同(compare 返回 0)时,出队顺序不确定(不保证保持插入顺序),因为二叉堆的非稳定排列。若需要稳定优先级(comparison 相同按插入顺序出队),应设计键:在比较中包含一个递增序号(如比较优先级后,再比较插入序号),使"优先级相同"的元素按序号有序,从而稳定。即 compareTo 在优先级相同时用 seq(插入序号)作为次级键,保证 FIFO 次序。这是稳定优先队列的常见实现。

PriorityQueue 不稳定,比较相同不保序。加递增序号作次级比较键,使优先级相同按插入顺序,实现稳定。

class StableTask implements Comparable<StableTask> {
    int prio; long seq;
    public int compareTo(StableTask o) {
        int c = Integer.compare(prio, o.prio);
        return c != 0 ? c : Long.compare(seq, o.seq);  // 次级键使稳定
    }
}
#

72. SequencedMap 的 putFirst 和 putLast 在有序映射上如何定义,SortedMap 实现为何可能拒绝操作

SequencedMap 的 putFirst 和 putLast 在有序映射上如何定义?SortedMap 实现为何可能拒绝操作?

  • SequencedMap putFirst/putLast
  • 有序映射
  • SortedMap 拒绝

SequencedMap(JDK 21)的 putFirst/putLast 在有序映射上定义首尾插入:putFirst 把键值插入到映射最前面,putLast 到最后面。对 LinkedHashMap 等保插入顺序的映射有意义。但 SortedMap 实现(如 TreeMap)的排序由 key 决定(Comparator/自然序),插入位置由键的相对顺序决定,不能由调用方指定"首/尾"——若 putFirst 的键在排序中不是最小,或 putLast 不是最大,则无法按请求插入,会抛 UnsupportedOperationException(或不支持该操作)。因此 SortedMap 可能拒绝 putFirst/putLast,因为其顺序由 key 排序而非插入位置决定。

SortedMap 顺序由 key 决定,putFirst/putLast 与排序语义冲突,可能抛 UnsupportedOperationException。LinkedHashMap 等保序映射才支持。

SequencedMap<String,Integer> lm = new LinkedHashMap<>();  // 保序映射
lm.putLast("a", 1);   // 支持
TreeMap<String,Integer> tm = new TreeMap<>();  // SortedMap
tm.putFirst("z", 1);  // 可能抛 UnsupportedOperationException(顺序由 key 决定)
#

73. Spliterator 在并行流中的角色

Spliterator 在并行流中的角色是什么?

  • Spliterator 拆分流
  • trySplit
  • 并行任务划分

Spliterator(可拆分迭代器)是 Stream 并行化的核心:它把数据源拆分成多个子块(trySplit),供并行流分配到不同线程并行处理。角色:1)提供元素遍历(tryAdvance/forEachRemaining);2)trySplit 递归拆分数据源成左右子 Spliterator,形成分治并行;3)特征(characteristics:SIZED、ORDERED、DISTINCT 等)告知 Stream 优化假设。并行流依赖 Spliterator 的拆分平衡与特征,决定并行度与结果正确性。错误实现 trySplit 会破坏并行结果。

Spliterator 通过 trySplit 拆分数据源供并行流分治处理,特征决定优化。是并行流的基石。

// Spliterator 接口
public interface Spliterator<T> {
    boolean tryAdvance(Consumer<? super T> action);
    Spliterator<T> trySplit();     // 拆分,供并行
    long estimateSize();
    int characteristics();
}
list.parallelStream().forEach(x -> ...);  // 内部用 Spliterator 拆分
#

74. Vector/Stack 的遗留问题与替代方案

Vector/Stack 的遗留问题与替代方案是什么?

  • Vector/Stack 遗留
  • 全方法同步
  • 替代方案

Vector 与 Stack 是 Java 1.0 遗留的同步集合:Vector 所有方法 synchronized(线程安全但性能差),Stack 继承 Vector 实现栈(也用同步)。遗留问题:1)全方法同步导致性能差,现代并发下用更细致的机制;2)Stack 继承 Vector 设计不良(栈语义被破坏,可随意访问非栈顶元素);3)API 落后(Stack 用 push/pop/peek,Vector 用 add/remove)。替代方案:Vector → ArrayList(非线程安全)或 Concurrent 集合(线程安全);Stack → ArrayDeque(用 Deque 做栈,性能好、API 现代)或并发 Deque。现代 Java 基本不用 Vector/Stack。

Vector/Stack 是遗留同步集合,性能差、设计不良(Stack 继承 Vector)。替代:ArrayList/ArrayDeque 或并发集合。

// 遗留:Vector/Stack 全同步
Vector<String> v = new Vector<>();   // 全方法 synchronized
Stack<String> s = new Stack<>();     // 继承 Vector
// 替代:
ArrayList<String> list = new ArrayList<>();   // 非线程安全
Deque<String> stack = new ArrayDeque<>();     // 栈(push/pop)
#

75. 比较 ArrayList、HashSet 与树结构的真实内存占用时,如何用 JOL 和基准测试避免只看元素数量

比较 ArrayList、HashSet 与树结构的真实内存占用时,如何用 JOL 和基准测试避免只看元素数量?

  • JOL(Java Object Layout)
  • 内存占用测量
  • 避免只看元素数量

比较 ArrayList、HashSet、树结构(TreeMap/TreeSet)的真实内存占用,应实测而非只看元素数量:1)用 JOL(Java Object Layout,jol-core)的 GraphLayout.parseInstance(obj).totalSize() 或 Instrumentation,测量对象及引用对象的完整内存占用(含对象头、字段、内部数组、节点);2)考虑集合的内部结构:ArrayList 是数组(容量可能大于 size),HashSet 是桶数组+节点(有负载因子、容量 2 的幂),树结构是红黑树节点(每节点含父/左右/颜色指针,开销大);3)基准测试:用相同元素数量、相同数据,测量内存(JOL)与吞吐(JMH),避免只看"元素数量"而忽略容量、节点开销、哈希空桶。实测 + 基准比理论估算更可靠。

内存占用受容量、节点结构、对象头影响,用 JOL 实测 totalSize + JMH 基准,避免只看元素数量的主观估算。

// JOL 测量内存占用
import org.openjdk.jol.info.GraphLayout;
long size = GraphLayout.parseInstance(list).totalSize();
// ArrayList、HashSet、TreeSet 分别测量,比较真实内存
// 配合 JMH 测量吞吐
#

76. 自定义 Spliterator 的 trySplit 应满足哪些覆盖与不重复条件,错误拆分会怎样破坏并行结果

自定义 Spliterator 的 trySplit 应满足哪些覆盖与不重复条件?错误拆分会怎样破坏并行结果?

  • trySplit 覆盖与不重复
  • 拆分正确性
  • 并行结果破坏

自定义 Spliterator 的 trySplit 必须满足:1)不重复:拆分后的原 Spliterator 与子 Spliterator 覆盖的元素不相交(不重复);2)覆盖:合并区间覆盖原数据源的全部元素(无遗漏);3)null 表示不能再拆(返回 null 表示已到最小块)。错误拆分:若 trySplit 重复或遗漏元素,并行流会把错误数据分给不同线程,导致并行结果错误(重复元素、缺失元素、乱序),且难以察觉。因为并行流假设 Split 是"划分且不重叠、覆盖全集"的。正确实现需保证拆分后两部分元素无交集且并集覆盖全部。

trySplit 必须划分不重叠且覆盖全集,否则并行流结果重复/缺失。正确性是并行正确性的前提。

public Spliterator<T> trySplit() {
    // 要求:拆分子块与原块无交集、并集覆盖全部
    // 错误:遗漏元素或重复元素 -> 并行结果错误
    if (size <= 0) return null;
    // 返回"前半部分"子 Spliterator,自身保留后半部分
}
#

77. Sequenced Collections(JDK 21)的 getFirst/getLast/addFirst/addLast 与集合接口演进

Sequenced Collections(JDK 21)的 getFirst/getLast/addFirst/addLast 与集合接口演进是什么?

  • SequencedCollection 首尾方法
  • 接口演进
  • 统一有序访问

Sequenced Collections(JDK 21,JEP 431)引入 SequencedCollection/SequencedSet/SequencedMap 接口,提供统一的首尾访问方法:getFirst/getLast、addFirst/addLast、removeFirst/removeLast 和 reversed()。演进:此前 List、Deque、SortedSet、LinkedHashSet 等有序集合的首尾操作 API 不统一(List 用 get(0)/get(size-1),Deque 用 peekFirst,TreeMap 有 firstKey 等),Sequenced Collections 统一了这些接口,让有序集合都有标准化的首尾访问与逆序视图。List、Deque、LinkedHashSet、SortedSet、TreeMap 等实现这些接口。这是集合接口演进,增强了有序集合的一致性。

Sequenced Collections 统一有序集合的首尾访问与逆序视图,是 JDK 21 的接口演进,让 List/Deque/有序集合操作一致。

List<String> list = new ArrayList<>(List.of("a","b","c"));
list.getFirst();  // "a"
list.addLast("d"); // 追加尾部
SequencedCollection<String> rev = list.reversed();  // 逆序视图
#

78. 如何选择 ArrayList/LinkedList/ArrayDeque,插入、随机访问、内存布局的综合对比如何?

如何选择 ArrayList/LinkedList/ArrayDeque:插入、随机访问、内存布局的综合对比?

  • 三种实现的复杂度
  • 插入/随机访问
  • 内存布局

综合对比:ArrayList:动态数组,随机访问 O(1)、尾部插入 O(1)(均摊)、中间/头部插入 O(n)(移位),内存紧凑(连续数组)、缓存友好;LinkedList:双向链表,头尾操作 O(1)、随机访问 O(n)、中间插入 O(n)(需遍历),内存开销大(每节点两指针)、缓存不友好;ArrayDeque:循环数组,双端操作 O(1)、无随机访问(不是 List)、内存紧凑、缓存友好。选择:需随机访问/尾部追加用 ArrayList;需双端队列(首尾操作)用 ArrayDeque;LinkedList 很少用(仅当需要 List 语义且频繁头尾且不接受 ArrayDeque 的 null 限制,或需要具体 List 中间插入但整体仍少用)。综合:ArrayList 与 ArrayDeque 内存与缓存更优,LinkedList 多用于特殊场景。

ArrayList 随机访问/尾部优,ArrayDeque 双端优,LinkedList 头尾优但随机访问/内存差。多数场景 ArrayList/ArrayDeque 优于 LinkedList。

// 随机访问/尾部追加:ArrayList
List<String> list = new ArrayList<>();
// 双端队列:ArrayDeque
Deque<String> dq = new ArrayDeque<>();
// 极少用 LinkedList
List<String> ll = new LinkedList<>();