Java之ArrayList源码分析(第二篇:添加元素)

(注意:本文基于JDK1.8)

 ArrayList添加元素的方法,共计4个

​    我将依次分析它们是如何实现添加单个元素或者添加多个元素的……

add(E)方法分析

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

    用于添加单个元素的方法,传入参数为类型E的参数

1、先检查数组对象的容量能否再添加一个新的元素 

ArrayList对象持有的实例变量size,实例变量size保存的是当前ArrayList对象实际存储的元素总数,size+1的值表示添加一个新的元素后的元素总数,新的元素总数传入到ensureCapacityInternal()方法(见下方介绍),ensureCapacityInternal()方法对ArrayList对象持有的数组对象容量进行检查,确保数组对象的当前容量能再存储一个新的元素,如果不能,此方法会对数组对象进行扩充容量

2、添加元素到数组中

确保数组容量已经有足够的空间添加一个新元素后,会将传入的元素对象e添加到数组的某个下标处,该下标值直接使用了ArrayList对象持有的实例变量size值。实例变量size在ArrayList中有两个用途,第一个用途为表示ArrayList对象实际持有的元素总数,第二个用途为添加新元素时,存储到数组对象时的下标!

3、元素总数增加1

添加元素至数组后,ArrayList对象实际持有的元素总数size值再增加1

4、向调用者返回添加元素的结果

该方法始终返回true,true表示元素添加成功

ensureCapacityInternal()方法分析

    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

用于扩充数组容量的API,传入的int参数minCapacity表示预期数组的最小容量

1、首先调用calculateCapacity()方法,用于计算一个即将扩充数组的新容量

传入的局部变量minCapacity表示预期的数组最小容量

elementData为ArrayList对象持有的底层数组对象

将minCapacity和elementData同时传入calculateCapacity()方法中,稍后分析该方法如何计算出一个合适的容量值?

2、接着调用ensureExplicitCapacity()方法,会实际完成数组的扩充容量

calculateCapacity()方法的返回值,会传入到ensureExplicitCapacity()方法,由此方法完成实际的数组扩充容量

calculateCapacity()方法分析

    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

静态方法calculateCapacity()用于计算数组扩容时的最佳容量值(该方法主要用于第一次添加元素时,获取默认容量)

1、判断elementData是否与DEFAULTCAPACITY_EMPTY_ELEMENTDATA常量指向同一个对象

当使用默认构造方法ArrayList()创建ArrayList对象时,elementData指向的是这个常量对象,此时直接返回一个常量DEFAULT_CAPACITY和局部变量minCapacity二者之间的最大值作为数组的最小容量,常量DEFAULT_CAPACITY的值是10,说明ArrayList未指定容量时,默认的底层数组容量为10

2、返回最佳容量值

如果不是通过构造方法ArrayList()创建的ArrayList对象,则直接返回传入的局部变量minCapacity值,它表示预期的数组最小容量值

ensureExplicitCapacity()方法分析

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

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

扩充数组容量前的检查工作

1、fast-fail机制,防止多线程下使用ArrayList

ArrayList对象持有的实例变量modCount代表修改次数,此处加1,这个实例变量用于多线程下的校验工作,防止用户在多线程下使用ArrayList

2、判断当前底层数组是否需要扩容

传入的预期的最小数组容量值minCapacity减去当前数组对象的容量,如果大于0,说明预期的数组容量已经比现有数组的容量大,此时必须得调用grow()方法,并将预期的数组最小容量值传入进去,grow()方法将完成数组的容量扩容。当然如果预期的数组容量小于当前数组的容量,则什么都不会做了……

grow()方法分析

    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

grow()方法,用于扩充数组容量,后续我会在单独的文章中进行分析扩容分析,这里简单讲一下

1、获取当前数组对象的容量

局部变量oldCapacity负责存储当前数组对象的长度

2、计算出新长度,用作新数组对象的长度

使用当前数组对象的长度与当前数组对象的一半容量相加,计算出一个新的长度作为新创建数组对象的长度,并赋值给局部变量newCapacity负责保存

举个例子,假设当前数组对象的长度为10,则新的数组对象的容量是:10 + 10 / 2 = 15,结果为15

3、二次检查新的数组长度能否满足扩容需求

将newCapacity(表示新长度)与minCapacity(表示预期长度)的值比较,当新的长度newCapacity仍然小于预期的数组最小容量minCapacity时,将直接使用传入的minCapacity作为newCapacity的值(这个容错好)

