Java集合系列 ArrayList底层源码 万字细致解读(超通俗易懂)

一、概述

ArrayList是基于数组实现,底层的数据结构是顺序表(物理内存上连续),并且支持动态扩容。因而相较于数组而言,因为其支持自动扩容,成为我们开发中最常用的集合之一。

类图

在这里插入图片描述
从以上类图中我们能得知:ArrayList实现了四个接口和继承了一个抽象类:

  • List接口,主要提供数组的添加、删除、修改、遍历等操作
  • Cloneable接口,表示ArrayList支持克隆
  • RandomAccess接口,表示ArrayList支持快速地随机访问
  • Serializable接口,表示ArrayList支持序列化功能
  • AbstractList抽象类,主要提供迭代遍历等操作
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
	// ......
}

二、源码解读

注:不同jdk版本的源码会有一定差异,不过大致上是相同的,我使用的是 openjdk version “1.8.0_342”。

1、成员变量

  	//Default initial capacity.数组默认大小
    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;
	// 数组最大值
	private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

ArrayList 的源码中,主要有上述的几个成员变量:

  • elementData:动态数组,也就是我们存储数据的核心数组(ArrayList底层就是使用的数组)
  • DEFAULT_CAPACITY:数组默认长度
  • size:记录有效数据长度,size()方法直接返回该值
  • MAX_ARRAY_SIZE:数组最大长度,如果扩容超过该值,则设置长度为 Integer.MAX_VALUE
  • EMPTY_ELEMENTDATA DEFAULTCAPACITY_EMPTY_ELEMENTDATA :两个空数组

2、构造方法

ArrayList 中提供了三种构造方法:

  • ArrayList():指向全局空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA
  • ArrayList(int initialCapacity):如果初始容量大于0,则创建指定长度的数组。如果等于0,则指向全局空数组EMPTY_ELEMENTDATA
  • ArrayList(Collection<? extends E> c):将集合c转为数组,如果数组为空,则指向EMPTY_ELEMENTDATA

第一种构造函数 ArrayList()

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

从源码中的注释中所知:此构造函数是构造一个初始容量为10的空列表。细心一点可以发现:从上文可知DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一个空的数组对象。

不过这里并不是一个错误,而是确实了一些补充描述,当为指定初始化大小的时候,ArrayList首先是初始化一个空的数组,也就是此DEFAULTCAPACITY_EMPTY_ELEMENTDATA。但是在首次添加元素的时候,ArrayList会初始化一个容量为10的数组(后文会提到)。

这样做的目的是为了节省内存空间,如果在一些场景下某数组并未使用的话,那么不会造成不必要的空间浪费。

第二种构造函数 ArrayList(int initialCapacity)

根据指定大小初始化ArrayList中数组的大小,根据传入的initialCapacity的值去初始化容量创建elementData数组。(注意:在创建ArrayList时,我们应该尽可能使用此构造函数创建,这样能够避免容量浪费)

如果初始容量大于0,则创建指定长度的数组。如果等于0,则指向全局空数组EMPTY_ELEMENTDATA

源码如下:

public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        // 初始化容量大于0,创建Object数组
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        // 初始化容量为 0 时,使用 EMPTY_ELEMENTDATA 对象
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        // 容量参数异常
        throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
    }
}

第三种构造函数 ArrayList(Collection<? extends E> c)

此构造方式是传入集合c作为ArrayList中的elementData

    public ArrayList(Collection<? extends E> c) {
    	// 把集合c转换成数组数组对象,再赋值给此中转数组a
        Object[] a = c.toArray();
        // 将a的长度赋值给size,并判断是否不等于为0
        if ((size = a.length) != 0) {
        	// 这里没太明白为什么需要一次校验 懂的大佬麻烦评论区解读一下
            if (c.getClass() == ArrayList.class) {
                elementData = a;
            } else {
                elementData = Arrays.copyOf(a, size, Object[].class);
            }
        } else {
            // replace with empty array.
            elementData = EMPTY_ELEMENTDATA;
        }
    }

3、添加元素类方法

