您的位置:首页 > 编程语言 > Java开发

ArrayList源码分析(jdk1.8)

2017-07-11 00:00 561 查看
ArrayList:

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方法。

下面分析源码:

首先看维护的变量,有5个:

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)

/**
* 默认容量
*/
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);
}
}

第三种构造方法:

public 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之类的对象,那么同样情况下改变一方另一方是不会变的。

接下来看方法:

public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}

顾名思义,修剪到size大小,即:数组容量=size,即:数组整个是被填满的,适用于什么情况呢:当你确定你的ArrayList在很长一段时间里不会被添加新元素了,那么你可以显示调用这个方法去释放你的list中的数组中多余那一部分空间。(因为ArrayList的扩容操作通常会是以 当前容量*1.5 来扩容的,为了减少扩容操作的次数我们就暂时牺牲了一些空间)

接下来看2个方法:

public boolean contains(Object o) {
return indexOf(o) >= 0;
}

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

就像String类的contions方法一样,此方法返回布尔变量来表示list中是否包含该元素,可以看到有对null的判断,说明是可以传入null,说明ArraList是可以存放null值的(可以重复存放)。

接下来看clone这个方法:

public Object clone() {
try {
ArrayList<?> v = (ArrayList<?>) 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(e);
}
}

常规的克隆方式,调用super.clone()方法,也就是最终调用了Object中写好的clone方法(native的),所以这个方法是所谓的shallow copy,也就是浅拷贝。

什么是浅拷贝:

public class Demo implements Cloneable {
public Foo foo = new Foo();
public static void main(String[] args) {
Demo demo1 = new Demo();
Demo demo2 = null;
try {
demo2  = (Demo)demo1.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
System.out.println(demo1==demo2);  //false
System.out.println(demo1.foo==demo2.foo);   //true
}
}
class Foo {
}

拷贝的只是引用,其指向的对象是同一个,就像Demo类里的foo。

如果想要Demo类里的foo都指向不同对象呢?自己去重写引用的这个类的clone方法:

public class Demo2 implements Cloneable {
public Foo2 foo = new Foo2();
public static void main(String[] args) {
Demo2 demo1 = new Demo2();
Demo2 demo2 = null;
try {
demo2  = (Demo2)demo1.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
System.out.println(demo1==demo2);  //false
System.out.println(demo1.foo==demo2.foo);   //false
}

@Override
protected Object clone() throws CloneNotSupportedException {
Demo2 demo = (Demo2)super.clone();
demo.foo = (Foo2) this.foo.clone();
return demo;
}
}
class Foo2 implements Cloneable {
@Override
protected Object clone() throws CloneNotSupportedException {
return (Foo2) super.clone();
}
}

说的可能有点笼统,因为这毕竟不是arraylist的东西,具体的可以百度。

接下来的方法其实都比较常规,源码很也容易看懂。接下来看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 class SubList extends AbstractList<E> implements RandomAccess

我们可以看到该类定义为了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上。例如set方法:

public E set(int index, E e) {
rangeCheck(index);
checkForComodification();
E oldValue = ArrayList.this.elementData(offset + index);
ArrayList.this.elementData[offset + index] = e;
return oldValue;
}

最终我们都是通过offset,index这些值对ArrayList.this(原list)中的elementData进行操作的。

再例如:我们现在想要删除某个ArrayList的100-200个元素,就可以利用subList:list.subList(a,b).clear()

其实自己看源码:Sublist类里是没有定义这个方法的,所以最终调用的其实是他的父类AbstractList的clear方法,AbstractList中实现了clear方法。

Sublist类中甚至还实现了自己的迭代器。

最后我们看2个内部类:Itr和ListItr

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

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

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

final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}

expectedModCount在这个迭代器被构造出来时会被赋值成modcount的值

int 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操作都非常强大,这里就不举例了。

未完待续... (7/17 11:03pm )

我只是一个大三生,以上是我的学习记录,也许会有错。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  java 源码 集合 ArrayList