[{"data":1,"prerenderedAt":690},["ShallowReactive",2],{"\u002Fjava-interview\u002Fcollections":3,"java-interview-search:\u002Fjava-interview\u002Fcollections":645},{"id":4,"title":5,"body":6,"category":631,"cover":632,"date":633,"description":634,"extension":635,"group":636,"meta":637,"navigation":638,"order":639,"path":640,"seo":641,"sitemap":642,"stem":643,"__hash__":644},"javaInterview\u002Fjava-interview\u002Fcollections.md","Java 集合：选型、扩容与并发容器",{"type":7,"value":8,"toc":611},"minimark",[9,13,134,146,154,166,186,193,208,219,241,253,264,275,278,282,300,315,324,331,338,341,356,368,384,401,410,421,436,441,455,459,473,491,498,501,556,574,577],[10,11,12],"p",{},"集合题的失分点通常不是「不知道 API」，而是选型理由说不清、边界条件记混。本文把常用实现放在同一张表里对照，再逐题拆开。",[14,15,16,32],"table",{},[17,18,19],"thead",{},[20,21,22,26,29],"tr",{},[23,24,25],"th",{},"需求",[23,27,28],{},"首选",[23,30,31],{},"避免",[33,34,35,53,69,84,99,115],"tbody",{},[20,36,37,41,47],{},[38,39,40],"td",{},"随机访问、尾部追加",[38,42,43],{},[44,45,46],"code",{},"ArrayList",[38,48,49,50],{},"头部频繁插入用 ",[44,51,52],{},"LinkedList",[20,54,55,58,63],{},[38,56,57],{},"栈、队列、双端队列",[38,59,60],{},[44,61,62],{},"ArrayDeque",[38,64,65,66,68],{},"用 ",[44,67,52],{}," 充当队列",[20,70,71,74,79],{},[38,72,73],{},"键值查找，无需排序",[38,75,76],{},[44,77,78],{},"HashMap",[38,80,81,82],{},"多线程共享时用 ",[44,83,78],{},[20,85,86,89,94],{},[38,87,88],{},"保留插入顺序",[38,90,91],{},[44,92,93],{},"LinkedHashMap",[38,95,96,97],{},"为排序而用 ",[44,98,93],{},[20,100,101,104,109],{},[38,102,103],{},"按 key 排序 \u002F 范围查询",[38,105,106],{},[44,107,108],{},"TreeMap",[38,110,111,112,114],{},"大数据量下用 ",[44,113,108],{}," 做计数",[20,116,117,120,125],{},[38,118,119],{},"高并发读写",[38,121,122],{},[44,123,124],{},"ConcurrentHashMap",[38,126,127,130,131],{},[44,128,129],{},"Hashtable","、",[44,132,133],{},"Collections.synchronizedMap",[135,136,138,139,130,141,130,143,145],"h2",{"id":137},"_1-arraylistlinkedlistarraydeque-怎么选","1. ",[44,140,46],{},[44,142,52],{},[44,144,62],{}," 怎么选？",[10,147,148,150,151,153],{},[44,149,46],{}," 基于连续数组，随机访问 O(1)，尾部追加摊还 O(1)，中间插入要移动元素；",[44,152,52],{}," 是双向链表，头尾操作 O(1)，但按下标访问是 O(n)，而且每个节点都有额外的对象头和前后指针，缓存局部性差。",[10,155,156,157,159,160,162,163,165],{},"实践结论：绝大多数场景选 ",[44,158,46],{},"。即使频繁在头部插入，",[44,161,62],{}," 的环形数组通常也比 ",[44,164,52],{}," 快；只有在持有节点引用做大量中间删除时，链表才有优势。",[10,167,168,170,171,174,175,178,179,181,182,185],{},[44,169,62],{}," 不允许 ",[44,172,173],{},"null","，容量按 2 倍增长，作为栈比 ",[44,176,177],{},"Stack"," 更合适，因为 ",[44,180,177],{}," 继承自 ",[44,183,184],{},"Vector","，每个方法都加锁。",[135,187,189,190,192],{"id":188},"_2-arraylist-的扩容过程是怎样的","2. ",[44,191,46],{}," 的扩容过程是怎样的？",[10,194,195,196,199,200,203,204,207],{},"默认构造出来的是空数组，第一次 ",[44,197,198],{},"add"," 才扩到 10。之后容量不够时按 ",[44,201,202],{},"oldCapacity + (oldCapacity >> 1)"," 扩到约 1.5 倍，再用 ",[44,205,206],{},"Arrays.copyOf"," 复制到新数组。",[10,209,210,211,214,215,218],{},"扩容是 O(n) 的复制，所以能预估大小时应该用 ",[44,212,213],{},"new ArrayList\u003C>(size)"," 一次性分配，避免多次搬移。",[44,216,217],{},"trimToSize()"," 可以回收多余容量，但会再复制一次，不要频繁调用。",[10,220,221,222,225,226,229,230,233,234,236,237,240],{},"注意 ",[44,223,224],{},"subList()"," 返回的是视图而非拷贝，原列表结构修改后视图会抛 ",[44,227,228],{},"ConcurrentModificationException","；",[44,231,232],{},"Arrays.asList()"," 返回定长列表，",[44,235,198],{}," 会抛异常，但 ",[44,238,239],{},"set"," 可以改。",[135,242,244,245,130,247,130,249,130,251,145],{"id":243},"_3-hashmaplinkedhashmaptreemapconcurrenthashmap-怎么选","3. ",[44,246,78],{},[44,248,93],{},[44,250,108],{},[44,252,124],{},[10,254,255,257,258,260,261,263],{},[44,256,78],{}," 用哈希表，平均 O(1)，不保证顺序；",[44,259,93],{}," 在哈希表基础上加了双向链表，可保留插入顺序或访问顺序，适合做 LRU；",[44,262,108],{}," 基于红黑树，O(log n)，支持排序和范围查询，但 key 必须可比较。",[10,265,266,268,269,271,272,274],{},[44,267,124],{}," 用 CAS 加桶级锁保证并发安全，读操作基本无锁，适合多线程共享的缓存和计数场景。它不允许 ",[44,270,173],{}," 键值，这一点和 ",[44,273,78],{}," 不同。",[10,276,277],{},"选择顺序通常是：先问要不要并发，再问要不要顺序，最后才看单次操作复杂度。很多「用 TreeMap 排序」的需求，其实在查询时对结果排一次序更划算。",[135,279,281],{"id":280},"_4-什么是-fail-fast遍历时删除为什么会抛异常","4. 什么是 fail-fast？遍历时删除为什么会抛异常？",[10,283,284,130,286,288,289,292,293,296,297,299],{},[44,285,46],{},[44,287,78],{}," 等非并发集合在结构修改时会改变 ",[44,290,291],{},"modCount","；迭代器创建时记下期望值，每次 ",[44,294,295],{},"next()"," 都检查两者是否一致，不一致就抛 ",[44,298,228],{},"。",[10,301,302,303,306,307,310,311,314],{},"所以 ",[44,304,305],{},"for (String s : list) list.remove(s)"," 会失败，正确写法是 ",[44,308,309],{},"iterator.remove()","，或者用 ",[44,312,313],{},"removeIf","，后者内部同样走了迭代器。单线程下这是主动检测；多线程下它只是「尽力而为」，不能当并发保护使用。",[10,316,317,319,320,323],{},[44,318,124],{}," 的迭代器是弱一致的：不抛异常，但不保证看到遍历开始后的所有修改。",[44,321,322],{},"CopyOnWriteArrayList"," 的迭代器基于快照，同样不抛异常，也看不到快照之后的更新。",[135,325,327,328,330],{"id":326},"_5-copyonwritearraylist-适合什么场景代价是什么","5. ",[44,329,322],{}," 适合什么场景？代价是什么？",[10,332,333,334,337],{},"写操作时复制整个数组，在副本上修改，再用 ",[44,335,336],{},"ReentrantLock"," 保护发布；读操作完全无锁，直接读当前数组引用。",[10,339,340],{},"它适合「读多写极少、且读方不能接受加锁」的场景，例如监听器列表、黑白名单、路由配置。写操作是 O(n) 复制，元素多或写频繁时性能会很差，内存峰值也会翻倍。",[10,342,343,344,229,346,348,349,352,353,355],{},"它的迭代器是快照式的：遍历期间看不到新写入，也不会抛 ",[44,345,228],{},[44,347,309],{}," 不支持，要删元素得直接调列表的 ",[44,350,351],{},"remove","。如果要求实时一致，应该换 ",[44,354,124],{}," 对应的并发结构或加锁。",[135,357,359,360,363,364,367],{"id":358},"_6-comparable-和-comparator-有什么区别排序要注意什么","6. ",[44,361,362],{},"Comparable"," 和 ",[44,365,366],{},"Comparator"," 有什么区别？排序要注意什么？",[10,369,370,372,373,376,377,379,380,383],{},[44,371,362],{}," 由元素自身实现 ",[44,374,375],{},"compareTo","，定义「自然顺序」，一个类通常只有一种；",[44,378,366],{}," 是外部比较器，可以按需定义多种排序规则，配合 ",[44,381,382],{},"Comparator.comparing(...).thenComparing(...)"," 组合。",[10,385,386,387,390,391,394,395,229,398,400],{},"注意三点：比较器必须满足自反、反对称、传递，否则 ",[44,388,389],{},"TimSort"," 可能抛 ",[44,392,393],{},"IllegalArgumentException: Comparison method violates its general contract","；用减法实现比较可能整数溢出，应该用 ",[44,396,397],{},"Integer.compare",[44,399,173],{}," 值要显式处理。",[10,402,403,363,406,409],{},[44,404,405],{},"Collections.sort",[44,407,408],{},"Arrays.sort"," 对对象数组使用稳定的归并类算法，相等元素的相对顺序会保留；对基本类型数组则使用不稳定但更快的双轴快排。",[135,411,413,414,363,417,420],{"id":412},"_7-listof-和-collectionsunmodifiablelist-有什么区别","7. ",[44,415,416],{},"List.of()",[44,418,419],{},"Collections.unmodifiableList()"," 有什么区别？",[10,422,423,425,426,130,428,130,430,432,433,435],{},[44,424,416],{}," 创建的是真正不可变且独立的列表：内部拷贝了元素，之后原数组或原集合的修改不会影响它，任何 ",[44,427,198],{},[44,429,239],{},[44,431,351],{}," 都抛异常，且不允许 ",[44,434,173],{}," 元素。",[10,437,438,440],{},[44,439,419],{}," 是包装视图：它自己拒绝修改，但底层列表变化时会「透传」出来，因此只读保证是相对的。适合「对外暴露只读视图、内部仍需修改」的场景。",[10,442,443,445,446,448,449,451,452,454],{},[44,444,232],{}," 又是另一种：返回定长视图，",[44,447,239],{}," 可以，",[44,450,198],{},"\u002F",[44,453,351],{}," 不行，且与原数组共享数据。三者不要混用。",[135,456,458],{"id":457},"_8-并发场景下常用哪些阻塞队列怎么选","8. 并发场景下常用哪些阻塞队列？怎么选？",[10,460,461,464,465,468,469,472],{},[44,462,463],{},"ArrayBlockingQueue"," 有界、数组实现、单把锁配两个条件队列，适合容量可控的生产消费；",[44,466,467],{},"LinkedBlockingQueue"," 默认近似无界（",[44,470,471],{},"Integer.MAX_VALUE","），用两把锁分离入队出队，吞吐更高但必须显式设容量，否则堆积时容易 OOM。",[10,474,475,478,479,482,483,486,487,490],{},[44,476,477],{},"SynchronousQueue"," 不存储元素，生产者必须等到消费者接手，适合直接交接；",[44,480,481],{},"PriorityBlockingQueue"," 无界、按优先级出队；",[44,484,485],{},"DelayQueue"," 用于定时任务和缓存过期；",[44,488,489],{},"LinkedTransferQueue"," 在「生产者等待消费者」的场景下效率更高。",[10,492,493,494,497],{},"线程池选队列要结合拒绝策略一起看：用无界队列时 ",[44,495,496],{},"maximumPoolSize"," 基本不会生效，任务会一直排队，这是很多线上事故的根源。",[135,499,500],{"id":500},"易错点与自测",[502,503,504,524,542],"ul",{},[505,506,507,130,510,130,513,516,517,363,520,523],"li",{},[44,508,509],{},"subList",[44,511,512],{},"Arrays.asList",[44,514,515],{},"unmodifiableList"," 都是视图，只有 ",[44,518,519],{},"List.of",[44,521,522],{},"new ArrayList\u003C>(...)"," 是独立拷贝。",[505,525,526,527,529,530,533,534,537,538,541],{},"「线程安全」不等于「复合操作原子」：",[44,528,124],{}," 的 ",[44,531,532],{},"get"," 后再 ",[44,535,536],{},"put"," 仍需 ",[44,539,540],{},"computeIfAbsent"," 或显式锁。",[505,543,544,546,547,549,550,552,553,555],{},[44,545,78],{}," 允许一个 ",[44,548,173],{}," 键和多个 ",[44,551,173],{}," 值，",[44,554,124],{}," 全都不允许。",[10,557,558,559,561,562,564,565,567,568,570,571,573],{},"自测：① 说明为什么 ",[44,560,62],{}," 比 ",[44,563,52],{}," 更适合做队列；② 写出安全删除 ",[44,566,46],{}," 元素的三种方式；③ 解释 ",[44,569,519],{}," 与 ",[44,572,515],{}," 在数据同步上的差异。",[135,575,576],{"id":576},"参考资料",[502,578,579,589,600],{},[505,580,581,588],{},[582,583,587],"a",{"href":584,"rel":585},"https:\u002F\u002Fdocs.oracle.com\u002Fen\u002Fjava\u002Fjavase\u002F21\u002Fdocs\u002Fapi\u002Fjava.base\u002Fjava\u002Futil\u002Fpackage-summary.html",[586],"nofollow","java.util 包 API（Java 21）"," — 各集合实现的复杂度与线程安全声明",[505,590,591,596,597,599],{},[582,592,595],{"href":593,"rel":594},"https:\u002F\u002Fgithub.com\u002Fopenjdk\u002Fjdk\u002Fblob\u002Fmaster\u002Fsrc\u002Fjava.base\u002Fshare\u002Fclasses\u002Fjava\u002Futil\u002FArrayList.java",[586],"ArrayList 源码（OpenJDK）"," — 扩容与 ",[44,598,291],{}," 检查",[505,601,602,607,608,610],{},[582,603,606],{"href":604,"rel":605},"https:\u002F\u002Fopenjdk.org\u002Fjeps\u002F269",[586],"JEP 269：Convenience Factory Methods for Collections"," — ",[44,609,519],{}," 的不可变语义",{"title":612,"searchDepth":613,"depth":613,"links":614},"",2,[615,617,619,621,622,624,626,628,629,630],{"id":137,"depth":613,"text":616},"1. ArrayList、LinkedList、ArrayDeque 怎么选？",{"id":188,"depth":613,"text":618},"2. ArrayList 的扩容过程是怎样的？",{"id":243,"depth":613,"text":620},"3. HashMap、LinkedHashMap、TreeMap、ConcurrentHashMap 怎么选？",{"id":280,"depth":613,"text":281},{"id":326,"depth":613,"text":623},"5. CopyOnWriteArrayList 适合什么场景？代价是什么？",{"id":358,"depth":613,"text":625},"6. Comparable 和 Comparator 有什么区别？排序要注意什么？",{"id":412,"depth":613,"text":627},"7. List.of() 和 Collections.unmodifiableList() 有什么区别？",{"id":457,"depth":613,"text":458},{"id":500,"depth":613,"text":500},{"id":576,"depth":613,"text":576},"java-interview",null,"2026-10-24","对比 List、Map、Set 与阻塞队列的实现差异，讲清 fail-fast、CopyOnWrite 和不可变集合的适用边界","md","Java 核心",{},true,"20","\u002Fjava-interview\u002Fcollections",{"title":5,"description":634},{"loc":640},"java-interview\u002Fcollections","_f4iZ7Sm42r63hnX1xzkOGyJveWHm7tvdemmEZ4z0w4",[646,650,654,658,662,666,670,674,678,682,686],{"id":640,"title":5,"titles":647,"content":648,"level":649},[],"对比 List、Map、Set 与阻塞队列的实现差异，讲清 fail-fast、CopyOnWrite 和不可变集合的适用边界 集合题的失分点通常不是「不知道 API」，而是选型理由说不清、边界条件记混。本文把常用实现放在同一张表里对照，再逐题拆开。 需求 首选 避免 随机访问、尾部追加 ArrayList 头部频繁插入用 LinkedList 栈、队列、双端队列 ArrayDeque 用 LinkedList 充当队列 键值查找，无需排序 HashMap 多线程共享时用 HashMap 保留插入顺序 LinkedHashMap 为排序而用 LinkedHashMap 按 key 排序 \u002F 范围查询 TreeMap 大数据量下用 TreeMap 做计数 高并发读写 ConcurrentHashMap Hashtable、Collections.synchronizedMap",1,{"id":651,"title":616,"titles":652,"content":653,"level":613},"\u002Fjava-interview\u002Fcollections#_1-arraylistlinkedlistarraydeque-怎么选",[5],"ArrayList 基于连续数组，随机访问 O(1)，尾部追加摊还 O(1)，中间插入要移动元素；LinkedList 是双向链表，头尾操作 O(1)，但按下标访问是 O(n)，而且每个节点都有额外的对象头和前后指针，缓存局部性差。 实践结论：绝大多数场景选 ArrayList。即使频繁在头部插入，ArrayDeque 的环形数组通常也比 LinkedList 快；只有在持有节点引用做大量中间删除时，链表才有优势。 ArrayDeque 不允许 null，容量按 2 倍增长，作为栈比 Stack 更合适，因为 Stack 继承自 Vector，每个方法都加锁。",{"id":655,"title":618,"titles":656,"content":657,"level":613},"\u002Fjava-interview\u002Fcollections#_2-arraylist-的扩容过程是怎样的",[5],"默认构造出来的是空数组，第一次 add 才扩到 10。之后容量不够时按 oldCapacity + (oldCapacity >> 1) 扩到约 1.5 倍，再用 Arrays.copyOf 复制到新数组。 扩容是 O(n) 的复制，所以能预估大小时应该用 new ArrayList\u003C>(size) 一次性分配，避免多次搬移。trimToSize() 可以回收多余容量，但会再复制一次，不要频繁调用。 注意 subList() 返回的是视图而非拷贝，原列表结构修改后视图会抛 ConcurrentModificationException；Arrays.asList() 返回定长列表，add 会抛异常，但 set 可以改。",{"id":659,"title":620,"titles":660,"content":661,"level":613},"\u002Fjava-interview\u002Fcollections#_3-hashmaplinkedhashmaptreemapconcurrenthashmap-怎么选",[5],"HashMap 用哈希表，平均 O(1)，不保证顺序；LinkedHashMap 在哈希表基础上加了双向链表，可保留插入顺序或访问顺序，适合做 LRU；TreeMap 基于红黑树，O(log n)，支持排序和范围查询，但 key 必须可比较。 ConcurrentHashMap 用 CAS 加桶级锁保证并发安全，读操作基本无锁，适合多线程共享的缓存和计数场景。它不允许 null 键值，这一点和 HashMap 不同。 选择顺序通常是：先问要不要并发，再问要不要顺序，最后才看单次操作复杂度。很多「用 TreeMap 排序」的需求，其实在查询时对结果排一次序更划算。",{"id":663,"title":281,"titles":664,"content":665,"level":613},"\u002Fjava-interview\u002Fcollections#_4-什么是-fail-fast遍历时删除为什么会抛异常",[5],"ArrayList、HashMap 等非并发集合在结构修改时会改变 modCount；迭代器创建时记下期望值，每次 next() 都检查两者是否一致，不一致就抛 ConcurrentModificationException。 所以 for (String s : list) list.remove(s) 会失败，正确写法是 iterator.remove()，或者用 removeIf，后者内部同样走了迭代器。单线程下这是主动检测；多线程下它只是「尽力而为」，不能当并发保护使用。 ConcurrentHashMap 的迭代器是弱一致的：不抛异常，但不保证看到遍历开始后的所有修改。CopyOnWriteArrayList 的迭代器基于快照，同样不抛异常，也看不到快照之后的更新。",{"id":667,"title":623,"titles":668,"content":669,"level":613},"\u002Fjava-interview\u002Fcollections#_5-copyonwritearraylist-适合什么场景代价是什么",[5],"写操作时复制整个数组，在副本上修改，再用 ReentrantLock 保护发布；读操作完全无锁，直接读当前数组引用。 它适合「读多写极少、且读方不能接受加锁」的场景，例如监听器列表、黑白名单、路由配置。写操作是 O(n) 复制，元素多或写频繁时性能会很差，内存峰值也会翻倍。 它的迭代器是快照式的：遍历期间看不到新写入，也不会抛 ConcurrentModificationException；iterator.remove() 不支持，要删元素得直接调列表的 remove。如果要求实时一致，应该换 ConcurrentHashMap 对应的并发结构或加锁。",{"id":671,"title":625,"titles":672,"content":673,"level":613},"\u002Fjava-interview\u002Fcollections#_6-comparable-和-comparator-有什么区别排序要注意什么",[5],"Comparable 由元素自身实现 compareTo，定义「自然顺序」，一个类通常只有一种；Comparator 是外部比较器，可以按需定义多种排序规则，配合 Comparator.comparing(...).thenComparing(...) 组合。 注意三点：比较器必须满足自反、反对称、传递，否则 TimSort 可能抛 IllegalArgumentException: Comparison method violates its general contract；用减法实现比较可能整数溢出，应该用 Integer.compare；null 值要显式处理。 Collections.sort 和 Arrays.sort 对对象数组使用稳定的归并类算法，相等元素的相对顺序会保留；对基本类型数组则使用不稳定但更快的双轴快排。",{"id":675,"title":627,"titles":676,"content":677,"level":613},"\u002Fjava-interview\u002Fcollections#_7-listof-和-collectionsunmodifiablelist-有什么区别",[5],"List.of() 创建的是真正不可变且独立的列表：内部拷贝了元素，之后原数组或原集合的修改不会影响它，任何 add、set、remove 都抛异常，且不允许 null 元素。 Collections.unmodifiableList() 是包装视图：它自己拒绝修改，但底层列表变化时会「透传」出来，因此只读保证是相对的。适合「对外暴露只读视图、内部仍需修改」的场景。 Arrays.asList() 又是另一种：返回定长视图，set 可以，add\u002Fremove 不行，且与原数组共享数据。三者不要混用。",{"id":679,"title":458,"titles":680,"content":681,"level":613},"\u002Fjava-interview\u002Fcollections#_8-并发场景下常用哪些阻塞队列怎么选",[5],"ArrayBlockingQueue 有界、数组实现、单把锁配两个条件队列，适合容量可控的生产消费；LinkedBlockingQueue 默认近似无界（Integer.MAX_VALUE），用两把锁分离入队出队，吞吐更高但必须显式设容量，否则堆积时容易 OOM。 SynchronousQueue 不存储元素，生产者必须等到消费者接手，适合直接交接；PriorityBlockingQueue 无界、按优先级出队；DelayQueue 用于定时任务和缓存过期；LinkedTransferQueue 在「生产者等待消费者」的场景下效率更高。 线程池选队列要结合拒绝策略一起看：用无界队列时 maximumPoolSize 基本不会生效，任务会一直排队，这是很多线上事故的根源。",{"id":683,"title":500,"titles":684,"content":685,"level":613},"\u002Fjava-interview\u002Fcollections#易错点与自测",[5],"subList、Arrays.asList、unmodifiableList 都是视图，只有 List.of 和 new ArrayList\u003C>(...) 是独立拷贝。 「线程安全」不等于「复合操作原子」：ConcurrentHashMap 的 get 后再 put 仍需 computeIfAbsent 或显式锁。 HashMap 允许一个 null 键和多个 null 值，ConcurrentHashMap 全都不允许。 自测：① 说明为什么 ArrayDeque 比 LinkedList 更适合做队列；② 写出安全删除 ArrayList 元素的三种方式；③ 解释 List.of 与 unmodifiableList 在数据同步上的差异。",{"id":687,"title":576,"titles":688,"content":689,"level":613},"\u002Fjava-interview\u002Fcollections#参考资料",[5],"java.util 包 API（Java 21） — 各集合实现的复杂度与线程安全声明 ArrayList 源码（OpenJDK） — 扩容与 modCount 检查 JEP 269：Convenience Factory Methods for Collections — List.of 的不可变语义",1793263559657]