Java集合框架分析-ArrayList

本篇文章主要分析一下Java集合框架中的List部分,ArrayList,该源码分析基于JDK1.8,分析工具,AndroidStudio,文章分析不足之处,还请指正!

相关文章
1、Java 集合框架分析-概述
2、Java集合框架分析-HashMap
3、Java集合框架分析-LinkedHashMap
4、Java集合框架分析-LinkedList
5、Java集合框架分析-Iterator
6、Java集合框架分析-HashSet

一、ArrayList简介

首先来看下java集合框架的总图,在网上找了两张关于集合框架的架构图:

ArrayList底层维护的是一个动态数组,每个ArrayList实例都有一个容量。该容量是指用来存储列表元素的数组的大小。它总是至少等于列表的大小。随着向 ArrayList 中不断添加元素,其容量也自动增长。ArrayList不是同步的(也就是说不是线程安全的,同HashMap、LinkedHashMap一样),如果多个线程同时访问一个ArrayList实例,而其中至少一个线程从结构上修改了列表,那么它必须保持外部同步,在多线程环境下,可以使用Collections.synchronizedList方法声明一个线程安全的ArrayList,例如:

1
List arraylist = Collections.synchronizedList(new ArrayList());

二、源码分析

2.1 类结构定义

首先我们来看一下关于ArrayList的类结构,

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

上面是是ArrayList类的定义,它继承了抽象类AbstractList,但是真正继承的方法只有equals和hashCode,别的方法在ArrayList 中都有自己的重新实现;List接口在AbstractList中已经实现,这里是为了表明一下,没有太大含义;RandomAccess没有方法,表明ArrayList支持快速随机访问;实 现了 Cloneable 接口,以指示 Object.clone() 方法可以合法地对该类实例进行按字段复制,如果在没有实现 Cloneable 接口的实例上调用 Object 的 clone 方法,则会导致抛出 CloneNotSupportedException 异常;类通过实现 java.io.Serializable 接口以启用其序列化功能,未实现此接口的类将无法使其任何状态序列化或反序列化

2.2 变量定义

接着我们分析一下ArrayList定义的变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//序列化ID
private static final long serialVersionUID = 8683452581122892189L;
//数组初始容量大小
private static final int DEFAULT_CAPACITY = 10;
//
private static final Object[] EMPTY_ELEMENTDATA = {};
//elementData存储ArrayList内的元素
transient Object[] elementData;
//存储在ArrayList内的元素的数量
private int size;

2.3 构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//设定初始容量大小的构造函数
public ArrayList(int initialCapacity) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
//数组初始化
this.elementData = new Object[initialCapacity];
}
//无参数构造函数
public ArrayList() {
super();
this.elementData = EMPTY_ELEMENTDATA;
}
//将提供的集合转成数组返回给elementData(返回若不是Object[]将调用Arrays.copyOf方法将其转为Object[])
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
size = elementData.length;
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
}

构造函数还是比较简单的,我们来看看ArrayList的最常用的方法add,

2.4 add(E e)添加数据

1
2
3
4
5
6
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}

发现ArrayList的add方法,只有简单的两行代码,粗略的看一下是在数组elementData的尾部添加一个元素E,那么到底是怎么样操作的呢?我们详细查看一下源码,首先进入ensureCapacityInternal方法中一探究竟。看这个方法的名字就大概知道这是确保数组容量的,怎么个确保法呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private void ensureCapacityInternal(int minCapacity) {
//当我们调用ArrayList的无参构造函数时调用此代码
if (elementData == EMPTY_ELEMENTDATA) {
//设置数组容量为10,默认的大小
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
//
ensureExplicitCapacity(minCapacity);
}
//确保容量大小,modCount自增,并判断数组大小是否足够,不够的话将增大数组
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
//如果最小需要空间比elementData的内存空间要大,则需要扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}

到了这里还是有点模糊到底怎么数组扩容的?我们接着看grow的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//扩容并重新copy一份数组
private void grow(int minCapacity) {
// oldCapacity为当前数组大小
int oldCapacity = elementData.length;
// newCapacity为新容量的大小等于旧容量的1.5倍大小
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果扩充后的容量还是比最少需要的容量还要小的话,就设置扩充容量为最小需要的容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//判断最新的容量是否已经超出最大数组范围,溢出判断
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 调用Arrays.copyOf方法将elementData数组指向新的内存空间时newCapacity的连续空间
// 并将elementData的数据复制到新的内存空间
elementData = Arrays.copyOf(elementData, newCapacity);
}

我们总结一下add操作的过程,首先我们先要判断是否需要扩容,在扩容判断里面,我们主要进行一些判断,如果当前所需要的容量和当前数组的容量进行比较,如果不够的话则进行扩容,在扩容的同时则需要判断是否溢出了,然后将旧的数据数组进行copy到新的容量大小的数组中,最后再将需要添加的数据添加到数组的最后一位。

我们接着分析add的另一个方法,重载add(int index, E element)方法,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//在指定的位置上面插入一个数据
public void add(int index, E element) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
//判断是否需要扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
//需要将指定位置及其后面数据向后移动一位,然后在位置上面插入数据
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
//插入数据
elementData[index] = element;
//大小改变
size++;
}

中心思想就是,首先判断指定位置index是否超出elementData的界限,之后调用ensureCapacity调整容量(若容量足够则不会拓展),调用System.arraycopy将elementData从index开始的size-index个元素复制到index+1至size+1的位置(即index开始的元素都向后移动一个位置),然后将index位置的值指向element,同时改变数组的大小。

2.5 addAll方法

