java1.8 ArrayList源码解读

概述

在日常使用当中,ArrayList使用率非常频繁,它基于数组的线性结构,由于添加时都是向末端添加且是连续的所以它表现为有序性,每个元素都对应一个下标,通过下标来获取数据,所以时间复杂度表现为O(1),不受元素的多少影响。在存储上,它是连续性的,所以在存放ArrayList时,需要一块没有碎片的完整的内存区域用来存放ArrayList,并且该内存的大小需要大于等于ArrayList的大小。

  1. 成员变量
/**
     * 默认初始容量。
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * 用于空实例的共享空数组实例。
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
	*用于默认大小的空实例的共享空数组实例。我们
	*将其与EMPTY_ELEMENTDATA区分开来,以了解何时膨胀多少
	*添加第一个元素。
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

	/**
	*
	存储ArrayList元素的数组缓冲区。
	* ArrayList的容量是这个数组缓冲区的长度。任何
	*使用elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA清空ArrayList
	*将在添加第一个元素时扩展为DEFAULT_CAPACITY。
	 */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * ArrayList的大小(它包含的元素的数量)。
     *
     * @serial
     */
    private int size;
  1. 无参构造函数
  /**
     * 构造一个初始容量为10的空列表。
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
  1. 有参构造函数
 /**
     * 构造具有指定初始容量的空列表。
     *
     * @param  初始容量列表的初始容量
     * @throws IllegalArgumentException 如果指定的初始容量为负
     *         
     */
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
       		 //参数大于0 创建大小为initialCapacity的空数组
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
        	//参数等于0 默认创建为10大小的空数组
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }
  1. add(e)方法
 /**
     * 将指定的元素追加到此列表的末尾。
     *
     * @param e 元素添加到此列表
     * @return <tt>true</tt> (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  
        elementData[size++] = e;
        return true;
    }

add 方法第一个就调用了ensureCapacityInternal 方法 ,size(注意这里不是数组的大小 而是数组包含了多少个元素)加一传给了该方法

   private void ensureCapacityInternal(int minCapacity) {
   	//如果数组大小为空,则取默认值10和参数minCapacity中的最大值 这里我们把minCapacity比作元素的数量,这里需要保证数组的大小足够容纳所有元素。求得最小容量
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        ensureExplicitCapacity(minCapacity);
    }

该方法主要是取最大值作为初始数组的大小
接着我们看ensureExplicitCapacity方法

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

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

这里对modCount加一,我们来看看官方对modcount的定义

   /**
    *此列表被结构修改的次数。
	*结构修改是指改变的大小
	*列表,或者以迭代的方式扰乱它
	*进度可能会产生不正确的结果。
	*
	*
	该字段由迭代器和列表迭代器实现使用
	*由{@code iterator}和{@code listIterator}方法返回。
	*如果该字段的值发生意外更改,则迭代器(或列表)
	将抛出一个{@code ConcurrentModificationException}
	*响应{@code next}, {@code remove}, {@code previous},
	* {@code set}或{@code add}操作。这提供了
	* fail-fast行为,而不是非确定性行为
	*迭代过程中并发修改的面。
	*
	*
	使用该字段的子类是可选的。如果是子类
	*希望提供故障快速迭代器(和列表迭代器),然后它
	*只需要在它的{@code add(int, E)}和中增加这个字段
	* {@code remove(int)}方法(以及它覆盖的任何其他方法)
	*导致对列表的结构修改)。打给
	* {@code add(int, E)}或{@code remove(int)}不能添加超过
	*一个到这个字段,否则迭代器(和列表迭代器)将抛出
	*伪{@code concurrentmodificationexception}。如果一个实现
	*不希望提供故障快速迭代器,此字段可能是
	*忽略。
     */
    protected transient int modCount = 0;

