一、概述
ArrayList是经常用到的一个容器,它是Java Collections Framework的一个成员,然后底层是基于定长数组来实现的,如果数组存放满了,就会通过扩容机制重新生成一个更大的数组来存放数据。
因此,扩容机制是ArrayList的核心所在,这一点是务必要掌握的;除此之外,本文还会叙述ArrayList的基本操作是怎样实现的,以及其它的细节。
二、ArrayList源码解读
2.1、继承关系
从UML类图中可以看到,ArrayList直接或间接实现了Iterable、Collection、List、RandomAccess、Cloneable、Serializable这6个接口;ArrayList直接或间接继承了AbstractList、AbstractCollection这2个抽象类。
疑问来了:为什么AbstractList实现了List接口,ArrayList还要去再实现一次List接口,是吃饱了撑着吗?
带着疑问,我立马去网上冲浪一波,然后发现了3个对此的解释。
观点1:如果ArrayList没去实现List接口,会导致class的getInterfaces方法返回不同的结果,这么做是为了方便基于List接口的动态代理。
观点2:这可能是为了增加继承结构的可追踪性。当浏览Javadoc或类似的东西时,就不必遍历整个继承树,它不会有任何不良影响,并且可以帮助理解代码。
观点3:ArrayList的作者Josh Bloch曾经认为ArrayList再实现一次List接口是有一些价值的,但后来发现这是个错误。
以上3个观点均出自stackoverflow的一个帖子,我们了解下就好了,深究意义不大,具体原因还得是作者自己才知道。
2.1.1、Iterable接口
2.1.1.1、概述
Iterable表示可迭代的,它有个iterator方法,需要返回Iterator对象,Iterator是一个接口,表示为迭代器。Iterable接口的定义如下:
1
2
3
4
5
6
|
public interface Iterable<T> {
Iterator<T> iterator();
// .....省略
}
|
那么实现Iterable接口有什么作用呢?先说结论:只要对象实现了Iterable接口,就可以使用for-each语法,编译器会转换为调用Iterable和Iterator接口的方法。
我们平常用for-each语法遍历list时,你可能会写下如下代码:
1
2
3
4
5
6
7
|
List<Integer> list = new ArrayList<>(3);
list.add(1);
list.add(2);
list.add(3);
for (Integer value : list) {
System.out.println("value: " + value);
}
|
这种for-each语法背后是怎么实现的呢?其实,编译器会将它转换为类似如下代码:
1
2
3
4
|
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()){
System.out.println("value: " + iterator.next());
}
|
除了Iterable接口的iterator方法外,List接口也有一个listIterator方法,需要返回ListIterator对象,ListIterator是一个接口,它对Iterator接口做了扩展。比如,可以从末尾往前遍历,如下代码:
1
2
3
4
|
ListIterator<Integer> iterator = list.listIterator(list.size());
while (iterator.hasPrevious()){
System.out.println("value: " + iterator.previous());
}
|
2.1.1.2、fail-fast机制
那些年,年少时所犯下的错误,想起自己刚入门Java时,曾经写过如下代码:
1
2
3
4
5
|
for (Integer value : list) {
if (value < 3) {
list.remove(value);
}
}
|
当年的直觉告诉你,这么写不存在问题,但是运行却会报并发修改的错误:
1
|
Exception in thread "main" java.util.ConcurrentModificationException
|
显然,这里是没有并发的代码,我们去看看ConcurrentModificationException的注释描述。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
/**
* 当不允许进行此类修改时,已检测到对象的并发修改的方法可能会抛出此异常。
* This exception may be thrown by methods that have detected concurrent
* modification of an object when such modification is not permissible.
* <p>
* 例如,通常不允许一个线程在另一个线程迭代集合时修改集合。
* For example, it is not generally permissible for one thread to modify a Collection
* while another thread is iterating over it.
* 通常,在这些情况下迭代的结果是不确定的。
* In general, the results of the iteration are undefined under these circumstances.
* 如果检测到此行为,某些 Iterator 实现(包括 JRE 提供的所有通用集合实现)可能会选择抛出此异常。
* Some Iterator implementations (including those of all the general purpose collection implementations
* provided by the JRE) may choose to throw this exception if this behavior is
* detected.
* 执行此操作的迭代器被称为fail-fast迭代器,因为它们快速而干净地失败,而不是冒着在未来不确定的时间出现任意的、不确定的行为的风险。
* Iterators that do this are known as <i>fail-fast</i> iterators,
* as they fail quickly and cleanly, rather that risking arbitrary,
* non-deterministic behavior at an undetermined time in the future.
* <p>
* 请注意,此异常并不总是表示对象已被不同的线程并发修改。
* Note that this exception does not always indicate that an object has
* been concurrently modified by a <i>different</i> thread.
* 如果单个线程发出一系列违反对象契约的方法调用,则该对象可能会抛出此异常。
* If a single thread issues a sequence of method invocations that violates the
* contract of an object, the object may throw this exception.
* 例如,如果线程在使用fail-fast迭代器迭代集合时直接修改集合,则迭代器将抛出此异常。
* For example, if a thread modifies a collection directly while it is
* iterating over the collection with a fail-fast iterator, the iterator
* will throw this exception.
* 请注意,不能保证fail-fast行为,因为一般来说,在存在非同步并发修改的情况下不可能做出任何硬性保证。
* <p>Note that fail-fast behavior cannot be guaranteed as it is, generally
* speaking, impossible to make any hard guarantees in the presence of
* unsynchronized concurrent modification.
* ail-fast操作会尽最大努力抛出ConcurrentModificationException。
* Fail-fast operations throw {@code ConcurrentModificationException} on a best-effort basis.
* 因此,编写依赖于此异常的正确性的程序是错误的: ConcurrentModificationException应该仅用于检测错误。
* Therefore, it would be wrong to write a program that depended on this
* exception for its correctness: <i>{@code ConcurrentModificationException}
* should be used only to detect bugs.</i>
*
* @author Josh Bloch
* @see Collection
* @see Iterator
* @see Spliterator
* @see ListIterator
* @see Vector
* @see LinkedList
* @see HashSet
* @see Hashtable
* @see TreeMap
* @see AbstractList
* @since 1.2
*/
|
上面的注释说的很明显了,如果线程在使用fail-fast迭代器迭代集合时直接修改集合,则迭代器将抛出此异常。
那么如何避免异常呢?可以使用迭代器的remove方法,如下所示:
1
2
3
4
5
6
|
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
if (iterator.next() < 3) {
iterator.remove();
}
}
|
它自己的remove方法为何又可以使用呢?我们需要看下迭代器的原理了。
2.1.1.3、迭代器的原理
我们先来看下ArrayList中iterator方法的实现,代码如下:
1
2
3
4
5
6
7
|
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
public Iterator<E> iterator() {
return new Itr();
}
}
|
iterator方法实现很简单,直接new一个Itr对象返回,Itr是ArrayList的一个成员内部类,实现了Iterator接口,它的代码量并不多,所以直接贴出来了,看注释应该就能明白。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
private class Itr implements Iterator<E> {
int cursor; // index of next element to return 下一个要返回的元素位置
int lastRet = -1; // index of last element returned; -1 if no such 最后一个返回的索引位置,如果没有,为-1
int expectedModCount = modCount; // 期望的修改次数,初始化为外部类当前的修改次数modCount
// prevent creating a synthetic constructor
Itr() {}
public boolean hasNext() {
return cursor != size; // cursor与数组元素数量的比较
}
@SuppressWarnings("unchecked")
public E next() {
// 校验是否发生了结构性变化
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
// 更新cursor的值,每次+1
cursor = i + 1;
// 更新lastRet值,返回对应的元素
return (E) elementData[lastRet = i];
}
public void remove() {
// 注意:lastRet < 0时会抛出异常,所以调用remove()之前需要先调用next()去更新lastRet的值
if (lastRet < 0)
throw new IllegalStateException();
// 校验是否发生了结构性变化
checkForComodification();
try {
// 执行ArrayList的remove方法,modCount++
ArrayList.this.remove(lastRet);
// 更新cursor的值,remove后元素数量少了一个,相当于cursor=cursor-1
cursor = lastRet;
// 重置lastRet
lastRet = -1;
// 更新expectedModCount
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
// 校验是否发生了结构性变化,所谓结构性变化就是添加、插入和删除元素,只是修改元素内容不算结构性变化。
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
|
迭代器源码中next方法,remove方法都会调用checkForComodification方法进行校验是否发生了结构性变化,由此可见,迭代器的内部维护了索引位置相关的数据,要求在迭代过程中,不能发生结构性变化,否则这些索引位置功能就会失效。
2.1.2、RandomAccess接口
RandomAccess内部是没有任何代码的接口,它属于标记接口,其定义如下:
1
2
|
public interface RandomAccess {
}
|
实现了RandomAccess接口的类表示支持快速随机访问,用在一些通用的算法代码中,它可以根据这个声明而选择效率更高的实现,比如Collections类的binarySearch方法,会根据List是否实现了RandomAccess接口而采用不同的实现,代码如下:
1
2
3
4
5
6
7
|
public static <T>
int binarySearch(List<? extends Comparable<? super T>> list, T key) {
if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
return Collections.indexedBinarySearch(list, key);
else
return Collections.iteratorBinarySearch(list, key);
}
|
2.2、成员变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
// 默认初始容量
private static final int DEFAULT_CAPACITY = 10;
// 用于空实例的共享空数组实例
private static final Object[] EMPTY_ELEMENTDATA = {};
// 用于默认大小的空实例的共享空数组实例。注意:要将它与EMPTY_ELEMENTDATA区分开来,以了解添加第一个元素时要膨胀多少
// 可以理解为标记调空参数构造方法
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 存储ArrayList元素的数组缓冲区。ArrayList的容量就是这个数组缓冲区的长度。添加第一个元素时,任何具有 elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的空 ArrayList 都将扩展为 DEFAULT_CAPACITY。
transient Object[] elementData;
// ArrayList的大小(它包含的元素数)
private int size;
// 要分配的数组的最大大小(除非必要)
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
// 此列表在结构上被修改的次数。结构修改是那些改变列表大小的修改,或者以其他方式扰乱列表,使得正在进行的迭代可能会产生不正确的结果。
protected transient int modCount = 0;
|
疑问来了:我们知道了ArrayList实现了Serializable接口,但是elementData为何要用transient修饰,这不表示elementData不能被序列化?
其实玄机在于ArrayList中的两个方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
// 将ArrayList实例的状态保存到流中(即序列化它)。
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 behavioral compatibility with clone()
s.writeInt(size);
// Write out all elements in the proper order.
for (int i = 0; i < size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
// 从流中重构ArrayList实例(即反序列化)
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in capacity
s.readInt(); // ignored
if (size > 0) {
// like clone(), allocate array based upon size not capacity
SharedSecrets.getJavaObjectInputStreamAccess().checkArray(s, Object[].class, size);
Object[] elements = new Object[size];
// Read in all elements in the proper order.
for (int i = 0; i < size; i++) {
elements[i] = s.readObject();
}
elementData = elements;
} else if (size == 0) {
elementData = EMPTY_ELEMENTDATA;
} else {
throw new java.io.InvalidObjectException("Invalid size: " + size);
}
}
|
ArrayList在序列化的时候会调用writeObject方法,直接将size和element写入ObjectOutputStream;反序列化时调用readObject方法,从ObjectInputStream获取size和element,再恢复到elementData。
elementData是存储ArrayList元素的数组缓冲区,通常扩容后都会预留一些空间,也就是说有部分空间实际没有存储元素,序列化时只序列化实际存储的那些元素,而不是整个数组,从而可以节省空间和时间。
疑问来了:为什么MAX_ARRAY_SIZE的值是Integer.MAX_VALUE - 8而不是Integer.MAX_VALUE?
因为存储了Array的头部信息,所以这里需要减去8。
2.3、构造方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
// 空参构造方法
public ArrayList() {
// 由于没有指定初始容量,所以赋值一个空实例的共享空数组实例,可以理解为标记调空参数构造方法
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 指定了初始容量的构造方法
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
// 创建指定大小的Object数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
// 这里单纯就是元素个数为0后,赋值一个空实例的共享空数组实例
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
public ArrayList(Collection<? extends E> c) {
// c.toArray()底层实际调用的也是Arrays.copyOf(),根据集合c的长度创建一个Object[]新数组,把数据拷贝到新数组上
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// 重复调用Arrays.copyOf()是为了防止c.toArray()不返回Object[]
// defend against c.toArray (incorrectly) not returning Object[]
// (see e.g. https://bugs.openjdk.java.net/browse/JDK-6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
// 这里单纯就是元素个数为0后,赋值一个空实例的共享空数组实例
this.elementData = EMPTY_ELEMENTDATA;
}
}
|
2.4、核心方法
2.4.1、add方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
// 将指定的元素附加到此列表的末尾
public boolean add(E e) {
// 此列表在结构上被修改的次数
modCount++;
add(e, elementData, size);
return true;
}
private void add(E e, Object[] elementData, int s) {
// 数组容量满了,执行扩容机制
if (s == elementData.length)
// 得到一个扩容后的新数组
elementData = grow();
// size位置插入元素
elementData[s] = e;
// 更新size大小:原元素个数大小+1
size = s + 1;
}
// 在此列表中的指定位置插入指定元素
public void add(int index, E element) {
// 校验index的范围
rangeCheckForAdd(index);
modCount++;
// 当前元素个数大小
final int s;
Object[] elementData;
// 数组容量满了,执行扩容机制
if ((s = size) == (elementData = this.elementData).length)
// 得到一个扩容后的新数组
elementData = grow();
// 将当前位于该位置的元素(如果有)和任何后续元素向右移动(将其索引加一)。
System.arraycopy(elementData, index,
elementData, index + 1,
s - index);
// 移动后,index位置空余出来,即可将element插入到index位置
elementData[index] = element;
// 更新size大小:原元素个数大小+1
size = s + 1;
}
// 将指定集合中的所有元素追加到此列表的末尾
public boolean addAll(Collection<? extends E> c) {
// c.toArray()底层实际调用的也是Arrays.copyOf(),根据集合c的长度创建一个Object[]新数组,把数据拷贝到新数组上
Object[] a = c.toArray();
modCount++;
int numNew = a.length;
if (numNew == 0)
return false;
Object[] elementData;
final int s;
// 原数组的剩余容量不够插入数组的大小时,就进行扩容
if (numNew > (elementData = this.elementData).length - (s = size))
// 得到一个扩容后的新数组
elementData = grow(s + numNew);
// 将插入数组的元素拷贝到扩容数组上
System.arraycopy(a, 0, elementData, s, numNew);
// 更新size大小:原元素个数大小+插入数组的长度
size = s + numNew;
return true;
}
|
关于add方法,如果是末尾插入,那么平均时间复杂度为O(1);如果是非末尾插入,因为需要移动元素,那么平均时间复杂度为O(n)。因此,我们应该尽量避免在大数据量中调用add带索引参数的方法。
2.4.2、grow方法(扩容机制)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
private Object[] grow() {
return grow(size + 1);
}
// 增加容量以确保它至少可以容纳最小容量参数指定的元素数
private Object[] grow(int minCapacity) { // minCapacity = size + 1
// 先计算新容量大小,再根据新容量大小创建一个Object[]新数组,把数据拷贝到新数组上
return elementData = Arrays.copyOf(elementData,
newCapacity(minCapacity));
}
// 返回至少与给定最小容量一样大的容量。如果足够,返回增加 50% 的当前容量。除非给定的最小容量大于 MAX_ARRAY_SIZE,否则不会返回大于 MAX_ARRAY_SIZE 的容量。
private int newCapacity(int minCapacity) {
// overflow-conscious code
// 旧容量 = 原数组的长度
int oldCapacity = elementData.length;
// 新容量 = 旧容量 + 旧容量右移一位(相当于除于2)
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity <= 0) {
// 如果是调用了无参数构造方法,没有指定初始容量时
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
// 默认初始容量就为10
return Math.max(DEFAULT_CAPACITY, minCapacity);
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
// 其它情况一律size + 1
return minCapacity;
}
// 如果还没达到最大容量,那就是新容量大小,否则返回一个返回一个巨大的容量
return (newCapacity - MAX_ARRAY_SIZE <= 0)
? newCapacity
: hugeCapacity(minCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
// size+1 > MAX_ARRAY_SIZE时,那么返回一个巨大的容量Integer.MAX_VALUE,否则MAX_ARRAY_SIZE
return (minCapacity > MAX_ARRAY_SIZE)
? Integer.MAX_VALUE
: MAX_ARRAY_SIZE;
}
|
2.4.3、remove方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
// 移除此列表中指定位置的元素
public E remove(int index) {
// 校验index的范围
Objects.checkIndex(index, size);
final Object[] es = elementData;
// 取出要移除的元素
@SuppressWarnings("unchecked") E oldValue = (E) es[index];
// 删除元素
fastRemove(es, index);
// 返回要移除的元素
return oldValue;
}
// 跳过边界检查并且不返回删除的值的私有删除方法
private void fastRemove(Object[] es, int i) {
modCount++;
final int newSize;
// 如果删除的元素索引位置非最后一个元素位置,那么将当前位于该位置的元素(如果有)和任何后续元素向左移动(将其索引减一)。
if ((newSize = size - 1) > i)
System.arraycopy(es, i + 1, es, i, newSize - i);
// 移动原size-1位置多余了,需要置null
es[size = newSize] = null;
}
// 从此列表中移除第一次出现的指定元素(如果存在)。如果列表不包含该元素,则它不变。
public boolean remove(Object o) {
final Object[] es = elementData;
final int size = this.size;
int i = 0;
// 使用了java label语法
found: {
// 如果传入null,那么遍历去查看是否存在null的元素,找到就跳出循环
if (o == null) {
for (; i < size; i++)
if (es[i] == null)
break found;
} else {
// 如果传入不为null,那么遍历去查看是否存在相同的元素,找到就跳出循环
for (; i < size; i++)
if (o.equals(es[i]))
break found;
}
// 上面两个条件下都找不到要查找的元素,则返回false
return false;
}
// 找到要删除的元素后执行删除
fastRemove(es, i);
return true;
}
|
2.4.4、set方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// 用指定元素替换此列表中指定位置的元素
public E set(int index, E element) {
// 校验index的范围
Objects.checkIndex(index, size);
// 获取索引位置对应的元素
E oldValue = elementData(index);
// 设置索引位置对应的元素
elementData[index] = element;
// 返回要修改的元素
return oldValue;
}
// 返回索引位置对应的元素
E elementData(int index) {
return (E) elementData[index];
}
|
2.4.5、get方法
1
2
3
4
5
6
7
|
// 返回此列表中指定位置的元素
public E get(int index) {
// 校验index的范围
Objects.checkIndex(index, size);
// 返回索引位置对应的元素
return elementData(index);
}
|
2.5、其它方法
2.5.1、ensureCapacity方法
1
2
3
4
5
6
7
8
9
10
11
12
|
// 如有必要,增加此ArrayList实例的容量,以确保它至少可以容纳最小容量参数指定的元素数。
// 也就是说,这个方法的使用场景是如果已经预知ArrayList需要比较大的容量,调用这个方法可以减少ArrayList内部分配和扩展的次数
public void ensureCapacity(int minCapacity) {
// 最小容量不得低于原数组的长度,同时原数组不是第一次调用空参数构造方法和第一次扩容
if (minCapacity > elementData.length
&& !(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
&& minCapacity <= DEFAULT_CAPACITY)) {
modCount++;
// 进行扩容
grow(minCapacity);
}
}
|
2.5.2、trimToSize方法
1
2
3
4
5
6
7
8
9
10
11
12
|
// 将此ArrayList实例的容量修剪为列表的当前大小。应用程序可以使用此操作来最小化ArrayList实例的存储
public void trimToSize() {
modCount++;
// 数组容量没装满元素
if (size < elementData.length) {
// 如果元素个数为0,那么赋值一个空实例的共享空数组实例
elementData = (size == 0)
? EMPTY_ELEMENTDATA
// 否则拷贝元素到新数组上,新数组的大小就是元素个数的数量
: Arrays.copyOf(elementData, size);
}
}
|
三、总结
在ArrayList的源码中,并没有与并发相关的代码,所以说ArrayList是非线程安全类,在并发环境下多个线程同时操作,会引发不可预知的错误。应该使用Collections.synchronizedList方法包装列表,最好在创建时完成,如下代码:
1
|
List list = Collections.synchronizedList(new ArrayList(...));
|
另外,ArrayList是经常用到的一个容器,要根据实际的业务场景来使用,比如说,当添加、删除数据不是在前面或中间,并且需要随机地访问其中的元素时,使用ArrayList会有更好的性能。