ArrayList底层实现

ArrayList实现

ArrayList集合继承AbstractList类,实现List接口
要注意的是AbstractList 抽象类中的modCount表示List数据结构被修改的次数.

由于ArrayList在实现过程中JDK8和JDK11基本上一样,所以就以JDK8来说。
先来看看ArrayList类声明的参数.

// 初始化容量
private static final int DEFAULT_CAPACITY = 10;

// 用于空实例的共享数组实例 不保存数据
private static final Object[] EMPTY_ELEMENTDATA = {};

// 用于空实例的共享数组实例 用于默认大小空实例
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

// 存放数据的数组 ArrayList存数据的真正数组.
transient Object[] elementData;

// Object中所能存放数据的个数. 也就是说当前ArrayList所能支持的大小是Integer.MAX_VALUE-8.
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

// size 即表示当前有多少元素,也表示元素的下标
// size -1 就是第size个元素在Object数组中的下标.
private int size ; 

要注意的是ArrayList没有和HashMap一样的扩容因子,但是ArrayList也会进行动态扩容,ArrayList进行扩容的是以当前数组大小的一半进行扩容,也就是扩容当前一半. 看不懂,没关系,我们向下慢慢看。

我们看ArrayList初始化的空的构造函数可知,其构建的是一个空的elementData的Object数组,我们来看具体的代码.
我们以add(E) 为例子来进行详细说明

	// 我们在初始化的时候,基本上都是用的该构造函数进行初始化的,而我们知道DEFAULTCAPACITY_EMPTY_ELEMENTDATA
	// 的含义就是一个空的Object数组,所以在初始化的时候并没有初始化一个10个大小的Object数组elementData
	// 我们只是初始化了一个空的Object数组.
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

既然我们知道了ArrayList进行初始化为一个空的Object数组,那么为什么网上都说ArrayList的初始大小为10呢?
当我们看过ArrayList的add源码之后就一目了然了。
ArrayList在进行第一次add的时候,会进行判断当前elementData是否能装下本次插入的数据,如果不能装入则用移位的方式进行动态扩容,如果能装入,那么就让如到elementData[size]位置上将当前要出入的数据.
好了,只说理论应该看的不太明白,那我们就来看看ArrayList的源码.

	// 该方法是我们初始化ArrayList直接调用的add方法.
	// 具体的扩容是在grow()方法中进行扩容的,而grow()扩容方法是通过ensureCapacityInternal方法内调用.
    public boolean add(E e) {
    	// 该方法的作用就是判断当前elementData数组上是否有位置插入当前要增加的e元素,如果不能确保当前位置上一定能插入e元素,
    	// 那么就会自动调用grow()方法进行扩容.
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        // 当JVM执行到了这里的时候就说明此时elementData一定能在size位置上插入e元素
        elementData[size++] = e;
        // size++ 表示的是 先在elementData[size]位置上插入元素e,然后才对size++.
        return true;
    }

