Java集合框架-ArrayList的源码解析

273 阅读13分钟

一、前言

站在巨人的肩膀上,本系列的Java集合框架文章参考了skywang12345——Java 集合系列,真心讲的很好,可以参考。但是不足的是,时间过于长久了,大佬的文章是基于JDK1.6.0_45,对于现在来说至少都用JDK 8.0以上了,而且JDK 6.0与JDK 8.0中集合框架的改动挺大的,所以本系列的文章是基于JDK_1.8.0_161进行说明的。

二、介绍

我们先来看看ArrayList的类定义,以及继承关系

java.util.Collection<E>
	-> java.util.AbstractCollection<E>
        -> java.util.AbstractList<E>
            -> java.util.ArrayList<E>

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable

ArrayList 是一个数组队列,相当于动态数组。与Java中的数组相比,它的容量能动态增长。它继承于AbstractList,实现了List, RandomAccess, Cloneable, java.io.Serializable这些接口。

  • ArrayList 继承了AbstractList,实现了List。它是一个数组队列,提供了相关的添加、删除、修改、遍历等功能。
  • ArrayList 实现了RandmoAccess接口,即提供了随机访问功能。RandmoAccess是java中用来被List实现,为List提供快速访问功能的。在ArrayList中,我们即可以通过元素的序号快速获取元素对象;这就是快速随机访问。稍后,我们会比较List的“快速随机访问”和“通过Iterator迭代器访问”的效率。
  • ArrayList 实现了Cloneable接口,即覆盖了函数clone(),能被克隆。
  • ArrayList 实现java.io.Serializable接口,这意味着ArrayList支持序列化,能通过序列化去传输。

再来从整体看一下ArrayList与Collection关系,如下图:

ArrayList包含了两个重要的对象:elementData 和 size。
(1) elementData 是"Object[]类型的数组",它保存了添加到ArrayList中的元素。实际上,elementData是个动态数组,我们能通过构造函数ArrayList(int initialCapacity)来执行它的初始容量为initialCapacity;如果通过不含参数的构造函数ArrayList()来创建ArrayList,则elementData默认是一个空数组。elementData数组的大小会根据ArrayList容量的增长而动态的增长,具体的增长方式,请参考源码分析中的ensureCapacity()函数。
(2) size 则是动态数组的实际大小。

三、解析

对源码的解析,我个人喜欢从方法的使用上去看,而不是把源码所有的方法从头到尾的都去看一遍,除非是那种比较短的源码还行,如果是上千行代码的话,我觉得会很崩溃的。这样不仅学习效率底下,而且看源码本身就是一件非常枯燥的事情,容易看着看着就不想看下去了(大佬忽略),非常打击学习源码的兴趣。

1、要了解一个类,首先我们要去了解的就是它的构造方法。那么我们就先从ArrayList的构造方法开刀。ArrayList它有3个构造方法。