通过描述我们知道该变量是用来表示该数组的结构发生了几次变化,说白了就是该数组的大小被扩展了多少次
如果需扩展的大小大于本身的数组大小则调用grow方法

  /**
	*增加容量,以确保它至少可以容纳
	*由最小容量参数指定的元素数量。
     *
     * @param minCapacity 所需的最小容量
     */
    private void grow(int minCapacity) {
        int oldCapacity = elementData.length;
        //将原来的数组大小除以2在加上原来的数组大小,说白了就是自身扩大了1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //预扩展后的大小必须大于所需的最小容量
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //预扩展后的大小大于MAX_ARRAY_SIZE,则根据最小容量比对MAX_ARRAY_SIZE 大返回Integer.MAX_VALUE 小则返回MAX_ARRAY_SIZE
        //这里确保数组大小最大只能为Integer.MAX_VALUE,考虑到有些虚拟机会保留些头信息,所以尽量最大值取MAX_ARRAY_SIZE 也就是Integer.MAX_VALUE上减8
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity通常接近于size,所以这是一个优势
        //复制elementData数组并扩充大小到newCapacity大小,返回新的数组
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
  /**
     * 要分配的数组的最大大小。
	*有些虚拟机在数组中保留一些头信息。
	*尝试分配更大的数组可能会导致
	* OutOfMemoryError:请求的数组大小超过VM限制
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }
elementData[size++] = e;

最后将新的元素e放到该数组最后一个元素的末尾

总结

  • 因为要新增一条元素,所以必须增加后(加一)的数组大小必须大于当前大小,如果小于就在原来的大小上扩充1.5倍
  • 扩充最大值为Integer.MAX_VALUE,但是尽量不要大于Integer.MAX_VALUE - 8 保留8位是考虑到一些虚拟机会保留一些头信息,如果大于Integer.MAX_VALUE 请求的数组大小超过VM限制,会报OutOfMemoryError 内存溢出
    -直接在后面添加,复杂度O(1)
  1. get(index) 方法
 /**
     * 返回列表中指定位置的元素。
     *
     * @param  index要返回的元素的索引
     * @return 位于列表中指定位置的元素
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E get(int index) {
        rangeCheck(index);

        return elementData(index);
    }
  /**
     *检查给定索引是否在范围内。如果没有,则抛出一个适当的
	*运行时异常。此方法不*不*检查索引是否为
	*否定:它总是在数组访问之前使用,
	*如果索引为负,则抛出ArrayIndexOutOfBoundsException。
     */
    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

总结

  • 判断索引是否合法,否则抛出ArrayIndexOutOfBoundsException异常
  • 直接返回数组指定索引的元素 ,复杂度O(1)
  1. remove(e)
  /**
*从列表中移除指定元素的第一个出现项,
如果有的话。如果列表不包含该元素,则它包含
*不变。更正式地说,删除索引最低的元素
* <tt>i</tt>这样
*   * <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>
*(如果存在这样的元素)。返回true如果这个列表
*包含指定的元素(或相等,如果此列表
*由于调用而更改)。
     *
     * @param o 元素,如果存在,则从该列表中删除
     * @return <tt>true</tt> 如果此列表包含指定的元素
     */
    public boolean remove(Object o) {
    //如果o为null,就把数组所有的元素都清空
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
        //遍历循环所有元素,如果当前元素相同,就删除集合当中该元素
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        //没有找到相同元素返回false
        return false;
    }

我们看看fastRemove方法

    /*
     * Private remove method 该方法跳过边界检查,但不跳过
	*返回删除的值。
     */
    private void fastRemove(int index) {
    //数组结构变量加1
        modCount++;
        //计算离最后一个元素中间有多少个元素
        int numMoved = size - index - 1;
        //如果numMoved大于0 就将numMoved后面的元素复制到index的前面,目的是为了让要删除的元素始终在末尾
        //如果要移除的原始刚好在末尾,则忽略复制的性能消耗
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,numMoved);
        elementData[--size] = null; // clear to let GC do its work
    }

在这里插入图片描述
总结

  • 删除指定元素,首先需遍历获取到该元素在数组当中指定的下标,然后根据下标,如果下标后面还有元素,就将这些元素通过复制移到下标的前面,这个时候需要删除的元素就会被移动数组的末尾,然后将末尾置空
  • 如果要删除的元素刚好为最后一位,因为需要遍历,所以时间复杂度为O(n)
  • 如果要删除的元素不是最后一位,在遍历基础上,还需要复制,就是移动元素的性能损耗,时间复杂度为O(n) + 元素挪动(这个有谁知道怎么计算吗?)
  • 那问题来了,为什么要将删除的元素移动到最后面呢?我们都知道数组是连续性的,所以在存储上需要在内存上找到一块没有碎片且足够大(容纳所有元素的空间),如果我们不这样做,直接 elementData[index] = null 的话,就会产生很多空间碎片的,打破了内存的连续性,这是其一,其二是add方法是直接在元素的末尾添加的,正好对元素的添加做好了铺垫,也就是对资源方面的极致利用(空间的再次利用)
  1. add(index, E)方法
   /**
     * 将指定元素插入其中的指定位置
	*列表。将当前位于该位置(如果有)的元素移动,并
	*右边的任何后续元素(将一个元素添加到它们的索引中)。
     *
     * @param index 要插入指定元素的索引
     * @param element 要插入的元素
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public void add(int index, E element) {
    	//下标越界校验,越界抛出IndexOutOfBoundsException 
        rangeCheckForAdd(index);
		//上文说过,这里是对数组大小初始化的判定,数组大小不足够容纳下一个元素就自身容量大小扩充1.5倍
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //插入的index后的所有元素往后面挪一位
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        //空出的下标index赋值element
        elementData[index] = element;
        //元素大小自增一
        size++;
    }

在这里插入图片描述
总结

  • 和add(e)多了一个步骤,就是元素往后移动的消耗,时间复杂度为O(n) + 元素挪动(这个有谁知道怎么计算吗?)
  1. addAll(Collection<? extends E> c) 方法

    /**
     * 将指定集合中的所有元素追加到
	*此列表,按其返回的顺序排列
	指定集合的迭代器。这个操作的行为是
	*如果在操作期间修改了指定的集合,则未定义
	*正在进行中。(这意味着这个调用的行为是
	*如果指定的集合是这个列表,则未定义
	*列表非空。)
     *
     * @param c 集合,其中包含要添加到此列表中的元素
     * @return <tt>true</tt> 如果此列表因调用而更改
     * @throws NullPointerException 如果指定的集合为空
     */
    public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();
        //需要插入的集合的大小
        int numNew = a.length;
        //计算两个集合合并的大小来初始化该集合的大小(最小容量无法满足就自身扩充1。5倍)
        ensureCapacityInternal(size + numNew);  // Increments modCount
        //新的集合直接复制到该集合的末尾
        System.arraycopy(a, 0, elementData, size, numNew);
        //自身大小自加上插入集合的大小
        size += numNew;
        return numNew != 0;
    }

总结

  • 根据两个集合合并的大小来计算初始化该集合的大小,插入的集合直接放置末尾
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值