一文搞懂 Java ArrayList:把它想成一辆会自动扩容的超市购物车

文章目录

    • 一、开场:购物车装满以后,是换车还是自动加长
    • 二、数组、ArrayList 与 LinkedList 怎么选
    • 三、add、get、set、remove 基础操作
      • 3.1 JDK 25 中的首尾操作
    • 四、elementData、size 与 capacity
    • 五、为什么 new ArrayList<>() 没有立刻创建长度 10 的数组
    • 六、add 与 grow:商品怎样放进购物车
    • 七、10 → 15 → 22:约 1.5 倍扩容
    • 八、中间插入与删除为什么比较贵
    • 九、modCount 与 fail-fast:购物清单版本变了
    • 十、三个高频陷阱
      • 10.1 remove(1) 删除下标还是数字 1
      • 10.2 subList 是视图还是副本
      • 10.3 Arrays.asList 和 List.of 能否增删
    • 十一、容量优化与并发边界
    • 十二、完整实验:观察扩容、搬运与遍历
    • 十三、总结:车有容量,货有数量,装满再搬家
    • 参考资料

一、开场:购物车装满以后,是换车还是自动加长

周末晚上,超市正在促销。小李把牛奶、面包、水果和纸巾不断放进购物车,很快就没有空位了。

普通购物车装不下时,我们只能换一辆更大的车,再把原来的商品搬过去。如果这辆购物车叫ArrayList,它会自动完成这件事:

  1. 发现内部数组已经没有空位;
  2. 申请容量更大的新数组;
  3. 把旧数组里的元素复制过去;
  4. elementData指向新数组;
  5. 放入新元素,旧数组等待垃圾回收。

这就是 ArrayList 的核心:外表是一张可以不断添加元素的 List,内部仍然是一块长度固定的数组;所谓自动扩容,其实是创建新数组并搬家。

超市场景ArrayList 概念
购物车ArrayList对象
固定格子内部数组elementData
已放入的商品数size
一共拥有的格子数capacity
放到车尾add(E e)
换更大的车grow(minCapacity)
搬运旧商品Arrays.copyOf/System.arraycopy
购物清单版本modCount

本文以 Java SE 25 API 和 OpenJDK JDK 25 源码为背景。JavaDoc 承诺 ArrayList 会自动增长,并说明尾部追加具有摊销常数时间;具体扩容比例属于实现细节。

二、数组、ArrayList 与 LinkedList 怎么选

数组创建后长度不能改变,ArrayList 则帮我们管理扩容:

String[]fixed=newString[3];fixed[0]="牛奶";fixed[1]="面包";fixed[2]="苹果";// fixed[3] = "纸巾"; // ArrayIndexOutOfBoundsExceptionList<String>cart=newArrayList<>();cart.add("牛奶");cart.add("面包");cart.add("苹果");cart.add("纸巾");System.out.println(cart);

数组结构简单、内存紧凑、按下标访问快,但要提前确定长度。ArrayList 仍用数组保存元素,只是把申请新数组、复制元素和维护有效数量封装了起来。

LinkedList 通过节点引用连接。它修改链接很方便,但按下标寻找节点通常需要遍历:

操作ArrayListLinkedList
按下标读取O(1)O(n)
尾部添加摊销O(1)O(1)
中间插入定位快,搬运约O(n)定位约O(n),改链接O(1)
内存局部性较好节点分散、额外保存引用

所以“LinkedList 插入删除永远更快”并不准确。大多数业务要先找到位置,定位成本不能忽略。

三、add、get、set、remove 基础操作

List<String>cart=newArrayList<>();cart.add("牛奶");// 尾部添加cart.add("面包");cart.add(1,"苹果");// 在下标 1 插入Stringfirst=cart.get(0);// 牛奶Stringold=cart.set(2,"纸巾");// 返回面包Stringremoved=cart.remove(1);// 删除苹果System.out.println(first);System.out.println(old);System.out.println(removed);System.out.println(cart);// [牛奶, 纸巾]

下标从0开始。假设size为 3:

  • getsetremove的合法下标是0~2
  • add(index, value)的合法下标是0~3,因为允许插到末尾;
  • 越界会抛出IndexOutOfBoundsException

size()返回有效元素数量,不是内部数组长度。购物车有 10 个格子,但只放了 3 件商品时,size 是 3,capacity 是 10。

