一 、ArrayList是什么?
ArrayList 是 Java 集合框架中非常常用的一个类,位于:
java.util.ArrayList
它的底层本质是一个动态数组。
也就是说,ArrayList 内部其实维护了一个数组,用来存储元素。当元素数量超过当前数组容量时,ArrayList 会自动扩容,创建一个更大的数组,并把旧数组中的元素复制到新数组中。
简单理解:
ArrayList = 可以自动扩容的数组
普通数组的缺点是长度固定:
String[] arr = new String[3];
这个数组最多只能存放 3 个元素,不能自动变大。
而 ArrayList 可以这样使用:
ArrayList<String> list = new ArrayList<>();
list.add("Java");
list.add("Spring");
list.add("MySQL");
list.add("Redis");
即使一开始没有指定容量,它也可以不断添加元素。
二、ArrayList的使用以及其原理
1. ArrayList的创建
1.1. 无参构造方法
最常见的创建方式:
ArrayList<String> list = new ArrayList<>();
从使用者角度看,好像创建了一个空集合。
但从底层来看,ArrayList 并不会一开始就创建一个长度为 10 的数组。
它通常采用的是懒加载思想:刚创建对象时,内部数组还是一个空数组,等第一次添加元素时,才真正初始化容量。
可以简化理解为:
ArrayList<String> list = new ArrayList<>();
具体看一下无参构造的源码,其中DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}:
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
此时:
size = 0 #存储元素的个数
elementData = 空数组
1.2. 指定初始容量
也可以这样创建:
ArrayList<String> list = new ArrayList<>(20);
这表示底层数组一开始就准备 20 个位置。
适合已经知道大概元素数量的场景。
例如:
ArrayList<Integer> ids = new ArrayList<>(1000);
如果你知道后面大概率要放 1000 个元素,那么提前指定容量可以减少扩容次数,提高性能。
根据源码可以观察到,其实是创建了一个固定大小的对象数组:
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
💡Tip
初始容量不是越大越好,而是应该根据业务预估,否则会造成内存的浪费
2. ArrayList添加元素
创建数组之后就是向其中添加元素的,通常我们使用add()方法向ArrayList中添加元素。
list.add("爱学习的小菜鸟");
那么针对于ArrayList的元素添加,其又是怎么实现的呢?附上大师级程序员的源码。
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 判断是否选需要扩容,确保可以容纳新元素
elementData[size++] = e; //在末尾添加新元素
return true; //添加成功
}
参数e为要添加的元素,size为ArrayList的元素个数,此时为0.
2.1 如何确保可以容纳新的元素
如果要想理解ArrayList如何确保可以容纳新的数据以及自动扩容,我们来继续跟进一下ensureCapacityInternal()方法。
/**
* 确保容纳指定容量的元素
* @param minCapacity 指定容量的最小值
*/
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
/**
* 计算容量
* @param elementData 元素数组
* @param minCapacity 指定容量的最小值
* @return
*/
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 判断是不是第一次添加元素(底层数组还是默认空数组)
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 在默认容量DEFAULT_CAPACITY = 10 和 minCapacity(1) 之间取最大值
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
其中ensureCapacityInternal()方法中:
minCapacity为 1 ,因为size初始值为空(int类型,即为0),所以传参 size+1 即为1。
elementData为用于存放元素的底层数组,此时为空{}。
所以,calculateCapacity()方法中,if判断语句为true,最后minCapacity的值为10。
接下来执行ensureExplicitCapacity()方法:
/**
* 检查集合的容量,不够时会自动扩容
* @param minCapacity 集合所需的最小容量
*/
private void ensureExplicitCapacity(int minCapacity) {
//记录数组修改次数,用于并发校验(modCount是fail‑fast快速失败机制核心)
modCount++;
//如果最小需要的容量 > 底层数组的长度,就执行真正的扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
其中ensureExplicitCapacity()方法中,
minCapacity为集合所需的最小容量,第一次新增元素的时候为10。
elementData.length为底层数字的长度为0。
所以if条件成立,执行扩容函数grow()方法:
private void grow(int minCapacity) {
// 1. 获取旧数组长度
int oldCapacity = elementData.length;
// 2. 新容量 = 原容量 + 原容量/2,也就是扩容1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 3. 如果扩容1.5倍后还不满足最小需要的容量,直接使用minCapacity
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 4. 校验容量是否超过ArrayList允许的最大数组容量
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 5. 数组拷贝:开辟新长度的数组,把旧数组数据复制过去,完成扩容
elementData = Arrays.copyOf(elementData, newCapacity);
}
针对本次第一次添加元素的代入演算:
oldCapacity = 0
newCapacity = 0 + 0 >> 1 = 0
0 < 10,因此直接赋值 newCapacity = 10
没有超出最大限制
Arrays.copyOf 创建一个长度为 10 的新 Object 数组,赋值给elementData
最后回到add()方法:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 判断是否选需要扩容,确保可以容纳新元素
elementData[size++] = e; //在末尾添加新元素
return true; //添加成功
}
执行elementData[size++] = e,因此,第一个元素被赋值为"爱学习的小菜鸟",然后返回true,结束第一次添加元素的方法。
3. ArrayList指定位置添加元素
除了使用add(E e)方法,也可以通过add(int index, E element)方法向指定位置index插入元素:
list.add(1,"菜鸟开发");
源码如下:
public void add(int index, E element) {
rangeCheckForAdd(index);// 检查索引是否越界
ensureCapacityInternal(size + 1); // 判断是否选需要扩容,确保可以容纳新元素
System.arraycopy(elementData, index, elementData, index + 1,
size - index); // 将index及其后面的元素往后移一位
elementData[index] = element; //将元素插入指定位置
size++; // 元素个数加一
}
4. 更新ArrayList中元素
使用set()方法来更改ArrayList中元素,例如修改索引为1位置的元素内容:
list.set(1,"菜鸟开发分享");
源码如下:
public E set(int index, E element) {
rangeCheck(index);// 索引是否越界
E oldValue = elementData(index);// 获取指定位置的旧元素
elementData[index] = element;// 将新元素替换到指定位置
return oldValue; // 返回修改前指定位置的元素
}
该方法会先对指定的下标进行检查,看是否越界,然后替换新值并返回旧值。
5. 删除ArrayListz中元素
第一个方法就是remove(int index)方法,用于删除指定索引位置的元素:
list.remove(0); //删除list数组中的第一个元素
接下来看下remove(int index)方法的源码:
public E remove(int index) {
rangeCheck(index); //检查是否越界
modCount++; // 记录数组修改次数,用于并发校验(modCount是fail‑fast快速失败机制核心)
E oldValue = elementData(index); //获取要移除位置的元素
int numMoved = size - index - 1; // 需要移动的元素个数
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved); //移动元素
elementData[--size] = null; // 将数组末尾的元素位置为null,让GC回收元素占用的空间
return oldValue; //返回被删除的元素
}
第二个方法是remove(Object o)方法,用来移除找到的第一个元素,源码如下:
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
✏️Note
有相同元素的时候,只会删除第一个