ArrayList 概述
ArrayList
是 Java 开发中广泛使用的集合之一,它是List
接口的一种实现方式。它采用可变数组作为底层数据结构,允许存储包括null
在内的任何元素。
关键特点
存储有序: ArrayList
内部维护了一个可变数组,用来存储列表中的元素,所以元素可以按照被添加到列表中的顺序进行保存。当遍历列表时,元素会按照它们被插入的顺序输出(也就是插入顺序 = 输出顺序)。动态扩容:当向 ArrayList
添加元素时,如果当前容量不足以容纳新的元素,ArrayList
会自动扩充其容量。默认情况下,它会增加到当前容量的 1.5 倍。开发时,我们可以通过调用ensureCapacity(int minCapacity)
或者trimToSize()
方法来手动调整控制容量,以优化其存储性能。查询高效: ArrayList
在查询时底层是通过访问数组元素方式进行查询的,而数组在声明时,会在内存中申请一块连续相邻的内存空间。当要通过索引访问数组元素时,可通过数组地址值和偏移量,直接计算出要查找元素的内存地址,所以数组支持通过索引直接访问元素,效率非常高。线程不安全: ArrayList
是线程不安全的。如果多个线程同时访问一个ArrayList
实例,并且至少有一个线程对列表结构进行了修改(如添加或删除元素),则调用者必须从外部进行同步处理。可以使用Collections.synchronizedList()
方法中来创建一个线程安全的列表。
接口实现
List 接口:提供 List
线性列表集合框架的基础行为,如添加、删除、获取和更新元素等。RandomAccess 接口: RandomAccess
是一个标记接口,没有任何接口方法,只用作标识某个类是否支持高效的随机访问。ArrayList
内部使用数组来存储元素,因此可以通过索引直接访问元素,时间复杂度为 O(1)。相比之下,LinkedList
没有实现RandomAccess
接口,因为它需要遍历链表来找到特定位置的元素,时间复杂度为 O(n)。Cloneable 接口: ArrayList
实现了Cloneable
接口,允许通过clone()
方法创建一个新的对象实例。不过这个操作是一个浅拷贝,也就是说新旧对象中的元素仍然是相同的引用。
ArrayList clone() 代码示例:
public class ArrayListCloneExample {
public static void main(String[] args) {
ArrayList<Person> list = new ArrayList<>();
list.add(new Person("张三"));
System.out.println("Original: " + list);
System.out.println("---------------clone---------------");
ArrayList<Person> cloneList = (ArrayList<Person>) list.clone();
cloneList.get(0).setName("李四");
System.out.println("Original: " + list);
System.out.println("Clone: " + cloneList);
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
class Person {
private String name;
}
运行结果:
Original: [Person(name=张三)]
---------------clone---------------
Original: [Person(name=李四)]
Clone: [Person(name=李四)]
Serializable 接口:实现这个接口表示支持序列化,允许对象被转换为字节流以保存到文件或在网络上传输。需要注意的是:只有当被添加到 ArrayList
的元素也实现了Serializable
接口时,整个ArrayList
才能成功序列化。
深拷贝(Deep Copy)和浅拷贝(Shallow Copy)
深拷贝:创建一个新对象,并且复制原对象中的所有字段,包括引用类型的字段。对于每个引用类型的字段,它都会创建一个新的实例,确保源对象和目标对象之间没有任何共享的引用。 浅拷贝:也创建一个新对象,该对象与原对象具有相同的值,但对引用类型字段只会复制引用地址,而不复制实际的对象。意思是原对象和新对象的引用类型属性指向同一块内存。
ArrayList 源码分析
核心成员变量
// 默认的初始化容量
private static final int DEFAULT_CAPACITY = 10;
// 调用带参构造初始化集合时,指定初始化容量为0时(new ArrayList(0)),使用该数组赋值
private static final Object[] EMPTY_ELEMENTDATA = {};
// 调用空参数构造初始化时,使用该数组赋值
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* 实际存放 ArrayList 元素的数组,数组的容量即是 ArrayList 的容量,
* 当一个空 ArrayList 的 elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA 这个空数组的时候,
* 添加第一个元素时,ArrayList 将被扩容至 DEFAULT_CAPACITY 默认容量
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* 该字段表示当前添加到 ArrayList 的元素的实际个数,需要注意的是它必定小于等于数组对象 elementData 的长
* 度。一旦当 size 与 elementData 的长度相同,并且还在往列表中添加元素的时,ArrayList 会执行扩容操作,用一
* 个更长的数组对象存储先前的元素。
*/
private int size;
EMPTY_ELEMENTDATA
与DEFAULTCAPACITY_EMPTY_ELEMENTDATA
两者均为静态成员变量,代表空的数组实例,是为了减少空
ArrayList
对象创建时的内存占用。通过共享相同的空数组实例,多个空的ArrayList
实例不再各自持有独立的空数组,从而节省了内存资源。此外,这两个静态成员还帮助区分不同构造函数创建的ArrayList
在首次扩容时的行为逻辑,以优化性能(下文扩容逻辑中会进行说明)。
构造方法
// 无参构造函数,初始化为空数组 EMPTY_ELEMENTDATA
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 指定初始容量的构造函数,允许提前指定数组容量,减少扩容次数
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
// 指定容量大于0时,直接创建一个指定容量的数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
// 指定容量为0时,使用默认空数组初始化
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
}
}
// 根据集合初始化的构造函数,接收一个集合并将其转换为 ArrayList
public ArrayList(Collection<? extends E> c) {
// 将集合转为数组,如果集合本身是一个 ArrayList,那么它的 toArray() 方法返回的就是它的内部数组引用。
Object[] a = c.toArray();
if ((size = a.length) != 0) {
// 如果传入的集合是 ArrayList
if (c.getClass() == ArrayList.class) {
// 直接将 elementData 指向传入 ArrayList 的内部数组 a,这样做避免了额外的数组复制操作,提高了性能
elementData = a;
} else {
// 传入其他类型的集合,直接调用 Arrays.copyOf() 方法来创建一个新的数组,并将传入集合的元素复制到这个新数组中。
// 这是因为不同类型的集合可能有不同的内部表示形式,直接使用它们的 toArray() 方法返回的结果可能会导致类型不匹配或其他问题
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
// 传入空集合时,使用默认空数组初始化
elementData = EMPTY_ELEMENTDATA;
}
}
添加元素与扩容
public boolean add(E e) {
// 新增一个元素时,要确保有足够的容量容纳新元素
ensureCapacityInternal(size + 1); // Increments modCount!!
/**
* 将新元素放置在数组的当前位置(size 位置),然后递增 size。
* 这行代码实际上进行了两个操作:
* 1. 赋值操作:elementData[size] = e;
* 2. 自增操作:size++;
* 在执行 elementData[size++] = e; 这行代码之前,size 变量记录了当前 ArrayList 中已经存储了多少个元素。
* 因为数组的索引是从 0 开始的,所以 size 的值实际上指向了下一个应该放置新元素的位置,即第一个空闲的位置。
* 这样做确保了新元素总是被添加到列表的末尾,并且保持了元素之间的相对顺序不变。
* 总结:从这里可以看出,size 不仅是 ArrayList 中实际元素数量的计数器,它也是一个逻辑上的“指针”,指向下一个可用的插入位置。
*/
elementData[size++] = e;
return true;
}
// 计算所需的最小容量并确保数组有足够的空间
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
// 计算实际需要的最小容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
/**
* 当 elementData 是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 时,第一次添加元素进行扩容时,
* 会直接跳过小规模的逐步增长,直接达到或超过 DEFAULT_CAPACITY,从而减少了后续可能发生的频繁扩容操作。
*
* 如果 elementData 是 EMPTY_ELEMENTDATA,calculateCapacity 方法就不会进入上述的 if 分支,而是直接返回 minCapacity,
* 这时候会仅按需分配空间,而不是立即扩展到较大的默认容量,这可以节省内存资源,特别是当不确定列表最终会有多大时。
*/
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
// 确保数组具有足够的容量
private void ensureExplicitCapacity(int minCapacity) {
modCount++; // 记录结构修改次数
// overflow-conscious code
if (minCapacity - elementData.length > 0)
// 如果添加当前元素后需要的容量比 elementData 数组的长度要大了(当前数组已满,无法存储新加入的元素),则进行扩容。
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// 原来的容量带符号右移 1 位再与原来的容量相加,也就是 1.5 倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果新容量超过最大数组大小(MAX_ARRAY_SIZE),则调用 hugeCapacity(minCapacity) 处理大容量情况,最大分配值为 Integer.MAX_VALUE
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// copy 数组,将内部所有元素放到长度为 newCapacity 的新数组中,并将新数组对应的引用赋值给 elementData,
// 此刻 ArrayList 内部引用的对象就是更新了长度后的新数组,至此,扩容完成。
elementData = Arrays.copyOf(elementData, newCapacity);
}
// 添加元素的重载方法,添加元素到指定位置
public void add(int index, E element) {
// 检查索引是否合法,如果越界则抛出异常
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
/**
* 1. 使用 System.arraycopy() 将从索引 index 开始的所有元素向后移动一位,腾出空位给新元素
* 2. 将新元素插入到索引 index 处
* 3. 增加 size
*/
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
add(E e)
与add(int index, E element)
对比:
add(E e)
:通常情况下,该方法非常高效,因为它直接在数组末尾添加元素,不需要任何额外的数据位移操作。只有当触发扩容机制时,才会出现性能下降的情况。add(int index, E element)
:该方法的效率依赖于插入点的位置。越靠近列表前端插入,性能开销越大;而在接近或恰好是列表末端插入时,性能损失较小。此外,频繁地使用该方法可能导致更多的内存分配和数据拷贝操作,进而影响整体性能。
扩容总结:在调用像add()
方法,向ArrayList
中添加新元素而当前容量不足以容纳这些元素时,就会触发扩容操作。
好啦,到这里,面试官爱问的扩容逻辑我们基本已经梳理清晰了,那么你可知道,ArrayList
其实还可以进行手动缩容。面试时,回答这一点,可能会让面试官刮目相看哦~
请看代码:
public void trimToSize() {
// 记录修改次数
modCount++;
// 如果当前列表实际存储的元素个数(size)小于内部数组的长度(elementData.length),则进行调整;
// 这说明 ArrayList 存在多余的容量,可以考虑减小内部数组的大小以节省内存。
if (size < elementData.length) {
// 如果列表为空,则将 elementData 设为空数组引用 EMPTY_ELEMENTDATA
// 否则,使用 Arrays.copyOf 创建一个新的、更小的数组,并复制现有元素到新数组中
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
trimToSize()
特别适用于那些在初始化阶段会经历一系列快速添加元素的过程,然后在之后的业务中几乎不再增长的列表。例如,加载一大批数据后,可以通过调用trimToSize()
来释放占用的多余的内存空间。
trimToSize()
虽然可以帮助我们去手动释放内存,提高资源利用率,但是它并不是一个轻量级的操作。因为每次调用都涉及到数组的复制,所以,我们尽量在已知数据量的情况下去手动指定ArrayList
的容量,而不是调用此方法,以免影响性能。
删除元素
public E remove(int index) {
// 检查提供的索引是否越界,如果越界则抛出异常
rangeCheck(index);
// 记录结构修改次数,用于迭代器检测并发修改
modCount++;
// 获取即将被删除的元素
E oldValue = elementData(index);
// 删除元素后,后边的元素需要向前移动,这里计算要向前移动的元素数量
// 比如 size 为5,索引分别是 0,1,2,3,4,删除下标为 2 的元素,
// 那么 numMoved = 5 - 2 - 1 = 2,也就是移动下标为 3 和 4 的元素
int numMoved = size - index - 1;
if (numMoved > 0)
// 进行元素移动
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 将数组中最后的位置设为 null,帮助 GC 回收不再使用的对象,减少内存占用
elementData[--size] = null; // clear to let GC do its work
// 返回被删除的元素
return oldValue;
}
public boolean remove(Object o) {
// 处理传入的对象是 null 的情况
if (o == null) {
// 遍历整个数组,找到第一个 null 删除并返回
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
// 删除逻辑与 remove(int index) 方法基本一致,这里不再赘述
fastRemove(index);
return true;
}
} else {
// 对于非 null 的对象,需要遍历列表并调用对象的 equals() 方法逐个进行比较
for (int index = 0; index < size; index++)
// 找到匹配项,删除并返回
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
// 如果遍历结束仍未找到匹配项,则返回 false 表明没有执行任何删除操作
return false;
}
remove(int index)
与remove(Object o)
对比:
remove(int index)
直接定位到特定索引处进行删除,效率较高,时间复杂度为 O(1),因为它不需要遍历列表。remove(Object o)
必须遍历列表直到找到匹配项为止,最坏情况下时间复杂度为 O(n),其中 n 是列表的长度,在处理大型列表时可能不如remove(int index)
方法更高效。总结:无论是哪种形式的删除操作,底层最终都会使用
System.arraycopy()
方法来进行数组元素的移动,所以效率上都会受到影响。
获取元素
public E get(int index) {
// 检查索引是否合法
rangeCheck(index);
// 调用 elementData 方法返回指定索引处的元素
return elementData(index);
}
E elementData(int index) {
// 返回数组中指定索引位置的元素,并进行类型转换
return (E) elementData[index];
}
get(int index)
方法的时间复杂度为 O(1),因为它只需要一次直接的数组访问就可以完成元素的检索。理论上,无论ArrayList
有多大,获取任意位置上的元素所需的时间几乎是恒定的,这是ArrayList
的一大优势。
修改元素
public E set(int index, E element) {
// 检查索引是否合法
rangeCheck(index);
// 获取当前索引位置的旧值
E oldValue = elementData(index);
// 将新元素设置到指定索引位置
elementData[index] = element;
// 返回旧值,这里允许调用者知道被替换的元素是什么
return oldValue;
}
set(int index, E element)
方法不会改变列表的大小,它只替换现有位置上的元素。它的时间复杂度为 O(1),因为它也是只需要一次直接的数组访问就可以完成元素的替换操作。
结语
本篇文章中,我们通过分析ArrayList
中的成员变量与几个关键方法的源码,了解了ArrayList
的内部工作原理。在接下来的文章中,我们将讨论ArrayList
的序列化机制以及并发修改相关的问题。
您的支持就是对我最大的鼓励!如果本文对您有帮助,请记得点赞、分享、在看哦~~~谢谢!