3.1 JDK 25 中的首尾操作

ArrayList 实现的List继承了SequencedCollection,JDK 25 可以直接使用addFirstaddLastgetFirstgetLastreversed。这些 API 让首尾语义更清楚,但addFirst仍要把已有元素整体向后移动,底层成本不会消失。

四、elementData、size 与 capacity

ArrayList 最重要的字段可以压缩成:

publicclassArrayList<E>{transientObject[]elementData;// 真正保存元素privateintsize;// 有效元素个数}

放入牛奶、面包和苹果后,内部结构大致如下:

下标 0 1 2 3 4 elementData 牛奶 面包 苹果 null null size = 3 capacity = elementData.length = 5

需要注意:

  1. size只统计有效区间[0, size)
  2. capacity 是数组长度,公开 API 没有直接读取方法;
  3. 后面的null是预留槽位,不属于 List;
  4. ArrayList 允许主动添加null,不能靠 null 判断 size;
  5. 删除后源码会清空不再使用的尾部槽位,避免继续引用旧对象。

五、为什么 new ArrayList<>() 没有立刻创建长度 10 的数组

OpenJDK 25 使用两个长度都是 0、但身份不同的空数组:

privatestaticfinalObject[]EMPTY_ELEMENTDATA={};privatestaticfinalObject[]DEFAULTCAPACITY_EMPTY_ELEMENTDATA={};publicArrayList(){this.elementData=DEFAULTCAPACITY_EMPTY_ELEMENTDATA;}publicArrayList(intinitialCapacity){if(initialCapacity>0){elementData=newObject[initialCapacity];}elseif(initialCapacity==0){elementData=EMPTY_ELEMENTDATA;}else{thrownewIllegalArgumentException();}}

new ArrayList<>()刚执行完时,内部数组长度仍然为 0,第一次添加才创建默认长度为 10 的数组。这样,如果程序创建了很多 ArrayList,但其中一部分从未放入元素,就不用提前为每个空 List 分配 10 个槽位。

JavaDoc 把无参构造器描述为构造一个初始容量为 10 的空列表,这是对外可理解的容量语义;当前 OpenJDK 实现采用延迟分配。阅读 API 和源码时,要区分“公开行为”和“内部时机”。

六、add 与 grow:商品怎样放进购物车

尾部添加和扩容的主干可以精简为:

publicbooleanadd(Eelement){modCount++;if(size==elementData.length){grow(size+1);}elementData[size++]=element;returntrue;}privateObject[]grow(intminCapacity){intoldCapacity=elementData.length;if(oldCapacity>0||elementData!=DEFAULTCAPACITY_EMPTY_ELEMENTDATA){intnewCapacity=ArraysSupport.newLength(oldCapacity,minCapacity-oldCapacity,oldCapacity>>1);returnelementData=Arrays.copyOf(elementData,newCapacity);}returnelementData=newObject[Math.max(10,minCapacity)];}

minCapacity是本次操作至少需要的容量,oldCapacity >> 1是期望增长量。ArraysSupport.newLength还要处理一次添加大量元素、整数上限和超大数组等边界,并不只是简单计算 1.5 倍。

七、10 → 15 → 22:约 1.5 倍扩容

当前 JDK 25 常规路径的期望容量可以这样理解:

intpreferredGrowth=oldCapacity>>1;intnewCapacity=oldCapacity+preferredGrowth;// 10 + 5 = 15// 15 + 7 = 22// 22 + 11 = 33

右移一位相当于非负整数除以 2并向下取整,所以常说“扩容为原来的约 1.5 倍”。

每次只增加一个槽位会频繁复制;一次扩大很多倍又可能浪费内存。约 1.5 倍是在复制次数和空闲空间之间做折中。

但必须强调:

Java SE API 没有规定 ArrayList 必须按 1.5 倍扩容。业务代码只能依赖容量会自动增长和相关复杂度语义,不能依赖某个版本的具体容量序列。

扩容不是在旧数组后面直接接几格。Java 数组长度创建后不能改变,真正过程是:

旧数组 capacity=10 ↓ 创建新数组 新数组 capacity=15 ↓ 复制已有元素 elementData 指向新数组 ↓ 旧数组等待 GC

扩容那一次需要复制元素,成本为O(n);但不是每次追加都扩容,因此连续添加 n 个元素的总成本仍为O(n),单次追加表现为摊销O(1)

八、中间插入与删除为什么比较贵

[A, B, C, D]的下标 1 插入 X,需要把 B、C、D 向后搬;删除中间元素则要把后面的内容向前补位:

publicvoidadd(intindex,Eelement){rangeCheckForAdd(index);modCount++;if(size==elementData.length){grow(size+1);}System.arraycopy(elementData,index,elementData,index+1,size-index);elementData[index]=element;size++;}privatevoidshiftTailOverGap(Object[]data,intindex){intmoved=size-index-1;if(moved>0){System.arraycopy(data,index+1,data,index,moved);}data[--size]=null;}

插入或删除的位置越靠前,需要搬动的元素越多。删除后把尾部设为null也很重要,否则内部数组可能继续强引用已经删除的对象。

九、modCount 与 fail-fast:购物清单版本变了

迭代器创建时会记住expectedModCount。如果遍历期间 ArrayList 被其他路径结构性修改,modCount发生变化,迭代器会尽快抛出ConcurrentModificationException

List<String>cart=newArrayList<>(List.of("牛奶","过期面包","苹果"));try{for(Stringitem:cart){if(item.startsWith("过期")){cart.remove(item);// 错误}}}catch(ConcurrentModificationExceptione){System.out.println("检测到结构性修改");}Iterator<String>iterator=cart.iterator();while(iterator.hasNext()){if(iterator.next().startsWith("过期")){iterator.remove();// 正确}}

fail-fast 只是尽力而为的错误检测,不是线程安全协议,也不保证所有并发修改都一定抛异常。不能依赖这个异常维持业务正确性。

十、三个高频陷阱

10.1 remove(1) 删除下标还是数字 1

10.2 subList 是视图还是副本

10.3 Arrays.asList 和 List.of 能否增删

把三个问题放在一起看:

List<Integer>numbers=newArrayList<>(List.of(1,2,3,2));numbers.remove(1);// 删除下标 1numbers.remove(Integer.valueOf(1));// 删除对象 1List<String>cart=newArrayList<>(List.of("A","B","C","D"));List<String>view=cart.subList(1,3);view.clear();System.out.println(cart);// [A, D]List<String>independent=newArrayList<>(cart.subList(0,cart.size()));List<String>fixed=Arrays.asList("牛奶","面包");List<String>immutable=List.of("牛奶","面包");// fixed.add("苹果"); // UnsupportedOperationException// immutable.set(0, "苹果"); // UnsupportedOperationException

remove(1)优先匹配remove(int index);要删除值 1,需要包装成Integer

subList返回原列表的区间视图,修改会反映到原列表;需要独立集合时应再new ArrayList<>(subList)。创建视图后,如果绕开视图直接结构性修改原列表,再继续使用旧视图,可能抛出ConcurrentModificationException

Arrays.asList返回由数组支持的定长 List,List.of返回不可修改 List,它们都不是普通可增删 ArrayList。

十一、容量优化与并发边界

已知大致数据量时,可以指定初始容量或使用ensureCapacity

可以直接写new ArrayList<>(expectedSize),也可以先创建列表再调用ensureCapacity(expectedSize)。提前规划能减少扩容和复制,但容量设置得过大也会浪费内存。trimToSize()可以把容量缩到当前 size,却同样需要创建新数组并复制元素,不适合在热路径里反复调用。

ArrayList 不是线程安全容器。并发修改时应在外部用锁保护完整操作,或根据场景评估Collections.synchronizedListCopyOnWriteArrayList。即便使用 synchronizedList,遍历期间也要按照文档要求在同一把锁上同步。

十二、完整实验:观察扩容、搬运与遍历

下面的程序一次验证扩容序列、元素搬运、fail-fast、安全删除、remove重载、容量规划和首尾操作:

importjava.lang.reflect.Field;importjava.util.*;publicclassArrayListShoppingCartDemo{privatestaticfinalFieldDATA=elementDataField();privatestaticFieldelementDataField(){try{Fieldfield=ArrayList.class.getDeclaredField("elementData");field.setAccessible(true);returnfield;}catch(ReflectiveOperationExceptione){thrownewIllegalStateException(e);}}publicstaticvoidmain(String[]args)throwsException{ArrayList<String>list=newArrayList<>();intprevious=-1;for(inti=0;i<=23;i++){if(i>0){list.add("商品-"+i);}intcurrent=capacity(list);if(current!=previous){System.out.printf("[扩容] size=%d, capacity=%d%n",list.size(),current);previous=current;}}varcart=newArrayList<>(List.of("A","B","C","D"));cart.add(1,"X");cart.remove(3);System.out.println("[搬运] "+cart);varwrong=newArrayList<>(List.of("牛奶","面包"));varstale=wrong.iterator();wrong.add("纸巾");try{stale.next();}catch(ConcurrentModificationExceptione){System.out.println("[fail-fast] 检测到版本变化");}varsafe=newArrayList<>(List.of("牛奶","过期面包","苹果"));variterator=safe.iterator();while(iterator.hasNext()){if(iterator.next().startsWith("过期")){iterator.remove();}}System.out.println("[安全删除] "+safe);varnumbers=newArrayList<>(List.of(1,2,3,2));numbers.remove(1);numbers.remove(Integer.valueOf(1));System.out.println("[remove 重载] "+numbers);vardefaultList=newArrayList<Integer>();varplannedList=newArrayList<Integer>();plannedList.ensureCapacity(100);System.out.printf("[容量规划] 默认扩容=%d 次,提前规划=%d 次%n",countGrowths(defaultList,100),countGrowths(plannedList,100));varsequenced=newArrayList<String>();sequenced.addFirst("面包");sequenced.addFirst("牛奶");sequenced.addLast("苹果");System.out.printf("[首尾] first=%s, last=%s, reversed=%s%n",sequenced.getFirst(),sequenced.getLast(),sequenced.reversed());}privatestaticintcountGrowths(ArrayList<Integer>list,intcount)throwsException{inttimes=0,previous=capacity(list);for(inti=0;i<count;i++){list.add(i);intcurrent=capacity(list);if(current!=previous){times++;previous=current;}}returntimes;}privatestaticintcapacity(ArrayList<?>list)throwsException{return((Object[])DATA.get(list)).length;}}

反射读取了 JDK 私有字段,因此运行时需要显式开放包:

javac ArrayListShoppingCartDemo.javajava--add-opens java.base/java.util=ALL-UNNAMED ArrayListShoppingCartDemo

关键输出包括:

[扩容] size=0, capacity=0 [扩容] size=1, capacity=10 [扩容] size=11, capacity=15 [扩容] size=16, capacity=22 [扩容] size=23, capacity=33 [搬运] [A, X, B, D] [fail-fast] 检测到版本变化 [安全删除] [牛奶, 苹果] [remove 重载] [3, 2] [容量规划] 默认扩容=7 次,提前规划=0 次 [首尾] first=牛奶, last=苹果, reversed=[苹果, 面包, 牛奶]

反射与--add-opens只用于观察实验,不应成为业务代码读取容量的方式。生产代码应依赖公开 API。

十三、总结:车有容量,货有数量,装满再搬家

  1. ArrayList 的底层核心是Object[] elementData
  2. size是有效元素数量,capacity 是内部数组长度;
  3. 默认构造器使用空数组占位,第一次添加时才落实默认容量;
  4. 当前 JDK 25 常规路径约按 1.5 倍扩容;
  5. 具体扩容比例属于源码实现,不是 Java SE 契约;
  6. 扩容需要创建新数组并复制旧元素;
  7. 尾部添加是摊销O(1),中间插入和删除通常需要搬运;
  8. fail-fast 用来尽早发现错误修改,不保证线程安全;
  9. remove重载、subList视图和不可修改 List 是高频坑;
  10. 已知数据量时合理规划容量,可以减少复制。

一句话记忆:

数组定长,List 管账;size 看货,capacity 看车;装满扩容,复制搬家。

参考资料

  • Oracle Java SE 25:ArrayList API
  • Oracle Java SE 25:List API
  • OpenJDK JDK 25:ArrayList.java 源码
  • OpenJDK JDK 25:ArraysSupport.java 源码

本文完整示例使用 JDK 25 编译运行。源码部分为了突出主线进行了删减;实际开发请以目标 JDK 的公开 API、对应版本源码和真实压测结果为准。


  • 个人小游戏