4、检查新的数组长度是否超出数组可用的最大长度

常量MAX_ARRAY_SIZE保存的是数组可用的最大长度,如果新的数组长度newCapacity超过MAX_ARRAY_SIZE的值,会使用一个hugeCapacity()方法作为容错办法,容错后的值仍然保存在newCapacity中

5、使用Arrays的静态方法copyOf()将当前数组对象中的元素一个一个拷贝到一个新创建的数组对象中

copyOf()方法将返回一个新的数组对象,ArrayList对象持有的实例变量elementData将指向新的数组对象,代表数组扩容工作完成(这个Arrays的静态方法copyOf()挺好,干实事!)

hugeCapacity()方法分析

    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

用于检查需要扩充的数组最小容量值minCapacity,是否符合数组容量的合理范围

1、对minCapacity小于0的情况进行处理

竟然直接抛出一个OutOfMemooryError()的Error(这个容错费解呀,莫非是超过int的范围了,才会是负数吧?)

2、对超大容量值进行处理

大于常量MAX_ARRAY_SIZE值,则使用Integer.MAX_VALUE作为底层数组的容量(整型最大值)

小于常量MAX_ARRAY_SIZE值,则使用常量MAX_ARRAY_SIZE作为底层数组的容量

add(int,E)方法分析

    public void add(int index, E element) {
        rangeCheckForAdd(index);

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }

向ArrayList指定位置(下标)添加一个元素,接受两个参数,int表示下标,另一个表示参数类型E的对象,即元素对象

1、检查传入的下标值,是否合理

将下标值index传入到一个rangeCheckForAdd()方法中,进行下标范围的检查,只有符合要求的下标才能插入元素

2、检查当前数组容量能否再插入一个新的元素(不行就会执行数组扩容)

同样调用ensureCapacityInternal()方法,并将当前元素总数+1的值传入进去

3、移动插入位置与后方的所有元素,为新插入元素挪出一个空余位置

通过System的arraycopy()方法将数组中的每个元素执行复制操作,并腾出一个空余位置,用于插入新的元素对象element。举个例子:假设现在ArrayList对象中已经添加了9个元素,此时size为9,向下标index为4的位置插入一个新的元素,此时需要挪动(向后复制)size - index个元素,即9 - 4 = 5个元素,以下标index处为起点,包括4、5、6、7、8共五个下标的元素,逐个向后挪动一位,此时下标index为4的位置虽然仍然保存的旧元素,但因为下标indexx为4的元素已经复制到新的位置(index=5),所以新增的元素直接覆盖掉下标index为4的位置即可。

【备注:arraycopy()方法没有挪动元素,而是采用向后复制元素,模拟挪动元素的过程】

4、将新的元素插入到指定下标index处

通过数组的元素赋值操作完成单个元素的插入

5、最后将已添加元素总数size再+1即可

rangeCheckForAdd()方法

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

检查下标是否符合范围的方法,不符合要求会抛出异常

1、检查指定的下标index是否大于当前元素总数或者小于0

当下标index大于元素总数size或者小于0时,会先调用outOfBoundMsg()方法返回一行用于描述的字符串,返回的字符串会再传入到IndexOutOfBoundsException对象中,异常对象IndexOutOfBoundsException会被抛出(中文名数组越界异常)

outOfBoundsMsg()方法分析

    private String outOfBoundsMsg(int index) {
        return "Index: "+index+", Size: "+size;
    }

用于产生并返回一行提示字符串,此字符串用于展示异常信息

上文中的IndexOutBoundsException对象使用此方法返回的字符串,字符串内容是由:下标值和当前ArrayList持有的元素总数组成的……

arraycopy()方法分析

public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);

arraycopy()方法位于System类中,一个静态的native方法,用于从一个src数组中、向另外一个dest数组中拷贝元素

5个参数

第一个参数src表示将要拷贝元素的源数组对象(选择哪个数组对象作为源数组)

第二个参数srcPos表示源数组对象的起始下标int(从源数组的哪个下标开始复制)

第三个参数dest为拷贝元素的目标数组对象(拷贝元素到哪个目标数组对象中)

第四个参数destPos表示目标数组对象的起始下标(目标数组对象的哪个下标作为起点)

第五个参数length表示拷贝元素的总数(拷贝多少个元素)

addAll(Collection)方法分析

    public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        return numNew != 0;
    }

