描述
ArrayList是一个以数组数据结构为基础的列表容器。它是一个有序、可重复且值可以为null的容器。因为是基于数组的数据结构,ArrayList需要在堆内存维持一个连续的内存空间。依照数组数据结构的原理,ArrayList对根据下标查询的速度非常快,但如果是插入或删除操作的支持就非常不友好。下图是ArrayList的类图
变量说明
private static final int DEFAULT_CAPACITY = 10; //数组对象默认长度
transient Object[] elementData; // ArrayList实际存储数据的对象数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; //一个数组长度为0的Object数组对象
private int size; //当前ArrayList记录的数据数量
protected transient int modCount = 0; //列表在结构上被修改的次数,结构修改是那些改变列表大小的修改,或者以这样一种方式扰乱列表,使得进行中的迭代可能产生不正确的结果。
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; //数组默认最大容量
构建方法
在ArrayList中提供了2个构建方法
- 无参构建方法
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
当使用无参构建方法时初始化elementData为一个数组长度为0的数组对象
2. 有参构建函数
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);
}
}
如果传入初始化参数大于0时给elementData变量初始化一个大小为传入参数的数组对象,如果等于0时给elementData变量初始化一个大小为0的数组对象。当传入参数小于0时会抛出IllegalArgumentException异常。
增加数据分析
- 在数组尾部追加数据add(E e)
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e; // 给elementData数组下标为size赋值,并且size加1
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数组是否是一个长度为0的数组对象
return Math.max(DEFAULT_CAPACITY, minCapacity); // 比较实际需要的数组长度和默认的数组长度,返回最大值。不在ArrayList初始化数组长度为默认长度,当实际插入才初始化数组长度,实现了懒加载的思想。
}
return minCapacity; // 返回实际需要的数组长度
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++; // 数据结构被修改次数 加1
// overflow-conscious code
if (minCapacity - elementData.length > 0) // 判断数组实际需要的长度是否大于当前的数组长度,如果大于则对数组进行扩容
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length; //记录当前数组长度
int newCapacity = oldCapacity + (oldCapacity >> 1); // 定义新的数组长度并且赋值为当前数组长度加上当前长度向右移一位(相当于除以2取整)
if (newCapacity - minCapacity < 0) // 判断新的数组长度是否满足当前需求的数组长度,若不满足则把当前需求的长度大小赋值给新数组长度大小
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0) //判断新数组长度是否大于容许创建的最大容量,则把新数组长度设置为容器可创建的最大值或int的最大值(容器可创建的最大值比int最大值小8)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
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;
}
- 在指定的下标索引插入数据add(int index, E element)
public void add(int index, E element) {
rangeCheckForAdd(index); //检查下标是否溢出和下标是否大于等0
ensureCapacityInternal(size + 1); // 检查数组是否需要扩容,并实现扩容。以及对操作次数进行加1 --!这个方法在上面已经说明过了
System.arraycopy(elementData, index, elementData, index + 1,
size - index); // 在源数组从下标index(包含)开始的数据复制到目标数组下标为index + 1的位置上。
elementData[index] = element; //把要插入的数据插入到index位置上
size++; // 存储记录加1
}
private void rangeCheckForAdd(int index) {
if (index > size || index < 0) //检查下标是否溢出和下标是否大于等0
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
总结: ArrayList是一个以数组结构为基础的容器,允许插入null,并且时一个有序的容器。默认初始数组大小为10,扩容增加当前数组长度大小除以2的长度。
删除数据分析
- 根据下标删除
public E remove(int index) {
rangeCheck(index); // 检查下标是否越界
modCount++; // 数组操作记录+1
E oldValue = elementData(index); // 获取下标元素
int numMoved = size - index - 1; // 数组中实际存储数据在index此下标后还有多少个
if (numMoved > 0) // 在index下标后面实际存在的元素个数大于0,表示当前下标不是最尾部那个元素
System.arraycopy(elementData, index+1, elementData, index,
numMoved); // 把index+1以及之后的元素复制到index下标开始的位置上
elementData[--size] = null; // 这个时候其实下标为size-1位置上的数据还存在,所以这里首先对size-1然后把size-1的下标设置为null,让gc回收这个元素
return oldValue; // 返回旧的数据列表
}
- 根据对象删除
public boolean remove(Object o) {
if (o == null) { // 当传入参数为null时
for (int index = 0; index < size; index++) // 循环遍历数组所有元素
if (elementData[index] == null) { // 判断元素等于null
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++) // 循环遍历数组所有元素
if (o.equals(elementData[index])) { // 比较两个对象是否相等
fastRemove(index);
return true;
}
}
return false;
}
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
总结: 在循环体中使用remove()方法删除某个元素时,会出现跳过遍历一个元素在根据元素的问题。可以在循环体对下标索引的值做减一操作。如果是需要在遍历时做移除操作建议使用Iterator。对象删除时如果存在多个相同的对象时,只会移除第一个元素