接着我们分析一下addAll方法。

1
2
3
4
5
6
7
8
9
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
//将a的第0位开始拷贝至elementData的size位开始,拷贝长度为numNew
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}

其实这个方法很好理解,首先将批量添加的数据转为数组,然后获取它的容量大小,判断一下,如果我们要在数组中插入这一批数据的话,是否需要扩容,经过这个扩容判断操作,我们的数组容量是足够了,然后我们开始批量导入数据,其中System.arraycopy(a, 0, elementData, size, numNew);意思就是,将需要导入的批数据从0开始,导入到数组从size位置开始,导入的大小数量就是需要导入的数量。这个时候,总的数组容量就变成了原先的容量加上导入的容量了。

addAll 还有一个重载方法,我们也来看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public boolean addAll(int index, Collection<? extends E> c) {
//判断插入的位置是否越界
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
int numMoved = size - index;
if (numMoved > 0)
System.arraycopy(elementData, index, elementData, index + numNew,
numMoved);
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}

这个方法其实在add的方法基础上面修改而来,复制数组的时候主要进行两步复制,第一步,现将原数组在指定的位置上面向后移动需要的位数,然后第二步再将需要导进去的数据从index位置复制进去。

2.6 get方法

分析完了add方法,我们来分析分析get 方法内容,get方法也是比较简单的

1
2
3
4
5
6
public E get(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
return (E) elementData[index];
}

首先判断需要返回的位置是否越界了,其次直接返回数组对应索引里面的值。非常简单,我们再看看其他方法。

2.7 其余方法

1、remove(int index)

我们来看看remove方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public E remove(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
modCount++;
//保存一下需要删除的数据
E oldValue = (E) 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;
}

同理,因为涉及到数组的问题,所以首先需要判断一下是否出现越界的问题,然后就开始将指定删除位置后面的数据都向前移动一位,然后将最后的一位设置为null,最后返回删除的数据。

删除一个数据,remove也有重载方法

2、remove(Object o)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}

删除一个数据,主要是遍历数组,看看数组中是否存在这个数据,如果存在的话则进行fastRemove,我们进入fastRemove中看看

3、fastRemove(int index)

1
2
3
4
5
6
7
8
9
//删除指定位置上面的数据
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
}

将指定位置index后面的数据全部向前移动一位,最后将最后一位回收掉。

另外我们看下全部删除数组中的数据方法clear

4、clear( )

1
2
3
4
5
6
7
8
9
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}

将数组中所有的数据都重置为null,等带回收。

5、removeRange(int fromIndex, int toIndex)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protected void removeRange(int fromIndex, int toIndex) {
if (toIndex < fromIndex) {
throw new IndexOutOfBoundsException("toIndex < fromIndex");
}
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;
}

执行过程是将elementData从toIndex位置开始的元素向前移动到fromIndex,然后将toIndex位置之后的元素全部置空顺便修改size。

6、set(int index, E element)

1
2
3
4
5
6
7
8
public E set(int index, E element) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
E oldValue = (E) elementData[index];
elementData[index] = element;
return oldValue;
}

这个方法很简单,就是修改数组index里面的数据,并返回旧的数据。

7、indexOf(Object o)

1
2
3
4
5
6
7
8
9
10
11
12
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;
}

返回在数组中第一次出现的值。如果找到这个值的话就返回index,找不到就返回-1。

8、lastIndexOf(Object o)

1
2
3
4
5
6
7
8
9
10
11
12
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;
}

遍历数组,从后往前遍历,找出第一次出现值的索引,找到就返回index,找不到就返回-1。

9、trimToSize()

1
2
3
4
5
6
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = Arrays.copyOf(elementData, size);
}
}

由于elementData的长度会被拓展,size标记的是其中包含的元素的个数。所以会出现size很小但elementData.length很大的情况,将出现空间的浪费。trimToSize将返回一个新的数组给elementData,元素内容保持不变,length跟size相同,节省空间。

以上便是ArrayList的源码内容,总的来说不是很难,接下来我们来总结一下关于ArrayList的方方面面。

三、总结

  1. ArrayList底层是基于数组来实现的,可以通过下标准确的找到目标元素,因此查找的效率高;但是添加或删除元素会涉及到大量元素的位置移动,效率低。
  2. ArrayList提供了三种不同的构造方法,无参数的构造方法默认在底层生成一个长度为10的Object类型的数组,当集合中添加的元素个数大于10,数组会自动进行扩容,即生成一个新的数组,并将原数组的元素放到新数组中。
  3. ensureCapacity方法对数组进行扩容,它会生成一个新数组,长度是原数组的1.5倍+1,随着向ArrayList中不断添加元素,当数组长度无法满足需要时,重复该过程。
  4. ArrayList不是同步的(也就是说不是线程安全的,同HashMap、LinkedHashMap一样),如果多个线程同时访问一个ArrayList实例,而其中至少一个线程从结构上修改了列表,那么它必须保持外部同步,在多线程环境下,可以使用Collections.synchronizedList方法声明一个线程安全的ArrayList,例如:List arraylist = Collections.synchronizedList(new ArrayList());

关于作者

github: https://github.com/crazyandcoder
博客: http://crazyandcoder.github.io
掘金:https://juejin.im/user/56b96af96240b8005865df59/share


参考链接

1、http://www.cnblogs.com/hzmark/archive/2012/12/20/ArrayList.html
2、http://blog.csdn.net/u010305706/article/details/51007826
3、http://blog.csdn.net/ljcitworld/article/details/52041836
4、http://www.cnblogs.com/fangfuhai/p/5767056.html