ArrayList 源码分析
本文共3521字,阅读本文大概需要7~12分钟
ArrayList 是 java 集合框架中比较常用的数据结构,继承自 AbstractList,实现了 List 接口。底层基于数组实现容量大小动态变化,允许 null 存在。同时还实现了 RandomAccess、Cloneable、Serializable接口,所以 ArrayList 是支持快速访问、复制、序列化的
ArrayList 原理
- ArrayList 是基于数组来实现的
- java 中的数组都是定长数组,比如数组大小设置为20,此时你不停的往 ArrayList 里面塞入数据,此时元素超过20以后就会自动扩容,就会用一个更大的数组,把以前的数组拷贝到新的数组里面去
ArrayList的优缺点
- 缺点
① 不要频繁的往 ArrayList 里面去塞数据,导致它频繁的数组扩容,避免扩容的时候较差的性能影响了系统的运行
② 底层是数组来实现的,若要在数组中间添加一个元素,需要将数组中那个新增的元素后面的元素全部往后面挪一位,所以说,如果往ArrayList中间插入一个元素,性能比较差,会导致后面的大量元素挪动一个位置 - 优点
① 基于数组来实现的,非常适合随机读,性能很高,它可以基于它底层对数组的实现来快速的随机读取到某个元素,list.get(n)
② 开发系统的时候,大量的场景,需要一个集合,里面可以按照顺序灌入一些数据,对于ArrayList的话,它的最重要的功能作用就是他里面的元素是有顺序的,我们在系统里的一些数据,都是需要按照我们插入的顺序来排列的
源码分析
构造方法
在ArrayList中,默认的构造方法,直接初始化一个ArrayList实例的话,会将内部的数组做成一个默认的空数组{}private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
,它有一个默认的初始化的数组的大小的值,默认为10,private static final int DEFAULT_CAPACITY = 10;
,也就是我们可以认为它默认的数组初始化的大小就是只有10个元素
对于ArrayList,如果要玩好的话,一般来说,你应该都不是使用这个默认的构造函数,你构造一个ArrayList的话,基本上来说就是默认它里面不会有太频繁的插入、移除元素的操作,大体上他里面有多少元素,你应该可以推测一下
基本上最好是给ArrayList构造的时候,给一个比较靠谱的初始化的数组大小,比如说,100个数据,1000个数据,避免数组太小,往里面塞入数据的时候,导致数据不断的扩容,不断的搞新数组
boolean add(E e)
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
你每次往ArrayList中插入数据的时候,都会判断一下,当前数组是否塞满了,如果塞满了,此时就会自动扩容,然后将老数组中的元素拷贝到新数组中,确保说数组一定是可以承受足够多的元素的,扩容的方法是grow()
,扩容的大小为int newCapacity = oldCapacity + (oldCapacity >> 1);
即扩容为老数组大小的1.5倍
E set(int index, E element)
public E set(int index, E element) {
// 用于检测索引是否超出元素大小,超出就报IndexOutOfBoundsException
rangeCheck(index);
// 替换索引位置的元素
// 获取索引位置老的元素
E oldValue = elementData(index);
// 设置为新的元素
elementData[index] = element;
// 返回索引所在的老的数据
return oldValue;
}
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
void add(int index, E element)
public void add(int index, E element) {
// 判断索引是否越界
rangeCheckForAdd(index);
// 该方法确保数组是否满了,若满了再次添加的时候会调用grow()进行扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
// 进行数组的拷贝
// 5个参数
// 第一个参数是要被复制的数组
// 第二个参数是被复制的数字开始复制的下标
// 第三个参数是目标数组,也就是要把数据放进来的数组
// 第四个参数是从目标数据第几个下标开始放入数据
// 第五个参数表示从被复制的数组中拿几个数值放到目标数组中
// eg:
// 数组1:int[] arr = { 1, 2, 3, 4, 5 };
// 数组2:int[] arr2 = { 5, 6,7, 8, 9 };
// 运行:System.arraycopy(arr, 1, arr2, 0, 3);
// 得到
// int[] arr2 = { 2, 3, 4, 8, 9 };
// 表示的是从数组arr中下标为1的位置取出3个数据,放到数组arr2中从下标为2的位置,放入3个数据。
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
// 把第index位置的元素设为element
elementData[index] = element;
size++;
}
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
E get(int index)
public E get(int index) {
// 判断数组是否越界
rangeCheck(index);
// 直接返回索引位置的元素
return elementData(index);
}
E remove(int index)
public E remove(int index) {
// 判断索引是否超出数组大小
rangeCheck(index);
// 查资料说这是 记录modCount的修改次数
modCount++;
E oldValue = elementData(index);
// 需要挪动的元素的个数
int numMoved = size - index - 1;
if (numMoved > 0)
// 数组的拷贝,见add()源码分析
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 将移动后最后一个位置设为 null
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
源码分析的总结
remove()、add(index,element)
这两个方法,都会导致数组的拷贝,大量元素的挪动,这种基于数组来做这种随机位置的插入和删除,性能都不是太高
add()、add(index,element)
这两个方法,都可以会导致数组需要扩容,数组长度是固定的,默认初始大小是10个元素,如果不停的往数组里赛数据,可能会导致瞬间数组不停的扩容,影响系统性能
set()、get()
定位到随机位置,替换那个元素,或者是获取那个元素,这个其实还是比较靠谱的,基于数组来实现随机位置的定位,性能还是很高的
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南