//默认的ArrayList构造函数。把elementData设置为一个空数组
public ArrayList() {
	this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

// ArrayList带容量大小的构造函数。
public ArrayList(int initialCapacity) {
	if (initialCapacity > 0) {
    	//如果initialCapacity初始容量大于0,则新建一个数组
		this.elementData = new Object[initialCapacity];
	} else if (initialCapacity == 0) {
    	//初始容量为0,把elementData是·初始化成一个空数组
		this.elementData = EMPTY_ELEMENTDATA;
	} else {
		throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
	}
}

//创建一个包含collection的ArrayList
public ArrayList(Collection<? extends E> c) {
	elementData = c.toArray();
	if ((size = elementData.length) != 0) {
    	//如果包含collection的ArrayList大小不为0,拷贝数据到elementData中
		if (elementData.getClass() != Object[].class)
			elementData = Arrays.copyOf(elementData, size, Object[].class);
	} else {
    	//否则,初始化一个空数组
		this.elementData = EMPTY_ELEMENTDATA;
	}
}

在构造方法中出现了几个字段:elementData、EMPTY_ELEMENTDATA、DEFAULTCAPACITY_EMPTY_ELEMENTDATA等等,那我们就去看看这些字段的定义。

//共享的空数组实例。
private static final Object[] EMPTY_ELEMENTDATA = {};
//一个用于共享的空数组实例,初始化一个默认的空数组,这里要和EMPTY_ELEMENTDATA区分,以便在添加第一个元素时知道要填充多少
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

//对于上面两个空数组而言,感觉作用差不多,为什么会这样设计?从3个构造方法中可以看到,无参数的构造方法创建实例用到了
//DEFAULTCAPACITY_EMPTY_ELEMENTDATA,而有参数的构造方法用到的是EMPTY_ELEMENTDATA。在JDK 1.7时只有EMPTY_ELEMENTDATA,
//JDK 1.8就用DEFAULTCAPACITY_EMPTY_ELEMENTDATA代替了EMPTY_ELEMENTDATA,EMPTY_ELEMENTDATA是为了优化创建ArrayList空实例时,
//产生不必要的空数组,使得所有ArrayList空实例都指向同一个空数组。

//真正存放ArrayList数据的数组
transient Object[] elementData;

2、构造方法看完了,那我们就只有根据ArrayList的使用方法,来继续了解它的源码,要使用ArrayList首先需要添加数据,所以我们就从添加数据的add方法下手。ArrayList添加数据有两种添加方式。

//添加元素到ArrayList末尾位置
public boolean add(E e) {
	//第一次添加元素,此时size=0,所以传递过去的参数是1
	ensureCapacityInternal(size + 1);  // Increments modCount!!
    //把添加的元素放到最后
	elementData[size++] = e;
	return true;
}

//添加元素到ArrayList的指定位置
public void add(int index, E element) {
	//检查index是否越界或者合法
	rangeCheckForAdd(index);

	ensureCapacityInternal(size + 1);  // Increments modCount!!
    //拷贝数据
	System.arraycopy(elementData, index, elementData, index + 1, size - index);
	elementData[index] = element;
    //大小+1
	size++;
}

private void ensureCapacityInternal(int minCapacity) {
	//如果是第一次添加数据,calculateCapacity方法返回的就是10,接着看ensureExplicitCapacity方法
	ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

//计算容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
	//如果elementData是空数组的话,就对比传递进来的minCapacity和DEFAULT_CAPACITY=10的最大值
    //由于是第一次新增,所以Math.max(1,10) = 10
	if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
		return Math.max(DEFAULT_CAPACITY, minCapacity);
	}
    return minCapacity;
}

private void ensureExplicitCapacity(int minCapacity) {
	//修改次数+1
	modCount++;
	if (minCapacity - elementData.length > 0)
		grow(minCapacity);
}

private void grow(int minCapacity) {
	//先获取当前elementData数组容量,即当前数组的长度
	int oldCapacity = elementData.length;
    //新的容量是:当前容量+当前容量/2(即扩容至原始容量的1.5倍)
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    //拷贝数组到新的容量
    elementData = Arrays.copyOf(elementData, newCapacity);
}

在add(E e)方法中遇到一个字段size,去看看定义此变量的地方,说明size表示ArrayList中元素的个数;DEFAULT_CAPACITY表示调用无参数的构造方法时,ArrayList的默认容量;
下面具体说说添加元素的add方法调用过程:
①. 当我们调用add方法首次添加元素时,不管是调用哪种方法添加,都会调用ensureCapacityInternal方法,调用add(int index, E element),只是在最开始判断了以下index是否越界合法,并且多了一个数据拷贝的操作。
②. ensureCapacityInternal这个方法很简单,只是调用了ensureExplicitCapacity方法。
③. ensureExplicitCapacity方法参数中调用了calculateCapacity计算容量的方法。
④. calculateCapacity方法先判断了elementData数组是否等于DEFAULTCAPACITY_EMPTY_ELEMENTDATA,这里就和调用无参数的构造方法有关联了,如果等于的话就会返回一个默认的初始容量10。
⑤. 进入ensureExplicitCapacity方法,首先是对修改次数的modCount变量+1,注意这个变量很重要,在添加、删除等很多地方都会有它的身影,后面再具体说明这个modCount。接着会进入一个if判断,这里肯定是true,调用grow方法,把初始容量10传递进去。
⑥. 进入grow方法,首先会获取当前容量,然后声明了一个新的容量是当前容量,再加上当前容量一半的值,这里也就说明了当ArrayList容量不足时,会自动扩容到原始容量的1.5倍,最后调用Arrays.copyOf进行数组元素的拷贝,这里就是说明了当调用无参数的构造方法时,会初始化一个默认容量为10的数组,并且扩容是原始容量的1.5倍。

