一、序
ArrayList作为常用的集合,频繁的出现在工作和面试中,今天咱们从源码层面来复习一下有关ArrayList的一些知识。
二、
1.简介
ArrayList底层是数组队列,可以动态的扩容,
它实现了java.io.Serializable接口,支持序列化,
它实现了Cloneable接口,可以被克隆,
它实现了
RandomAccess 接口,支持快速随机访问(根据下标获取元素)。
ArrayList可以根据数组下标快速的读取元素,他的查询时间复杂度为O(1),因此ArrayList适用于查询频繁的场景,反之它的插入删除操作的时间复杂度为O(n)。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{...}
2.ArrayList是否线程安全?
ArrayList是线程不安全的,在多线程操作同一个数组时,可能会导致数组下标越界,举例:
List list = new ArrayList();
假设已经添加了9个元素,此时list的size = 9,
private static final int DEFAULT_CAPACITY = 10;//默认大小
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
//根据下标赋值
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
//判断数组是否为空,如果为空最小扩容量为默认大小10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
//判断是否需要扩容
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
//最小扩容量大于当前数组长度就需要扩容
if (minCapacity - elementData.length > 0)
//开始扩容方法
grow(minCapacity);
}
thread_1 调用 list.add("a");走到ensureCapacityInternal(size + 1)方法时挂起,此时并不需要扩容;
thread_2 调用 list.add("b");发现数组不需要扩容,直接将"b"放在下标为9的位置,此时size=10;
然后thread_1 继续执行,尝试将"a"放在下标为10的位置,但是数组并没有扩容,最大下标为9,所以程序抛出数组下标越界异常。并且,elementData[size++] = e也不是原子操作,多线程操作时可能会导致值覆盖的问题。
所以当多线程的情况下可以考虑使用Vector或者CopyOnWriteArrayList
3.ArrayList扩容机制
ArrayList的无参构造是创建一个空的数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
这种方式创建一个ArrayList时,在第一次调用add()方法的时候就会进行一次扩容操作,在上面的ensureCapacityInternal()和ensureExplicitCapacity()方法中我们可以看到
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;//最大容量
//扩容方法
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;//旧容量
//位运算,新容量 = 旧容量 + 旧容量/2
int newCapacity = oldCapacity + (oldCapacity >> 1);
//判断新容量是否小于最小所需容量,true -> 新容量 = 最小所需容量
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();
//如果新容量超出最大容量,则新容量为Interger.MAX_VALUE,否则为MAX_ARRAY_SIZE
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
所以在使用ArrayList时,最好可以给定一个初始值大小,这样减少扩容的次数,提升性能
//指定初始大小的有参构造(还有一种是直接传入一个Collection集合)
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);
}
}
PS:再一个值的注意的就是在ArrayList源码中频繁的用到了
Arrays
.
copyOf()和System.arraycopy()两个方法,简单说下两种方式的区别:
arraycopy()需要目标数组,将原数组拷贝到你自己定义的数组里,而且可以选择拷贝的起点和长度以及放入新数组中的位置
copyOf()是系统自动在内部新建一个数组,并返回该数组。
其实在
Arrays.
copyOf()
内部调用了
System.arraycopy()
方法。
4.小结
总体来说ArrayList的源码还是比较容易看明白的,除去扩容方法其他的方法也是值的学习的,以上内容如果有不正确的地方欢迎大家指正批评。