添加元素类方法核心主要有以下四个:

  • public boolean add(E e) :在数组后面顺序新增一个元素
  • public void add(int index, E element) :在指定下标位置添加元素
  • public boolean addAll(Collection<? extends E> c):添加一个集合c的所有元素
  • public boolean addAll(int index, Collection<? extends E> c):在指定下标位置添加一个集合c的所有元素

第一种 public boolean add(E e)

在数组后面顺序新增一个元素

public boolean add(E e) {
    // 确保内部容量
    ensureCapacityInternal(size + 1);
    // 这个操作相当于 element[size] = e, size++
    // 此处是给elementData数组
    elementData[size++] = e;
    // 返回添加成功
    return true;
}

1、在这里我们重点关注ensureCapacityInternal(size + 1)方法,size变量是当前队列有效数据长度。此方法是为了确保有足够的内部容量来存储新的元素,如果当前容量不足这个(size + 1)的大小,那么会触发扩容机制。

继续向下解读,看看此ensureCapacityInternal方法:

    private void ensureCapacityInternal(int minCapacity) {
	    // 确保数组容量,如不足则触发扩容机制
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

2、还是嵌套调用方法,首先是调用calculateCapacity(elementData, minCapacity)方法,再将此方法的返回值当做ensureExplicitCapacity方法的参数传入。elementData即是用于存储数据的数组(上文也有说到,下文不再赘述),minCapacity意为:最小容量(即是size + 1)。

继续向下解读:

	// calculateCapacity 意为:计算容量
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
    	// 如果elementData数组为空(DEFAULTCAPACITY_EMPTY_ELEMENTDATA 是一个空数组且上文有提到)
    	// 则返回DEFAULT_CAPACITY 与 minCapacity其中最大者
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        	// DEFAULT_CAPACITY (默认容量)定义:private static final int DEFAULT_CAPACITY = 10;
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        // 如果不是则直接返回需要的最小容量
        return minCapacity;
    }

3、回到ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));方法。

在上文中所学习到calculateCapacity(elementData, minCapacity)返回的两种情况:1是 DEFAULT_CAPACITYminCapacity其中最大者 2是 返回需要的最小容量。

