“开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 2 天,点击查看活动详情” ArrayList实现了RandomAccess,一个空的接口,支持随机访问,用索引查询,删除,插入的数据结构,才会用到随机访问, 而且实现了这个接口,for循环的效率要比迭代器高(LinkedList就没有实现这个接口)
实现了Cloneable接口,标识着可以它可以被复制.注意,ArrayList里面的clone()复制其实是浅复制。
实现了Serializable,支持序列化传输
默认容量 DEFAULT_CAPACITY = 10,当超出后,会自动扩容为原来的1.5倍。数组的扩容是新建一个原数组大小+扩充容量的大数组,将原数组拷贝到新数组(扩容的代价很大,应该尽量减少)。如果是删除数组指定位置的元素,那么可能会挪动大量的数组元素;如果是删除末尾元素的话,那么代价是最小的.
ArrayList采用了Fail-Fast机制,面对并发的修改时,迭代器很快就会完全失败,报异常 concurrentModificationException(并发修改一次),而不是冒着在将来某个不确定时间发生任意不确定行为的风 险。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 其他两个构造函数,在不满足if条件的情况下,使用的是EMPTY_ELEMENTDATA
* 非常单纯的提供空的数组
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* 只有默认构造函数使用了DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* 用处是在空list首次调用add方法的时候判断如何初始化数组的容量。
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
*既然实现了序列化接口,为什么还要修饰不让其进行序列化?
*为了节省空间,自己重写了序列化方法
*由于ArrayList的数组是动态扩增的,并不是所有被分配的空间都存储了数据
*如果使用外部的序列化,会序列化整个数组,为了避免那些没有存储数据的空间被序列化,自己提供了writeObject和readObject来自己序列化和反序列化
*因此使用 transient 修饰数组,是防止对象数组被其他外部方法序列化
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* size 表示容器当中数据的个数 注意和容器的长度区分开来
*/
private int size;
/**
* 构造方法其一,当传入参数时,参数的不同会选择是开辟具体大小的数组还是空数组
* 当我们传入0参数的时候,走向EMPTY_ELEMENTDATA 空数组,平时不应该传入0这个参数,会滥用扩容
*/
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);
}
}
//数组最大长度减8,因为一些vm可能会在数组中保留一些header信息,分配更大的长度可能会导致OutOfMemoryError异常。
//这个8就是就是存了数组_length字段。
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
}
ps: 如果我们确定了一个固定的值,传入一个参数后就不要再进行add或者remove操作,让初始时就进行精确的扩容
扩容操作
add主要的执行逻辑如下:
- 确保数组已使用长度(size)加1之后足够存下 下一个数据
- 修改次数modCount 标识自增1,如果当前数组已使用长度(size)加1后的 大于当前的数组长度,则调用grow方法,增长数组,grow方法会将当前数组的 长度变为原来容量的1.5倍。
- 确保新增的数据有地方存储之后,则将新元素添加到位于size的位置上。
- 返回添加成功布尔值。
public boolean add(E e) {
//确保容量有size + 1,防止越界
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
//传入参数等于0就不会进这个if,进的话会被赋值一个10大小的数组,相当于进行一次扩容(至少一次)
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
//扩容方法
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
//近似1.5倍的扩容
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果新数组的长度,小于需要的最小的容量,则更新数组的长度为 minCapacity
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
//判断整数是否发生溢出
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
//将当前elementData进行拷贝,开辟1.5倍的内存空间,浅拷贝
elementData = Arrays.copyOf(elementData, newCapacity);
}
注意: 扩容是一件非常耗时且损耗空间的事情,如果能在使用ArrayList之前能够确定List的容量大小,那就不会调用grow()进行扩容。
再看看插入指定位置的方法,执行逻辑如下:
- 确保数插入的位置小于等于当前数组长度,并且不小于0,否则抛出异常
- 确保数组已使用长度(size)加1之后足够存下一个数据
- 修改次数(modCount)标识自增1,如果当前数组已使用长度(size)加1后的大于当前的数组长度,则调用grow方法,增长数组(grow方法会将当前数组的长度变为原来容量的1.5倍)
- 确保有足够的容量之后,使用System.arraycopy将需要插入的位置(index)后面的元素统统往后移动一位。
- 将新的数据内容存放到数组的指定位置(index)上
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
获取元素
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
删除元素
//remove方法会让下标到数组末尾的元素向前移动一个单位,并把最后一位的值置空,方便GC
public E remove(int index) {
//下标检查
rangeCheck(index);
//移除元素的时候也会改变modCount,并且是++操作
modCount++;
E oldValue = elementData(index);
// 这里需要计算需要移动的数据的个数
int numMoved = size - index - 1;
//如果移除的不是最后一个元素,进行arrayCopy进行元素向前移动
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
private void rangeCheck(int index) {
if (index < 0 || index >= this.size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
/**
* 没有重置list的size,即还是原来的数组,添加元素不会再次扩容
* 要对一个list重复使用的话就要使用这个方法,如果再次赋值new Arraylist(),再次添加元素后会再次扩容
* Removes all of the elements from this list. The list will
* be empty after this call returns.
*/
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
PS:
B站:河北王校长