前言
本文将通过阅读 JDK 中的 ArrayList 的源码,来全面分析 ArrayList 的一些特性。
2021-01-30更新:回过头来再看这片源码分析文章,觉得两年前的自己读源码,过于追求代码细节。这个方式也使得自己在后面阅读更复杂的源码过程中吃了亏。阅读源码应更多地去体会其整体设计,而不要陷入一些代码细节和技巧中。
一、ArrayList 简介
1.1 类继承结构
1.2 数据结构
二、源码分析
2.1 注释
可调整大小的List接口的数组实现,实现所有可选列表操作,并允许所有元素,包括null。除了实现List接口之外,该类还提供了一些方法来操纵内部使用的存储列表的数组的大小。 (这个类是大致相当于Vector,不同之处在于它是不同步的)。
该size,isEmpty,get,set,iterator和listIterator操作在固定时间内运行。 add操作以摊余常数运行 ,即添加n个元素需要O(n)个时间。 所有其他操作都以线性时间运行(粗略地说)。 与LinkedList实施相比,常数因子较低。
每个ArrayList实例都有一个capacity。 capacity是用于存储列表中的元素的数组的大小。 它总是至少与列表大小一样大。 当元素添加到ArrayList时,其容量会自动增长。 没有规定增长政策的细节,除了添加元素具有不变的摊销时间成本。
应用程序可以添加大量使用ensureCapacity操作元件的前增大ArrayList实例的容量。 这可能会减少增量重新分配的数量。
请注意,此实现不同步。 如果多个线程同时访问ArrayList实例,并且至少有一个线程在结构上修改列表,则必须在外部进行同步。 (结构修改是添加或删除一个或多个元素的任何操作,或明确调整后台数组的大小;仅设置元素的值不是结构修改。)这通常是通过在一些自然地封装了列表。 如果没有这样的对象存在,列表应该使用Collections.synchronizedList方法“包装”。 这最好在创建时完成,以防止意外的不同步访问列表:
List list = Collections.synchronizedList(new ArrayList(...));
快速失败:iterator和listIterator会返回快速失败,当迭代器被创建之后,该list的结构又被修改时。只有当迭代器自己修改list结构时(通过迭代器自身的remove或add方法)不会返回快速失败。返回快速失败的表现是抛出ConcurrentModificationException。因此,面对并发修改,迭代器的快速干净的失败(抛出异常)而不是冒着未来不确定性隐藏的风险。
请注意,迭代器的快速失败行为无法保证,因为一般来说,在不同步并发修改的情况下,无法做出任何硬性保证。快速失败的迭代器尽力ConcurrentModificationException 。 因此,编写依赖于此异常的程序的正确性将是错误的:迭代器的故障快速行为应仅用于检测错误。
这个类是Java Collections Framework的成员
2.2 定义
public class ArrayList<E> extends AbstractList<E>implements List<E>, RandomAccess, Cloneable, java.io.Serializable
类定义与类继承结构图是对应的。
从中我们可以了解到:
- ArrayList
:说明ArrayList支持泛型。 - extends AbstractList
:继承了AbstractList。AbstractList提供List接口的骨干实现,以最大限度地减少“随机访问”数据存储(如ArrayList)实现Llist所需的工作 - implements List
:实现了List。实现了所有可选列表操作。 - implements RandomAccess:表明ArrayList支持快速(通常是固定时间)随机访问。此接口的主要目的是允许一般的算法更改其行为,从而在将其应用到随机或连续访问列表时能提供良好的性能。
- implements Cloneable:表明其可以调用clone()方法来返回实例的field-for-field拷贝。
- implements java.io.Serializable:表明该类具有序列化功能。
2.3 域
/**
* 初始化默认容量.
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 指定该ArrayList容量为0时,返回该空数组。
*/
private static final Object[] EMPTY_ELEMENTDATA = {};
/**
* 当调用无参构造方法,返回的是该数组。刚创建一个ArrayList 时,其内数据量为0。
* 它与EMPTY_ELEMENTDATA的区别就是:该数组是默认返回的,而后者是在用户指定容量为0时返回。
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* 保存添加到ArrayList中的元素。
* ArrayList的容量就是该数组的长度。
* 该值为DEFAULTCAPACITY_EMPTY_ELEMENTDATA 时,当第一次添加元素进入ArrayList中时,数组将扩容至DEFAULT_CAPACITY。
* 被标记为transient,在对象被序列化的时候不会被序列化
*/
transient Object[] elementData; // non-private to simplify nested class access
/**
* ArrayList的实际大小(数组包含的元素个数)。
*
* @serial
*/
private int size;
- EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA的区别:
在调用无参构造函数的时候,返回的是后者,在调用指定了容量为0的构造函数时,返回的是前者;两者的初始容量一个为指定的0,一个为DEFAULT_CAPACITY(默认为10);对于后者,当第一次添加元素时,就会扩容至默认初始容量DEFAULT_CAPACITY。 - ArrayList的capacity和size的区别:
Capacity是整个数据结构的大小,size是目前存储了多少元素的大小。 - 标记为transient的elementData是怎么进行序列化和反序列化的?
ArrayList自定义了它的序列化和反序列化的方式,详情查看writeObject()和readObject方法。
2.4 构造方法
/**
* 构造一个指定初始化容量为initialCapacity的空ArrayList
*
* @param initialCapacity the initial capacity of the list
* @throws IllegalArgumentException 如果初始化容量为负
*/
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
//指定初始化容量为0,对应的是EMPTY_ELEMENTDATA这个空数组
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
/**
* 构造一个初始化容量为10的空列表
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* 构造一个包含指定collection的元素的列表,这些元素的排列是由该collection的迭代器的返回顺序决定的
*
* @param c the collection whose elements are to be placed into this list
* @throws NullPointerException if the specified collection is null
* */
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
共有三个构造函数
- ArrayList(int initialCapacity):
- ArrayList()
- ArrayList(Collection<? extends E> c)
2.5 核心方法
读源码的思路应该是通过核心方法去辐射相关方法,而不是割裂的按照代码顺序去读方法。
2.5.1 get(int index)
/**
* Returns the element at the specified position in this list.
*
* @param index index of the element to return
* @return the element at the specified position in this list
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
/**
* Checks if the given index is in range. If not, throws an appropriate
* runtime exception. This method does *not* check if the index is
* negative: It is always used immediately prior to an array access,
* which throws an ArrayIndexOutOfBoundsException if index is negative.
*/
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
@SuppressWarnings("unchecked")
E elementData(int index) {
return (E) elementData[index];
}
方法步骤:
- RangeCheck()判断是否越界;
- 直接通过数组下标获取元素
因为ArrayList的底层实现就是数组,get方法的时间复杂度为O(1)。
2.5.2 add(E e)
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return <tt>true</tt> (as specified by {@link Collection#add})
*/
public boolean add(E e) {
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);
}
/**
* Increases the capacity to ensure that it can hold at least the
* number of elements specified by the minimum capacity argument.
*
* @param minCapacity the desired minimum capacity
*/
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
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 int calculateCapacity(Object[] elementData, int minCapacity) {
//如果数组是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,则最小扩容的大小为DEFAULT_CAPACITY
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
/**
* The maximum size of array to allocate.
* Some VMs reserve some header words in an array.
* Attempts to allocate larger arrays may result in
* OutOfMemoryError: Requested array size exceeds VM limit
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
add(E e)涉及到了ArrayList的扩容机制,整个方法的步骤如下:
- ensureExplicitCapacity(int minCapacity):modCount++,modCount是父类AbstractList的变量,用于保存数据结构的结构修改次数。当添加一个元素之后的元素个数大于当前数组的容量时,进行扩容:
if (minCapacity - elementData.length > 0)
grow(minCapacity);
- grow(int minCapacity):扩容机制:将现数组的长度增加到1.5倍,然后取所需的长度(minCapacity)和增加到1.5倍的长度的较大值为新数组的长度;在此之前,判断这个新数组长度是否超过了ArrayList所允许的长度最大值即MAX_ARRAY_SIZE(定义private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;)超过的话就采用Integer.MAX_VALUE。
2.5.3 add(int index, E element)
/**
* 在指定位置插入元素,然后将当前位置和其往后的元素向后移一位
*
* @param index index at which the specified element is to be inserted
* @param element element to be inserted
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public void add(int index, E element) {
//1、越界检查
rangeCheckForAdd(index);
//2、空间检查,是否需要扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
//arraycopy(Object src, int srcPos,Object dest, int destPos,int length):
//将src从srcPos位置往后的length长度的元素复制到dest中,从destPos位置开始
//结果就是将index和其往后的元素向后移一位并且空出了indx位置
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
/**
* A version of rangeCheck used by add and addAll.
*检查index位置是否合法
*/
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
add(int index E e)需要先对元素进行移动,然后完成插入操作,意味着需要O(n)。
2.5.4 remove(int index)
/**
* 删除指定位置的元素并返回该元素,并将该位置之后的元素向左移一位
*
* @param index the index of the element to be removed
* @return the element that was removed from the list
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E remove(int index) {
//1、检查index是否越界
rangeCheck(index);
//2、结构修改都要在modCount上加1,添加元素的方法这个语句在调用的ensureExplicitCapacity()方法中
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
//3、将该位置之后的元素向左移一位
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//4、置为null,GC会对这块进行回收
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
注意:为了让GC起作用,必须显式的为最后一个位置赋null值。上面代码中如果不手动赋null值,除非对应的位置被其他元素覆盖,否则原来的对象就一直不会被回收。
由于删除指定位置元素需要将后面的元素左移一位,因此时间复杂度为O(n)
三、小结
总结几个比较重要的知识点:
ArrayList 的扩容机制
当添加一个元素之后的元素个数大于当前数组的容量时,需要进行扩容。将现数组的长度增加到1.5倍,然后取所需的长度(minCapacity)和增加到1.5倍的长度的较大值为新数组的长度;在此之前,判断这个新数组长度是否超过了ArrayList所允许的长度最大值即MAX_ARRAY_SIZE(定义private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;)超过的话就采用Integer.MAX_VALUE
ArrayList 的读写特性
- 对于读操作,get,O(1)的复杂度。
- 对于写操作,add 或者 remove,在指定特定位置时,由于需要移动数组中的元素(移动元素依赖system.arraycopy方法,该方法比较耗时),会使得写入速度较慢。
- 另外,ArrayList 是线程不安全的。