
文档教程后端【免费下载链接】JCSprout Java Core Sprout : basic, concurrent, algorithm项目地址https://gitcode.com/gh_mirrors/jc/JCSprout点击查看免费下载导读本文基于 JCSprout 仓库中的《ArrayList/Vector 的底层分析》文档见 docs/collections/ArrayList.md 及其原始版本 MD/ArrayList.md结合仓库内真实的 JMH 基准测试源码 CollectionsTest.java系统拆解 ArrayList 的动态数组扩容机制、指定位置插入的数组拷贝开销、自定义序列化实现以及 Vector 作为同步容器的线程安全策略。读完本文你将能深入理解 ArrayList 与 Vector 的底层工作原理掌握指定初始容量、减少指定位置插入等实战优化手段并能在面试中从源码层面讲清两者的本质区别。一、ArrayList 概览动态数组的接口与核心属性ArrayList实现了List与RandomAccess接口是一个基于动态数组实现的顺序存储结构。它具备两个关键能力可以插入空数据null底层数组只存放对象引用不限制空值支持随机访问实现了RandomAccess标记接口后通过下标即可在 O(1) 时间内访问元素。在 ArrayList 中最重要的两个属性分别是elementData真正存放数据的Object[]数组也就是动态数组的容器size当前实际存放的元素个数注意它并不等同于数组的容量elementData.length数组中往往存在未被使用的空位。正是因为数组容量 实际元素个数才衍生出了扩容与自定义序列化这两大核心话题下文将逐一展开。二、add() 的扩容校验与尾部追加当调用无参的add(E e)向尾部追加元素时源码逻辑如下public boolean add(E e) { ensureCapacityInternal(size 1); // Increments modCount!! elementData[size] e; return true; }整个过程只有两步扩容校验调用ensureCapacityInternal(size 1)确保数组至少有size 1的空位尾部追加将新元素写入elementData[size]随后size自增 1完成追加。这里传入的是size 1而不是固定值原因是扩容只在空间不足时才触发只要当前容量足够就只是 O(1) 的数组赋值这也是尾部追加append效率远高于指定位置插入的根本原因。三、add(index, e) 的数组拷贝与数据搬移在指定位置插入数据时ArrayList 无法像链表那样只移动指针它必须为腾出插入位置而搬移后续所有元素public void add(int index, E element) { rangeCheckForAdd(index); ensureCapacityInternal(size 1); // Increments modCount!! //复制向后移动 System.arraycopy(elementData, index, elementData, index 1, size - index); elementData[index] element; size; }具体步骤为下标校验rangeCheckForAdd(index)检查index是否在[0, size]范围内越界会抛出IndexOutOfBoundsException扩容校验与尾部追加一样先确保容量足够数组搬移通过System.arraycopy将[index, size)区间内的元素整体向后移动一位把index位置空出来——这一步的时间复杂度为 O(n)是最主要的性能开销写入与自增将新元素写入elementData[index]size。System.arraycopy是 JVM 提供的高效原生数组复制方法即便如此当index越靠近数组头部、被搬移的元素越多时代价就越大。因此在实际业务中应当尽量减少在 ArrayList 头部或中间插入数据的操作若确实存在大量此类需求应优先考虑 LinkedList其插入仅需移动指针。四、扩容机制 grow()1.5 倍增长与溢出保护无论是尾部追加还是指定位置插入扩容校验最终都会汇聚到grow()方法它才是真正执行扩容的核心private void grow(int minCapacity) { // overflow-conscious code int oldCapacity elementData.length; int newCapacity oldCapacity (oldCapacity 1); if (newCapacity - minCapacity 0) newCapacity minCapacity; if (newCapacity - MAX_ARRAY_SIZE 0) newCapacity hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData Arrays.copyOf(elementData, newCapacity); }这段代码的关键点如下步骤说明计算新容量newCapacity oldCapacity (oldCapacity 1)即扩容为原来的 1.5 倍右移一位等价于除以 2。例如容量 10 扩容后为 15容量 100 扩容后为 150最小容量兜底若 1.5 倍后的容量仍小于minCapacity例如首次添加时旧容量为 0则直接取minCapacity作为新容量上限保护若新容量超过MAX_ARRAY_SIZE一般指Integer.MAX_VALUE - 8则转入hugeCapacity(minCapacity)做最大容量处理防止数组过大导致 OOM执行扩容elementData Arrays.copyOf(elementData, newCapacity)本质仍是一次数组复制申请新数组并把旧数据整体拷贝过去从代码中的注释overflow-conscious code也可以看出JDK 在容量计算上刻意做了防溢出设计例如用newCapacity - minCapacity 0而非newCapacity minCapacity避免极端情况下加法溢出后误判。由此可以得出一个重要结论ArrayList 的主要性能消耗集中在数组扩容与指定位置插入两者本质上都是数组复制。因此日常使用时最佳实践是在构造时预估并指定初始容量如new ArrayList(10000)尽可能减少扩容次数避免在指定位置插入数据尤其避免在头部/中部频繁插入。仓库中的 JMH 基准测试 CollectionsTest.java 正是对这一结论的量化验证它对比了new ArrayList()默认容量需多次扩容、new ArrayList(TEN_MILLION)预分配容量和new LinkedList()三种方式各自向列表追加一千万个元素时的平均耗时Mode.AverageTime单位微秒是理解预分配容量能显著降低扩容拷贝开销的可复现实验。五、序列化优化transient 自定义 writeObject/readObject由于 ArrayList 基于动态数组实现elementData的实际长度容量往往大于已使用的size。如果直接序列化整个数组会把大量未被使用的空位也写进流中造成空间浪费。为此 ArrayList 做了两件事1. 用transient修饰数组屏蔽默认序列化transient Object[] elementData;transient关键字告诉 JVM默认序列化机制不要序列化这个字段。2. 自定义序列化与反序列化方法private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{ // Write out element count, and any hidden stuff int expectedModCount modCount; s.defaultWriteObject(); // Write out size as capacity for behavioural compatibility with clone() s.writeInt(size); // Write out all elements in the proper order. //只序列化了被使用的数据 for (int i0; isize; i) { s.writeObject(elementData[i]); } if (modCount ! expectedModCount) { throw new ConcurrentModificationException(); } } private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { elementData EMPTY_ELEMENTDATA; // Read in size, and any hidden stuff s.defaultReadObject(); // Read in capacity s.readInt(); // ignored if (size 0) { // be like clone(), allocate array based upon size not capacity ensureCapacityInternal(size); Object[] a elementData; // Read in all elements in the proper order. for (int i0; isize; i) { a[i] s.readObject(); } } }这里有两个值得深入理解的设计点序列化契约当对象中自定义了writeObject和readObject方法时JVM 会优先调用这两个自定义方法来实现序列化与反序列化而不再使用默认的反射式序列化流程只序列化有效数据writeObject只遍历[0, size)区间逐个写出被使用的元素空位完全被跳过readObject则按size读取并通过ensureCapacityInternal(size)按实际元素个数而非原始容量重新分配数组代码注释allocate array based upon size not capacity也印证了这一点且与clone()行为保持兼容。此外两个方法都在读写完成后校验modCount与expectedModCount是否一致若不一致则抛出ConcurrentModificationException这正是对序列化过程中集合被并发修改的防御性检测。六、Vectorsynchronized 加持的同步容器Vector同样实现于List接口底层数据结构和ArrayList类似也是一个动态数组。两者的核心差异在于Vector 在add()方法上使用synchronized进行同步写数据。public synchronized boolean add(E e) { modCount; ensureCapacityHelper(elementCount 1); elementData[elementCount] e; return true; }指定位置插入时同样走同步路径public void add(int index, E element) { insertElementAt(element, index); } public synchronized void insertElementAt(E obj, int index) { modCount; if (index elementCount) { throw new ArrayIndexOutOfBoundsException(index elementCount); } ensureCapacityHelper(elementCount 1); System.arraycopy(elementData, index, elementData, index 1, elementCount - index); elementData[index] obj; elementCount; }从源码可以清晰看出insertElementAt是synchronized方法且先对index做了越界检查index elementCount时抛出ArrayIndexOutOfBoundsException随后同样是扩容校验 System.arraycopy搬移 写入的动态数组套路Vector 通过synchronized保证了单次写操作的原子性但这也意味着每个方法调用都要经历加锁/解锁的完整开销。因此从并发编程的角度严格来说Vector 是一个同步容器synchronized container而不是并发容器concurrent container。原因在于它使用粗粒度的方法级锁锁的粒度大、开销高多线程竞争时吞吐量受限单个方法内部虽然安全但先判断再操作这类复合操作如if (!v.isEmpty()) v.get(0)依然存在竞态窗口无法提供真正的并发安全现代 Java 并发编程中一般推荐使用CopyOnWriteArrayList、Collections.synchronizedList()或并发包下的其他容器来替代 Vector。七、实战建议与源码验证综合全文对 ArrayList 与 Vector 的选型和使用可以给出如下经过源码验证的结论能预估规模就预分配容量new ArrayList(expectedSize)可以大幅减少grow()触发的数组复制次数这是仓库基准测试 CollectionsTest.java 中专门对比的场景避免指定位置插入add(index, e)需要System.arraycopy搬移 O(n) 个元素频繁在中部/头部插入应改用 LinkedList随机访问场景坚持用 ArrayList实现RandomAccess接口使其按下标访问为 O(1)而 LinkedList 的node()最坏需要 O(n/2) 遍历并发场景别用 Vector方法级synchronized使其成为高开销的同步容器而非并发容器应选择 JUC 包下的并发容器理解序列化语义ArrayList 通过transient数组 自定义writeObject/readObject只序列化有效数据这也是面试中关于为什么 ArrayList 的 elementData 用 transient 修饰的标准答案。本仓库的 docs/collections/ArrayList.md 为本文核心参考文档其原始版本位于 MD/ArrayList.md可在阅读时相互对照同系列文档 docs/collections/LinkedList.md 则从链表角度给出了与动态数组的对比视角建议一并阅读以形成完整的 List 体系认知。赞分享文档教程后端【免费下载链接】JCSprout Java Core Sprout : basic, concurrent, algorithm项目地址https://gitcode.com/gh_mirrors/jc/JCSprout点击查看免费下载相关推荐JCSprout项目解析深入理解ArrayList与Vector的底层实现JCSprout项目解析深入理解ArrayList与Vector的底层实现 引言为什么需要深入理解ArrayList和Vector 在日常Java开发中文档教程后端JCSprout 源码解读从 HashMap 到 HashSetJava 去重集合的底层实现原理JCSprout 源码解读从 HashMap 到 HashSetJava 去重集合的底层实现原理 HashSet 是 Java 集合框架中最常用的去重容器文档教程后端JCSprout 源码精读LinkedList 底层双向链表实现与增查性能分析JCSprout 源码精读LinkedList 底层双向链表实现与增查性能分析 导读 本文基于 JCSprout 知识库中的 LinkedList 底层分析文档教程后端上一篇解决Bruno变量插值对象异常从调试到修复全指南下一篇Metabase 后端开发实战用 clj-nrepl-eval 驱动 nREPL 的 Clojure 求值工作流创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考