YY's Studio.

【Java常用容器】ArrayList源码分析

2018/07/17

阅读提醒:将本文结合源码一起使用味道更佳哦!~

前言

ArrayList 是我们最常用的一个集合类之一。了解它的实现,有助于我们理解自己写下的代码背后更深层次的逻辑。同时也能从其中学习到JDK的设计思想。

类定义

1
2
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
  • AbstractList:实现了List,其内部通过cursor实现迭代器逻辑

  • RandomAccess:标记接口,声明ArrayList支持快速随机访问

  • Cloneable:标记接口,声明ArrayList重写clone方法。这里ArrayList实现的是浅拷贝,即复制内部数组

  • Serializable:标记接口,可被默认的序列化机制序列化与反序列化

主要内容

upload successful

数据结构

ArrayList采用的数据结构为:数组。数组在内存中会开辟一组连续的、大小相同的空间。

ArrayList在内存上的分布类似于以下的图

upload successful

也是基于此特性,数组的查询能实现O(1)的时间复杂度;

在代码中

1
2
3
4

transient Object[] elementData; //内部数组

private int size; //数组的长度

可以看到 elementData 是被 transient 修饰的所以在序列化的时候不会被保存,后面会讲到ArrayList为什么要这么处理。

数据操作

1. 插入

因为篇幅有限,这里只截取了最简单的 add(E e) 方法,其他插入方法的流程于此类似

1
2
3
4
5
6
7
8
public boolean add(E e) {
//1. 判断是否要扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
//2. 插入数据
//3. size+1
elementData[size++] = e;
return true;
}

插入这部分代码主要做了两件事

  1. 是否需要扩容
  2. 插入数据

以上要深入的的部分(1)ensureCapacityInternal方法。在这部分会涉及扩容,这个我们稍后再去分析。

可以看到在插入的时候,时间复杂度可能会变化,如果要扩容的话,必定会设计数组的转移,所以时间复杂度是O(n),如果不扩容的情况下时间复杂度是O(1).

2. 删除

同上面一样,也以 remove(int index) 方法为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public E remove(int index) {
//1. 判断是否超过数据最大长度
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
//2. modifyCount +1
modCount++;
//3. 获取旧值
E oldValue = (E) elementData[index];
//4. 计算index位置向后偏移量
int numMoved = size - index - 1;
//5. 将index后的元素前移一位
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//6. size--
elementData[--size] = null;

return oldValue;
}

删除这部分代码主要做了两件事

  1. 获取旧的值
  2. 用后位覆盖的方式,替换原位置的值

以上内容中(2)中的 modCount++的作用是实现迭代器中的多线程操作校验也就是 fast-fail机制;(5)其中调用了System.arraycopy方法,底层是用C实现,能快速的实现数组之间的复制。

我们会发现当ArrayList要remove一个元素的时候,是直接将后面的元素前移顶替,而不是仅仅将其置空。在日常开发中,我们经常会对ArrayList进行遍历操作。如果在正向遍历的情况下,删除数组的某个元素,会导致异常,因为数据索引已经不准确。

同时,我们会发现当删除一个元素的时候,尽管是调用了C层来实现,可是一样是需要O(n)的时间复杂度。

3. 查询

1
2
3
4
5
6
7
public E get(int index) {
//1. 判断是否超过数据最大长度
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
//2. 获取指定位置上的值
return (E) elementData[index];
}

查询的部分没有太多值得深入的部分。就是通过索引获取值。也看明显的可以看出,ArrayList的查询速度很快,只需要O(1)的时间复杂度。

扩容机制

对于容器来说,扩容的重要性不言而喻。我们在上面的插入中发现调用到了ensureCapacityInternal方法,接下来我们好好了解一下其内部的实现机制;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

private void ensureCapacityInternal(int minCapacity) {
//1. 判断当前的elementData是否为空
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//2. 得到数组需要的最小的容量大小
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}

ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
// modifyCount +1
modCount++;

//3. 如果所需最小容量大于当前数组容量,则执行扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}