继续向下解读:

    private void ensureExplicitCapacity(int minCapacity) {
    	// 这是一个自增操作,增加了列表的修改计数。这个计数是用来跟踪列表被修改了多少次,这对于一些并发控制是非常有用的。(可以暂时不用关注)
        modCount++;

        // 最小容量  减去  elementData长度  是否>0
        // 如果大于0 则触发扩容机制,如果不大于0 则直接返回
        // 个人觉得写为:minCapacity > elementData.length 更好理解
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

4、最核心的扩容方法

继续向下解读:

	/**
	  * 扩容
	  * 旧容量经过运算扩展为1.5后与最小容量minCapacity进行比较
	  * 如果大于则采用旧容量扩展1.5倍后的大小,否则采用最小容量minCapacity
	  */
    private void grow(int minCapacity) {
        // 旧容量  ==  elementData数组长度
        int oldCapacity = elementData.length;
        // 新容量  ==  旧容量  +  (旧容量向右移1位)
        // 粗糙地说 大约是原容量的1.5倍数
        int newCapacity = oldCapacity + (oldCapacity >> 1);

		// 如果计算出的新容量 还是小于 指定的最小容量 则将此最小容量赋给新容量
		// 个人觉得写为:写为 newCapacity < minCapacity 更好理解
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;

        // 如果新容量超过了数组的最大限制 则调用hugeCapacity(minCapacity)继续扩容
        // private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
            
        // 根据新的容量 newCapacity 来创建一个新的数组,并将原数组 elementData 的元素复制到新数组中。
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

5、hugeCapacity(minCapacity)

继续向下解读:

    private static int hugeCapacity(int minCapacity) {
    	// 判断入参minCapacity是否小于0
        if (minCapacity < 0) 
            throw new OutOfMemoryError();
        // 如果最小容量 > MAX_ARRAY_SIZE 则返回 Integer.MAX_VALUE
		// 如果最小容量 < MAX_ARRAY_SIZE 则返回 MAX_ARRAY_SIZE
        // private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
        // public  static final int MAX_VALUE = 0x7fffffff;
        // private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

一句话概括首先是通过calculateCapacity(elementData, minCapacity)方法计算出本次新增元素所需要的最小容量;再者再判断if (minCapacity - elementData.length > 0)最小容量是否大于数组长度,如果大于则触发扩容机制;最后将原有数组的元素拷贝到新数组上,然后往后追加新的元素。

第二种 public void add(int index, E element)

在指定下标位置添加元素

    public void add(int index, E element) {
    	// rangeCheckForAdd 意为:范围添加检查
    	// 是一个辅助方法,用于检查插入位置的索引是否合法,即是否在有效范围内。如果索引不合法,会抛出一个 IndexOutOfBoundsException 异常。
        rangeCheckForAdd(index);
		// 上文已经介绍过 确保数组容量,如不足则触发扩容机制
        ensureCapacityInternal(size + 1);
        // 使用 System.arraycopy 方法进行数组元素的移动。该方法会将从 index 开始的元素向后移动一位,为新元素腾出插入位置。
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
		// 将 element 插入到指定的 index 位置,即 elementData[index] = element。
        elementData[index] = element;
        // 增加 size 的计数,表示列表的大小增加了一个元素
        size++;
    }

一句话概括首先校验插入位置参数的合法性;其次确保数组容量,如不足则触发扩容机制;再者使用System.arraycopy 方法进行数组元素的移动;最后插入元素并进行size++操作。

第三种 public boolean addAll(Collection<? extends E> c)

添加一个集合c的所有元素

    public boolean addAll(Collection<? extends E> c) {
    	// 将集合 c 转化为一个数组 并赋值给临时中转数组 a
        Object[] a = c.toArray();
        // 获取中转数组 a 的长度
        int numNew = a.length;
        // 上文已经介绍过 确保数组容量,如不足则触发扩容机制
        // 注意一下的是 传入的最小容量是 size + numNew
        ensureCapacityInternal(size + numNew);
        // 将数组 a 中的元素拷贝到 elementData 数组中。拷贝的起始位置是 size,即将元素添加到列表的尾部
        System.arraycopy(a, 0, elementData, size, numNew);
        // 增加 size 的计数,表示列表的大小增加了 numNew 个元素
        size += numNew;
        // 返回一个布尔值,表示是否有新增的元素被添加到了列表中。
        // 如果 numNew 不为 0,即集合 c 非空且有元素被添加到列表中,则返回 true,否则返回 false
        return numNew != 0;
    }

这里不过多赘述,同上述的添加操作类似。

第四种 public boolean addAll(int index, Collection<? extends E> c)

在指定下标位置添加一个集合c的所有元素

    public boolean addAll(int index, Collection<? extends E> c) {
    	// 同上 不过多赘述
        rangeCheckForAdd(index);
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);
        
        // 计算要移动的元素数量 numMoved,即从插入位置 index 开始到列表末尾的元素数量
        int numMoved = size - index;
        // 如果 numMoved 大于 0,表示需要将一部分元素向后移动,以为新元素腾出插入位置
        if (numMoved > 0)
            System.arraycopy(elementData, index, elementData, index + numNew, numMoved);
		// 将数组 a 中的元素拷贝到 elementData 数组中的指定位置 index。
        System.arraycopy(a, 0, elementData, index, numNew);
        // 增加 size 的计数,表示列表的大小增加了 numNew 个元素
        size += numNew;
        // 表示是否有新增的元素被添加到了列表中。如果 numNew 不为 0,即集合 c 非空且有元素被添加到列表中,则返回 true,否则返回 false
        return numNew != 0;
    }

3、删除元素类方法

删除元素类方法核心主要有以下四个:

  • public E remove(int index) :移除指定下标位置的元素,并返回该元素
  • public boolean remove(Object o) :移除指定元素,并返回是否成功
  • public boolean removeAll(Collection<?> c):批量移除集合与集合c中所共有的元素
  • protected void removeRange(int fromIndex, int toIndex):批量移除指定的多个元素

第一种 public E remove(int index)

移除指定下标位置的元素,并返回该元素

    public E remove(int index) {
	    // 校验此index索引是否在合理范围内,如果不在,这个方法将抛出一个IndexOutOfBoundsException
        rangeCheck(index);
        modCount++;
        // 获取了指定索引处的元素值,并将其赋值给oldValue
        // elementData方法其实就是 return (E) elementData[index]
        E oldValue = elementData(index);
        // 每删除一个元素,都需要对原有数组进行移动,因此这里也能表现出ArrayList,不适用于删除操作较多的场景
		// 计算需要移动的元素数量
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
		// 将列表的大小减一,并将最后一个元素的值置为null。
		// 这是为了垃圾回收
		// 个人觉得写为:写为 elementData[size] = null;  size --; 更好理解
        elementData[--size] = null;
		// 返回删除的元素
        return oldValue;
    }

一句话概括:首先涉及到index下标的,那肯定是对此index校验参数合法性;其次对元素进行移动;最后返回被删除的元素。

注:ArrayList删除操作,都会伴随着数组元素的移动操作,当数组较长的时候对性能的消耗较大,因此这里也能表现出ArrayList,不适用于删除操作较多的场景。

第二种 public boolean remove(Object o)

移除指定元素,并返回是否成功

    public boolean remove(Object o) {
     	// 情况1:o的值为null
        if (o == null) {
			// 遍历elementData
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {\
                	// 匹配成功 使用fastRemove方法删除此元素
                    fastRemove(index);
                    return true;
                }
        } 
	    // 情况2:o的值不为null
		else {
			// 遍历elementData
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
					// 匹配成功 使用fastRemove方法删除此元素
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

继续向下解读:

	// 真实执行删除操作的方法·
    private void fastRemove(int index) {
        modCount++;
		// 计算需要移动的元素数量
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null;
    }

第三种 public boolean removeAll(Collection<?> c)

批量移除集合与集合c中所共有的元素

    public boolean removeAll(Collection<?> c) {
    	// 对集合c进行null校验 如果为null 抛出空指针异常
        Objects.requireNonNull(c);
        // 调用真实的删除方法
        return batchRemove(c, false);
    }

继续向下解读:

	/**
	  * complement 补充信息,这个方法在removeAll和retainAll中都被使用到,该字段用以确认是remove还是retain
	  * 当complement为false的时候,用以删除 当complement为true的时候,用以保留
	  * 这个方法的设计非常灵性 值得一学
	  */
    private boolean batchRemove(Collection<?> c, boolean complement) {
    	// 定义一个变量指向 elementData
        final Object[] elementData = this.elementData;
        // r 是遍历elementData的参数
        // w 是记录删除后的index
        int r = 0, w = 0;
        // modified标志,如果进行了修改则返回true,否则返回false
        boolean modified = false;
        try {
        	// 遍历 elementData 
            for (; r < size; r++)
            	// removeAll是false  retainAll的是true
            	// 如果集合c中包含elementData[r] 则为true 反之false
            	// 情况1、如果为true  则不等于complement,进入下一次循环
            	// 情况2、如果为false 则等于complement,进入循环体
                if (c.contains(elementData[r]) == complement)
                	// 将集合c中不包含elementData[r]的元素(也就是不需要移除的元素) 赋值给 elementData[w++]
                	// elementData[w] = elementData[r]; w++;
                    elementData[w++] = elementData[r];
        } finally {
            // 如果r != size, 证明上面的遍历提前结束了 意思就是遇到了异常情况
            if (r != size) {
                System.arraycopy(elementData, r,
                                 elementData, w,
                                 size - r);
                w += size - r;
            }
            // 如果 w != size 则表示有移除的元素
            if (w != size) {
                // 清空数组后面的无用index的元素 垃圾回收
                for (int i = w; i < size; i++)
                    elementData[i] = null;
                modCount += size - w;
                size = w;
                modified = true;
            }
        }
        return modified;
    }

第四种 protected void removeRange(int fromIndex, int toIndex)

批量移除指定的多个元素,注意不包括 toIndex位置的元素

 /**
   * 移除 [fromIndex, toIndex) 范围内的元素
   * @param fromIndex 包括
   * @param toIndex 不包括
   */
protected void removeRange(int fromIndex, int toIndex) {
    modCount++;

    // toIndex后面的元素都需要被移动到前面
    int numMoved = size - toIndex;
    // 因此可以看到为啥子右边是),因为它并没有 + 1
    System.arraycopy(elementData, toIndex, elementData, fromIndex, numMoved);

    // 计算新的数组长度
    int newSize = size - (toIndex - fromIndex);
    // 数组往前移动后,后面所有空闲的位置都设为null
    for (int i = newSize; i < size; i++) {
        elementData[i] = null;
    }
    // 更新新的长度
    size = newSize;
}

三、总结

本文主要解读了ArrayList 属性、构造函数、新增元素、删除元素的源码。其他方法的源码大同小异,感兴趣的同学自行阅读。

属性

ArrayList核心属性有2个:

  • Object[] elementData:底层数组 用来存储元素
  • int size:用来记录数组的有效元素个数

构造方法

构造方法有三个

  • 无参构造:ArrayList()
  • 根据指定大小初始化ArrayList中数组的大小:ArrayList(int initialCapacity)
  • 传入集合c作为ArrayList中的elementDataArrayList(Collection<? extends E> c)

注:无参构造方法首次初始化的容量是0,只有在第一次添加元素的时候,出发扩容机制。容量变为10.

添加元素方法

ArrayList是支持动态扩容的数组,所以新增元素的时候会确保数组的容量是否充足,如果不够的话会触发扩容机制。

删除元素方法

ArrayList每次删除元素的时候都会伴随着大量数据的移动,因此我们能看出 ArrayList并不是那么适用于新增、删除比较频繁的场景。

扩容

ArrayList添加元素前会检查数据容量,如果不足的话会触发扩容机制

  • 在使用无参构造器初始化的时候,首次添加元素时会直接扩容到10的容量
  • 其他情况下,会直接扩容到旧容量的大约1.5倍,如果最小容量 大于 本次1.5倍的扩容,那么本次扩充的容量会变为本次最小容量。

加餐1:ArrayList和LinkedList的区别

  • 1、数据结构ArrayListLinkedList都是线性结构,都继承自List接口。不过LinkedList还实现了Deque接口,是基于链表的栈或队列。与之对应的是ArrayDeque是基于数组的栈或队列
  • 2、线程安全ArrayListLinkedList都是线程不安全
  • 3、底层实现:在底层实现上,ArrayList是基于动态数组,而LinkedList是基于双向链表。导致它们很多区别都是因为底层实现的不同引发的。比如说:
    • 遍历速度:数组是一块连续的内存空间,基于局部性原理能够更好地命中CPU缓存行,而链表是离散的内存空间,对缓存行不友好。
    • 访问速度:数组是一块连续的内存空间,支持O(1)时间复杂度随机访问,而链表需要O(n)时间复杂度查找元素。
    • 添加与删除操作:在数组中进行添加或删除操作,平均时间复杂度是 O(n),因为牵涉到移动元素。而链表的添加或删除操作本身只是修改引用指向,只需要O(1)的时间复杂度(如果考虑查询节点的时间,复杂度分析上依然是O(n))

加餐2:为什么 ArrayList 属性要区分出 2 个空数组?

  • ArrayList():指向全局空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA
  • ArrayList(int initialCapacity):如果初始容量大于0,则创建指定长度的数组。如果等于0,则指向全局空数组EMPTY_ELEMENTDATA
  • ArrayList(Collection<? extends E> c):将集合c转为数组,如果数组为空,则指向EMPTY_ELEMENTDATA

首先:无参构造函数应该使用默认行为,也就是DEFAULTCAPACITY_EMPTY_ELEMENTDATA

其次:设置初始化容量为0的数组,是开发者的意图,就是为了设定一个容量0的数组,不应该使用默认容量为0的数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA,而是应该使用全局空数组EMPTY_ELEMENTDATA

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值