数组和ArrayList源码分析

本文需要你具备一些简单的Java知识,主要是在ArrayList这个集合框架有一定的使用经验,如果你没有太多经验,可以查看该网站之前有关于集合框架的文章中ArrayList的内容,本文JDK版本为1.8

数组

什么是数组

用非常官方的话来说:

数组(Array)是有序的元素序列。 若将有限个类型相同的变量的集合命名,那么这个名称为数组名。组成数组的各个变量称为数组的分量,也称为数组的元素,有时也称为下标变量。用于区分数组的各个元素的数字编号称为下标。数组是在程序设计中,为了处理方便, 把具有相同类型的若干元素按有序的形式组织起来的一种形式。 这些有序排列的同类数据元素的集合称为数组。

看不懂也没关系

游泳馆柜子

简单来说:想象一个容器,一个空间,有很多小柜子,每个小柜子上面都有一个序号(注意序号是有序的,即类似1,2,3,4这样),其中每个小柜子里面都存放着相同的东西。想象游泳馆的存放物品的小柜子或者丰巢快递柜(不过快递柜标号没有显示出来)

数组在内存中的储存方式

根据上面的描述,你完全可以把计算机的内存想象成游泳馆的小柜子,计算机中的信息都储存在一个一个小的柜子里,无非是把序号换成了地址而已。然后在该地址存放数据。

数组

当我们新建一个数组的时候,会从计算机中拿出顺序的几个空间大小的柜子当作一个整体。接下来就可以填充数据进入数组中了,把数据放入与之对应的序号(地址)中。

然后0号代表第一位元素,1代表第二位,以此类推。

为啥用0代表第1位而不是用1呢?那是因为我们在计算的时候,假如第1位用1表示的话,该位置的地址=初始地址+1-1。假如使用0的话可以少进行一次减法运算,计算机的运算资源十分宝贵,某一方面也是这样考虑的。

数组的优点和缺点

优点:支持快速访问数据

因为数据与对应的序号一一对应,所以你把拿取数据时,通过相应的序号就可以。哪怕不知道地址,你知道在第几位,也可以通过首位地址+第几位来快速拿到数据。

缺点:写入和删除数据时比较麻烦

我们写入数据的时候要把数据放入柜子,如果序号3这个柜子有东西 了,那我们把数据放入3号柜的话就必须把3号柜清空了。那假如我们就一定要在3号柜子放入数据,而不影响之前的数据那该怎么办呢?

把3号柜的所有物品都统一向后面移动1位。当然如果追加数据是在末尾的话就不用移动元素了,删除也是一样的。

这样操作就会造成只要不是在末尾操作数组,数组每次更改都会影响该序号后面的数据(统一前移或者后移)

ArrayList源码分析

在Java中,数组是在声明时已经写定了大小,但是不可变的数组实际上对于编程来说是很麻烦的事情,因为我们不知道会在该数组中存入多少数据。于是引入了ArrayList这个集合框架类,当然不只是它做到这个功能,而是该类我们比较常用而已。

ArrayList运行总览

ArrayList和核心操作是通过数组复制来达到为我们实现可变化的数组这样的操作。

运行流程如下

类初始化(创建空数组)-加入第1个元素时比较该数组能否承受这个数据-如果不能就扩容以后加入

所以该类的重点在于类初始化,扩容方法,加入数据的方法。

ArrayList方法源码具体分析

类的基础属性

在关注构造器之前,我们先关注类中的几个属性和该类继承的接口

RandomAccess这个接口是一个标志接口,表明实现这个接口的List集合是支持快速随机访问的。在该类中,我们可以通过get方法快速获取元素对象。

Cloneable实现了克隆接口,覆盖了函数clone(),能被克隆

java.io.Serializable该类支持序列化

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
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
/**
* Default initial capacity.
* 默认初始容量
*/
private static final int DEFAULT_CAPACITY = 10;

/**
* Shared empty array instance used for empty instances.
* 用于空实例的共享空数组实例。
*/
private static final Object[] EMPTY_ELEMENTDATA = {};

/**
* Shared empty array instance used for default sized empty instances. We
* distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
* first element is added.
* 共享空数组实例用于默认大小的空实例。 我们
* 将此与EMPTY_ELEMENTDATA区分开来,以了解添加第一个元素时要扩大多少
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

/**
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer. Any
* empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* will be expanded to DEFAULT_CAPACITY when the first element is added.
* 存放ArrayList元素的数组缓冲区。ArrayList的容量就是这个数组缓冲区的长度。当添加第一个
* 元素时,任何带有 elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的空
* ArrayList 都将扩展为 DEFAULT_CAPACITY。就是扩充10了
*/
transient Object[] elementData; // non-private to simplify nested class access

/**
* The size of the ArrayList (the number of elements it contains).
* ArrayList 的大小(它包含的元素数)。
* @serial
*/
private int size;

DEFAULT_CAPACITY是默认的扩容系数,在第1此添加数据的时候会扩容,这个在后面添加方法时会具体讲到,后面两个空数组效果都差不多,不过系统会根据是不是第二个数组来判断是不是第一次添加数据。elementData你可以理解为存放着这个ArrayList的数据,size很好理解,就是这个ArrayList的长度,就像数组中的length

类的初始化