private void grow(int minCapacity) {

int oldCapacity = elementData.length;
//4. 新数组的容量= 旧数组容量*1.5 (>>1 等效于除以2)
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 校验新的容量是否满足最小容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 判断新的容量是否已经为最大容量了( MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8)
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);
}

private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();

return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}

扩容部分做的判断很多,总结起来就是以下三个部分

  1. 是否需要扩容
  2. 计算出扩容值
  3. 进行扩容

里面有几点个点值得我们去分析:

(3)不是每次都会去扩容,只有当前容量不满足最低要求容量时候才会去扩容
(4)一般情况下扩容大小会是是1.5倍,了解这个有助于我们了解使用容器时候的内存情况

现在将上面插入部分与扩容整合在一起,整理出流程图:
这里写图片描述
以上的图省略了一些边界情况,如果当数组为空,或者当数组已经最大等情况

序列化

正常情况下,我们使用序列化,都不需要重写以下方法(writeObject/readObject),这两个方法的作用是用来指定序列化过程中,如何写入与读取值的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
private void writeObject(ObjectOutputStream s) throws IOException{
//记录期望值
int expectedModCount = modCount;
s.defaultWriteObject();
//写入size
s.writeInt(size);
//1.循环写入数组元素
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
//期望值对比,防止在此方法执行过程中elementData被修改
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}

private void readObject(ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
//读取写入的对象
s.defaultReadObject();

s.readInt();

if (size > 0) {
ensureCapacityInternal(size);

Object[] a = elementData;

for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}

这段代码很简单。就是重写了wirteObject与readObject。但是我这里有一个疑问

为什么不使用默认的wirteObject与readObject,而重写他们呢?

这里以我的观点来看,是为了节省内存的使用。正常情况下ArrayList中的elementData数组是不会被放满的,存在一定的剩余空间(这部分没有看明白的可以回头再看看扩容)。而在序列化过程中,只序列化元素而非数组,能省去很多无用的内存。

浅拷贝

ArrayList实现了clone方法,从拷贝的维度上区分,ArrayList为浅拷贝,为什么Arraylist是浅拷贝呢?我们贴出以下源码

1
2
3
4
5
6
7
8
9
10
11
12
13
public Object clone() {
try {
//1 生成ArrayList
ArrayList<?> v = (ArrayList<?>) super.clone();
//2 复制其内所有元素
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);
}
}

以上代码做了两件事情

  1. 通过clone方法生成新的ArrayList,复制了其中的基础类型
  2. 复制了原数组中的元素引用

区分浅拷贝和深拷贝的一个很重要的标准是:拷贝对象的元素和原对象的元素是否相同。如果相同则为浅拷贝,不相同为深拷贝

因此虽然生成了新的ArrayList,可是其内的数组对象引用还是与原对象相同。因此是浅拷贝无疑了。

fast-fail

fast-fail 的主要作用是用来校验在某些操作(常为多线程操作)的过程中,元素是否被修改,如果被修改就报ConcurrentModificationException的错误。在ArrayList或其他容器中 一般实现是由expectedModCount 与 modCount 进行对比,来保证元素没有被操作过。
就这部分的内容,是具有通用性的,在java.util中被广泛使用,如HashMap等。我将另开章节fast-fail机制来绍

总结

ArrayList的实现很简单。只需要花上半小时的时间大致就可以看完。而关于ArrayList,我们必须要掌握的要点如下:

  1. 扩容机制,什么时候扩容,扩容后的大小
  2. 查询快、删除慢的原因
  3. 为什么是线程不安全,modCount

了解了这些,会有助于你避免一些日常开发过程中的坑。 Best wish!~

CATALOG
  1. 1. 前言
  2. 2. 类定义
  3. 3. 主要内容
    1. 3.1. 数据结构
    2. 3.2. 数据操作
      1. 3.2.1. 1. 插入
      2. 3.2.2. 2. 删除
      3. 3.2.3. 3. 查询
    3. 3.3. 扩容机制
    4. 3.4. 序列化
    5. 3.5. 浅拷贝
    6. 3.6. fast-fail
  4. 4. 总结