ArrayList源码分析(超详细)

Posted 知道什么是码怪吗?

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ArrayList源码分析(超详细)相关的知识,希望对你有一定的参考价值。

目录

ArrayList简介

成员变量

构造函数

无参构造 

有参构造

指定初始数组大小

传入集合 

增加元素

add方法 

addAll方法 

总结 

删除元素

remove方法 

获取元素

常见问题

问:ArrayList 如何进行扩容?

问:ArrayList 与LinkList 的区别?

问:ArrayList的遍历和LinkedList遍历性能比较如何?

问:ArrayList(int initialCapacity)会不会初始化数组大小?

问:ArrayList 是线程安全的吗?

问:既然线程不安全,为啥使用频率这么高?

问:ArrayList 频繁扩容导致性能下降,如何处理?


ArrayList简介

ArrayList 继承自 AbstractList,实现了 List 接口。底层基于数组实现容量大小动态变化。允许 null 的存在。同时还实现了 RandomAccess、Cloneable、Serializable 接口,所以支持快速访问、复制、序列化的。

ArrayList 查询效率高,增加和删除效率低,线程不安全。具体原因下文分析。

以下出现的源代码JDK版本为:OpenJDK version 1.8.0_271

成员变量

作用:记录ArrayList的大小(也就是其包含的元素数)。

作用:真正存放元素的数组,size 的值为这个数组中实际元素的个数,而 elementData.length 为这个数组最大的容量。 

作用:设置默认初始容量为10。

作用:标识最大能创建的数组长度。

作用:用于空实例的共享空数组实例。

作用:其注释的翻译为:“用于默认大小的共享空数组实例。将其和EMPTY_ELEMENTDATA区别开来是为了知道第一次加入数据时扩容的大小”,简单来说就是用来区分是从哪个构造函数来初始化ArrayList的。接下来构造函数中将会具体分析。

 作用:记录对 List 操作的次数,主要使用是在 Iterator,是防止在迭代的过程中集合被修改。该变量定义在AbstractList中,被ArrayList继承。思考一下,如果我们在遍历集合时,对集合进行了修改,比如说删除了某一个元素,那么很容易导致结果出错或者下标越界。

 

综上,ArrayList中用到的变量如下所示:

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;// 标记数组最大容量
private static final int DEFAULT_CAPACITY = 10;// 初始容量
private static final Object[] EMPTY_ELEMENTDATA = ;// 用于空实例的共享空数组实例
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = ;// 用于默认大小的空实例的共享空数组实例
transient Object[] elementData;// 存放元素的数组
private int size;// 这个数组中实际元素的个数
protected transient int modCount = 0;// 记录对 List 操作的次数

构造函数

通过查看源码得知,ArrayList共有三个构造函数。

接下来具体分析三个构造方法

public ArrayList()// 无参构造

public ArrayList(int initialCapacity)// 指定初始容量

public ArrayList(Collection<? extends E> c)// 传入Collection集合

无参构造 

ArrayList<Integer> list = new ArrayList<Integer>();

无参构造首先将 elementData 变量赋值为 DEFAULTCAPACITY_EMPTY_ELEMENTDATA,也就是先将 elementData 赋值为一个空数组。值得注意的是,这里并没有对数组容量进行分配。具体给数组分配容量要等到第一次增加元素时才分配。

有参构造

指定初始数组大小

ArrayList<Integer> list = new ArrayList<Integer>(20);// 设置初始容量为20

首先对传入的初始大小进行判断,如果大于0,将 elementData 赋值为一个容量指定容量的数组。如果等于0,赋值为 EMPTY_ELEMENTDATA,其他情况抛出异常。仔细观察,在之前的无参构造中,elementData 被赋值为 DEFAULTCAPACITY_EMPTY_ELEMENTDATA,而这里传入 0时,elementData 被赋值为 EMPTY_ELEMENTDATA,这两个变量的用途就是用于区分是通过哪个构造函数来进行初始化的。

传入集合 

ArrayList<Integer> list01 = new ArrayList<Integer>(20);
ArrayList<Integer> list02 = new ArrayList<Integer>(list01);

 首先对传入的集合进行判断,然后将集合的大小赋值给 size,如果 size != 0 ,先判断集合的类型是否是ArrayList.class,如果是,直接赋值,如果不是,转化为Object[].class再赋值。如果 size == 0,将 elementData 赋值为 EMPTY_ELEMENTDATA。

