ArrayList源码分析(jdk1.8)

时间:2022-04-06 19:32:32

首先看一下这个类的申明:

public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
可以看到ArrayList除了List实现了3个接口,而这3个接口都是空实现,没有定义任何方法,简单的我们可以理解为是一种约束,例如不实现Cloneable接口而去调用其clone方法就会抛出异常,又例如对于RandomAccess接口,实现了这个接口就表明了ArrayList类是支持fast random access的,可以在常数时间里去访问任意索引的值,因为ArrayList是一种基于数组的数据结构,所以在某些场景下程序可以去动态的判断你是不是实现了该接口(instance of RandomAccess)而给你分配最优算法。例如Collections类的shuffle方法。

接着看维护的变量:

private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData; // non-private to simplify nested class access
private int size;

第一个变量要讲的变量是这个Object数组:elementData。这说明arraylist的底层就是一个数组。

接着的一个变量就是size,这里需要注意的是size并不是数组的长度,而是数组中放了东西(对象引用,ArrayList存放的不可能是基本类型)的这一部分的长度。

剩下的3个变量看注释:(可能会有点绕,没必要深究,只需要知道默认容量为=10,该默认容量是在第一次add元素时分配的,和jdk1.7有所不同)

/**
* 默认容量
*/
private static final int DEFAULT_CAPACITY = 10;

/**
* 这个空数组用于我们构造对象时自己传入了一个初始容量,并且是0,那么这个空数组会被赋给elementData,并且
* 第一次add元素时我们不会为你扩充到默认容量。
*/
private static final Object[] EMPTY_ELEMENTDATA = {};