那么你们有没有想知道ensureCapacityInternal() 方法到底做了什么,就能触发grow()方法进行动态扩容?
我们直接上代码 以下是扩容函数的整个调用链.

	// 扩容第一步
    private void ensureCapacityInternal(int minCapacity) {
    	// 如果是第一次调用add方法增加一个元素,那么calculateCapacity方法返回的是DEFAULT_CAPACITY值10
    	// 也就是说ArrayList真正的初始化时在第一次调用的时候进行初始化的.
    	// 如果ArrayList不是第一次进行调用add方法,那么将会返回minCapacity
    	// 所以也就是说,该方法的目的就是判断当前ArrayList是否为空的.
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
    // 该方法对要出入的位置进行检测.
    // 该方法在初始化ArrayList进行第一次调用的时候将会返回DEFAULT_CAPACITY. 也就是返回10.
    // 该方法的根本目的就是判断当前ArrayList实例是否为空的,
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
    	// 如果当前elementData为空的元素,那么就返回原始自定义的DEFAULT_CAPACITY
    	// 默认的DEFAULT_CAPACITY 为10.
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        	// 初始化ArrayList之后,在第一次向ArrayList中增加第一个元素的时候,将会触发该判断语句.
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

	// 该方法会修改ArrayList的数据结构
	// 该方法会判断是否进行扩容.
	private void ensureExplicitCapacity(int minCapacity) {
        // 表示修改ArrayList数据结构的次数+1
        // 不管是否进行扩容的都先增加modCount的值是因为add方法会修改ArrayList的数据结构
        // 而在该方法中进行修改modCount的值是最合适不过的了
        // 不管add方法需不需要进行扩容,都能对modCount进行+1, 因为add方法的修改了ArrayList的数据结构.
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
        	// 如果当前elementData中存放的数量+1 要比elementData.length大,那么就进行扩容.
            grow(minCapacity);
    }

那么我们就来看看JDK8中ArrayList具体是怎么进行扩容的. 在看扩容源代码之前我们需要稍微了解下Java中的移位操作。
我们写的Java代码在底层都是01运算,因为代码都是依赖计算机进行运算的,那么使用01运算也就是移位来运算,将会很大的提升性能.
在Java中左移是乘法,右移是除法
例如:1<<2 就是12的2次幂 1<<4 就是12的四次幂
1>>2 就是1/2的2次幂 1>>4 就是1/2的四次幂
如果了解了Java中移位运算了解了,那么我们就来看ArrayList中的扩容方法具体是怎么实现的.

    // 扩容的具体方法. 传进来的是 size+1 也就是要保存e的位置.
	private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        // 右移一位 也就是要右移的一半. newCapacity是扩容之后的容量.
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            // 如果右移运算的结果要比size+1小,那么扩容后的Object数组大小为size+1
            // 如果友谊运算的结果要比size+1小,那么就说明newCapacity没有起到扩容的关键性作用,那么此时就使用size+1来进行扩容一个位置.
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            // 如果右移之后的数值大小要比当前数据结构所能允许的大,那么就执行该方法
            // 该方法返回值分为三种情况,第一种是size+1<0则抛出OutOfMemoryError错误
            // 如果size+1 比Integer.Max_VALUE-8还要大,那么就返回Integer的最大值
            // 否则返回Integer的最大值-1
            newCapacity = hugeCapacity(minCapacity);   // 具体的hugeCapacity待会将介绍,这个方法键返回真正的能存入数据的大小.
        // minCapacity is usually close to size, so this is a win:
        // 利用Arrays.copyOf进行扩容
        elementData = Arrays.copyOf(elementData, newCapacity);
        // Arrays.copyOf(arg1,arg2) 参数含义:
        // 第一个参数: 要复制的具体的数组
        // 要复制的具体的数组的长度  假如说 (elementData,100) 那么就新建一个Object数组,只是将这elementData数组里面已经存在的值存入
        // 新数组里面相同的位置,只不过是将长度变为100.
        // 扩容完毕.
    }

当扩容后的值大于当前数据结构(Object数组)所能允许的最大值的时候,将会调用hugeCapacity方法

	// 只要调用该方法就说明,扩容之后的newCapacity要大于MAX_ARRAY_SIZE
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
            // 如果size+1 要比Integer.MAX_VALUE要大(此时是不能以MAX_ARRAY_SIZE进行扩容的)时,就返回Integer的最大值,
            // 否则就返回Integer的最大值减去8.
            // 从方法调用角度来看, minCapacity传进来的值一定是size+1 而在调用该方法的时候就已经对newCapacity和minCapacity判断过了.
            // 如果newCapacity要大于MAX_ARRAY_SIZE 那么才进行调用的. 
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

到这里扩容ArrayList增加就已经说完了,总结一下:

  • ArrayList在new的时候内部elementData是不会进行初始化的,而是在第一次调用add方法的时候才进行初始化为10个大小的elementData的.
  • ArrayList在进行扩容的时候是以当前elementData的长度的1/2进行扩容的,如果非要说一个ArrayList的扩容因子的话,那么0.5就是ArrayList的扩容因子.
  • ArrayList有最大长度限制,也就是说ArrayList最大能存储Integer.MAX_VALUE个值.

我们对扩容流程进行下总结:
ArrayList在调用add方法的时候,会先判断当前位置也就是size位置上是否能存储数据,如果不能则存储数据,否则就进行扩容。但是这个add方法应该分开来看,首先我们来看第一次进行调用add方法的时候将会在calculateCapacity()方法判断是不是一个空的数组,如果是一个空的数组那么就返回默认的大小进行初始化.初始化不是调用的nwe而是调用的Arrays.copyOf()方法. 如果不是第一次添加数据,那么将会执行grow(size+1)方法进行扩容.

