Java入门系列之集合ArrayList源码分析(七)

前言

上一节咱们经过排队类实现了相似ArrayList基本功能,固然还有不少欠缺考虑,只是为了咱们学习集合而准备来着,本节咱们来看看ArrayList源码中对于经常使用操做方法是如何进行的,请往下看。html

ArrayList源码分析

上一节内容(传送门《http://www.javashuo.com/article/p-zgvtuxem-kh.html》)咱们在控制台实例化以下一个ArrayList,并添加一条数据,以下java

  ArrayList<Integer> list = new ArrayList<>();
  list.add(1);

初始化容量分析

首先实例化了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);
        }
 }


public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
.....

在咱们初始化集合且类型为基本数据类型时,会有如上两个函数,一个是默认的构造函数,一个是带参数的构造函数。由于给出的例子并未包含参数,因此则是走下面一个构造函数,咱们再来看看ArrayList中定义的变量,以下:函数

    //默认初始化容量
    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;

如上咱们未给定容量时,则初始化一个空数组实例,若咱们给定了容量,则走如上第一个构造函数,若是容量大于0则数组容量则为咱们给定的容量,若是等于0则为空数组实例,不然抛出容量非法。接下来到了第二步,当咱们添加元素2时,看看添加方法是如何操做的。源码分析

添加元素分析

//添加元素实现
public boolean add(E e) {
        ensureCapacityInternal(size + 1);
        elementData[size++] = e;
        return true;
}

咱们继续看看ensureCapacityInternal(size + 1)方法,此方法用来计算数组容量,看看最终方法实现,以下:学习

private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
    //计算容量
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        //当实例化集合时未给定数组容量或者指定容量为0时,则此时数组为空数组实例
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            //此时minCapacity为1,经过Math.max函数将minCapacity和DEFAULT_CAPACITY(默认容量)比较返回【10】
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        //当实例化时给定数组容量大于0,则直接返回添加一个元素后的容量即(size+1)
        return minCapacity;
    }
    //判断是否扩容
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // 若计算事后的数组容量大于数组存储长度时则扩容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
//扩容核心实现
private void grow(int minCapacity) {

        //被操做数组实际容量
        int oldCapacity = elementData.length;
        
        //新容量 = (实际容量 + 实际容量/2并去模)即1.5倍旧容量
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        
        //若新容量小于数组大小则以数组大小为新容量
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        
        //若新容量大于定义的最大数组大小
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
            
        //扩容后的新数组
        elementData = Arrays.copyOf(elementData, newCapacity);
}


//计算数组最大容量
private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        
        //若数组大小大于定义的最大数组大小则新容量最大为整数最大值,不然为定义的最大数组大小        
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
}

如上红色标记的是自动扩容定义的数组最大容量,这里须要解释下oldCapacity >> 1是啥意思,学校所学都还给了老师,查了资料才搞懂,这里也作个备忘录。>>在计算机表示右移,大部分状况下咱们使用这种运算符比较少,可是这里为什么不直接乘除呢?并且咱们还看的懂些,使用左右移,运算速度快,直接乘除须要cpu计算消耗内存。刚一开始看到这个我是懵逼的,其实很简单。好比32,咱们有2进制表示则为100000,怎么计算来的呢,以下:优化

(1 * 2 ^ 5)+(0 * 2 ^ 4)+(0 * 2 ^ 3)+(0 * 2 ^ 2)+(0 * 2 ^ 1)+(0 * 2 ^ 0)= 32 + 0 + 0 + 0 + 0 + 0 = 32。

 

好了咱们知道32表示为二进制则是【100000】,那么32>>1则表示将十进制32转换为二进制后总体向右移动一位,将左边空余的补0,右边多余的剔除,若是是左移则相反(这里需注意int为32位,可是数字没那么大,因此左侧确定所有为0,这里咱们省略了哦),以下:this

因此32>>1向右移动一位后如图,那么计算结果和上述第一张图同样,以下:spa

(0 * 2 ^ 5)+(1 * 2 ^ 4)+(0 * 2 ^ 3)+(0 * 2 ^ 2)+(0 * 2 ^ 1)+(0 * 2 ^ 0)= 0 + 16 + 0 + 0 + 0 + 0 = 16。

为了验证上述结果,咱们经过代码来打印看看是否正确,以下:code

 System.out.println( 32 >> 1);

经过如上图咱们很容易得出结论:若是是右移即>>,那么用原数据除以2的位数次幂并舍去模,若是是左移即<<,那么用原数据乘以2的位数次幂。好比11>>2,经过11除以2^2,立马得出结果为2。如果11<<2,则是11*2^2,结果将是44。分析源码到这里为止,咱们可得出以下结论:

若未给定初始化容量,则默认初始化容量为10且初始化默认容量的时机是在进行添加操做时。

