[Java8源码分析]---ArrayList

ArrayList源码分析


ArrayList简介

我们知道,ArrayList是基于数组实现的List类,完全支持List接口的全部功能,底层实质上就是一个Object[]数组。从源码注释的第一行“Resizable-array implementation of the List interface”中,可以看出ArrayList是List接口的可变长数组实现,即这是一个动态数组,与普通的数组相比,它可以实现容量的动态增长。至于其他的特征,我们可以从类的定义中得到:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

这是ArrayList的类结构层次图:
此处输入图片的描述

由以上信息,我们可以知道:

  • ArrayList:表明ArrayList支持泛型
  • extends AbstractList:继承了AbstractList
  • implements List:实现了List
  • implements RandomAccess:实现了RandmoAccess接口,表明其支持快速随机访问,即可以通过元素的序号快速获取元素对象
  • implements Cloneable:实现了Cloneable接口,表明其可以重写clone()方法实现对象的深拷贝
  • implements java.io.Serializable:实现java.io.Serializable接口,表明其支持序列化,能通过序列化进行传输

但是与Vector不同,ArrayList不是线程安全的。


ArrayList属性

    /**
     * 默认初始化容量,为10
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * 当指定容量为0时,返回该空数组
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * 调用无参构造函数时,默认返回该空数组,与前者空数组的不同在于:前者是用户指定容量为0
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * 保存添加到ArrayList中的元素
     * ArrayList的容量就是该数组的长度
     * 该值为DEFAULTCAPACITY_EMPTY_ELEMENTDATA时,当第一次添加元素进入ArrayList时,数组将扩容至DEFAULT_CAPACITY,即10
     * 被标记为transient,表示在对象被序列化的时候不参与序列化过程
     */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     *ArrayList当前包含的元素个数,而非容量
     * @serial
     */
    private int size;

ArrayList构造方法

ArrayList提供了三种构造方法:

  • ArrayList(int initialCapacity)
  • ArrayList()
  • ArrayList(Collection<? extends E> c)

(1) ArrayList(int initialCapacity)

传入参数,则指定ArrayList的初始化容量。如果传入参数>=0,则使ArrayList初始化,如果传入参数小于0,则抛出异常IllegalArgumentException。

    /**
     * 构造一个指定初始化容量为initialCapacity的空ArrayList
     *
     * @param  initialCapacity  ArrayList的指定初始化容量
     * @throws IllegalArgumentException 如果ArrayList的指定初始化容量为负
     */
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

(2) ArrayList()

