ArrayList源码解析

Hello,小伙伴们,大家好:
今天是我们源码篇的第一天,在阅读源码之前我们应该清楚阅读源码的意义在哪儿。我认为阅读源码的意义有以下三点:
1、阅读源码能够学习前辈们许多优秀的代码设计方法。
2、阅读源码能够帮助我们提升编程能力。
3、阅读源码能够帮助我们快速的定位开发、运行时碰到的各种问题。
所以对于想提升技术的小伙伴们,我觉得阅读源码是一段必经的旅程。
好,我们进入正题,开篇一张图,了解一下ArrayList的继承关系。
在这里插入图片描述

Iterable接口:是java 集合框架的顶级接口,实现此接口使集合对象可以通过迭代器遍历自身元素,其核心的方法如下:

Iterator<T>	iterator()	返回一个内部元素为T类型的迭代器

也就是说,集成该接口的类,应该实现iterator()方法来遍历自身元素,具体关于iterable的细节,我们在设计模式篇再进行讲解。

Collection接口:集合层次结构中的根界接口。 集合表示一组被称为其元素的对象。 一些集合允许重复元素,而其他集合不允许。 有些被命令和其他无序。 JDK不提供此接口的任何直接实现:它提供了更具体的子接口的实现,如Set和List 。 该接口定义了基本方法,仅仅列举一些方法,如下:

boolean	add(E e)	确保此集合包含指定的元素。 如果此集合由于调用而更改,则返回true 。 (如果此集合不允许重复,并且已包含指定的元素,则返回false。 ) 
boolean	remove()	从该集合中删除指定元素的单个实例。
boolean	addAll(Collection<? extends E> c)	将指定集合中的所有元素添加到此集合。

AbstractCollection抽象类:为Collection中定义的方法提供了一些具体实现。

List接口:有序集合(也称为序列 ),又叫做列表。使用该接口的用户可以精确控制列表中每个元素的插入位置。 用户可以通过整数索引(列表中的位置)访问元素,并搜索列表中的元素。 与集合不同,列表通常允许重复的元素。且如果它们允许空元素,它们通常允许多个空元素。 有人可能希望实现一个禁止重复的列表,当用户尝试插入时会抛出运行时异常,但是我们预计这种使用是罕见的。 List中定义的方法比Collecition多,但是核心方法相同。
聊到这儿,读者应该注意几点,1、该列表有序;2、该列表允许空元素;3、该列表允许重复元素。当我们阅读集合的另外一条分支-Set我们会发现Set的实现与List有很大的区别。

RandomAccess接口:是一个标志,表明该类支持快速随机访问。这个接口的目的是允许通用算法改变它们的行为,以提供良好的性能,当应用到随机或顺序访问列表。也就是说:是当要实现某些算法时,会判断当前类是否实现了RandomAccess接口,会选择不同的算法。来达到最佳性能。

好了,下面就是今天的主角,ArrayList,(掌声):
ArrayList类:可调整大小的数组,继承List接口,实现了List接口定义的所有方法,并允许存储所有元素,包括null 。 除了实现List 接口之外,该类还提供了一些方法来操纵内部使用的存储列表的数组的大小。 (这个类是大致相当于Vector,不同之处在于它是不同步的)。我们来看看ArrayList是如何实现添加、删除元素的。
我们先了解一下ArrayList中的核心属性:

    /**
     *存储数组列表元素的数组缓冲区。
     */
    private transient Object[] elementData;
     /**
     * 默认的初始容量
     */
    private static final int DEFAULT_CAPACITY = 10;
    /**
     * ArrayList的大小
     * @serial
     */
    private int size;
     /**
     * 列表被结构修改的次数。结构修改是指改变列表的大小,或者以一种可能产生不正确结果的迭代方式扰乱它。继承自AbstractList
     * @serial
     */
    protected transient int modCount = 0;
    /**
     * Shared empty array instance used for empty instances.
     *
 * 用无参构造的共享空数组实例。
 */
    private static final Object[] EMPTY_ELEMENTDATA = {};
