ArrayList源码分析

我就是我 2023-01-13 10:47 297阅读 0赞

ArrayList源码分析

文章目录

  • ArrayList源码分析
    • 一、整体架构
        1. 结构图
        1. 使用时的注意事项
        1. 基本过程分析
    • 二、源码分析
        1. 成员属性
        1. 构造函数
        • 2.1 无参数初始化
        • 2.2 指定大小初始化
        • 2.3 指定初始数据初始化
        1. 成员方法
        • 3.1 add(E e)方法
        • 3.2 remove(Object o)方法
        • 3.3 get(int index)方法

一、整体架构

  • ArrayList是一种以数组实现的List,与数组相比,它具有动态扩展的能力,因此也可称之为动态数组
  • 继承体系

    image-20210405160012453

    • ArrayList实现了List,提供了基础的添加、删除、遍历等操作
    • ArrayList实现了RandomAccess,提供了随机访问的能力,也就是可以通过角标访问元素
    • ArrayList实现了Cloneable,可以被克隆
    • ArrayList实现了Serializable,可以被序列化

1. 结构图

image-20210405160125936

  • 图中展示的是长度为 10 的数组,index 表示数组的下标,从 0 开始计数,elementData 表示数组本身

2. 使用时的注意事项

  • ArrayList可以添加(多个)null值
  • size、isEmpty、get、set、add(E e) (添加到末尾) 方法时间复杂度是 O (1)

    • add(int index, E element) (添加到指定位置) 时间复杂度是 O (n),因为要将插入位置之后的元素后移
    • remove() 时间复杂度是 O (n),因为要将删除位置之后的元素前移
  • 是线程不安全的,效率较高,如果想要线程安全但损失一部分效率可以使用Vector替换

    • 只有当 ArrayList 作为共享变量时,才会有线程安全问题,当 ArrayList 是方法内的局部变量时,是没有线程安全问题的
    • ArrayList 有线程安全问题的本质是因为 ArrayList 在进行添加、扩容等各种操作时,都没有加锁,而且自身的 elementData、size、modConut 这些变量并非是可见的(volatile),所以如果多个线程对这些变量进行操作时,可能会有值被覆盖的情况
  • 增强 for 循环,或者使用迭代器迭代过程中,如果数组大小被改变,会快速失败,抛出异常

    • 比如下面这段代码,会抛出 ConcurrentModificationException 异常(并发修改异常)

      1. ArrayList<String> list = new ArrayList<>();
      2. list.add("贾玲");
      3. list.add("贾冰");
      4. list.add("贾岛");
      5. //获取集合迭代器
      6. Iterator<String> iterator = list.iterator();
      7. //遍历集合元素
      8. while (iterator.hasNext()) {
      9. String name = iterator.next();
      10. if(name.equals("贾冰")) {
      11. //改变集合元素个数
      12. list.add("假货");
      13. }
      14. }
    • 原因分析

      • 调用 list.iterator(); 方法创建迭代器时,会执行 expectedModCount = modCount;, modcount 表示ArrayList集合被修改的次数,每执行一次add方法,就会将modCount+1,此时的 modCount 和 expectedModCount 值为3(添加了三个元素)
      • 当执行到 list.add("假货"); 时,modCount值为4,但此时expectedModCount值为3
      • 调用 iterator.next(); 方法时,会调用next方法中的 checkForComodification(); ,这个方法中有一行代码

        1. if (modCount != expectedModCount)
        2. throw new ConcurrentModificationException();
      • 由于 4 != 3,故抛出异常

3. 基本过程分析

  • ArrayList中维护了一个Object类型的数组elementData
  • 创建ArrayList对象时,如果使用的是空参构造器,则初始elementData容量为0

    • 第一次添加元素时,将elementData扩容为10,再次扩容时,将elementData扩容为1.5倍
  • 创建ArrayList对象时,如果使用的是指定大小的构造器

    • 初始elementData容量为指定大小,如果需要扩容,将elementData扩容为1.5倍

二、源码分析