上面我们看了ArrayList的add()方法,那么我们现在来看看ArrayList是怎么进行元素删除的?
在看源码之前,不如我们先猜猜,ArrayList是怎么进行删除的。如果不看JDK集合源码你是否会觉得ArrayList是通过将指定索引出的元素设置为null来进行删除的 ? 那好,我们就带着这个疑问去探索ArrayList的源码.

    // 移除指定位置上的元素, 然后将整个数组向左移动,索引数-1.
    // index传进来的值就是索引值不需要-1.
	public E remove(int index) {
        // 判断数组下标是否越界.
        rangeCheck(index);
		// 修改 修改表结构的次数  自增
        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            // 将指定源数组中的数组从指定位置复制到目标数组的指定位置
            // index+1 表示将要删除的那个元素后一个元素的个数不是下标 如果要表示下标应该是index 
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        // 这行代码一定会执行, 先将size-- 然后再见这个位置上的元素赋值为null
        // 将目标数组中的第index个位置上的元素赋值为null,因为通过System.arraycopy方法已经,将目标数组中的第index个
        // 个位置上的元素赋值到了目标数组中的第index个位置上
        // 也就是说在目标数组中,第index个位置上的元素和第index+1个位置上的元素是相同的
		// 此时要通过将第index个位置上的元素赋值为null ?????  这个不就表示要删除刚刚被复制过来的元素吗?
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

让我们来看看是怎么检查数组下标越界的

    // 如果当前位置大于或者等于Object数组所存的数据,那么就抛出数组下标越界异常.
	private void rangeCheck(int index) {
        if (index >= size)
        	// size 就是当前要存入数据的索引.
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

举个删除的例子

    public static void main(String[] args) {

        List<Integer> list = new ArrayList<>();
        // 此时size为1
        list.add(1);
        // 移除第一个元素
        list.remove(0);
        // 如果remove参数是在的位置而不是索引的话那么就会抛出数组下标异常异常.
        // IndexOutOfBoundsException
    }

我们来看看System.arraycopy(arg1,arg2,arg3,arg4,arg5)四个参数的意思
第一个参数是:第一个参数是源数组
第二个参数是:第二个参数是源数组中的起始位置 不是下标
第三个参数是:第三个参数是目标数组
第四个参数是:目标数组中的起始位置也就是要接受被覆盖的位置,不是下标.
第五个参数是:要复制的数组元素的个数.
总结下ArrayList的删除:

  • remove参数传进来的是索引.
  • remove操作会修改ArrayList的数据结构修改的次数.
  • remove操作是通过使用System.arraycopy()函数来进行数组的删除,这样会把target目标数组中的第size-1个位置设置为null,因为将中间的删除了之后,尾部是占用空间的,所以我们要将尾部所占的内存空间释放掉.

到此ArrayList的增加和移除就已经讲述清楚了!
既然已经了解了ArrayList的基本增加和修改了,那么我们再来修改下ArrayList是否线程安全? 如果不是线程安全的,是为什么?

ArrayList是线程不安全的集合框架,在并发情况下不建议使用ArrayList.

我们来看个例子先:

    public static void main(String[] args) {

        List<Integer> list = new ArrayList<>();
        // 此时size为1
        list.add(1);
        Iterator<Integer> iterator = list.iterator();
        list.add(2);
        while (iterator.hasNext()) {

            // 这里将会抛出异常.
            Integer next = iterator.next();
        }
        
    }

当我们不去运行的时候,可能会发现这没有什么错误,但是当我们运行该例子的时候将会发现,这里有个异常发生了。
java.util.ConcurrentModificationException

这里我把list.add(2) 放在了list.iterator之后,只是为了模拟出现错误,但是在多线程的情况下是很有可能出现这种错误的。
我们看下ArrayList中迭代器源码

        public E next() {
        	// 在调用next的时候,会调用方法检查expectedModCount值是否与modCount相同
        	// expectedModCount是在调用iterator方法初始化一个Ltr对象的时候将ArrayList当前实例的modCount赋值给expectedModCount
        	// 当我们在外部继续修改ArrayList的时候将会修改modCount值,这个时候expectedModCount将和modCount不同步
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

		// 调用这个方法进行检查,并抛出ConcurrentModificationException异常.
       final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }

所以我们说ArrayList是线程不安全的集合框架,在多线程下modCount不是多个线程共享的变量,所以抛出ConcurrentModificationException异常.

到了这里我们已经明白了ArrayList底层的实现和ArrayList为什么不是线程安全的集合,并且给出了一个模拟不安全的例子。
如果觉的上述文章有哪里写的不对的地方还请在留言区指出,当看到了一定进行修改.

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值