java.util.ArrayList学习笔记
概述
继承结构
基本功能
ArrayList是一种可变长列表,基于数组实现。在这个类中,实现了List接口中定义的所有的可选方法,并且对其中可以放入的元素也没有限制。出了实现List接口中定义的方法外,本类还提供了用于控制内部数组大小的方法。在java中,List基本鱼Vector等价,但是List并不保证线程安全,而Vector保证线程安全性。
在本类中,执行size, isEmpty, get, set, iterator, 和 listIterator等方法的时间复杂度为O(1);执行add方法的时间复杂度与放入元素的个数呈正相关关系,即,当放入n个元素时,时间复杂度为:O(n);其它的方法的时间复杂度则保持在线性关系,与LinkedList实现相比,其常量因子的较小。
每个ArrayList实例均包含一个容量参数。这个参数指定了实例中用于保存列表元素的数组大小。每个链表的容量一般大于每个链表中有效元素的个数。当元素被添加到列表中时,其对应的容量可能发生改变。数组的扩容操作的时间消耗并没有计入添加元素的时间复杂度中。
一个应用可以通过调用ensureCapacity方法,在插入大量元素之前对列表进行扩容操作。这种做法可能降低渐进性扩容的次数(如:插入大量元素时,可能需要扩容3次,通过采用这种策略,可以将扩容次数降低至1次,从而达到提高执行效率的目的)。
ArrayList并不时线程安全的。也就是说,当有多个线程同步访问同一个对象,且至少有一个线程对列表结构进行了修改(即:进行了添加,删除等操作),我们则必须在调用的外层进行同步控制。实现这中策略的方法一般是在包含该列表对象的外层对象中进行同步控制。如果没有这中类型的对象,则需要调用Collections.synchronizedList方法来进行同步控制。一般情况下,最好在创建列表时调用Collections.synchronizedList方法,从而避免对列表对象的非同步访问,其实例代码如下:
List list = Collections.synchronizedList(new ArrayList(...));
通过iterator或者listIterator方法返回的列表迭代器采用fail-fast机制,即:当列表对象在迭代器创建后,除调用该迭代器的add和remove方法外,发生结构性性改变时,迭代器将抛出一个同步修改异常ConcurrentModificationException。因此,当遇到对象的同步修改时,迭代器将会立刻失败,而不是继续进行可能失效的访问。
注意:fail-fast机制并不能保证强同步性,即,不能保证在发生非同步并发修改时,列表的访问的正确性。fail-fast机制会在尽量在在发生非同步修改时抛出ConcurrentModificationException。但是在编程时,不能基于该异常实现程序的正确性,这中机制应仅仅用于检测潜在bug。
属性介绍
private static final int DEFAULT_CAPACITY = 10
一个包含元素的列表的默认初始化长度;
private static final Object[] EMPTY_ELEMENTDATA = {};
由所有空列表对象共享的数组实例。
private transient Object[] elementData;
列表对象数据的保存数组,数组长度代表列表的容量,数组中有效元素的个数,代表数组的长度。任何一个满足条件elementData == EMPTY_ELEMENTDATA空列表中数据数组长度在添加第一个新元素时,均会被扩展至DEFAULT_CAPACITY。
private int size;
当前列表的大小,即:列表中包含元素的个数;
方法介绍
构造方法
public ArrayList(int initialCapacity) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
this.elementData = new Object[initialCapacity];
}
构造一个初始容量为initialCapacity的空列表。
public ArrayList() {
super();
this.elementData = EMPTY_ELEMENTDATA;
}
构造一个容量为DEFAULT_CAPACITY的空列表。在默认的构造方法中,将数据数组指向默认的共享空数组。
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
size = elementData.length;
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
}
创建一个包含集合c中所有元素的列表。其中元素的顺序与c中迭代器返回元素的顺序相同。
其它方法介绍
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = Arrays.copyOf(elementData, size);
}
}
将当前列表的容量缩减至列表当前的长度。可以通过这个方法来降低列表对象占用的内存。
public void ensureCapacity(int minCapacity) {
int minExpand = (elementData != EMPTY_ELEMENTDATA)
// any size if real element table
? 0
// larger than default for empty table. It's already supposed to be
// at default size.
: DEFAULT_CAPACITY;
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}
用于对列表对象进行扩容,并保证列表至少可以容纳minCapacity
个元素。一般通过列表对象数组是否指向共享空数组来判断来确定其初始容量。在此有一点儿疑惑,将在下个函数中进行说明。
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
这个函数的主要作用,我理解为:保证列表对象的值数组至少可以容纳minCapacity
个元素,否则进行列表的扩容操作。我们看到函数ensureCapacity(int)
保留一个对本方法的引用。在本方法中,进行扩容操作之前首先修改了modCount
参数,用于指定列表对象被修改。但是我们看到,当当前列表中值数组的长度大于指定的最小长度时,不会调用扩容参数。由此可以看出,参数modCount
指定的并不是列表发生修改的绝对次数,而是其可能发生修改的次数。
private void ensureCapacityInternal(int minCapacity) {
if (elementData == EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
扩展列表对象的值数组,当指定的元素个数小于列表默认的容量时,则将列表值数组扩展至默认容量,否则扩展至:minCapacity
。
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);
}
对列表进行扩容操作,使之至少可以容纳minCapacity
个元素。其基本思想是:首先将列表中的值数组扩展到原来的1.5倍,如果比预期的最小容量minCapacity
小,则直接将扩容至minCapacity
。如果扩容后的容量大于列表允许的最大容量,则扩容至Integer.MAX_VALUE
,否则扩容至MAX_ARRAY_SIZE
。
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
当扩容后的预期容量超出列表规定的最大容量时,则调用本函数,确定列表最后完成扩容操作后的容量。
public int size()
返回当前列表中元素的个数。
public boolean isEmpty()
判断列表中是否包含元素。
public boolean contains(Object o)
判断列表中是否包含某个元素。当列表至少包含一个与o
相等的对象时,返回true
。其内部实现采用的方式为:确定对象在列表中第一出现位置的下标是否为-1;
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
返回对象在列表中第一次出现位置的下标。当对象在列表中不存在时,则返回-1。其内部采用的实现方式为:遍历列表值数组中前size个元素(这里,我理解为有效元素),返回第一个与指定对象相等的元素下标。
public int lastIndexOf(Object o) {
if (o == null) {
for (int i = size-1; i >= 0; i--)
if (elementData[i]==null)
return i;
} else {
for (int i = size-1; i >= 0; i--)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
返回对象在列表中最后一次出现位置的下标。采用逆向遍历列表值数组中元素的方式。
public Object clone() {
try {
@SuppressWarnings("unchecked")
ArrayList<E> v = (ArrayList<E>) super.clone();
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError();
}
}
克隆一个新的列表对象。注意:复制后的列表对象的修改次数为0,虽然复制了列表中的值数组对象,但是值数组对象中包含的所有元素并没有复制。
public Object[] toArray()
将列表中的元素转化为数组,其内部实现基于Arrays.copyOf()方法,复制后的数组中元素的顺序与列表对象中的值数组中的元素顺序相同。
public <T> T[] toArray(T[] a)
基于Arrays.copyOf
和System.arraycopy
实现。其接口说明参见:java.util.List学习笔记。
E elementData(int index) {
return (E) elementData[index];
}
返回当前列表对象值数组中下标为index
的元素,并转化为列表对象声明时的对象类型。
public E get(int index)
获取列表中下标为index的元素。本方法基于elementData(int)
实现,并具有访问下标的合法性检验,确保其访问的元素的有效性。
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
校验访问元素下标有效性。但是,方法中只校验了上限,但是没有校验下限,因此在使用时要注意。
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
将列表中下标为index
的元素替代为指定元素,并将该位置原元素返回。由于在替换操作中,需要访问元素存在,因此在替换之前,需要执行访问元素下标校验。
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
将一个元素追加到列表的末尾。该方法中需要注意以下几点:插入元素前,需要确保列表容量至少为size + 1。
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++;
}
在列表值数组下标为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);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
删除列表下标为index的位置的元素,并将其返回,其后的元素依次前移。其采用的方式为:首先检测删除下标是否合法,基于System.arraycopy
实现数组元素的前移,最后将列表值数组的实效元素设定为null,由GC负责回收。
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index, numMoved);
elementData[--size] = null; // clear to let GC do its work
}
针对列表特定位置的元素的快速删除算法,算法原理基本与删除特定位置的元素的方法相同,但是这个算法并不进行边界的校验。
public boolean remove(Object o)
删除列表中第一个与指定对象相等的元素,其判断元素相等的算法与contains(object)
基本相同。
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
删除列表中的所有元素。其内部实现采用简单的将列表值数组中所有的元素设成null实现,并将列表的大小设定为0。但是并不修改列表的容量。经过前面这些方法源码的阅读,可以确定modCount并不是指列表发生所有改变的次数,而是列表可能发生的结构性改变的次数。
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
将集合c中所有的元素追加到列表的末尾,其添加顺序与集合c的迭代器返回其中元素的顺序相同。添加原理与添加单个元素基本相同,只是在申请空间时,将预期的容量设定成了:size + numNew。
public boolean addAll(int index, Collection<? extends E> c)
将集合c中的所有元素添加到列表的指定位置。其实现原理与添加单个元素基本相同,详情可以参见add(Object)
方法的说明。
protected void removeRange(int fromIndex, int toIndex)
删除列表指定范围内的元素。其区间范围是fromIndex <= indedx < toIndex。其实现的基本原理与删除单个元素基本相同,只是在数组的元素的移动时的下标计算不同,详细说明可以参见:remove(int)
方法。
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
判断新增元素的下标是否盒饭,当index=size时,则代表向列表末尾处添加一个新元素。
private String outOfBoundsMsg(int index)
生成IndexOutOfBoundsException异常的详细信息,指明访问列表的非法下标和当前列表的有效元素个数。
private boolean batchRemove(Collection<?> c, boolean complement) {
final Object[] elementData = this.elementData;
int r = 0, w = 0;
boolean modified = false;
try {
for (; r < size; r++)
if (c.contains(elementData[r]) == complement)
elementData[w++] = elementData[r];
} finally {
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws.
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
if (w != size) {
// clear to let GC do its work
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
size = w;
modified = true;
}
}
return modified;
}
批量删除列表中的元素。当complement为true时,则取列表与c的交集,否则取差集。其基本思想为:指定两个游标,分别标记读坐标和写坐标,遍历列表中的元素,每次访问,读坐标加1,依次判定是否满足指定条件,当满足时,将写坐标当前指向的元素替代为读坐标当前指向的元素。最后根据读、写坐标。在执行本操作过程中,如果contains抛出异常,则由抛出异常时的读坐标开始,将其复制到写数组的末尾,并对写坐标进行修改。最后将写坐标后的元素全部指定为null,并进行列表结构性修改的次数。
public boolean removeAll(Collection<?> c)
取列表与c的交集,并将其保存到列表中。
public boolean retainAll(Collection<?> c)
取列表与c的差集,并将其保存到列表中。
public ListIterator<E> listIterator(int index) {
if (index < 0 || index > size)
throw new IndexOutOfBoundsException("Index: "+index);
return new ListItr(index);
}
获取由指定位置开始的列表迭代器。即,用户第一次调用迭代器的next()
方法返回的是列表中下标为index
的元素。但是迭代器的调用方仍然可以使用本迭代器进行列表中元素的遍历。在这里需要特别注意列表迭代器游标的有效位置为:0 ~ size。当 index = size时,则代表列表迭代器的初始游标在列表的末尾,其next()方法将返回null或者抛出异常,一般可以用于逆向遍历列表。
public ListIterator<E> listIterator()
返回一个从列表头部开始的列表迭代器,其内部实现采用的是:listIterator(0)
。
public Iterator<E> iterator()
返回一个从列表头部开始的基本迭代器。
public List<E> subList(int fromIndex, int toIndex)
返回当前列表的一个子列表。其中元素共享,即:在子列表中发生的修改会直接反映到初始列表中,反之亦然。
私有内部类
基础迭代器(Itr)
参数介绍
int cursor
迭代器当前的游标。迭代器的游标代表的为:当调用next()
方法时,返回的列表元素的下标。
int lastRet = -1
最后一次调用next()方法返回的元素的下标。默认为-1。
int expectedModCount = modCount
迭代器创建时,列表对象瞬态的可能的结构变化次数,主要用于检测是否存在并发修改,从而导致并发错误。
方法介绍
public boolean hasNext()
判断当前迭代器调用next()方法是否还有有效元素返回。当列表的游标已经达到列表末尾时,即:cursor == size时候,则返回false,否则返回true。
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
返回当前迭代器正向遍历的下一个元素。注意,本算法中有两次并发结构修改检测,分别是,强校验:方法开始时,调用checkForComodification进行结构性修改次数检测;弱校验:如果当前游标的位置高于列表的容量,产生情形为:列表发生元素的删除操作,并调用了trimToSize来降低列表的空间占用。在成功完成校验后,需要交迭代器游标后移,并将最后一次访问的元素的坐标设定为访问元素的下标。
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
删除列表迭代器刚刚访问的元素。当迭代器并没有访问任何元素,或者刚刚访问的元素已经被删除时,其内部基于ArrayList.this.remove()
实现。在完成删除后,需要将迭代器游标前移,将最后一次访问元素的下标置为-1,并对迭代器中结构性修改次数进行更新。
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
检测除持有本迭代器的线程外,是否有潜在的并发修改冲突。其基本原理是检测列表对象的
列表迭代器(ListItr)
继承结构
方法介绍
具体方法说明,请参见:java.util.ListIterator学习笔记。
其内部基本基于ArrayList中的方法实现。在进行元素访问操作时,均会进行同步修改校验。
子列表(SubList)
构造方法
SubList(AbstractList<E> parent, int offset, int fromIndex, int toIndex) {
this.parent = parent;
this.parentOffset = fromIndex;
this.offset = offset + fromIndex;
this.size = toIndex - fromIndex;
this.modCount = ArrayList.this.modCount;
}
由构造方法,我们可以看出:子列表持有的并不是父亲列表中元素的复制,而是直接维持一个父亲列表的引用,因此子列表对其中元素的修改会直接影响到父亲列表,甚至是根列表,反之亦然。并且,子列表的结构修改次数是与根列表的相同,用于检测并发的修改冲突。
基本属性
private final AbstractList<E> parent;
代表当前子列表的直接父亲列表。即:列表3是列表2的子列表,列表2为列表1的子列表,则在列表3中,parent指向的是列表2。
private final int parentOffset;
当前子列表相对于直接父亲列表的元素偏移量。即:列表3相对于列表2的元素偏移量。
private final int offset;
当前子列表相对于根列表的元素偏移量,即:列表3相对于列表1的元素偏移量。
int size;
当前子列表的长度。
注:对于子列表的接口说明,可以参见java.util.AbstractList学习笔记。在ArrayList的实现中,通过数组的下标和数组的copyOf函数实现subList的所有功能。其实现的逻辑与ArrayList本身的实现逻辑大体相同,在此不做赘述。