1. 成员属性

  1. //默认容量为10
  2. private static final int DEFAULT_CAPACITY = 10;
  3. //空数组,如果构造器指定的容量为0时使用
  4. private static final Object[] EMPTY_ELEMENTDATA = { };
  5. //空数组,构造器为空参时使用,添加第一个元素的时候会重新初始为默认容量大小10
  6. private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = { };
  7. //存储元素的数组
  8. transient Object[] elementData;
  9. //数组中元素的个数,注意不是elementData数组的长度
  10. //没有使用volatile修饰,非线程安全的
  11. private int size;
  12. //数组的最大容量,Integer类型的最大整数-8
  13. private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
  14. //AbstractList类中的属性
  15. //统计当前数组被修改的次数,数组结构有变动,就会+1
  16. protected transient int modCount = 0;

2. 构造函数

2.1 无参数初始化

  1. public ArrayList() {
  2. //如果没有传入初始容量,则使用空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA
  3. //添加第一个元素的时候会扩容到默认大小10
  4. this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
  5. }

2.2 指定大小初始化

初始容量如果大于0就初始化elementData为指定大小,如果等于0就使用EMPTY_ELEMENTDATA空数组,如果小于0抛出异常

  1. public ArrayList(int initialCapacity) {
  2. if (initialCapacity > 0) {
  3. this.elementData = new Object[initialCapacity];
  4. } else if (initialCapacity == 0) {
  5. this.elementData = EMPTY_ELEMENTDATA;
  6. } else {
  7. throw new IllegalArgumentException("Illegal Capacity: "+
  8. initialCapacity);
  9. }
  10. }

2.3 指定初始数据初始化

会使用拷贝把参数集合的元素拷贝到elementData数组中,如果参数集合元素个数为0,则初始化elementData数组为EMPTY_ELEMENTDATA空数组

  1. public ArrayList(Collection<? extends E> c) {
  2. //将参数集合转换成数组赋值给elementData,elementData长度与参数c一致
  3. elementData = c.toArray();
  4. //如果给定的集合c有数据
  5. if ((size = elementData.length) != 0) {
  6. //如果集合元素类型不是Object类型,将其转换成Object类型
  7. if (elementData.getClass() != Object[].class) {
  8. //调用Arrays.copyOf方法
  9. elementData = Arrays.copyOf(elementData, size, Object[].class);
  10. }
  11. }
  12. //如果给定的集合c没有数据
  13. else {
  14. this.elementData = EMPTY_ELEMENTDATA;
  15. }
  16. }

Arrays.copyOf方法源码如下:

  1. public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
  2. @SuppressWarnings("unchecked")
  3. //要将复制后的元素放在copy数组中,即copy是目标数组
  4. T[] copy = ((Object)newType == (Object)Object[].class)
  5. ? (T[]) new Object[newLength]
  6. : (T[]) Array.newInstance(newType.getComponentType(), newLength);
  7. //调用arraycopy方法
  8. System.arraycopy(original, 0, copy, 0,
  9. Math.min(original.length, newLength));
  10. return copy;
  11. }

System.arraycopy 方法源码如下:

此方法是native本地方法

  1. /** * @param src 被拷贝的数组 * @param srcPos 从源数组哪里开始拷贝 * @param dest 目标数组 * @param destPos 从目标数组哪个索引位置开始放被拷贝的元素 * @param length 从源数组拷贝的长度 * 此方法是没有返回值的 */
  2. public static native void arraycopy(Object src, int srcPos,
  3. Object dest, int destPos,
  4. int length);

问题:为什么 c.toArray() 返回的有可能不是Object[]类型呢?

  • Collection集合的toArray()源码 Object[] toArray();
  • 看下述代码

    1. public class Test {
    2. public static void main(String[] args) {
    3. Collection<String> collection = new MyList();
    4. System.out.println(collection.toArray().getClass());
    5. //class [Ljava.lang.String;
    6. //不是Object[]类型
    7. }
    8. }
    9. class MyList extends ArrayList<String> {
    10. @Override
    11. public String[] toArray() {
    12. return new String[]{ "我爱", "学习"};
    13. }
    14. }

