多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Java 集合框架万字梳理:从底层原理、源码细节到生产选型,一篇全搞定

Java 集合框架万字梳理:从底层原理、源码细节到生产选型,一篇全搞定 集合框架是 Java 开发中使用频率最高的 API 体系之一也是后端面试的核心必考点。从日常的数据存储到高并发场景下的缓存、任务队列集合的选型直接决定了代码的性能、稳定性与可维护性。很多开发者对集合的认知停留在「会用」层面对底层实现、并发特性、边界坑点一知半解面试时答不深原理生产中频繁踩坑。本文系统梳理 Java 集合框架完整知识体系涵盖核心接口、主流实现类底层原理、迭代机制、高频坑点、Stream API 以及生产场景选型指南一篇帮你建立完整的集合知识体系。一、集合框架整体体系1.1 顶层接口划分Java 集合框架主要分为两大分支顶层接口职责清晰Iterable所有集合的根接口提供迭代器能力支持增强 for 循环遍历。Collection单值集合的顶层接口下分三大子体系List有序可重复、Set无序去重、Queue队列结构。Map键值对集合的顶层接口独立于 Collection 体系以 key-value 形式存储数据。1.2 主流实现类核心特性速查表这张表可以快速定位各个集合的基础属性面试和选型时可以快速对照类型接口常用实现底层结构是否有序null 规则线程安全ListListArrayListObject[] 数组按索引有序允许所有元素为 null否ListListLinkedList双向链表按索引有序允许所有元素为 null否ListListVectorObject[] 数组按索引有序允许所有元素为 null是全方法加 synchronizedListListCopyOnWriteArrayList数组 写时复制机制按索引有序允许所有元素为 null是写加锁读无锁SetSetHashSet基于 HashMap无序允许一个 null 元素否SetSetLinkedHashSet基于 LinkedHashMap插入有序允许一个 null 元素否SetSetTreeSet基于 TreeMap红黑树自然/自定义排序有序默认不允许 null自定义比较器可支持否SetSetEnumSet位向量枚举自然顺序不允许 null否MapMapHashMap数组 链表/红黑树无序key 允许一个 nullvalue 允许多个 null否MapMapLinkedHashMapHashMap 双向链表插入/访问有序key 允许一个 nullvalue 允许多个 null否MapMapTreeMap红黑树key 自然/自定义排序key 默认不允许 null否MapMapHashtable数组 链表无序key/value 均不允许 null是全方法加 synchronizedMapMapConcurrentHashMap数组 链表/红黑树无序key/value 均不允许 null是CAS 桶级锁MapMapConcurrentSkipListMap跳表key 有序key/value 均不允许 null是无锁算法QueueQueueArrayDeque循环数组FIFO 队列顺序不允许 null否QueueQueuePriorityQueue二叉堆优先级顺序不允许 null否BlockingQueueBlockingQueueArrayBlockingQueue数组FIFO 队列顺序不允许 null是全局锁BlockingQueueBlockingQueueLinkedBlockingQueue链表FIFO 队列顺序不允许 null是读写分离锁二、List 集合深度解析2.1 ArrayListArrayList 是日常开发最常用的 List 实现底层基于动态数组实现随机访问性能优异。核心特性懒初始化JDK8无参构造不会立即创建容量为 10 的数组首次调用 add 方法时才会初始化默认容量。扩容规则默认扩容为原容量的 1.5 倍计算公式为oldCapacity (oldCapacity 1)扩容时会复制整个底层数组。时间复杂度随机下标访问get(int index)O(1)直接通过数组下标定位尾部添加add(E e)均摊 O(1)仅扩容时触发数组复制中间插入/删除O(n)需要移动后续所有元素实现了RandomAccess标记接口遍历时优先使用普通 for 循环性能优于迭代器。线程不安全多线程并发修改需手动加锁或使用并发集合。性能优化批量插入大量元素前可调用ensureCapacity(int minCapacity)预分配容量减少多次扩容拷贝。序列化特性底层数组elementData被transient修饰通过writeObject/readObject自定义序列化只序列化真实存在的元素不序列化空槽位节省空间。遍历删除坑点正向 for 循环遍历调用remove会出现元素漏检问题推荐使用迭代器Iterator.remove()或反向遍历删除。可通过trimToSize()方法将底层数组容量压缩至当前元素个数适合长期存活的集合节省内存。subList 高频坑点subList(int fromIndex, int toIndex)返回的是原列表的视图而非独立副本这是非常容易踩的坑对子列表的非结构性修改会同步反映到原列表反之亦然。对原列表进行结构性修改add/remove后再访问子列表会抛出ConcurrentModificationException。子列表是 ArrayList 内部类SubList的实例自身未实现Serializable接口直接序列化会抛出NotSerializableException原 ArrayList 本身支持序列化。ListString list new ArrayList(List.of(a, b, c)); ListString sub list.subList(0, 2); sub.set(0, x); // 修改会同步到原列表 System.out.println(list); // 输出 [x, b, c] list.add(d); // 原列表结构性修改 // sub.get(0); // 抛出 ConcurrentModificationException2.2 LinkedList底层基于双向链表实现每个节点包含prev、item、next三个属性。核心特性同时实现List和Deque接口可作为列表、双端队列、栈使用。时间复杂度头尾节点增删O(1)直接修改指针指向按索引访问get(int index)O(n)源码会先判断 index 靠近头还是尾选择从头部或尾部就近遍历最坏时间复杂度仍为 O(n)按对象查找删除O(n)需先遍历定位节点通过ListIterator或已知节点引用进行增删操作时间复杂度为 O(1)。随机访问性能远低于 ArrayList且每个节点需要额外存储指针内存占用更大。适用于频繁头尾增删、双端队列的场景随机访问多的场景优先选 ArrayList。2.3 Vector / StackVector 底层同样基于数组实现所有方法都加了synchronized关键字线程安全但性能很差属于过时类。Vector 默认扩容为原容量的 2 倍若指定了capacityIncrement则扩容为旧容量 增量值。Stack 继承自 Vector是栈的古老实现同样过时。栈场景推荐使用ArrayDeque替代性能更好且接口更规范DequeString stack new ArrayDeque(); stack.push(a); stack.push(b); String top stack.pop(); // 返回 b2.4 CopyOnWriteArrayList基于**写时复制Copy-On-Write**机制实现的并发 List是读多写少场景的首选。核心原理写操作add/set/remove使用ReentrantLock加锁复制整个底层数组修改完成后替换原数组引用。读操作完全无锁直接读取当前数组引用读性能极高。迭代器基于创建时的数组快照具有快照一致性迭代期间完全看不到后续的新增、删除修改不会抛出ConcurrentModificationException。迭代器不支持 add/remove/set 等修改操作调用会直接抛出UnsupportedOperationException。写操作开销极大每次复制整个数组绝对不适合高频写场景。适合读多写极少的并发场景比如配置列表、白名单等更新频率极低的数据。三、Set 集合深度解析3.1 HashSet最常用的去重集合底层完全基于 HashMap 实现。核心特性元素作为 HashMap 的 key 存储value 为固定的 Object 常量PRESENT。元素无序允许存入一个 null 元素。去重依赖hashCode()和equals()方法先根据 hashCode 定位哈希桶位置桶内已有元素时通过 equals 方法判断是否相等自定义对象作为元素时重写 equals 方法必须同时重写 hashCode否则会出现去重失效、查询异常等问题。3.2 LinkedHashSetHashSet 的子类内部使用 LinkedHashMap 维护元素的插入顺序。迭代顺序与元素插入顺序完全一致。仅维护插入顺序修改已有元素不会改变其在链表中的位置。性能略低于 HashSet因为需要额外维护双向链表。允许一个 null 元素。3.3 TreeSet底层基于 TreeMap 红黑树实现支持元素排序。支持自然排序元素实现 Comparable 接口或自定义 Comparator 比较器。null 规则默认自然排序下不允许 null 元素插入会抛出NullPointerException若传入支持 null 比较的自定义 Comparator如Comparator.nullsFirst(String::compareTo)则可以正常存入 null 元素。去重不依赖 equals而是依赖compareTo或compare方法返回 0。实现了NavigableSet接口提供了比SortedSet更丰富的近邻查询、范围截取能力。支持丰富的范围查询first()、last()、subSet()、headSet()、tailSet()。3.4 EnumSet专为枚举类型设计的 Set 实现是性能最高的 Set 实现类。内部使用位向量实现内存占用极小操作效率极高。根据枚举元素数量分为两个实现元素 ≤ 64RegularEnumSet单个 long 型位掩码元素 64JumboEnumSetlong 数组实现不允许 null 元素所有元素必须是指定枚举类型。推荐通过工厂方法创建noneOf()、allOf()、of()。enum Color { RED, GREEN, BLUE } SetColor colors EnumSet.of(Color.RED, Color.GREEN);四、Map 集合深度解析Map 是集合框架的重中之重尤其是 HashMap 和 ConcurrentHashMap几乎是面试必考题。4.1 HashMap底层结构演进JDK7数组 链表采用头插法插入元素并发扩容可能产生环形链表导致死循环。JDK8数组 链表/红黑树采用尾插法引入红黑树优化哈希冲突严重时的查询性能。通过扰动函数优化 hash 分布h key.hashCode() ^ (h 16)将哈希值高 16 位混合到低 16 位减少高位差异带来的哈希冲突。容量强制为 2 的幂通过hash (n - 1)快速定位哈希桶比取模效率更高。默认初始容量 16负载因子 0.75扩容阈值 容量 × 负载因子。树化与退化规则这是面试高频考点完整触发条件如下树化条件链表长度 ≥ 8 且数组容量 ≥ 64 时链表转化为红黑树如果容量小于 64优先扩容而非树化。退化条件扩容重哈希时若红黑树中元素个数 ≤ 6会退化为链表普通remove操作删除节点后若树结构过小如根节点为空、左右子树缺失也会立即触发退化无需等待扩容。树化阈值设为 8 的统计学依据负载因子 0.75 下链表长度达到 8 的概率符合泊松分布约为千万分之六概率极低。put 执行流程判断 table 数组是否为空为空则初始化数组。计算 key 的 hash 值通过hash (n - 1)定位哈希桶。桶为空直接新建节点插入。桶为红黑树节点执行红黑树插入逻辑。桶为链表遍历链表找到相同 key替换对应的 value。未找到相同 key尾插新节点插入后链表长度达到 8检查是否需要树化。插入完成后若 size 超过扩容阈值触发扩容。扩容机制扩容为原容量的 2 倍保证容量始终是 2 的幂。JDK8 优化了元素重定位逻辑判断hash oldCap是否为 0为 0 则留在原位否则移动到「原位 旧容量」的位置无需重新计算 hash。并发安全性说明JDK7头插法 扩容迁移多线程同时扩容可能形成环形链表导致 get 操作死循环。JDK8尾插法修复了环形链表问题但并发场景下仍可能出现数据覆盖、元素丢失等问题依旧线程不安全不能替代 ConcurrentHashMap。其他特性允许一个 null key多个 null value。自定义对象作为 key 时必须同时重写equals()和hashCode()否则会出现去重失效、get 查询返回 null 等问题。JDK8 支持getOrDefault、putIfAbsent、compute、merge等默认方法非原子操作。4.2 LinkedHashMapHashMap 的子类通过额外的双向链表维护元素顺序。accessOrder false默认维护元素插入顺序。accessOrder true维护访问顺序get/put 操作后元素会移到链表尾部。可通过重写removeEldestEntry方法实现 LRU 缓存LinkedHashMapString, Integer lruCache new LinkedHashMap(16, 0.75f, true) { Override protected boolean removeEldestEntry(Map.EntryString, Integer eldest) { return size() 3; // 插入第4个元素时淘汰最久未访问的头部元素 } };说明removeEldestEntry在每次插入新节点完成后调用返回 true 时会移除链表头部的最久未访问元素而非插入前拒绝新元素。4.3 TreeMap基于红黑树实现的有序 Map。key 支持自然排序或自定义 Comparator。默认自然排序下不允许 null key自定义比较器支持 null 时可存入 null keyvalue 允许为 null。实现了NavigableMap接口提供了比SortedMap更丰富的近邻查询和范围截取能力。支持丰富的范围查询firstKey()、lastKey()、subMap()、headMap()、tailMap()、ceilingKey()、floorKey()。去重依据compareTo/compare方法返回 0。4.4 Hashtable所有方法都加了 synchronized包括读操作性能极低属于过时类。不允许 null key 和 null value。默认初始容量 11扩容公式旧容量 × 2 1。Properties是其子类用于读取配置文件。并发场景优先使用 ConcurrentHashMap不要用 Hashtable。4.5 ConcurrentHashMap并发场景下的首选 Map面试绝对高频考点。版本演进JDK7采用分段锁 Segment 机制每段维护独立的 HashEntry 数组默认 16 个段锁粒度较粗。JDK8重构为CAS synchronized 锁桶头节点数据结构与 HashMap 一致锁粒度细化到单个哈希桶并发性能大幅提升。核心并发控制初始化通过sizeCtl变量 CAS 控制避免并发初始化。读操作基本无锁依赖 volatile 保证可见性树节点遍历可能加锁普通读无锁。写操作仅锁定冲突桶的头节点不同桶之间互不阻塞。扩容支持多线程协助迁移已迁移的桶用 ForwardingNode 标记。sizeCtl 完整语义0数组尚未初始化table 为 null。正数扩容阈值容量 × 负载因子。-1表示正在初始化。扩容期间整体为负数高 16 位存储扩容戳resizeStamp低 16 位存储「参与扩容的线程数 RESIZE_STAMP_OFFSET」不能直接等价于线程数。size 计算原理采用baseCount CounterCell 数组的分布式计数方案无竞争时直接累加 baseCount。有竞争时通过 CAS 操作 CounterCell 数组避免全局锁。高并发下size()返回的是近似值而非精确值。推荐使用mappingCount()替代size()前者返回 long 类型可避免 int 溢出问题。核心特性不允许 null key 和 null value避免 get 返回 null 时无法区分「key 不存在」和「值为 null」的二义性。提供丰富的原子操作putIfAbsent、replace、compute、merge。compute、merge是原子操作但如果传入的计算逻辑过重会阻塞其他线程访问同一个哈希桶生产环境应避免在其中执行耗时操作。迭代器具有弱一致性不抛出ConcurrentModificationException。4.6 其他特殊 MapConcurrentSkipListMap基于跳表实现的并发有序 Map无锁算法支持高并发有序访问和范围查询。WeakHashMapkey 为弱引用一旦 key 没有外部强引用下次 GC 时对应 Entry 会被自动清理value 为强引用若 value 直接或间接持有对应 key 的强引用会导致 key 无法被回收引发内存泄漏。仅适合特定弱引用缓存场景不推荐作为通用缓存使用。IdentityHashMap使用比较 key 引用地址而非equals方法底层采用线性探测法而非拉链法允许 key 为 null允许「重复」key只要对象引用不同。EnumMapkey 必须为枚举类型内部数组实现性能极高不允许 null key。五、迭代机制与并发特性5.1 fail-fast快速失败原理迭代时检测modCount变量变化若集合发生结构性修改立即抛出ConcurrentModificationException。代表ArrayList、HashMap、HashSet 等普通非并发集合。特性说明单线程下结构性修改迭代 100% 触发异常多线程场景下因 modCount 没有 volatile 修饰无法保证可见性可能出现漏检测不抛异常的情况属于尽力而为的快速失败机制不能替代线程安全控制。fail-fast 不是线程安全保证只是快速暴露问题的机制。ListString list new ArrayList(List.of(a, b, c)); for (String s : list) { if (b.equals(s)) { list.remove(s); // 抛出 ConcurrentModificationException } }5.2 fail-safe安全失败迭代过程不会抛出ConcurrentModificationException分为两类快照式CopyOnWriteArrayList、CopyOnWriteArraySet迭代器持有创建时的数组快照与后续修改完全隔离具备快照一致性。弱一致性ConcurrentHashMap、ConcurrentSkipListMap 等并发集合不检测修改迭代过程可能反映部分最新修改不保证实时一致。5.3 Collections.synchronizedXXX 迭代坑点Collections.synchronizedList/Set/Map返回的同步包装类迭代器不具备线程安全性迭代时必须手动对集合对象加锁否则并发修改仍会触发ConcurrentModificationExceptionListString syncList Collections.synchronizedList(new ArrayList()); synchronized (syncList) { // 必须手动加锁锁对象为返回的包装集合本身 for (String s : syncList) { // 业务逻辑 } }六、队列体系详解6.1 Queue 基础接口先进先出FIFO队列提供两类操作方法操作失败抛异常失败返回特殊值插入元素add(e)offer(e)移除并返回头部remove()poll()查看头部元素element()peek()6.2 Deque 双端队列两端均可入队/出队可作为栈使用。推荐实现ArrayDeque底层为循环数组默认容量 16扩容为原容量的 2 倍容量始终保持为 2 的幂。作为栈性能优于 Stack作为队列性能优于 LinkedList连续内存CPU 缓存命中率更高。不允许 null 元素。6.3 BlockingQueue 阻塞队列阻塞队列是生产者-消费者模型的核心组件广泛用于异步任务、流量削峰等场景。常用实现对比实现类底层结构有界性特点ArrayBlockingQueue数组有界全局单锁 两个 Condition支持公平/非公平模式读写互斥LinkedBlockingQueue链表默认无界双锁put 锁 take 锁读写可并行高并发性能更优默认容量 Integer.MAX_VALUE生产环境严禁使用无参构造必须手动指定容量避免 OOM 风险PriorityBlockingQueue二叉堆无界按优先级出队元素需实现 Comparable 或传入 ComparatorDelayQueue优先级队列无界元素需实现 Delayed 接口到期后才能出队getDelay实现不当可能导致元素永远无法出队SynchronousQueue无存储无容量插入必须等待消费用于线程间直接传递任务四类操作方法操作抛异常返回特殊值阻塞超时等待插入元素add(e)offer(e)put(e)offer(e, time, unit)移除并返回头部remove()poll()take()poll(time, unit)查看头部element()peek()无无生产者-消费者示例BlockingQueueString queue new ArrayBlockingQueue(10); // 生产者 new Thread(() - { try { queue.put(task); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }).start(); // 消费者 new Thread(() - { try { String task queue.take(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }).start();6.4 PriorityQueue 注意事项底层为最小堆仅保证堆顶元素最小不保证整个队列整体有序。元素必须实现Comparable接口或构造时传入自定义Comparator否则插入时会抛出ClassCastException。不允许 null 元素。七、工具类与高频坑点避坑7.1 Collections 工具类通用操作sort、binarySearch、reverse、shuffle、fill、copy、max、min、frequency、disjoint、swap。unmodifiableXXX返回不可修改视图原集合修改会反映到视图。synchronizedXXX返回同步包装类全方法加锁迭代需手动加锁。emptyXXX/singletonXXX返回空集合或单元素集合均不可变。7.2 Arrays.asList() 经典坑点这个方法几乎每个 Java 开发者都踩过坑返回内部类Arrays$ArrayList不是java.util.ArrayList。不支持 add/remove 等结构性修改抛出UnsupportedOperationException。支持 set 修改元素与原数组共享底层数组修改互相影响。传入基本类型数组时会被当作单个元素如int[]变成Listint[]。正确的基本类型数组转集合写法int[] nums {1, 2, 3}; // 错误Listint[] numList Arrays.asList(nums); ListInteger numList Arrays.stream(nums).boxed().collect(Collectors.toList());7.3 JDK9 不可变集合List.of()/Set.of()/Map.of()是真正的不可变集合。元素不能为 null修改抛出UnsupportedOperationException。Set.of()不允许重复元素Map.of()不允许重复 key最多支持 5 个键值对10 个参数超过请使用Map.ofEntries()。7.4 Comparator 与 ComparableComparable内部比较接口实现compareTo方法定义自然排序。Comparator外部比较器实现compare方法策略模式可灵活切换排序规则。常用工具方法comparing()、thenComparing()、reversed()、nullsFirst()、nullsLast()。7.5 Map 遍历方式对比keySet()遍历 key获取 value 需要二次查找性能较差。entrySet()遍历键值对推荐使用避免二次查找。values()遍历 value无法获取对应 key。迭代器遍历可安全删除元素。八、Stream API 核心要点8.1 操作分类中间操作惰性只记录操作不触发计算流可继续链式调用。无状态map、filter、peek等元素处理独立无需缓存有状态sorted、distinct、limit、skip等需要缓存历史元素性能开销更大终端操作触发计算流被消费不可复用如forEach、collect、count、reduce、findFirst等。短路操作无需遍历全部元素即可终止如limit、findFirst、anyMatch、allMatch。注有状态和短路操作并不互斥例如limit既属于有状态中间操作也属于短路操作。8.2 Collectors 常用收集器toList()/toSet()收集为 List / Set。toMap()key 重复默认抛出IllegalStateException可指定 mergeFunction 处理重复 key。无论是否指定合并函数value 为 null 都会抛出NullPointerException底层调用 Map.merge 方法这是高频生产 Bug。groupingBy()分组收集支持嵌套下游收集器。partitioningBy()按布尔值二分分组。joining()字符串拼接支持分隔符、前后缀。8.3 注意事项流只能消费一次重复使用抛出IllegalStateException。peek 陷阱peek是中间操作若没有终端操作触发计算peek 中的逻辑不会执行常出现 debug 打印无输出的问题。并行流parallelStream()默认使用公共 ForkJoinPool默认并行度为 CPU 核心数 - 1。并行流不要操作非线程安全的外部集合否则会出现并发修改异常或数据错乱并行流不适合 I/O 阻塞型任务I/O 密集场景应自定义线程池避免阻塞公共池影响其他程序。stream.collect(Collectors.toList())JDK8-15 返回 ArrayList允许 null 元素JDK16 后底层实现变更但仍允许 null 元素。JDK16 新增Stream.toList()实例方法返回不可变 List允许包含 null 元素若需要禁止 null 的不可变集合应使用Collectors.toUnmodifiableList()。九、生产场景选型指南List 选型场景选择频繁随机访问ArrayList频繁头尾增删LinkedList 或 ArrayDeque读多写极少的并发场景CopyOnWriteArrayList需要线程安全且写操作较多不推荐 Collections.synchronizedList全局锁性能差可考虑读写锁封装 ArrayList 或使用并发队列Set 选型场景选择普通去重HashSet保持插入顺序LinkedHashSet需要排序TreeSet并发读多写极少CopyOnWriteArraySet 或 ConcurrentHashMap.newKeySet()枚举元素EnumSetMap 选型场景选择普通键值对存储HashMap保持插入/访问顺序LinkedHashMap需要 key 排序TreeMap高并发场景ConcurrentHashMap高并发且需要有序ConcurrentSkipListMap枚举 keyEnumMapQueue 选型场景选择普通 FIFO 队列ArrayDeque双端队列 / 栈ArrayDeque优先级队列PriorityQueue阻塞生产者-消费者模型ArrayBlockingQueue / LinkedBlockingQueue延迟任务调度DelayQueue线程间直接传递任务SynchronousQueue高并发非阻塞队列ConcurrentLinkedQueue写在最后Java 集合框架不是只有 ArrayList 和 HashMap不同场景下选择合适的实现类能大幅提升代码性能和稳定性。普通业务场景优先使用主流实现并发场景优先用 JUC 包下的并发集合尽量避免 Vector、Hashtable 等老旧过时类。本文覆盖了集合框架 90% 以上的核心考点和生产常用知识点修正了大量网络常见误区补充了源码级细节和生产踩坑点建议收藏备用。如果有补充或者疑问欢迎在评论区留言交流~推荐标签#Java #集合框架 #HashMap #并发编程 #Java面试 #后端开发
返回列表