ArrayList源码,你想了解一下不?

12 篇文章 0 订阅
2 篇文章 0 订阅

前言:

作为一个名合格的程序员,不看看源码怎么行?ArrayList作为我们常用的一个集合类之一,它的源码不多并且不是很难,比较容易阅读理解。下面我就分析一下,我对ArrayList源码的理解。

一、ArrayList数据结构:

ArrayList的数据结构是一个数组(Object[] elementData),其中有size和capacity两个概念,size代表的数组中存储元素个数,capacity代表的是数组的长度。另外,ArrayList与Array的主要区别是:ArrayList是可变长度、Array是固定长度。那如何ArrayList可变长度的呢?这里就用到了ArrayList的扩容机制的知识,它的扩容机制底层是通过调用 Arrays.copyOf(T[] original, int newLength)实现的,即老数组复制成新数组。

二、ArrayList源码分析:

0.阅读方法:

我阅读的方法是在Idea中创建一个类(MyArrayList.java),把ArrayList类源码复制过来,然后在MyArrayList类中进行debug和做注释。细心的读者可能发现,我为什么又复制了AbstractList类,原因是ArrayList类继承了AbstractList类,AbstractList类中有的成员变量和方法是用protected访问修饰符修饰,protected访问修饰的权限是相同的包和其子类。我写的MyArrayList类继承AbstractList类,不能使用protected修饰的方法和成员变量。

1.成员变量:

    private static final long serialVersionUID = 8683452581122892189L;

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

    /**
     * Shared empty array instance used for empty instances.
     * 声明一个空实例数组,它是共享的空数组实例
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * 声明一个默认大小(10个)的空实例数组,它是共享的空数组实例。
     * 我们要把它与 EMPTY_ELEMENTDATA 区分开,在添加第一个元素时,知道如何去扩张它。
     * 即,它与 EMPTY_ELEMENTDATA 的区别在于当第一个元素被加入进来的时候它知道如何扩张
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * 存储ArrayList元素的数组缓冲区。ArrayList的容量是此数组缓冲区的长度。
     * 添加第一个元素时,elementData==DEFAULTCAPACITY_EMPTY_ELEMENTDATA的任何
     * 空ArrayList都将扩展为DEFAULT_CAPACITY
     */
    // non-private to simplify nested class access 非私有以简化嵌套类访问
    transient Object[] elementData;

    /**
     * 数组列表的大小
     * @serial
     */
    private int size;