/**
* 这个空数组用于:当你使用空构造方法构造一个ArrayList对象时,这个空数组会被赋给elementData,
* 当我们第一次add元素时会有判断,如果elementData指向这个空数组,那么我们就知道了当前容量其实
* 没有被显示指定过的,我们会扩容到定义好的默认容量:10.所以我们可以理解为当你使用空的构造函数去
* 创建一个ArrayList对象时,其初始容量是10,只是这个容量会在我们第一次添加元素时被扩充。
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

接下来看构造方法,有3种,这里可以对照着上面的3个变量看,就能很好的理解。
第一种:(可以看到此类的作者写的注释)
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

我们现在可以跳到add方法看一下初始扩容的流程,

public boolean add(E e) {
ensureCapacityInternal(size + 1);
elementData[size++] = e; //赋值,size++
return true;
}

跳到第一个方法里:(注意此时我们传入的minCapacity是size+1,也就是1)

private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { //可以看到,这里加了这个判断
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);//这里取二者最大值,肯定是默认容量大
}
ensureExplicitCapacity(minCapacity); //扩容操作
}

这就理解了这个空构造方法的注释。

第二种构造方法:(自己传入一个初始容量,推荐假如你的ArrayList会填充10个以上的数据,那么你最好传入一个比较贴切的初始容量,因为由于数组的不可变性,ArrayList的每次扩容其实都是重新生产了一个更大的数组然后将原数组的数据复制过去,所以扩容代价是挺大的。)

public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity]; //构造initialCapacity大的数组
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA; //可以看到另外一个空数组在这里被用到
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}

第三种构造方法:

ublic ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}

这个构造方法也很好理解,用了Arrays.copyOf方法直接复制传入的集合类所维护的elementData数组。这里需要注意的是复制的是引用,所以假如我们有一个Teacher类,Teacher类中有一个属性age,集合A保存了一个Teacher,age是100.此时我们new一个集合B,并且使用该构造方法,传入的参数是集合A,接着我们通过集合B去改变该Teacher的age,集合A里保存的那个teacherd而age会被改变吗? 显然是会的。 需要注意的是假如list中存放的是例如Integer之类的对象,那么同样情况下改变一方另一方是不会变的。

接下来的方法其实都比较常规,源码很也容易看懂。接下来看2个方法:

public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, false);
}

public boolean retainAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, true);
}
先调用了Objects的requireNonNull方法去确保传入的参数非空。紧接着调用同一个方法,传入2个不同的布尔变量。我们看这个方法的实现。
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;
}
这里用了一种十分巧妙的算法:维护了2个变量,r和w,初始值都是0。我们看这段代码:
for (; r < size; r++)
if (c.contains(elementData[r]) == complement)
elementData[w++] = elementData[r];

注意这个complement是我们最初传入的布尔变量。我们现在假定它是true。也就是retainAll方法传入的。

集合c是我们传入的集合,我们假定c里就是一个元素1;elementData是原list,我们假定其维护着3个元素:1,2,1。

接着我们遍历elementData,此时r是0,elementData[r]是1,显然c中刚好有这个元素,进入if语句,并且将elementData[w]赋值为elementData[r],目前他们本来就是相等的,所以这步相当于没用,接着w+1。接着r=1,elementData[r]是2,显然c中没有这个元素,所以进入不了if语句段,此时r+1变成了2,w没有改变,接下来,elementData[r]刚好又是1,显然c中有这个元素,又进入if语句段,此时elementData[w]赋值为elementData[r],即:elementData[1] = elementData[2]。此时for循环结束。elementData变成了{1,1,2}

所以现在我们就理解了这段代码,w就像1个游标,我们遍历原elementData数组,当传入的集合c包含elementData的元素,w就往前进一步,并且在前进一步之前会将这个时刻遍历到的元素填到w的这个位置。遍历结束,w所在的位置之前的格子填入的都是集合c所包含的元素。所以我们接下来该干嘛呢?自然是将w之后的格子置为null。所以我们看finally语句段的第二个if语句段,就做了这个事:

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; //size现在就变成了w
modified = true;
}
现在就明白了retainAll方法的含义:就是对原集合的一次筛选,最终保留下来的一定是我们传入的集合中的元素。removeAll就同理了。


接下来我们来看一个内部类:SubList。请注意他是继承AbstractList这个类的。我们可以看到该类定义为了private.也就意味着我们不能在ArrayList类外去实例化该类。所以理所当然,ArrayList提供了一个生产该类的共有方法。

public List<E> subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);
return new SubList(this, 0, fromIndex, toIndex);
}
我们看他的构造方法:

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;
}

其实看到这个构造方法我们就可以想到subList其实是原ArrayList的一个视图,你对他的任何操作都会映射到原ArrayList上。例如:我们现在想要删除某个ArrayList的100-200个元素,就可以利用subList:list.subList(a,b).clear()。

最后我们看2个内部类:Itr和ListItr。Itr是ArrayList的迭代器实现类。迭代器:在集合上遍访的接口,使用的人无需关心具体底层是怎么实现的。不同的集合类自然有对其迭代器的不同实现,但是其使用方法都是一样的,所以用起来非常方便。

需要指出的一点是:返回的迭代器是fast-fail机制的。也就是快速失败机制。

下面讲一下这个机制,首先最重要的一个变量:modcount。这是一个定义在AbstractList中的变量(protected),用于表明:The number of times this list has beenstructurally modified。注意是结构上的改变,也就是你add了一个元素,remove了一个元素都会改变。但是你对其中一个位置上的元素改变是不会改变modcount的。你看源码也会发现,add方法,remove方法里都有对modcount++的操作。所以为什么要这样呢?我们看迭代器里的代码,会发现其每个方法都会去调用同一个方法:

final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
expectedModCount在这个迭代器被构造出来时会被赋值成modcount的值。可以看到,如果这两个值不同, 就直接抛异常。这就意味着,如果你在迭代这个list的过程中,有一个别的线程改变了这个list(add,remove等),那么这次迭代失败,抛出异常。需要注意的是:
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();
}
}

remove方法中会对expectedModCount重新赋值。不然你使用迭代器remove一个元素之后继续遍历那不就会抛异常了。ListItr是Itr的一个拓展。增加了add,set方法。

下面记一下1.8带来的新特性吧。其实用1.8的新特性可以很方便优雅的操控arraylist。比如给你一个list,要你打印出其中的偶数。你可能会这样写:

List<Integer> list = Arrays.asList(1,2,3,4,5);
for (int i : list) {
if (i%2 == 0) {
System.out.println(i);
}
}
亦或是使用迭代器:
ListIterator<Integer> listIterator = list.listIterator();
while(listIterator.hasNext()) {
int temp = listIterator.next();
if (temp % 2 == 0) {
System.out.println(temp);
}
}

但是用1.8带来的强大的stream:

list.stream().filter(i->i%2==0).forEach(o-> System.out.println(o));

需要注意的是这样的操作不会改变list,如果你想要得到一个只有偶数的新list,可以这样:

List<Integer> list2 = list.stream().filter(o->o%2==0).collect(Collectors.toList());

比如你只想知道偶数的个数:

int count = (int)list.stream().filter(i->i%2==0).count();

比如你想知道list的偶数中的最大值:

int max = list.stream().filter(i->i%2==0).max((o1, o2) -> o1.compareTo(o2)).get();

filter里想要得到的是一个boolean变量。

还有许多其他的map,reduce操作都非常强大,这里就不举例了。