JAVA后端开发知识总结(持续更新…)
ArrayList扩容机制和主要方法(源码解析)
文章目录
- ArrayList扩容机制和主要方法(源码解析)
- 一、ArrayList基本概述
- 二、ArrayList的扩容机制
- 三、ArrayList的常用方法分析
- 参考文档
一、ArrayList基本概述
ArrayList是实现了List接口的基于动态数组的数据结构,可以用来存放各种类型的数据,ArrayList按照插入的顺序来存放数据。但是ArrayList不是线程安全的。
- ArrayList的主要属性
// 数组的默认初始容量大小
private static final int DEFAULT_CAPACITY = 10;
// 定义一个空数组以供其它需要用到的地方调用
private static final Object[] EMPTY_ELEMENTDATA = {};
// 定义一个空数组
// 用来判断 ArrayList第一次添加数据的时候要扩容多少
// 使用默认构造器的情况下返回这个空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// ArrayList的底层结构
// 使用默认构造器时,第一次添加数据,容量扩容为 DEFAULT_CAPACITY
transient Object[] elementData;
// 已经使用的容量大小
private int size;
DEFAULTCAPACITY_EMPTY_ELEMENTDATA:使用默认构造函数时返回的空数组。如果是 ArrayList第一次添加数据,数组扩容为 DEFAULT_CAPACITY = 10。
EMPTY_ELEMENTDATA:出现在需要用到空数组的地方,例如,当在构造函数中指定初始容量为0的时候就会返回它。
- ArrayList的默认构造函数
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
默认构造函数一开始使用参数DEFAULTCAPACITY_EMPTY_ELEMENTDATA 返回一个空数组,所以 ArrayList 在创建之初没有指定初始容量的情况下,就会返回一个长度为0的空数组。但它在第一次添加数据时就会返回一个初始容量为10(扩容后的结果)的数组。
二、ArrayList的扩容机制
所有添加数据的操作都要需要判断,当数组容量不足以容纳新的数据就需要进行扩容操作。
- ensureCapacityInternal
只有调用了默认构造函数,才可能返回默认为10的容量,如果是构造函数中传入的 Capacity为0,则不会生成默认大小。此时隐含有 minCapacity = size + 1的信息。
// 判断是否需要扩容
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
// 判断当前数组是否是默认构造函数法生成的空数组
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 如果是则取容量为 max(10, minCapacity)
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 不是就根据原来的值传入下一个方法完成后面的扩容判断
return minCapacity;
}
- ensureExplicitCapacity
判断当前ArrayList是否需要进行扩容,如果修改后的数组容量大于当前的数组长度,就需要调用grow() 进行扩容。
// 判断当前ArrayList是否需要进行扩容
private void ensureExplicitCapacity(int minCapacity) {
// 快速报错机制:防止多个进程同时修改同一个容器的内容
// 不一致会抛出ConcurrentModificationException
modCount++;
if (minCapacity - elementData.length > 0)
// 容量过小,需要扩容
grow(minCapacity);
}
- 扩容核心——grow
- 如果当前数组是由默认构造函数生成的空数组且第一次添加数据,则 minCapacity默认等于10,此时数组的容量会从0扩容为10。之后的数组扩容才是1.5倍扩容。
- 如果当前数组是由带参构造函数生成且初始容量为0,则minCapacity等于1,此时数组的容量会从0变成1。但是设定初始容量为0,会导致前四次扩容每次都 +1,只有在第5次添加数据进行扩容的时候才是按照当前容量的1.5倍进行扩容。
- 当扩容量大于ArrayList定义的最大值后进行判断,根据与MAX_ARRAY_SIZE的比较确定是返回Integer最大值还是MAX_ARRAY_SIZE。ArrayList允许的最大容量就是Integer的最大值。
// 用来决定扩容量
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
// 1.5倍扩容
int newCapacity = oldCapacity + (oldCapacity >> 1);
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 = 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的常用方法分析
- add(E e)
每次添加数据前,ArrayList都会先调用ensureCapacityInternal方法来判断是否需要扩容,接着再尾插,所以ArrayList是按插入的顺序排序。
public boolean add(E e) {
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
- add(int index, E element)
- index表示要插入的索引,先判断数组是否越界,然后调用ensureCapacityInternal方法进行扩容逻辑。
- 之后调用System.arraycopy方法来进行复制操作,该方法为本地(native)方法。ArrayList每次调用该方法添加数据时都会进行数组的复制,复制时把相对于index后面的数据都向后移动一位。
- 过多的复制导致ArrayList在对数据的插入操作效率比较差,ArrayList在随机插入数据的效率上比不了LinkedList。
// 按 index进行插入操作
public void add(int index, E element) {
// 越界判断
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1);
// 复制操作
System.arraycopy(elementData, index, elementData, index + 1, size - index);
elementData[index] = element;
size++;
}
- remove(int index)
该方法同样使用System的本地方法arraycopy进行复制,它把相对于index后几位的数据全部向前移动一位,最后返回被删除的数据。
// 删除指定位置的元素
public E remove(int index) {
rangeCheck(index);
// 快速报错机制
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index, numMoved);
// 便于GC回收最后一个的空间
elementData[--size] = null;
return oldValue;
}
- remove(Object o)
- 该方法是ArrayLsit对Object对象的删除,该方法在对Object对象删除时区分了Null与非空的情况。
- 不管是哪种情况,只调用fastRemove删除一次就返回。
- 非空时该方法采用equals来判断数组中的对象是否相等,如果这个被操作的类没有重写hashCode和equals,无法成功删除数据。所以对类对象(尤其是自定义的) 进行操作时必须重写这两个方法。
// 删除对象
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == 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)
// 依然需要arraycopy
System.arraycopy(elementData, index+1, elementData, index, numMoved);
elementData[--size] = null;
}
- iterator()
- 该方法实现了Iterator接口,并对几个方法都进行了自定义实现,同时保证了Java容器的快速报错机制。
- 快速报错机制能够防止多个进(线)程同时修改同一个容器的内容。在使用迭代器Iterator或者forEach进行迭代遍历时,如果有其它进程想要进行修改,就会抛出ConcurrentModificationException异常。
- iterator()方法在迭代遍历时会调用checkForComodification方法判断当前ArrayList是否是同步的。
- 迭代遍历时不能进行add和remove等操作,但是对于Iterator迭代器可以使用remove进行操作,因为此时操作的不是ArrayList而是它的Iterator对象。
public Iterator<E> iterator() {
return new Itr();
}
private class Itr implements Iterator<E> {
int cursor; // 下一个要返回元素的索引
int lastRet = -1; // 最后一个返回元素的索引; -1 代表没有
// 快速报错机制的标志,如果最后两者相等,则说明是同步的
int expectedModCount = modCount;
@SuppressWarnings("unchecked")
public E next() {
// 进行同步判定
checkForComodification();
...
}
// 同步判定函数
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
参考文档
《jdk1.8ArrayList主要方法和扩容机制(源码解析)》