不传入参数,则使用默认无参构建方法创建ArrayList对象。

    /**
     *  构造一个空ArrayList
     *
     * 源码中注释的是“Constructs an empty list with an initial capacity of ten”,即构造一个容量为10的空ArrayList,这里的意思应该指的是当进行第一次add时,elementData将会变成默认的容量:10。但在没有add的时候,创建ArrayList对象不指定容量,默认调用该无参构造函数,应该返回的是容量为0的空Object[]数组
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

(3) ArrayList(Collection<? extends E> c)

构造一个包含指定 collection 的元素的ArrayList。

    /**
     * 构造一个包含指定 collection 的元素的ArrayList,这些元素是按照该 collection 的迭代器返回它们的顺序排列的
     *
     * @param c 其元素将放置在此列表中的 collection
     * @throws NullPointerException 如果指定的 collection 为 null
     */
    public ArrayList(Collection<? extends E> c) {
        //将collection对象c转换成数组,然后将数组的地址的赋给elementData
        elementData = c.toArray();
        //更新size的值,同时判断size的大小
        if ((size = elementData.length) != 0) {
            //size>0,则执行Arrays.copy方法,把collection对象的内容深拷贝到elementData中
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            //如果size=0,直接将空对象EMPTY_ELEMENTDATA的地址赋给elementData
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

ArrayList常用方法

add方法——add(E e)

    /**
     * 添加元素到ArrayList末尾
     *
     * @param e 被添加的元素
     * @return true
     */
    public boolean add(E e) {
        //确认容量,如果不够,容量加1。注意:只加1,保证资源不被浪费
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

所以add(E e)应该有两个步骤:首先进行空间容量检查,如果有需要再进行扩容,然后插入元素至ArrayList末尾。

那么具体怎么实现扩容的呢?

    /**
     * 确定ArrarList的容量
     *
     * @param   minCapacity   想要的最小容量
     */

     //这是一个public方法,是一个让用户能手动设置ArrayList容量的方法,不参与动态扩容的过程
    public void ensureCapacity(int minCapacity) {
        //如果elementData等于DEFAULTCAPACITY_EMPTY_ELEMENTDATA,最小扩容量为DEFAULT_CAPACITY,否则为0
        int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            // any size if not default element table
            ? 0
            // larger than default for default empty table. It's already
            // supposed to be at default size.
            : DEFAULT_CAPACITY;
        //如果想要的最小容量大于最小扩容量,则使用想要的最小容量
        if (minCapacity > minExpand) {
            ensureExplicitCapacity(minCapacity);
        }
    }

    //私有
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        //如果数组是调用无参构造函数,即容量为0的空数组
        //所需的最小容量就是默认容量10
        //即我们所说的调用无参构造函数直到第一次add时,才进行数组扩容
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        //否则返回指定的最小容量
        return minCapacity;
    }

    //私有
    //数组容量检查,不够时则进行扩容,只供类内部使用
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

    //私有
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        // 确保指定的最小容量 > 当前数组的长度
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            //扩容
            grow(minCapacity);
    }

    /**
     * 数组可被分配的最大容量。当需要的数组尺寸超过VM的限制时,可能导致OutOfMemoryError
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    /**
     * 扩容,保证ArrayList至少能存储minCapacity个元素
     *
     * @param minCapacity the desired minimum capacity
     */
     //私有
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        //右移一位相当于除以2,所以这句代码就等于int newCapacity = oldCapacity + oldCapacity / 2;即容量扩大为原来的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //如果仍然小于minCapacity,则直接让newCapacity 等于minCapacity,而不再计算1.5倍的扩容
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //如果超过最大容量MAX_ARRAY_SIZE,则调用hugeCapacity方法
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:

        //即复制原数组内容到一个新容量的数组里。这里Arrays.copyof方法实际是调用System.arraycopy方法。
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

    /**
    * 进行大容量分配
    */
    //私有
    //如果minCapacity大于最大容量MAX_ARRAY_SIZE,则返回Integer的最大值。否则返回MAX_ARRAY_SIZE。
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

从源码和注释中,可以看出数组扩容实质上是通过私有的方法ensureCapacityInternal(int minCapacity) -> ensureExplicitCapacity(int minCapacity) -> grow(int minCapacity)来实现的,我们可以对扩容方法总结如下:

  1. 进行空间检查,决定是否进行扩容,以及确定最少需要的容量。如果确定扩容,就执行grow(int minCapacity),minCapacity为最少需要的容量。
  2. 第一次扩容,逻辑为newCapacity = oldCapacity + (oldCapacity >> 1),即在原有的容量基础上增加一半。
  3. 第一次扩容后,如果容量还是小于minCapacity,就将容量扩充为minCapacity。
  4. 对扩容后的容量进行判断,如果大于允许的最大容量MAX_ARRAY_SIZE,则将容量再次调整为MAX_ARRAY_SIZE。至此扩容操作结束。

还有两点值得注意的地方:

  1. 在进行扩容计算的时候使用了位运算,因为位运算是底层运算,计算机运算是以二进制为基础,而十进制运算在计算时会被转换成二进制再进行运算,这个转换过程就会导致运行速度降低。所以通过二进制移位可以提高运算速度。
  2. 扩容的实质是是使用了Arrays.copyOf(),即创建了一个新的数组,然后将旧数组上的元素copy到新数组,这是一个很大的消耗,所以在我们使用ArrayList时,最好能预计数据的大小,在第一次创建时就预先指定容量。

到这里,我们应该可以很清楚的知道ArrayList底层扩容的原理了。


add方法——add(int index, E element)

    /**
     * 在指定位置插入元素。当前位置的元素以及index之后的元素全部后移一位
     * @param index 即将插入元素的位置
     * @param element 即将插入的元素
     * @throws IndexOutOfBoundsException 如果索引超出size
     */
    public void add(int index, E element) {
        //越界检查
        rangeCheckForAdd(index);
        //确认容量,如果不够,容量加1。注意:只加1,保证资源不被浪费
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //对数组进行复制处理,从index位置开始复制size-index个元素到从index+1位置开始,目的就是空出index的位置插入element,并将index后的元素位移一个位置
        System.arraycopy(elementData, index, elementData, index + 1,size - index);
        //将指定的index位置赋值为element
        elementData[index] = element;
        //数组长度+1
        size++;
    }

所以add(int index, E element)应该有三个步骤:首先进行越界检查,然后进行空间容量检查,如果有需要再进行扩容,最后完成插入操作。
越界检查如下:

    private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

扩容操作同add(E e)。


remove(int index)

    /**
     * 删除指定索引index的元素
     *
     * @param index 被删除元素的索引
     * @return 被删除的元素
     * @throws IndexOutOfBoundsException 如果参数指定索引>=size,抛出越界异常
     */
    public E remove(int index) {
        rangeCheck(index);

        //修改次数+1
        modCount++;
        //记录index处的元素
        E oldValue = elementData(index);
        //删除指定元素后,需要左移的元素个数
        int numMoved = size - index - 1;
        //如果有,就移动(被删除的元素直接被覆盖)
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        //size-1,给最后一个位置null
        elementData[--size] = null; // clear to let GC do its work
        //返回被删除元素
        return oldValue;
    }

    /**
     * 越界检查
     */
    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

该方法的步骤总结:

  1. 检查索引是否越界
  2. 将索引后的元素全部左移一位(实际上是覆盖)
  3. 将索引为size-1的元素赋为null(否则对应对象一直不会被回收)

至此,我们发现有两个检查越界的方法,分别为rangeCheckAdd()和rangeCheck(),做其对比:

    //检验索引是否合法(校验了上限和下限)(index可以等于size(0<=index<=size))
    private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

    }
    //检验索引是否合法(只校验了上限)(index不能等于size(index<size))
    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
  • rangeCheckForAdd方法用于add(int index, E element)和addAll(int index, Collection<? extends E> c)方法中检验索引是否合法;很明显,添加时index是不能超出集合的范围,同时他的下标也不能为负数。

  • rangeCheck方法用于get、set、remove等方法中检验索引是否合法;应判断当前集合index是否大于数组长度,此时这个集合是存在的,所以他不需要去判断他的index小于零,只需要判断是否大于当前index。这里把get(-1)这样的越界检查交给了数组的访问。


get(int index)

    /**
     * 返回索引为index的元素
     *
     * @param  index 需要返回的元素的索引
     * @return the element at the specified position in this list 返回该index元素
     * @throws IndexOutOfBoundsException 越界异常
     */
    public E get(int index) {
        //越界检查
        rangeCheck(index);
        //返回索引为index的元素
        return elementData(index);
    }

    @SuppressWarnings("unchecked")
    E elementData(int index) {
        return (E) elementData[index];
    }

set(int index, E element)

    /**
     * 将指定索引的旧值修改为新值
     *
     * @param index 需要修改的值的index
     * @param element 新值
     * @return the element previously at the specified position 旧值
     * @throws IndexOutOfBoundsException 越界异常
     */
    public E set(int index, E element) {
        //越界检查
        rangeCheck(index);
        //记录旧值
        E oldValue = elementData(index);
        //赋新值
        elementData[index] = element;
        //返回旧值
        return oldValue;
    }

当然还有一些其他不怎么常用的方法,这里就不逐一分析啦~

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值