添加多个元素的方法,多个元素保存在一个接口类型Collection对象中,传入的参数类型为Collection是个interface,只要实现Collection接口的类,且元素类型同为参数类型E或者参数类型E的子类型的即可

1、将传入的Collection对象转换为数组对象

首先将传进来的Collection对象转为数组对象,并赋值给临时变量a进行存储,临时变量a是一个Object[]类的对象

2、获取要添加的数组长度

接着取出已经转换为数组对象的长度,使用数组a的length属性获取,然后赋值局部变量numNew保存

3、检查当前数组容量能否插入数组中的所有元素

使用当前元素总数size与在第二步中获取的numNew值相加,得到所需的自小长度值传入到ensureCapacityInternal()方法中,检查当前的数组容量能否插入数组中的所有元素

4、扩容检查后,将所有元素插入到底层数组中

将新增加的所有元素,一个一个的复制到底层数组elementData对象中,通过使用System的arraycopy()方法完成插入元素

第一个参数传入局部变量a指向的数组对象(作为源数组对象)

第二个参数为0(表示从源数组对象的下标0处的元素开始复制)

第三个参数传入elementData(ArrayList对象持有的elementData为目标数组对象)

第四个参数传入的是size值(表示目标数组对象elementData的下标size作为起点)

第五个参数传入局部变量numNew(表示插入数组中所有元素的总数)

5、更新元素总数

将size值加上添加的所有元素数量numNew,即为ArrayList持有的新的元素总数

6、返回添加所有元素的结果

当传入的Collection的数量不是0个元素,表示成功添加所有元素

addAll(int,Collection)方法分析

    public boolean addAll(int index, Collection<? extends E> c) {
        rangeCheckForAdd(index);

        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount

        int numMoved = size - index;
        if (numMoved > 0)
            System.arraycopy(elementData, index, elementData, index + numNew,
                             numMoved);

        System.arraycopy(a, 0, elementData, index, numNew);
        size += numNew;
        return numNew != 0;
    }

从指向下标处开始,添加多个元素的方法,传入的int参数表示插入元素的下标起点,传入一个Collection对象表示持有的多个元素

1、检查下标index的范围是否符合要求

由rangeCheckForAdd()方法完成,只需要将下标index传入进去即可,若下标index大于ArrayList持有的元素总数size(注意不是数组容量)或者小于0,会抛出IndexOutBoundsException异常,rangeCheckForAdd()方法的分析请在本文章中查找

2、将传入的Collection对象转为Object[]类的数组对象,并赋值给临时变量a

3、取出数组对象a的容量赋值给局部变量numNew存储

4、检查当前数组容量能否存储插入的所有元素

向ensureCapacityInternal方法()传入size+numNew的值,进行数组扩容判断

5、计算要进行复制的元素总数

当前ArrayList持有的元素总数size - 下标值index,计算结果由局部变量numMoved保存

6、当需要挪动的元素总数numMoved大于0时,开始挪动elementData中的元素,腾出空余位置给新插入的所有元素

若需要移动元素的数量大于0个,做一个复制操作,从index下标开始,从复制到index+numNew下标处,共计复制numMoved元素,这是在同一个数组对象中完成,所有你看System.arraycopy中源数组对象与目标数组对象都是同一个
7、将新添加的所有元素插入到elementData的空余位置中

也是通过System的arraycopy()方法完成的

8、更新元素总数

给size加上一个numNew值

9、返回添加所有元素的结果

当新增的元素数量numNew不为0,则表示添加多个元素成功

总结

1、作者使用元素总数size作为添加新元素到数组中时的下标,size的初始值是0,每添加一个元素,正好size+1,添加元素前,size的值永远指向着线性表尾部的一个下标

2、每次添加元素时,总是先检查底层数组容量能否存储下新的元素,如果现有数组的容量不够,则创建新的数组对象,新的数组对象容量为旧数组的1.5倍

3、如果1.5倍的新数组仍然不能满足需求,则直接使用预期的数组最小容量作为新的数组对象的容量

4、在ArrayList的中间添加元素,需要逐个向后复制元素,腾开一个位置供新添加的元素对象,所以添加元素的效率为O(n)

5、腾出一个或多个空余位置给新插入的元素使用,元素原来的位置并没有挪动,而是将旧元素在新的位置都复制了一份,然后旧的位置会被新的元素覆盖掉

6、当使用默认的ArrayList()构造方法时,第一次添加元素时,将会进行数组对象的初始化,且容量为10

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值