增加元素

list.add(5);// 增加一个元素

list.addAll(list1);// 将集合作为参数

add方法 

进入add方法首先进行容量大小判断,如果能够装下这个元素,那么就不用扩容。 ensureCapacityInternal 方法就是进行容量判断的方法。如果不需要扩容,我们可以只看下面两句代码,其意思就是将 size 对应的下标位置设置为我们要添加的值,然后返回 true。不扩容的话一切看来都很简单对吧?接下来让我们进入 ensureCapacityInternal 方法一探究竟。

不同版本的JDK代码实现可能存在差异,接下来以OpenJDK version 1.8.0_271为例

 进入 ensureCapacityInternal 方法中的 calculateCapacity 方法。其首先判断当前ArrayList的 elementData 是否等于 DEFAULTCAPACITY_EMPTY_ELEMENTDATA,还记得DEFAULTCAPACITY_EMPTY_ELEMENTDATA 在哪里出现过吗?没错,就是无参构造。这里进行比较,如果相等,说明这个 ArrayList 是通过无参构造方法创建的,返回默认容量大小:10。否则返回 minCapacity。这个方法的作用只是给通过无参构造创建的 ArrayList 在第一次 add 时进行初始容量的设置。因为只要不相等,返回的还是minCapacity。和传入时相同。

从 calculateCapacity 方法返回后,回到 ensureCapacityInternal 方法,接下来进入 ensureExplicitCapacity 方法。这里的 modCount 首先加一,表示对 List 的操作次数加一。然后进行判断,minCapacity 是否大于 elementData 的长度,如果大于就表示放不下当前元素,需要扩容。

接下来进入 grow 方法,此方法将数组进行扩容。

private void grow(int minCapacity) 
    int oldCapacity = elementData.length;// 获取 elementData的长度
    int newCapacity = oldCapacity + (oldCapacity >> 1);// 新的数组长度为旧的长度的1.5倍,这里右移一位相当于除以2
    if (newCapacity - minCapacity < 0)// 如果新的长度小于还是小于minCapacity
        newCapacity = minCapacity;// 直接赋值为minCapacity
    if (newCapacity - MAX_ARRAY_SIZE > 0)// 如果新的长度大于了最大的能够创建的长度
        newCapacity = hugeCapacity(minCapacity);// 首先思考一个问题,数组能不能无限扩容?
    elementData = Arrays.copyOf(elementData, newCapacity);// 将旧的数组复制到一个新的数组并返回新的数组

具体解释如上,这里应当思考数组扩容的最大限度,首先数组肯定不能无限制的扩容,所以这里进行了判断,if (newCapacity - MAX_ARRAY_SIZE > 0),如果新数组的长度大于了最大能扩容的大小,判断 minCapacity 与 MAX_ARRAY_SIZE 的大小关系,从而返回不同的值。其扩容的过程,就是将当前数组复制给一个更大的数组,然后修改 elementData 的引用为新的数组。这也是增加效率低的原因,要是数组长度过长,其扩容过程复制数组效率将会十分低。(增加元素效率低的原因之一)

这一系列操作执行完成之后,数组才算是真正的完成了扩容,接下来只需要加入数据返回true即可。 

add还有一个方法,将数据插入指定位置。其思路与上述类似,首先检查越界以及判断数组是否需要扩容,然后把指定下标以及其后的元素依次后移,最后把待加入的数据放入即可。(增加元素效率低的原因之二)

addAll方法 

addAll 方法和 add 方法类似,首先检查能否把数组元素全部放入,不能则扩容。能则拷贝到 elementData 末尾。

总结 

总结:①如果 ArrayList 是通过无参构造创建的,那么初始化时并不会初始化数组的大小,只是把数组标记为通过无参构造初始化的。然后在第一次加入数据时,初始化数组大小为10。

②如果超过了最大容量则需要扩容,扩容后的数组大小为原数组的1.5倍。扩容需要判断是否超过允许的最大的长度,并做相关的处理。

③插入对应位置是将数组元素后移腾出空位再放入数据,addAll 是将集合元素拷贝到数组末尾。

删除元素

public E remove(int index)// 删除对应下标的元素