然后我们关注该类的构造器

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
45
46
47
48
49
50
51
52
53
54
55
56
/**
* Constructs an empty list with the specified initial capacity.
* 构造一个具有指定初始容量的空列表。
*
* @param initialCapacity the initial capacity of the list 列表的初始容量
* @throws IllegalArgumentException if the specified initial capacity
* is negative 如果指定的初始容量为负
*/
public ArrayList(int initialCapacity) {
//如果大于0,就把缓冲数组初始化为具体的值
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
//如果是0,则直接赋值空数组给它
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
//如果小于0,则抛出异常
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}

/**
* Constructs an empty list with an initial capacity of ten.
* 构造一个初始容量为 10 的空列表。
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

/**
* Constructs a list containing the elements of the specified
* collection, in the order they are returned by the collection's
* iterator.
* 按照集合的迭代器返回的顺序构造一个包含指定集合元素的列表。
*
* @param c the collection whose elements are to be placed into this list 其元素将被放入此列表的集合
* @throws NullPointerException if the specified collection is null 如果指定的集合为空
*/
public ArrayList(Collection<? extends E> c) {
//该方法是List接口中的方法,该方法的作用是以一个适当的顺序返回包含此列表中所有元素的数组
Object[] a = c.toArray();
//判断该集合是否有数值
if ((size = a.length) != 0) {
//如果是当前类,直接赋值就行
if (c.getClass() == ArrayList.class) {
elementData = a;
} else {
//如果不是当前类,就通过数组复制的方法来赋值给elementData,也算是变相赋值了
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
// replace with empty array. 替换为空数组
elementData = EMPTY_ELEMENTDATA;
}
}

这三个构造器都挺常用的,可能你在想第二个构造器明明是返回一个空的,怎么注释里面写得是构造一个初始容量为10的数组,但是赋值的时候,那个常量明明是空数组。这个问题后面会解答。

第一个构造器是通过你手动传值来创建一个具有初始容量的数组。第三个构造器是通过其他集合类的数组来创建数据。

增加数据

来看看非常关键的add方法,虽然只有短短几行代码。

1
2
3
4
5
6
7
8
9
10
11
/**
* Appends the specified element to the end of this list.
* 将指定的元素附加到此列表的末尾。
* @param e element to be appended to this list 要附加到此列表的元素
* @return <tt>true</tt> (as specified by {@link Collection#add})
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}

非常朴实的代码,不过其中的重点在第1行,这是ArrayList扩容的方法。这个方法我们放在下面讲。总之这个方法就是扩容以后,把最后一个位置装上数据,返回true。

扩容方法

扩容方法是ArrayList的重点部分,这里将解释为什么我创建一个空的ArrayList,系统注释会说为创建一个长度为10的List

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
  /**
* add 方法中的扩容方法
* @param minCapacity 需要扩容的大小
*/
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

/**
* 上面方法最里面进入的方法,该方法DEFAULT_CAPACITY就是常量的10
* 这里方法的意思是如果ArrayList中的缓存数组是DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* 该值是无参构造器所赋的值,如果传入进来的需要扩容的大小小于10,且是无参构造器所创造的
* 就返回10
*/
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}

/**
* 第一行不需要太在意,这个是用来记录方法执行次数的,这个值只有在对应的迭代器中才使用
* 因此如果在使用迭代器的过程中有其他线程修改了map,那么将抛出异常
* 这个方法的本意是判断当前List是否需要扩容,只有调用了grow方法才是真正的扩容
*/
private void ensureExplicitCapacity(int minCapacity) {
modCount++;

// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}

/**
* Increases the capacity to ensure that it can hold at least the
* number of elements specified by the minimum capacity argument.
* 增加容量以确保它至少可以容纳最小容量参数指定的元素数量。
* @param minCapacity the desired minimum capacity
*/
private void grow(int minCapacity) {
// overflow-conscious code
// 获得存储数组的长度
int oldCapacity = elementData.length;
// 新的数组容量等于存储数组的1.5倍,(oldCapacity >> 1)是位运算
//将oldCapacity 右移一位,其效果相当于oldCapacity /2,位运算比算术运算更快
int newCapacity = oldCapacity + (oldCapacity >> 1);
//如果新的容量-输入进来的最小容量小于0,说明新容量扩容了1.5倍还是不够,那直接把需要的容量赋值给newCapacity
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//如果新的容量-最大容量>0,说明新的容量大于最大容量则调用其他方法(实际上这个方法就是返回最大容量)
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);
}

/**
* 校验minCapacity,如果是负值则报错。
* 如果大于最大值,就返回Integer的最大值
*/
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}

总之,扩容是从上至下的。每次add方法都需要调用校验方法,我们先假定这是第一次加入数据。首先判断是否是第一次添加,如果是直接把扩容值设置为10,然后用10这个变量走接下来的流程。在grow方法中oldCapacity为0,经过计算后的newCapacity也为0,则第一个if判断为true,进入其中,最后把newCapacity赋值为10,然后复制数组,结束扩容流程。

ArrayList中的数组复制方法

System.arraycopy() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
// 我们发现 arraycopy 是一个 native方法,也就是不用Java代码实现的方法,接下来我们解释一下各个参数的具体意义
/**
* 复制数组
* @param src 源数组
* @param srcPos 源数组中的起始位置
* @param dest 目标数组
* @param destPos 目标数组中的起始位置
* @param length 要复制的数组元素的数量
*/
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);

Arrays.copyOf()方法

源码

1
2
3
4
5
6
7
8
   public static int[] copyOf(int[] original, int newLength) {
// 申请一个新的数组
int[] copy = new int[newLength];
// 调用System.arraycopy,将源数组中的数据进行拷贝,并返回新的数组
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}

所以copyOf()本质上也是调用了System.arraycopy()的方法。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!