ArrayList 源码分析
1. 在阅读源码时做了大量的注释,并且做了一些测试分析源码内的执行流程,由于博客篇幅有限,并且代码阅读起来没有 IDE 方便,所以在 github 上提供JDK1.8 的源码、详细的注释及测试用例。欢迎大家 star、fork !
2. 由于个人水平有限,对源码的分析理解可能存在偏差或不透彻的地方还请大家在评论区指出,谢谢!
1. 结构
首先我们需要对 ArrayList 有一个大致的了解就从结构来看看吧.
1. 继承
该类继承自 AbstractList 这个比较好说
2. 实现
这个类实现的接口比较多,具体如下:
- 首先这个类是一个 List 自然有 List 接口
- 然后由于这个类需要进行随机访问,所谓随机访问就是用下标任一访问,所以实现了RandomAccess
- 然后就是两个集合框架肯定会实现的两个接口 Cloneable, Serializable 前面这个好说序列化一会我们具体再说说
3. 主要字段
1 | // 默认大小为10 |
4. 主要方法
下面的方法后面标有数字的就是表示重载方法
- ctor-3
- get
- set
- add-2
- remove-2
- clear
- addAll
- write/readObject
- fast-fail 机制
- subList
- iterator
- forEach
- sort
- removeIf
2. 构造方法分析
1. 无参的构造方法
里面只有一个操作就是把 elementData
设置为 DEFAULTCAPACITY_EMPTY_ELEMENTDATA
这个空数组。
1 | // 无参的构造函数,传入一个空数组 这时候会创建一个大小为10的数组,具体操作在 add 中 |
2. 传入数组大小的构造
这个就是 new 一个数组,如果数组大小为0就 赋值为 EMPTY_ELEMENTDATA
1 | // 按传入的参数创建新的底层数组 |
3. 传入 Collection 接口
在这个方法里面主要就是把这个 Collection 转成一个数组,然后把这个数组 copy 一下,如果这个接口的 size 为0 和上面那个方法一样传入 EMPTY_ELEMENTDATA
1 | public ArrayList(Collection<? extends E> c) { |
但是注意一点这里有一个 jdk 的 bug 也就是一个 Object 类型的数组不一定能够存放 Object类型的对象,有可能抛异常,主要是因为 Object 类型的数组可能指向的是他的子类的数组,存 Object 类型的东西会报错。 为了测试这个 bug 写了几行代码测试一下。这个测试是通不过的,就是存在上面的原因。
一个典型的例子就是 我们创建一个 string 类型的 list 然后调用 toArray 方法发现返回的是一个 string[] 这时候自然就不能随便存放元素了。
1 | class A{ |
3. 修改方法分析
1. Set 方法
这个方法也很简单 ,首先进行范围判断,然后就是直接更新下标即可。
1 | // 也没啥好说的就是,设置新值返回老值 |
2. Add(E e) 方法
这个方法首先调用了 ensureCapacityInternal()
这个方法里面就判断了当前的 elementData
是否等于 DEFAULTCAPACITY_EMPTY_ELEMENTDATA
如果是的话,就把数组的大小设置为 10 然后进行扩容操作,这里刚好解释了为什么采用无参构造的List 的大小是 10 ,这里扩容操作调用的方法是 ensureExplicitCapacity
里面就干了一件事如果用户指定的大小 大于当前长度就扩容,扩容的方法采用了 Arrays.copy
方法,这个方法实现原理是 new 出一个新的数组,然后调用 System.arraycopy
拷贝数组,最后返回新的数组。
1 | public boolean add(E e) { |
3. Add(int index, E e) 方法
这个方法比较简单和上面基本一样,然后只是最后放元素的时候的操作不一样,他是采用了 System.arraycopy 从自己向自己拷贝,目的就在于覆盖元素。 注意一个规律这里面只要涉及下标的操作的很多不是自己手写 for 循环而是采用类似的拷贝覆盖的方法。算是一个小技巧。
1 | public void add(int index, E element) { |
4. remove(int index)
同理这里面还是用了拷贝覆盖的技巧。 但是有一点注意的就是不用的节点需要手动的触发 gc ,这也是在 Efftive Java 中作者举的一个例子。
1 | public E remove(int index) { |
5. remove(E e)
这个方法操作很显然会判断 e 是不是 null 如果是 null 的话直接采用 ==
比较,否则的话就直接调用 equals
方法然后执行拷贝覆盖。
1 | public boolean remove(Object o) { |
6. clear()
这个方法就干了一件事,把数组中的引用全都设置为 null 以便 gc 。而不是仅仅把 size 设置为 0 。
1 | // gc 所有节点 |
7. addAll(Collection e)
这个没啥好说的就是,采用转数组然后 copy
1 | // 一个套路 只要涉及到 Collection接口的方法都是把这个接口转成一个数组然后对数组操作 |
4. 访问方法分析
1. get
直接访问数组下标。
1 | // 没啥好说的直接去找数组下标 |
2. subList
这个方法的实现比较有意思,他不是直接截取一个新的 List 返回,而是在这个类的内部还有一个 subList 的内部类,然后这个类就记录了 subList 的开始结束下标,然后返回的是这个 subList 对象。你可能会想返回的 subList 他不是 List 不会有问题吗,这里这个 subList 是继承的 AbstractList 所以还是正确的。
1 | public List<E> subList(int fromIndex, int toIndex) { |
5. 其他功能方法
1. write/readObject
前面在介绍数据域的时候我就有标注 elementData 是一个 transition 的变量也就是在自动序列化的时候会忽略这个字段。
然后我们又在源码中找到到了 write/readObject
方法,这两个方法是用来序列化 elementData
中的每一个元素,也就是手动的对这个字段进行序列化和反序列化。这不是多此一举吗?
既然要将ArrayList的字段序列化(即将elementData序列化),那为什么又要用transient修饰elementData呢?
回想ArrayList的自动扩容机制,elementData数组相当于容器,当容器不足时就会再扩充容量,但是容器的容量往往都是大于或者等于ArrayList所存元素的个数。
比如,现在实际有了8个元素,那么elementData数组的容量可能是8x1.5=12,如果直接序列化elementData数组,那么就会浪费4个元素的空间,特别是当元素个数非常多时,这种浪费是非常不合算的。
所以ArrayList的设计者将elementData设计为transient,然后在writeObject方法中手动将其序列化,并且只序列化了实际存储的那些元素,而不是整个数组。
1 | private void writeObject(java.io.ObjectOutputStream s) |
2. fast-fail
所谓的 fast-fail
就是在我们进行 iterator
遍历的时候不允许调用 Collection
接口的方法进行对容器修改,否则就会抛异常。这个实现的机制是在 iterator
中维护了两个变量,分别是 modCount
和 expectedModCount
由于 Collection
接口的方法在每次修改操作的时候都会对 modCount++
所以如果在 iterator
中检测到他们不相等的时候就抛异常。
1 | private class Itr implements Iterator<E> { |
3. forEach
这个是一个函数式编程的方法,看看他的参数 forEach(Consumer<? super E> action)
很有意思里面接受是一个函数式的接口,我们里面回调了 Consumer
的 accept
所以我们只需要传入一个函数接口就能对每一个元素处理。
1 |
|
写了一段测试代码,但是这个方法不常用,主要是 Collection 是可以自己生成 Stream 对象,然后调用上面的方法即可。这里提一下。
1 | public class ArrayListTest { |
4. sort
底层调用了 Arrays.sort 方法没什么好说的。
1 | public void sort(Comparator<? super E> c) { |
5. removeIf
这个和 forEach 差不多,就是回调写好了。
6. Vector
以上基本是把 ArrayList
的重要的方法和属性介绍完了,我们已经比较清楚他底层的实现和数据结构了。然后提到 ArrayList
自然也少不了一个比较古老的容器 Vector
这个容器真的和 ArrayList
太像了。因为你会发现他们连继承和实现的接口都是一样的。但是也会有一些不同的地方,下面分条介绍一下。
在
Vector
中基本所有的方法都是synchronized
的方法,所以说他是线程安全的ArrayList
构造方法不一样,在属性中没有两个比较特殊的常量,所以说他的构造方法直接初始化一个容量为 10 的数组。然后他有四个构造方法。
遍历的接口不一样。他还是有
iterator
的但是他以前的遍历的方法是Enumeration
接口,通过elements
获取Enumeration
然后使用hasMoreElements
和nextElement
获取元素。缺少一些函数式编程的方法。