public boolean remove(Object o)// 删除一个指定元素

public boolean removeAll(Collection<?> c)// 删除这个集合内的元素

remove方法 

remove 方法的逻辑很简单,rangeCheck 方法首先对下标进行判断,如果大于了 elementData 的大小,抛出数组越界异常。

若数组没有越界,首先获取要移除的元素,当作返回值。然后将该下标之后的元素依次前移,形成了删除的效果。其实只是赋值发生了变化。这是 ArrayList 删除效率低的原因。这里的 numMoved 是用作计算数组元素前移的个数。

另一个 remove 方法可移除指定的元素,过程为:首先判断参数是否为空,然后进行遍历,找到了对应的元素则进行移除。

可以很明显的发现,fastRemove 方法和之前提到的 remove(int index) 方法几乎一摸一样。

总结

①删除指定下标位置的元素首先会进行越界判断,然后将其后的元素依次往前移动,达到了删除的效果,其实只是覆盖了原来的数据。

②删除特定的元素是通过遍历获取删除元素的位置的,依旧是其后的元素依次往前移动,来达到删除的效果。

获取元素

public E get(int index)

由于 ArrayList 是基于数组实现的,所以获取元素十分方便。rangeCheck 方法用于判断是否越界,越界抛出 IndexOutOfBoundsException 异常。

常见问题

问:ArrayList 如何进行扩容?

答: ArrayList 在添加元素时,首先检查容量大小判断是否需要扩容,如果需要扩容,会重新定义一个容量为原来的1.5倍的数组,然后将原来的数组复制到新数组,再把指向原数组的地址指向新数组。

问:ArrayList 与LinkList 的区别?

答:①ArrayList 和 LinkList 都是线程不安全的。

②ArrayList 其底层用数组实现所以查找元素速度快,但新增和删除由于要在数组中移动元素,所以效率低。而 LinkedList 的查找元素速度慢,但新增和删除速度快。

③ArrayList需要一份连续的内存空间,LinkedList不需要连续的内存空间。

问:ArrayList的遍历和LinkedList遍历性能比较如何?

答:ArrayList 的遍历效率要比 LinkedList 的遍历效率高得多,因为 ArrayList 的内存是连续的,LinkedList  的内存是分散的。而CPU的内部缓存结构会缓存连续的内存片段,降低读取内存的性能开销,所以 ArrayList 遍历效率会比 LinkedList 效率高。

问:ArrayList(int initialCapacity)会不会初始化数组大小?

答:会初始化 elementData 数组的大小,但是对于这个 ArrayList 而言,其 size 并没有改变,依旧为0(详情请看上文有参构造源码分析)。我们看看下面这段代码。

ArrayList<Integer> list = new ArrayList<>(10);
System.out.println("list的大小:" + list.size());
list.add(5, 10);// 添加数据到下标为5的位置

其运行结果如下,诶?我们不是已经设置了容量为 10 了吗?为什么获取 list.size() 为 0 呢?而且也添加不了数据。这是因为通过这个构造方法创建 ArrayList,只是初始化了数组的大小,并没有对 size 进行修改。 而我们要在下标为 5 的位置加入数据,此时 size = 0,当然会爆越界异常。 

问:ArrayList 是线程安全的吗?

答:不是,线程安全的数组容器是Vector,Vector是在所有方法上都加上了 synchronized 进行修饰。

问:既然线程不安全,为啥使用频率这么高?

答: ArrayList 一般用于查询数据,实际情况下并不会对 ArrayList 进行频繁的增删。频繁增删用 LinkedList,线程安全用 Vector。

问:ArrayList 频繁扩容导致性能下降,如何处理?

答:扩容的原因就是容量不够用了,那我们可以直接通过 ArrayList(int initialCapacity)来指定一个较大的容量,减少扩容次数。当然,数据量大了扩容是不可避免的,我们只能减少扩容次数。

嗯,回去等通知吧………

以上是关于ArrayList源码分析(超详细)的主要内容,如果未能解决你的问题,请参考以下文章

ArrayList源码分析(超详细)

ArrayList源码分析(超详细)

源码那些事超详细的ArrayList底层源码+经典面试题

ArrayList 源码详细分析

Java ArrayList底层实现原理源码详细分析Jdk8

LinkedList源码分析(超详细)