ArrayList简介
ArrayList实现了AbstractList类和List接口,是基于数组实现的,是一个动态数组,其大小可以自动增长。所以它具备了数组的优势,可以通过元素索引快速查询,且是有序存储。
ArrayList不是线程安全的,多线程下可以考虑使用concurrent包下面的CopyOnWriteArrayList类。
同时实现了Serializable接口,因此支持序列化,能够通过序列化传输;实现了RandomAccess接口,支持快速随机访问;实现了Cloneable接口,能被克隆。
源码分析
核心实现其实就是通过数组底层实现,实现了数组的动态扩容。
具体是怎么实现的,通过源代码来看一下。
主要的成员变量:
/**
//默认的数组初始化容量
private static final int DEFAULT_CAPACITY = 10;
//定义一个空数组实例,以便其他需要的地方使用
private static final Object[] EMPTY_ELEMENTDATA = {};
//定义一个空数组,跟前面的区别就是这个空数组是用来判断ArrayList第一添加数据的时候要扩容多少。默认的构造器情况下返回这个空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//ArrayList数据存储的数组,ArrayList的容量就是数组的长度。使用DEFAULTCAPACITY_EMPTY_ELEMENTDATA构造实例时,第一次添加数据数组容量是10.
transient Object[] elementData;
//ArrayList的size,即元素的个数。
private int size;
构造函数:
//构造一个指定初始容量的空列表
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);
}
}
//构造一个初始容量为10的空列表
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//构造一个包含指定集合的元素的列表,按照集合的迭代顺序进行存储
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
this.elementData = EMPTY_ELEMENTDATA;
}
}
可以看到总有三种构造方法,第一种指定了初始容量,第二种默认构造方法,初始化了容量为10的数组,第三种初始化带数据的ArrayList。
我们可以看到默认的构造器,通过DEFAULTCAPACITY_EMPTY_ELEMENTDATA返回了一个空的数组,当第一次有数据添加时,会自动扩容一个容量为10的数组,
那么扩容是怎么实现的呢?
扩容机制:
当每次调用add或者addAll进行数据添加时都会先检查是否需要扩容,进入ensureCapacityInternal方法:
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
所以默认构造函数,当第一次添加数据时,会初始化容量为10的数组。如果不是,继续往下走,如果容量不够,则需要调用grow方法进行扩容:
private void ensureExplicitCapacity(int minCapacity) {
//快速报错机制
modCount++;
//如果容量不够,则需要调用grow方法进行扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
ArrayList扩容的核心方法grow():
- 调用默认构造函数,第一次添加数据时,会先扩容容量为10,后续再根据newCapacity = oldCapacity + (oldCapacity >> 1)进行扩容,即1.5倍大小进行扩容;
- 调用指定容量初始化并且初始化容量为0时,我能可以看到,前4次扩容都只能+1,第5次开始才进行1.5倍的扩容,所以不要指定初始化容量为0;
- 当扩容量newCapacity大于ArrayList数组定义的最大值后会调用hugeCapacity来进行判断。如果minCapacity已经大于Integer的最大值(溢出为负数)那么抛出OutOfMemoryError(内存溢出),否则的话根据与MAX_ARRAY_SIZE的比较情况确定是返回Integer最大值还是MAX_ARRAY_SIZE。
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
//如果数组长度超过
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
思考
关于ArrayList源码,比较重要的几点:
- 注意三个不同的构造方法。无参构造方法构造的ArrayList当第一次添加数据时会扩容容量为默认10,带有Collection参数的构造方法,将Collection转化为数组赋给ArrayList的实现数组elementData。
- 从上面的扩容过程可以看出,每次扩容完都需要将原来的元素拷贝到一个新的数组上,非常耗时,所以建议在事先能确定元素数量的情况下,才使用ArrayList,否则建议使用LinkedList。
- ArrayList基于数组实现,可以通过下标索引直接查找到指定位置的元素,因此查找效率高,但每次插入或删除元素,就要大量移动元素,插入删除效率低。