3. 成员方法

3.1 add(E e)方法

  • 判断是否需要扩容,如果需要则执行扩容操作

    • 扩容会先新建一个符合预期容量的新数组,然后把旧数组的数据拷贝过去
  • 添加元素到末尾,平均时间复杂度为 O(1)

add(E e) 方法源码如下:

  1. public boolean add(E e) {
  2. //调用ensureCapacityInternal判断数组能否放下添加的那个元素,不能的话则扩容
  3. ensureCapacityInternal(size + 1);
  4. //添加元素到末尾
  5. elementData[size++] = e;
  6. return true;
  7. }

ensureCapacityInternal 方法源码如下:

  1. private void ensureCapacityInternal(int minCapacity) {
  2. //调用calculateCapacity得到数组的目标容量
  3. //判断如果数组是空的(第一次扩容),则得到默认容量10,否则使用size + 1的容量
  4. //调用ensureExplicitCapacity确定数组的最终容量,如果目标容量 > 当前容量,则扩容,否则不扩容
  5. ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
  6. }

calculateCapacity 方法源码如下:

  1. private static int calculateCapacity(Object[] elementData, int minCapacity) {
  2. //如果是空数组(第一次扩容),就初始化为默认大小10
  3. if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
  4. return Math.max(DEFAULT_CAPACITY, minCapacity);
  5. }
  6. return minCapacity;
  7. }

ensureExplicitCapacity 方法源码如下:

  1. private void ensureExplicitCapacity(int minCapacity) {
  2. //对数组执行了add方法,计数器+1
  3. modCount++;
  4. //elementData.length表示此时还没有扩容的数组的容量
  5. //minCapacity表示将要被扩容的容量(期望的容量)
  6. //也就是说如果此时数组的容量还没有达到期望的容量,就要执行grow方法进行真正的扩容操作
  7. if (minCapacity - elementData.length > 0)
  8. grow(minCapacity);
  9. }

grow 方法源码如下:

  1. private void grow(int minCapacity) {
  2. //参数minCapacity表示期望容量
  3. //记录未扩容时的数组的容量,方便计算原数组的1.5倍
  4. int oldCapacity = elementData.length;
  5. //新容量为旧容量+旧容量的一半,也就是旧容量的1.5倍
  6. int newCapacity = oldCapacity + (oldCapacity >> 1);
  7. //如果扩容后的值<期望值,则将期望值作为扩容后的值
  8. //比如第一次扩容时,0的1.5倍还是0,当然要扩容为期望值10
  9. if (newCapacity - minCapacity < 0)
  10. newCapacity = minCapacity;
  11. //如果扩容后的值比数组的最大容量还大,调用hugeCapacity进行判断:
  12. //如果目标容量大于数组最大容量,则返回Integer的最大值
  13. //如果目标容量没有达到数组最大容量,则返回数组最大容量
  14. if (newCapacity - MAX_ARRAY_SIZE > 0)
  15. newCapacity = hugeCapacity(minCapacity);
  16. //调用Arrays.copyOf将原数组的元素复制到扩容后的新数组中
  17. //Arrays.copyOf方法之前已讲解过,不再赘述
  18. elementData = Arrays.copyOf(elementData, newCapacity);
  19. }

hugeCapacity 方法源码如下:

  1. private static int hugeCapacity(int minCapacity) {
  2. if (minCapacity < 0)
  3. throw new OutOfMemoryError();
  4. return (minCapacity > MAX_ARRAY_SIZE) ?
  5. Integer.MAX_VALUE :
  6. MAX_ARRAY_SIZE;
  7. }