当第二次调用add方法,此时数组的真实元素长度只有1个,即size=1,而数组的长度为10,传递到ensureCapacityInternal里的参数就是2,可以自己去看接下来是什么逻辑。也可以看看当调用其他构造方法后,add操作的逻辑。而对于addAll方法,由于它是添加的一个Collection集合,所以需要先把这个集合转化成数组,然后再根据这个数组的大小来调整是否需要扩容,也是调用的ensureCapacityInternal方法,这里就不再说明。

3、接下来我们看看删除元素的操作。

//根据下标删除,并返回此元素
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;
}

//删除指定的元素
public boolean remove(Object o) {
	if (o == null) {
		for (int index = 0; index < size; index++)
			if (elementData[index] == null) {
				fastRemove(index);
				return true;
			}
	} else {
		for (int index = 0; index < size; index++)
			if (o.equals(elementData[index])) {
				fastRemove(index);
				return true;
			}
		}
	return false;
}

private void fastRemove(int index) {
	modCount++;
	int numMoved = size - index - 1;
    //如果没有被移除
	if (numMoved > 0)
    	//数组拷贝
		System.arraycopy(elementData, index+1, elementData, index,numMoved);
    //把数组最后一项置为null,同时长度-1,使得GC可以进行垃圾回收
	elementData[--size] = null; // clear to let GC do its work
}

//根据范围进行批量删除
protected void removeRange(int fromIndex, int toIndex) {
	modCount++;
	int numMoved = size - toIndex;
	System.arraycopy(elementData, toIndex, elementData, fromIndex,numMoved);

	// clear to let GC do its work
	int newSize = size - (toIndex-fromIndex);
	for (int i = newSize; i < size; i++) {
		elementData[i] = null;
	}
	size = newSize;
}

//根据一个Collection集合进行删除
public boolean removeAll(Collection<?> c) {
	Objects.requireNonNull(c);
	return batchRemove(c, false);
}

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

删除操作有4种,removeAll(Collection<?> c)这个方法用的很少,就不再做说明了,有兴趣的可以自己去看看。下面具体说明方法的调用过程:
使用remove(Object o):
①. 当o为null时,这也就说明ArrayList是可以添加null的。然后循环遍历数组并调用fastRemove方法,fastRemove方法中判断此元素是否已经被删除,如果没有就先对数组进行拷贝,把数组最后一个元素置空,这样就可以让GC进行回收。
②. 当o不为null时,先遍历判断o是否和数组中的某项元素相等,如果有相等则操作和为null时是一样的,如果没有相等的元素,则会删除失败返回false。

使用remove(int index):
首先会进行下标检查,然后获取此下标的元素,并判断此下标的元素是否已经被删除了,如果没有的话,进行数组拷贝,然后把数组最后一个元素置空,并size-1,最后返回这个下标的元素。

使用removeRange(int fromIndex, int toIndex): 首先会对这个范围的数据进行拷贝,然后把对数组元素进行遍历置空,最后把数组的长度重新赋值。总的来说,ArrayList删除操作相对简单。

4、继续了解ArrayList获取元素的get操作和修改元素的set操作。

//通过下标获取元素
public E get(int index) {
	//检查下标是否越界
	rangeCheck(index);
	return elementData(index);
}

@SuppressWarnings("unchecked")
E elementData(int index) {
	//返回元素
	return (E) elementData[index];
}

//更新某个下标的元素
public E set(int index, E element) {
	rangeCheck(index);

	E oldValue = elementData(index);
	elementData[index] = element;
	return oldValue;
}

