- ArrayList是实现了List接口的动态数组。实现了所有的可选列表操作(add, remove, set, get, size, indexOf, contains …),允许所有的元素,包括null。
- ArrayList是线程不安全,如果多个线程访问同一个ArrayList实例,并且至少有一个线程修改了列表的结构(添加或删除一个或多个元素,或显式调整后备数组的大小都会造成结构修改;仅设置元素的值不是结构修改。),则必须在外部进行同步。通常采用另外一种方法解决这个问题:List list = Collections.synchronizedList(new ArrayList(…));
- fail-fast机制1,如果在迭代器创建之后发生列表的结构修改(除非通过迭代器自己创建的remove或者add方法),迭代器会抛出ConcurrentModificationException。
- size,isEmpty,get,set,iterator和listIterator方法以恒定的时间运行。add方法以“分摊后恒定的时间” (amortized constant time2 )运行,就是说,增加n个元素的时间复杂度为O(n)。所有其他的方法以线性时间运行。
1. 首先看下ArrayList的继承关系:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
着重提一下RandomAccess接口,这是一个标记接口,没有任何方法,实现此接口表明支持快速随机访问,比如对ArrayList使用for循环遍历就比iterator迭代器要快3。
2. 变量
/**
* 默认初始容量
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 用于空实例的共享空数组实例
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* 用于默认大小空实例的共享空数组实例。我们将此与EMPTY_ELEMENTDATA区分开来,
* 以便在添加第一个元素时知道要扩容多少。
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* 存储ArrayList元素的数组缓冲区。 ArrayList的容量是此数组缓冲区的长度。
* 添加第一个元素时,任何带有elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA的
* 空ArrayList都将扩展为DEFAULT_CAPACITY。
*/
transient Object[] elementData; // 非私有,以简化嵌套类访问
/**
* ArrayList的大小(它包含的元素个数)
*/
private int size;
elementData就是ArrayList底层维护的Object数组(transient4)。可以看到EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA都是由static和final修饰的,这也是一种尽量不浪费资源的设计5。
3. 方法
方法无疑是基于elementData的操作,我觉得如果掌握了add这个方法,基本就把ArrayList的结构理清楚了。
// 将指定的元素追加到此列表的末尾。
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
// 保证内部容量是充足的。即保证ArrayList内部维护的elementData数组不会发生溢出
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
// 计算本次操作所需的最小容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
// 保证明确的容量
private void ensureExplicitCapacity(int minCapacity) {
// 继承自AbstactList的属性,用来实现fail-fast机制(可参考文末注解)
modCount++;
// overflow-conscious code
// 考虑溢出的代码
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
// 实现elementData数组的自增长
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // oldCapacity >> 1右移一位相当于oldCapacity/2
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity总是接近于size,所以到此为止:
elementData = Arrays.copyOf(elementData, newCapacity);
}
可以看出grow方法是add操作最关键的一步,增长因子为1.56。
另,MAX_ARRAY_SIZE为Integer.MAX_VALUE - 8,hugeCapacity方法是为了限制分配更大的数组而导致OutOfMemoryError;Arrays.copyOf方法的底层是本地方法System.arraycopy。(从源数组某个位置开始复制指定个数元素到目标数组,同时需指定目标数组的接收起始位置)
/**
* @param src 源数组
* @param srcPos 源数组的起始位置
* @param dest 目标数组
* @param destPos 目标数组的起始位置
* @param length 准备复制的元素个数
*/
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
其他方法相比就较为简单,在此记录下我平时比较少用但又觉得必须掌握的几个方法:
- trimToSize 最小化ArrayList实例的存储,使内部elementData数组的大小等于size
- ensureCapacity 在添加大量元素之前,可以使用这个方法主动扩充ArrayList的容量,相比频繁的自动增长肯定效率要高
- toArray 充当数组和集合API之间的桥梁
- clear 清空列表所有元素(将每个元素置为null)
注:
- 1:fail-fast 快速失败机制
通过该类的iterator()和listIterator(int)方法返回的迭代器具有fail-fast机制:
不管什么时候,如果在迭代器创建之后发生列表的结构修改(除非通过迭代器自己创建的remove()或者add(Object)方法),迭代器会抛出ConcurrentModificationException。
// expectedModCount为迭代器创建时对modCount的拷贝。
int expectedModCount = modCount;
......
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
因此,在并发修改的情况下,迭代器快速利索地失败,而不是在 未来某个不确定的时间发生不确定的行为上随意冒险。
请注意,迭代器的fail-fast机制无法保证,一般来说,在非同步并发修改的情况下不可能做出任何严格的保证。迭代器的fail-fast机制会尽可能的抛出ConcurrentModificationException。因此,编写依赖于此异常的程序以确保其正确性是错误的,迭代器的快速失败行为应该仅用于检测错误。
- 2:amortized constant time 分摊后恒定的时间
比如说new一个ArrayList,前10次add操作所需时间是恒定的,第11次add操作时发现容量不够了需要扩容。我们发现前n次操作的时间总是恒定的,时间复杂度为O(1),但紧接着一次需要O(n),那么我们可以取个平均:O(n/n+1)=O(1),就认为ArrayList的add操作时间在分摊后是恒定的。 - 3:关于for循环与迭代器iterator的对比,与使用场景相关,可参考:
https://www.cnblogs.com/redcoatjk/articles/4863340.html
关于Iterator和ListIterator 迭代器的使用,可参考:
https://blog.youkuaiyun.com/u013087513/article/details/52232731 - 4:transient关键字
被修饰的成员属性变量不会被序列化。细心的朋友一定注意到了ArrayList实现了Serializable接口,这又是咋回事儿呢,最重要的一个属性居然不能被序列化。ArrayList提供了writeObject和readObject这两个方法来实现序列化和反序列化,这么折腾主要是为了节省空间和时间,elementData的数组长度通常是大于ArrayList的实际容量size的,如果直接将elementData序列化就会产生不必要的浪费。 更过细节可参考:
https://www.jianshu.com/p/14876ef38721 - 5:DEFAULTCAPACITY_EMPTY_ELEMENTDATA
当我们使用new ArrayList()返回实例中的elementData实际上都指向{}(DEFAULTCAPACITY_EMPTY_ELEMENTDATA),此时size为0,当第一次使用add方法(或其他添加元素方法)之后,elementData才会指向一个长度为10的数组。而new ArrayList(initialCapacity)则是直接返回一个长度为initialCapacity的数组(只要initialCapacity大于0)。 很多时候我们只是new一个ArrayList用来接收某个方法返回的对象,如果在new的时候就分配一个长度为10的数组就显得有点浪费了。