/**
 * Shared empty array instance used for default sized empty instances. We
 * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
 * first element is added.
 * 用于无参构造的实例大小共享空的数组实例。
 * 我们将其与 EMPTY_ELEMENTDATA 区分开来,以了解添加第一个元素时应该膨胀多少
 *
 */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

可能大家对最后两个属性会很迷糊,到底是做什么的,我们通过源码来了解这两个属性的用途。先看ArrayList的构造方法,看看ArrayList是如何产生的:

//如果构造ArrayList没有任何参数,那么直接让elementData等于空数组即可。
public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
//如果传入了容量,那么就创建一个指定容量大小的数组,如果容量等于0,那么就让elementData等于空数组。
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);
        }
    }
```java
为什么要定义DEFAULTCAPACITY_EMPTY_ELEMENTDATA 和EMPTY_ELEMENTDATA赋值给elementData,而不是每次都new 一个对象在赋值给它?大家可以想象一下如果每次都是new一个对象,如果系统中创建大量的ArrayList,那么整个系统会存在大量的空数组,无疑会占用部分内存,降低系统性能。如果将一个空数组定义成final的,让所有创建的ArrayList初始都指向它,那么会大大减少内存占用。我觉得我们阅读源码的意义也体现于此。
好了,接下来我们来看看向ArrayList添加元素的过程:
```java
public boolean add(E e) {
        //确保有足够的容量,新容量为当前容量大小加1
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
//看一下ensureCapacityInternal()的具体实现:
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);
    }
private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;//记录旧容量,就容量等于数组长度
        int newCapacity = oldCapacity + (oldCapacity >> 1);//新容量等于旧容量的1.5倍。例如如果旧的容量是10,那么新的容量等于10+5=15。
        if (newCapacity - minCapacity < 0)//如果新容量小于期望的最小容量,就让新容量等于最小期望的容量
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)//如果新容量等于最大的数组长度,那么调用hugeCapacity方法获取一个容量,该方法根据传入的容量和MAX_ARRAY_SIZE比较,如果minCapacity>MAX_ARRAY_SIZE,则返回Integer.MAX_VALUE,否则返回MAX_ARRAY_SIZE。
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        //Arrays.copyOf()方法,需要把数组中的数据复制一份,到新数组中。而这个方法底层是System.arrayCopy是一个native方法,效率不高。
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

通过阅读源码我们发现,如果我们可以事先估计出数据量,那么最好给ArrayList一个初始值,这样可以减少其扩容次数,从而省掉很多次内存申请和数据搬移操作。能够提升系统的性能。
到此,我们对ArrayList添加元素的过程已经了如指掌,我们在来看看它是如何删除元素的:
public E remove(int index) {
    RangeCheck(index);//进行范围检查,如果index>size那么就抛出异常
    modCount++;//记录被修改的次数
    E oldValue = (E) elementData[index];//记录要被删除的元素
    int numMoved = size - index - 1;//要拷贝的数组长度
    if (numMoved > 0)
        //移动数组元素位置
        System.arraycopy(elementData, index+1, elementData, index,
                 numMoved);--size的位置置为null。
    elementData[--size] = null; // Let gc do its work
    return oldValue;
    }
//System.arraycopy源码如下:
public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);
代码解释:
Object src : 原数组、int srcPos : 从元数据的起始位置开始、Object dest : 目标数组、int destPos : 目标数组的开始起始位置、int length  : 要copy的数组的长度

删除的过程就是:检查要删除索引的范围,向前移动数组元素,将size-1位置的元素置为空让GC回收。此外对于add()方法和remove()方法中涉及到modCount属性的修改,这是在使用迭代器遍历元素时,判断 modCount 跟 expectedModCount 是否相等,如果不相等就表示已经有其他线程修改了ArrayList,细节之后我们讲解迭代器模式的时候再进行讲解。
ArrayList的大致工作流程到此咱们就了解清楚了。本文若有不足之处,还请大家多多指正。若有疑问,请小伙伴们私信我,或者评论区留言。更多IT技术资讯,请大家关注《炫酷的Java》公众号阅读,谢谢大家。

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值