需要注意的地方:

  • 什么时候需要扩容?

    • 期望容量(初次为10,其余为size + 1) > 目前数组容量
  • 扩容的规则并不是翻倍,是原来容量大小 + 容量大小的一半,即原来容量的 1.5 倍
  • ArrayList 中的数组的最大值是 Integer.MAX_VALUE,超过这个值,JVM 就不会给数组分配内存空间了
  • 新增时,并没有对值进行严格的校验,所以 ArrayList 是允许 null 值的
  • 没有任何锁控制,所以这些操作是线程不安全的
  • 对于 add(int index, E element) 方法,也就是将元素插入到指定位置,需要判断index是否越界,判断是否需要扩容,还需要将插入位置之后的元素全部往后挪一位,代码基本与 add(E e) 一致,不再赘述
  • 对于 addAll(Collection c) 方法,也就是将参数c的元素全部添加到原数组的尾部,需要求出c的长度,判断是否需要扩容,代码基本与 add(E e) 一致,不再赘述

3.2 remove(Object o)方法

ArrayList删除元素有很多种方式,比如根据数组索引删除、根据值删除或批量删除等等,原理和思路都差不多,这里选取根据值删除方式来进行源码的讲解

remove(Object o) 方法源码如下:

删除集合中第一个取值为o的元素,删除成功需要将后面的元素全部向前挪一位,删除失败返回false

  1. public boolean remove(Object o) {
  2. //如果要删除的值是null,找到第一个值是null的元素将其删除
  3. if (o == null) {
  4. for (int index = 0; index < size; index++)
  5. if (elementData[index] == null) {
  6. //调用fastRemove方法删除
  7. fastRemove(index);
  8. return true;
  9. }
  10. }
  11. //如果要删除的值不为null,找到第一个和要删除的值相等的元素将其删除
  12. else {
  13. for (int index = 0; index < size; index++)
  14. //根据equals方法判断是否相等,调用fastRemove方法删除
  15. //因为调用equals,故需要将null值分开判断,否则出现空指针异常
  16. if (o.equals(elementData[index])) {
  17. fastRemove(index);
  18. return true;
  19. }
  20. }
  21. //删除失败返回false
  22. return false;
  23. }

fastRemove 方法源码如下:

  1. private void fastRemove(int index) {
  2. //删除元素,计数器加1
  3. modCount++;
  4. //numMoved表示删除位置之后有多少个元素需要向前挪动
  5. //假设长度为5,删除位置是1,那么需要将后面的3个元素向前挪动
  6. int numMoved = size - index - 1;
  7. if (numMoved > 0)
  8. //通过复制的方式将元素向前挪动
  9. System.arraycopy(elementData, index+1, elementData, index, numMoved);
  10. //数组最后一个位置赋值null,帮助GC
  11. elementData[--size] = null;
  12. }

需要注意的地方:

  • 新增的时候没有对 null 进行校验,所以删除的时候也是允许删除 null 值的
  • 找到值在数组中的索引位置是通过 equals 来判断的,如果数组元素不是基本类型,需要关注 equals 的具体实现
  • ArrayList删除元素的时候并没有缩容,只是将元素前移后空出的最后一个位置赋予null值,方便回收

3.3 get(int index)方法

获取指定索引位置的元素,时间复杂度为O(1)

get(int index) 方法源码如下:

  1. public E get(int index) {
  2. //越界检查
  3. rangeCheck(index);
  4. //调用elementData方法
  5. return elementData(index);
  6. }
  7. //越界检查方法如下
  8. private void rangeCheck(int index) {
  9. if (index >= size)
  10. throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
  11. }
  12. //elementData方法如下
  13. E elementData(int index) {
  14. return (E) elementData[index];
  15. }

发表评论

表情:
评论列表 (有 0 条评论,297人围观)

还没有评论,来说两句吧...

相关阅读

    相关 ArrayList分析

    ArrayList是一种最常用的集合类,底层数据结构是数组,提供动态扩展数组长度的特性,允许元素的值为null。ArrayList是一种非线程安全的集合类,若要在多线程的环境,

    相关 ArrayList分析

    构造函数(有参和无参): 无参:有个被transient关键字修饰的elementData的Object类型长度为0的数组。 有参:参数的含义就是这个集合的含量,在Arra