从上面的代码可以看出,获取元素以及更新元素并不复杂。首先获取元素时检测下标,再直接返回数组index位置的元素;更新数据也是先检测下标,取出当前位置的元素,再直接替换原来的元素后返回旧的元素。

5、ArrayList的迭代器
对于ArrayList的迭代,我们可以使用:for、foreach、iterator。如果对列表数据修改操作时,是会出问题的,甚至造成崩溃,我们在平时也会遇到这种需求。

public class Main {
	public static void main(String[] args) {
		List<String> list = new ArrayList<String>();
		list.add("a");
		list.add("b");
		list.add("c");
		for (int i = 0; i < list.size(); i++) {
			System.out.println(list.get(i));
		}

		System.out.println("-------------------------");

		for (String str : list) {
			System.out.println(str);
		}
	}
}

我们使用命令行javap -c -l Main.class编译class文件后,会得到以下Main.class文件中的内容。可以看到在使用for循环进行遍历时,没有调用Iterator相关的方法,所以在for循环中进行添加、删除等操作时不会造成ConcurrentModificationException异常,但会引发其他的问题,这里就不展开了;但是在使用foreach进行遍历时,我们看到下面的第78行,会调用到java/util/List.iterator:()Ljava/util/Iterator;方法进行遍历,当我们在foreach遍历中进行添加/删除元素,就会产生ConcurrentModificationException异常,可以自己尝试一下。当然直接用iterator进行迭代的同时修改数据,也会导致ConcurrentModificationException异常,这个异常就和我们之前说的modCount有关了。

C:\Users\Administrator\Desktop\新建文件夹>javap -c -l Main.class
Compiled from "Main.java"
public class Main {
  public Main();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return
    LineNumberTable:
      line 4: 0

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class java/util/ArrayList
       3: dup
       4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
       7: astore_1
       8: aload_1
       9: ldc           #4                  // String a
      11: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      16: pop
      17: aload_1
      18: ldc           #6                  // String b
      20: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      25: pop
      26: aload_1
      27: ldc           #7                  // String c
      29: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      34: pop
      35: iconst_0
      36: istore_2
      37: iload_2
      38: aload_1
      39: invokeinterface #8,  1            // InterfaceMethod java/util/List.size:()I
      44: if_icmpge     69
      47: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
      50: aload_1
      51: iload_2
      52: invokeinterface #10,  2           // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
      57: checkcast     #11                 // class java/lang/String
      60: invokevirtual #12                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      63: iinc          2, 1
      66: goto          37
      69: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
      72: ldc           #13                 // String -------------------------
      74: invokevirtual #12                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      77: aload_1
      78: invokeinterface #14,  1           // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
      83: astore_2
      84: aload_2
      85: invokeinterface #15,  1           // InterfaceMethod java/util/Iterator.hasNext:()Z
      90: ifeq          113
      93: aload_2
      94: invokeinterface #16,  1           // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
      99: checkcast     #11                 // class java/lang/String
     102: astore_3
     103: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
     106: aload_3
     107: invokevirtual #12                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
     110: goto          84
     113: return
}

接下来我们就来看看iterator()迭代方法。

//直接返回了Itr对象
public Iterator<E> iterator() {
	return new Itr();
}

//Itr实现了Iterator接口
private class Itr implements Iterator<E> {
		//下一个要返回的元素的索引
        int cursor;
        int lastRet = -1;
		//这里对expectedModCount进行赋值
        int expectedModCount = modCount;

        Itr() {}

		//如果下一个元素的索引不等于ArrayList长度,说明还没有指向最后一个元素
        public boolean hasNext() {
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        public E next() {
			//ConcurrentModificationException异常检测
            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];
        }

		//删除元素
		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();
            }
        }

        final void checkForComodification() {
			//当modCount修改次数不等于所期望的修改次数时,就抛出异常。
			//这里能引起modCount变化的,就是我们的add、remove、clear等等相关操作
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
		
		//省略其他方法
		......
    }

ArrayList有专门用于迭代的迭代器——listIterator()。它有2个重载的方法。都直接返回了一个ListItr对象。