自动扩容大小为1.5倍原始容量。

容量最大为Integer.MAX_VALUE即2147483647。

添加指定索引元素分析

上述咱们只是分析完了初始化集合实例以及添加元素,接下来咱们在指定索引位置添加元素看看,以下:

public static void main(String[] args) {

        ArrayList<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);

        //添加元素2到索引5
        list.add(5,2);
}

依据上述咱们所分析,由于在初始化集合时咱们并未指定容量,因此当咱们添加元素时,此时集合的容量默认为10,接下来咱们在索引为5的位置添加元素2,那么是否是就能够呢?

咱们觉得默认容量为10,在指定索引为5插入元素不会有问题,可是结果倒是抛出了异常,这说明不是以数组默认容量或提供的初始容量来做为判断依据,而是以数组实际大小来进行判断,为了证实咱们的观点,咱们来分析在指定索引位置插入元素的方法,以下:

//添加指定索引元素
public void add(int index, E element) {

        //检查索引范围,确认是否添加
        rangeCheckForAdd(index);

        ensureCapacityInternal(size + 1);
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
}
private void rangeCheckForAdd(int index) {

        //要添加的元素索引不能大于数组实际大小或小于0,不然抛出异常
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

有的人可能就问了,分析源码有什么意义或做用吗?做用太多了,一是了解背后本质原理不会出现自认为所谓的“坑”,二是经过学习并写出高质量的代码,三其余等等。咱们有了对原理的了解,接下来咱们就来作一个题目,以下:

public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);
        list.add(6);
        list.add(7);
        list.add(8);
        list.add(9);
        list.add(10);
        list.add(6,2);
}

由于咱们知道默认初始化容量为10,因此当添加元素到11时即上述在索引6的位置插入元素2,此时将自动扩容且容量大小为15(若是仍是不懂,建议再重头复习下本篇文章)。接下来咱们再来分析分析trimToSize方法。

trimToSize分析

首先咱们来看以下一段代码:

public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>(20);
        list.add(1);
        list.add(2);

        list.add(6,2);
        list.trimToSize();
}

如上咱们提供初始化容量为20,可是呢结果咱们实际仅仅只添加了三个元素,在数组中剩余17个元素却占着坑,因此这个时候为了解决这样的问题就引入了trimToSize方法,旨在解决以下三个问题

将集合缩减到当前集合实际存储大小

最小化集合实例的存储

当咱们须要缩减集合并最小化存储时

public void trimToSize() {
        modCount++;
        //若数组实际大小小于数组容量时
        if (size < elementData.length) {
            //若数组实际大小为0时则数组为空实例,不然复制数组到当前数组大小
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
        }
}

remove分析

在java中能够针对指定元素所在索引位置删除,也能够直接删除元素,下面咱们首先来看看根据索引删除元素,以下:

//删除指定索引元素并返回删除元素值
public E remove(int index) {
        //判断索引是否小于数组实际大小,不然抛出异常
        rangeCheck(index);

        modCount++;
        
        //获取索引元素
        E oldValue = elementData(index);

        //获取复制数组时要复制元素个数
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
                             
        //将被删除的元素置为空,便于垃圾收集器回收                     
        elementData[--size] = null; 

        return oldValue;
}

若咱们要直接元素,好比删除上述添加的元素2,此时针对删除方法尤为重载,其参数是对象,因此咱们须要将元素2转换为包装类,好比以下:

//删除指定元素
public boolean remove(Object o) {
        //若对象为空
        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;
                }
        }
        //返回删除失败
        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 static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);
        list.add(6);

        Integer[] array = list.toArray(new Integer[0]);

        System.arraycopy(array, 3, array, 2,
                3);

        for (int i = 0; i < array.length; i++) {
            System.out.println(array[i]);
        }
}

如上咱们经过调用系统提供的复制方法模拟删除,咱们删除数组中为3的元素,而后打印数组中元素,以下:

根据调用复制来看,复制的起始位置为索引3,而后将数组中元素四、五、6进行复制,可是将原有数组中的元素三、四、5进行了覆盖,可是此时元素6没有元素覆盖,因此数组中依然有6个元素,因此为了GC,咱们须要将元素6设置为空,而且长度设置为5,这样才是最优代码,一样也就达到了在删除元素时elementData[--size] = null同等效果。

总结 

本节咱们详细分析了ArrayList源码,ArrayList的本质上是经过动态扩容一维数组来实现,同时介绍了比较经常使用的几个方法,固然还有好比java 8中出现的经过lambda表达式进行遍历没有再详细去一一解释,后续在学习或作项目时用到了发现有须要补充的地方,我会回过头来再进行研究,暂且到这里为止,下节咱们继续学习其余集合并分析源码,感谢您的阅读,下节见。

相关文章
相关标签/搜索