2.构造函数: 

  /**
     * 构造函数,指定容量大小
     *
     * @param  initialCapacity  the initial capacity of the list
     * @throws IllegalArgumentException if the specified initial capacity
     *         is negative
     */
    public MyArrayList(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);
        }
    }

    /**
     * 构造函数,初始化空的数组,此时,数组长度为0
     */
    public MyArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    /**
     * 构造函数,构造一个包含指定集合元素的列表,顺序按照集合元素的顺序
     *
     * @param c 要将其元素放入此列表中的集合
     * @throws NullPointerException
     */
    public MyArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray 可能(不正确)返回 Object[] (see 6260652)
            if (elementData.getClass() != Object[].class){
                elementData = Arrays.copyOf(elementData, size, Object[].class);
            }
        } else {
            // 替换为空数组
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

3.get方法: 

   /**
     * 返回此列表中指定位置的元素
     *
     * @param  index 要返回的元素的索引
     * @return 此列表中指定位置的元素
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    @Override
    public E get(int index) {

        // 检查给定索引是否在范围内
        rangeCheck(index);
        // 返回索引位置的元素
        return elementData(index);

    }

 get方法比较简单, 首先调用rangeCheck(int index)方法检查给定索引是否在范围内,如果没有在范围内抛出IndexOutOfBoundsException异常,如果在范围内,则调用elementData(int index)方法获取元素。简单点说就是通过索引值(数组下标)获取数组中元素。

rangeCheck(int index):

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

 elementData(int index):

    /**
     * 获取索引位置的元素
     *
     * @param index
     * @return
     */
    @SuppressWarnings("unchecked")
    E elementData(int index) {
        return (E) elementData[index];
    }

4.set方法:

  /**
     * 将此列表中指定位置的元素替换为指定元素
     *
     * @param index 要替换的元素的索引
     * @param element 要存储在指定位置的元素
     * @return 返回该索引位置的老元素
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    @Override
    public E set(int index, E element) {

        // 检查给定索引是否在范围内
        rangeCheck(index);
        // 获取该索引位置的老元素
        E oldValue = elementData(index);
        // 替换该索引位置的元素
        elementData[index] = element;
        // 返回该索引位置的老元素
        return oldValue;

    }

 set方法也比较简单, 首先也是检查给定索引是否在范围内,如果在范围内,则调用elementData(int index)方法获取老的元素,然后替换该索引位置的元素,最后返回老元素的值。

5.add方法:

    /**
     * 将指定的元素追加到此列表的末尾
     *
     * @param e 要添加到此列表的元素
     * @return  只返回为true,这是实现Collection接口add()原因,只能是boolean类型
     */
    @Override
    public boolean add(E e) {
        // 确保需要的容量(校验验添加元素后是否需要扩容),并且 Increments modCount
        ensureCapacityInternal(size + 1);
        // 添加元素,并size自增一(注意这里是i++,先赋值,后+1。原因:数组索引从0开始,数组长度从1开始)
        elementData[size++] = e;
        // 返回成功
        return true;
    }

 add方法就相对复杂一些,这里会涉及到“数组容量的初始化”、“数组的扩容”、“fail-fast机制”等,在这里主要介绍说一下“数组容量的初始化”和“数组的扩容”,“fail-fast机制”在后边的章节会详细介绍一下。 add方法首先调用ensureCapacityInternal(int minCapacity),该方法会进行“数组容量的初始化”、“数组的扩容”使用“fail-fast机制”等的操作,然后再添加元素并size自增一(JDK源码中,比较喜欢一行代码干多件事情),最后返回true。

ensureCapacityInternal(int minCapacity):

    /**
     * 确保需要的容量,数据组为空时初始化数组的默认长度
     *
     * @param minCapacity
     */
    private void ensureCapacityInternal(int minCapacity) {
        // 判断elementData是不是为空,即判断ArrayList是否为空
        // 如果是空的,minCapacity等于默认容量与minCapacity中的大值,即初始化数组的容量
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

该方法,首先判断elementData是不是为空(判断ArrayList是否为空),如果是空的,minCapacity等于默认容量与minCapacity中的大值,这里操作就是在初始化数组的容量。然后调用ensureExplicitCapacity(int minCapacity)方法。

 ensureExplicitCapacity(int minCapacity):

   /**
     * 确保明确需要的容量,判断是否需要扩容
     *
     * @param minCapacity
     */
    private void ensureExplicitCapacity(int minCapacity) {

        //fail fast机制的维护的计数值
        modCount++;

        // 最小容量大于数组长度时,扩容
        if (minCapacity - elementData.length > 0) {
            grow(minCapacity);
        }
    }

该方法, 首先会维护fail fast机制的计数值,然后判断最小容量是否大于数组长度,大于则需要扩容,调用 grow(int minCapacity)。

 grow(int minCapacity):

   /**
     * 扩容方法
     * 增加容量以确保它至少可以容纳由最小容量参数指定的个元素
     *
     * @param minCapacity 所需的最小容量
     */
    private void grow(int minCapacity) {
        //老容量
        int oldCapacity = elementData.length;
        //新容量=老容量+老容量向右移1位(缩小一倍,就是0.5倍),即1+0.5=1.5
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //新容量小于所需的最小容量时,新容量赋值为所需的最小容量时
        if (newCapacity - minCapacity < 0) {
            newCapacity = minCapacity;
        }
        //新容量大于MAX_ARRAY_SIZE时,调用hugeCapacity(int minCapacity)方法
        if (newCapacity - MAX_ARRAY_SIZE > 0) {
            newCapacity = hugeCapacity(minCapacity);
        }
        // 扩容
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

该方法, 前几行都是在确定扩容后数组的新容量大小,然后调用了Arrays.copyOf(T[] original, int newLength)进行扩容。

hugeCapacity(int minCapacity):

   /**
     * 计算最大容量
     *
     * @param minCapacity
     * @return
     */
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) {
            throw new OutOfMemoryError();
        }
        // 最小容量大于MAX_ARRAY_SIZE,则使用Integer.MAX_VALUE,反之MAX_ARRAY_SIZE
        // 注意:使用Integer.MAX_VALUE存在内存溢出的风险
        return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
    }

 6.remove方法:

  /**
     * 删除此列表中指定位置的元素,将任何后续元素向左移动(从其索引中减去一个)
     *
     * @param index 要删除的元素的索引
     * @return 返回该索引位置的老元素
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    @Override
    public E remove(int index) {

        // 检查给定索引是否在范围内
        rangeCheck(index);
        // 修改次数自增
        modCount++;
        // 获取该索引位置的老元素
        E oldValue = elementData(index);

        // 计算需要移动的元素个数
        int numMoved = size - index - 1;
        if (numMoved > 0) {
            // 将index+1位置及之后的所有元素,向左移动一个位置
            System.arraycopy(elementData, index + 1, elementData, index, numMoved);
        }

        // size自减,并且原数组的最大索引位置设置为null,允许GC完成其工作
        elementData[--size] = null;
        // 返回该索引位置的老元素
        return oldValue;

    }

remove方法,首先检查给定索引是否在范围,如果在范围内,modCount会自增1;然后获取该索引位置的老元素;之后计算需要移动的元素个数,如果需要移动,会调用System.arraycopy()方法,将index+1位置及之后的所有元素,向左移动一个位置;最后,size自减,并且原数组的最大索引位置设置为null,允许GC完成其工作并返回老元素;

7.clear方法:

   /**
     * 从列表中删除所有元素
     */
    @Override
    public void clear() {
        // 修改次数自增
        modCount++;

        // 允许GC完成其工作
        for (int i = 0; i < size; i++) {
            elementData[i] = null;
        }
        size = 0;
    }

 clear方法也比较简单, 首先修改次数自增,然后将数组中所有元素设置为null。

8.其他方法:

其他方法,我这里就不多分享了,大家可以看一看我的MyArrayList.java文件,其中添加和删除的另几个方法,还是值得看一看的。

三、错误机制fail-fast:

1.fail-fast机制介绍:

在ArrayList源码类注释上有对“fail-fast”机制的两段描述,内容如下:

* <p><a name="fail-fast">
 *  错误机制fail-fast机制
 * The iterators returned by this class's {@link #iterator() iterator} and
 * {@link #listIterator(int) listIterator} methods are <em>fail-fast</em>:</a>
 * if the list is structurally modified at any time after the iterator is
 * created, in any way except through the iterator's own
 * {@link ListIterator#remove() remove} or
 * {@link ListIterator#add(Object) add} methods, the iterator will throw a
 * {@link ConcurrentModificationException}.  Thus, in the face of
 * concurrent modification, the iterator fails quickly and cleanly, rather
 * than risking arbitrary, non-deterministic behavior at an undetermined
 * time in the future.
 * 由iterator()和listIterator()返回的迭代器使用fail-fast机制,如果列表在迭代器创建后,任何时候进行结构上的修改,除了迭代器自身之外的任何方式,
 * 即ListIterator#remove()或ListIterator#add(Object),迭代器将抛出ConcurrentModificationException异常。因此,在面对并发修改时,
 * 迭代器会快速利索地失败,而不是在将来某个不确定的时间,冒着任意的、不确定的行为的风险。
 *
 *
 * <p>Note that the fail-fast behavior of an iterator cannot be guaranteed
 * as it is, generally speaking, impossible to make any hard guarantees in the
 * presence of unsynchronized concurrent modification.  Fail-fast iterators
 * throw {@code ConcurrentModificationException} on a best-effort basis.
 * Therefore, it would be wrong to write a program that depended on this
 * exception for its correctness:  <i>the fail-fast behavior of iterators
 * should be used only to detect bugs.</i>
 *
 * 注意,迭代器的fail-fast并不能得到保证,它不能够保证一定出现该错误。一般来说,fail-fast会尽最大努力
 * 抛出ConcurrentModificationException异常。因此,为提高此类操作的正确性而编写一个依赖于此异常的程序
 * 是错误的做法,正确做法是:ConcurrentModificationException应该仅用于检查bug。

简单来说就是:迭代器在遍历集合对象时,如果遍历过程中对集合对象的内容进行了修改(增删改),则会抛出Concurrent ModificationException。它的实现原理也很简单:迭代器在遍历时,首先将modCount变量赋值给expectedmodCount变量,如果在遍历集合期间,集合的内容发生变化,modCount的值就会改变。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。

Tips:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出(ABA问题)。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。

2.为什么在增强for循环中的删除会抛ConcurrentModificationException?

上面介绍了“fail-fast”机制,估计大家也能想到“增强for循环中的删除会抛ConcurrentModificationException”与“fail-fast”机制有关,那为什么有关系呢?聪明的人可能已经想到了“增强for就是通过迭代器实现的”,下面我们来证明一下吧,准备一个ForeachCode.java类:

package basicknowledgecode;

import java.util.List;

/**
 * 〈一句话功能简述〉<br>
 * 〈强for循环原理〉
 *  通过查看字节码(.class文件),得知强for循环是通过Iterator(迭代器)实现的
 *
 * @author hanxinghua
 * @create 2020/5/8
 * @since 1.0.0
 */
public class ForeachCode {

    List<Integer> list;

    /**
     * 查看foreach
     *
     */
    public void foreachSeeClass() {

        for (Integer i : list) {

        }
    }
}

首先,我们把.java文件编译成.class文件,然后,通过 javap -verbose ForeachCode命令查看class字节码,相关class字节码如下:

通过字节码圈红框的地方,可以看出增强for使用的是迭代器。

四、MyArrayList类源码:

springboot_demo: springboot_demo 《 springboot-java-basic-note模块-sourcecode.list.arraylist包》

五、参考文件:

JDK1.8 ArrayList类源码;

java中快速失败(fail-fast)和安全失败(fail-safe)的区别是什么?https://www.cnblogs.com/songanwei/p/9387745.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hanxiaozhang2018

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值