public ListIterator<E> listIterator(int index) {
	if (index < 0 || index > size)
		throw new IndexOutOfBoundsException("Index: "+index);
	return new ListItr(index);
}

public ListIterator<E> listIterator() {
	return new ListItr(0);
}

private class ListItr extends Itr implements ListIterator<E> {
	ListItr(int index) {
    	super();
        cursor = index;
    }
	
	//是否有前一个元素
    public boolean hasPrevious() {
        return cursor != 0;
    }

	//返回下一个元素的索引
    public int nextIndex() {
        return cursor;
    }

	//返回前一个元素的索引
    public int previousIndex() {
        return cursor - 1;
    }

	//返回前一个元素
    @SuppressWarnings("unchecked")
    public E previous() {
        checkForComodification();
        int i = cursor - 1;
        if (i < 0)
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        cursor = i;
        return (E) elementData[lastRet = i];
    }

	//更新数据
    public void set(E e) {
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();

        try {
            ArrayList.this.set(lastRet, e);
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }

	//添加数据
    public void add(E e) {
        checkForComodification();
        try {
            int i = cursor;
            ArrayList.this.add(i, e);
            cursor = i + 1;
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }
}

可以看到ListItr继承了Itr类,实现了ListIterator接口,而ListIterator接口也是继承了Iterator接口。并且我们也看到了在ListItr类中有add、set等操作,这就说明我们在迭代的同时可以进行数据的新增和修改操作,还有获取上一个元素、上一个元素的索引等新方法,同样也有删除等其他操作,所以在ArrayList中进行遍历的话,可以使用listIterator()方法。

6、ArrayList其他常用的方法。

//从数组头部,查询某个元素的下标索引
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;
	}
	//没有找到此元素,直接返回-1
	return -1;
}

//返回数组中是否包含某个元素,此时调用的是indexOf方法
public boolean contains(Object o) {
	return indexOf(o) >= 0;
}

//从数组的尾部,查询某个元素的下标索引
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;
	}
	//没有找到此元素,直接返回-1
	return -1;
}

//清空数组列表
public void clear() {
	modCount++;
	//遍历数组,把全部元素至为null,使得GC可以进行垃圾回收
	for (int i = 0; i < size; i++)
		elementData[i] = null;

	//把长度至为0
	size = 0;
}

//判断列表是否是空数组
public boolean isEmpty() {
	return size == 0;
}

//转化成Object数组,注意调用此方法把ArrayList列表转化成数组时,此方法可能会抛出ClassCastException类型转化异常
//如果需要转化成特定的类型请使用toArray(T[] a)方法
public Object[] toArray() {
	return Arrays.copyOf(elementData, size);
}

//把ArrayList列表转化成数组,代码都很简单,就是对数据的拷贝
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
	if (a.length < size)
		// Make a new array of a's runtime type, but my contents:
		return (T[]) Arrays.copyOf(elementData, size, a.getClass());
	System.arraycopy(elementData, 0, a, 0, size);
	if (a.length > size)
		a[size] = null;
	return a;
}

以上就是ArrayList中常用的几个方法了,至于其余方法就不再说明,有兴趣的可以自己去看看。

四、总结

1、ArrayList实际上是通过一个动态数组去保存数据的。当我们构造ArrayList时;若使用默认构造函数,在首次添加数据时,ArrayList会初始化默认容量为10。
2、当ArrayList容量不足以容纳全部元素时,ArrayList会重新设置容量:新的容量=原始容量 + 原始容量 / 2。即原始容量的1.5倍。
3、ArrayList的克隆函数,即是将全部元素克隆到一个数组中。
4、ArrayList实现java.io.Serializable的方式。当写入到输出流时,先写入“容量”,再依次写入“每一个元素”;当读出输入流时,先读取“容量”,再依次读取“每一个元素”。
5、ArrayList不是线程安全的,从ArrayList方法中我们没有看到任何的锁以及同步块,所以它不是线程安全的。

五、参考

Java 集合系列03之 ArrayList详细介绍(源码解析)和使用示例