ArrayList 底层源码详解

ArrayList概述

      我们先复习一下ArrayList:它封装了一系列操作,如增删改查,对比使用传统的数组方便了很多,因为java代码已经替我们写好了,我们直接用即可。
      但是,面对即将走向工作岗位的你们,单单知道如何使用远远不够,首先面试官就是一个槛,大部分面试都会问集合的实现原理,这也是大部分面试者的薄弱点。
      我们今天就来详细了解一下ArrayList的实现原理

扩容技术

      ArrayList的最大优点其一就是实现了数组长度可变,因为其底层实现还是一个数组,但不局限于固定长度的普通数组了,所以最重要的就是ArrayList的数组扩容技术。其实原理也很简单,ArrayList就是新创建了一个长度大一点的数组,并将原数组复制到新数组中,仅此而已。

add()方法源码

      既然想知道数组是如何扩容的,所以直接看add()方法即可,我们上源码:

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

      add()方法接收了一个E类型的参数e(想添加的元素),其实就是任意类型,进入方法体后我们发现,它其实就是把传进来的参数e赋值给elementData[]的索引size上,我们先来看看elementData是什么:

transient Object[] elementData;

      elementData就是一个存放Object类型的数组,并且定义为transient,是不可被序列化和反序列化的。既然它定义了elementData[]数组,那必然会对其初始化,我们查看ArrayList的构造方法:

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

      到这里我们就蒙圈了,DEFAULTCAPACITY_EMPTY_ELEMENTDATA这一长串英文是什么意思呢?
      我们查看它的定义:

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

      它实际上是一个空数组,是初始化了的。当然这里只说了ArrayList的无参构造方法,有参构造方法与这是不同的,这里暂且先放一放,后续会提到。
那么此无参构造方法作用就是让elementData数组指向DEFAULTCAPACITY_EMPTY_ELEMENTDATA这个空数组,所以此时的elementData也是一个空数组。
      了解完elementData之后,继续查看add()方法,执行elementData[size++] = e时,因为elementData数组长度是0,必然此元素e是添加不进去的,所以执行赋值之前需要对数组进行扩容,所以就有了ensureCapacityInternal(size + 1),我们继续跟进:

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

第一次add添加元素

      假设我们现在是第一次添加元素,此时的size+1就是1,因此minCapacity = 1,minCapacity可以理解为最小容量。该方法先调用了calculateCapacity(elementData, minCapacity),我们继续跟进:

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

      这里先做了一个判断:elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA,因为之前的无参构造方法就是将DEFAULTCAPACITY_EMPTY_ELEMENTDATA赋值给elementData的,所以进入if语句,返回DEFAULT_CAPACITY与minCapacity中的较大者,我们看看DEFAULT_CAPACITY:

private static final int DEFAULT_CAPACITY = 10;

      好!这里我们知道了一个最重要的点:默认数组容量是10,注意这点很关键,先记住。继续往下看,因为minCapacity = 1,所以这里返回的是10。走出方法,继续查看:

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

      调用了ensureExplicitCapacity(10):

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

      modCount这个变量先不用管,因为它涉及到迭代器,我们后面仍会详细说明。
      这里的minCapacity参数是10,进行minCapacity与数组长度的大小判断,10 - 0 > 0,因此满足条件,执行grow(10)方法:

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);
}

这里涉及了很多判断,我们一个一个来看:

  • int oldCapacity = elementData.length;//int oldCapacity = 0;
  • int newCapacity = oldCapacity + (oldCapacity >> 1);//int newCapacity = 0 + 0/2 = 0
    注意这里,每次扩容的大小就是这一段代码的体现,即扩容至原数组长度的1.5倍
  • if(newCapacity - minCapacitry = 0 - 10 = -10 < 0)
  • newCapacity = 10;//新数组容量为10
if (newCapacity - MAX_ARRAY_SIZE > 0)
    newCapacity = hugeCapacity(minCapacity);

      这一段较为复杂,我们先看MAX_ARRAY_SIZE:

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

      我们再来看Integer.MAX_VALUE的值是多少:

public static final int   MAX_VALUE = 0x7fffffff;

      0x7fffffff(16进制) = 2147483647(10进制),Integer.MAX_VALUE - 8 = 2147483639,所以说这一个判断(newCapacity - MAX_ARRAY_SIZE > 0)遇到的情况很少,意思就是如果新数组容量比2147483639还要大,则执行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是否比MAX_ARRAY_SIZE大,如果是则使用MAX_VALUE作为数组的容量,如果不是则使用MAX_ARRAY_SIZE作为数组的容量。
      经过一系列比较后,最终算得newCapacity = 10,即新数组容量,这时总算开始进行数组复制了,也就是grow()方法的最后一行代码:

elementData = Arrays.copyOf(elementData, newCapacity);

      创建了一个新的容量为10数组,并覆盖原数组elementData,兜了这么一大圈总算完成数组的扩容了,这回我们再回到add()方法:

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

      ensureCapacityInternal()方法完成数组扩容后,将添加的元素e加入进数组elementData中,到此整个add()方法执行完成。


第二次add添加元素

      那么当第二次进行add操作会是怎么个执行流程呢?我们继续分析:

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

      这时候size = 1,以为前面一个元素在进入集合后,size进行了自增++操作。因此传入的size + 1 = 2,进入ensureCapacityInternal()方法

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

      注意,这个时候calculateCapacity方法计算容量,直接返回minCapacity = 2了。
      因为此时的elementData数组不是空数组了,并不指向DEFAULTCAPACITY_EMPTY_ELEMENTDATA,所以说该判断语句只会在第一次添加元素时才会执行,并且只执行一次,以后再不会执行,而是直接进行数组扩容了。

//此方法只会执行一次,并且只在第一次添加元素时执行
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
    return Math.max(DEFAULT_CAPACITY, minCapacity);
}

      返回minCapacity值为2后,调用ensureExplicitCapacity(2),进行最小容量minCapacity与数组长度大小比较,minCapacity = 2,elementData.length = 10,因此不进行数组扩容,当然添加第10个元素时也不会扩容,直到第11个。


add添加时大于原容量的情况

      那么在添加第11个元素时会发生什么呢?我们继续分析:

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

      调用ensureCapacityInternal(11):

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

      calculateCapacity(elementData, 11)方法返回11,然后进入ensureExplicitCapacity(11)方法进行判断:minCapacity = 11 > elementData.length = 10,所以需要进入grow(11)方法进行扩容:

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);
}

逐个计算分析:

  • int oldCapacity = elementData.length = 10;
  • int newCapacity = oldCapacity + (oldCapacity >> 1) = 10 + 10/2 = 15;
  • if (newCapacity - minCapacity < 0) 不进入
  • if (newCapacity - MAX_ARRAY_SIZE > 0) 亦不进入
  • 直接进行Arrays.copyOf(elementData, 15);//复制并扩容

      到此,我们基本上了解add方法的执行流程,需要注意的点,每次进行数组扩容时,均扩容至原来的1.5倍,如果说一开始创建ArrayList时没有指定容量大小,默认是10,因此进行一次扩容后为15,第二次为22…33…51以此类推。


有参构建与无参构建

      说完这些,我们继续分析如果在创建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);
    }
}

      当指定的容量参数initialCapacity为0时,EMPTY_ELEMENTDATA会赋值给elementData,紧接着我们继续看什么是EMPTY_ELEMENTDATA:

private static final Object[] EMPTY_ELEMENTDATA = {};

      咦,这个不也是一个空的Object类型数组吗,跟之前的:

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

      DEFAULTCAPACITY_EMPTY_ELEMENTDATA空数组有区别吗?答案肯定是有区别的。
      我们不妨回顾一下,之前除了在初始化DEFAULTCAPACITY_EMPTY_ELEMENTDATA数组时出现,还有在哪出现过吗?答案在下面,就是计算容量的方法中用到了:

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

      这里它判断了当前的elementData数组是否为DEFAULTCAPACITY_EMPTY_ELEMENTDATA,是的话则将DEFAULT_CAPACITY(10)和minCapacity中的较大值返回,否则直接返回minCapacity。
      也就是说,如果使用的是无参构造方法创建的ArrayList,则elementData值即为DEFAULTCAPACITY_EMPTY_ELEMENTDATA,所以他才会默认指定大小为10;如果使用的是指定容量的有参构造方法创建的ArrayList,假设定义的容量参数是0,则elementData值为EMPTY_ELEMENTDATA 。
      所以使用有参构造方法创建的ArrayList(这里假设参数是0,不是0的情况一会再分析),是不会默认指定大小的,只能是添加一个,检查一个,判断是否满足容量,经过分析可以得知其容量的变化0->1->2->3->4->6->9->13以此类推。
当参数不为0且大于0的情况下:

if (initialCapacity > 0) {
    this.elementData = new Object[initialCapacity];
}

      这写得已经非常明确了,直接创建一个Object型数组,并且长度为initialCapacity,即我们传递来的参数。
      使用有参构造方法创建,添加元素时,不会设置默认数组初始大小。对此,我们已经对两个构造方法有初步的了解了,为了更深刻地理解,我们再举个例子:

ArrayList<Integer> array1 = new ArrayList<Integer>();
ArrayList<Integer> array2 = new ArrayList<Integer>(0);

      这里创建了两个ArrayList对象,分别使用无参构建和有参构建,且有参构建情况下参数指定了0。
      现在我们分别对两者进行add操作:

array1.add(1);
array2.add(1);

有参构建与无参构建的区别:

  • 使用无参构建时,因为当前elementData空数组与DEFAULTCAPACITY_EMPTY_ELEMENTDATA数组是相等的,所以返回的最小容量minCapacity是默认值10,因此数组扩容至默认长度10,当前数组elementData.length
    = 10;
  • 使用有参构建时,因为当前的elementData空数组是EMPTY_ELEMENTDATA
    ,而不是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,因此计算容量的函数返回值是minCapacity =
    1,因此数组扩容至长度1,当前数组elementData.length = 1。

      这也是这两者比较直观的区别了。


modCount、迭代器解析

      我们在讲解add()时,其中的一个函数是不是使用了modCount这个变量,我们看源码:

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

      这个modCount究竟是什么?这里我明确告诉大家,他与迭代器遍历密切相关,我们直接看迭代器Iterator源码:

public Iterator<E> iterator() {
    return new Itr();
}
private class Itr implements Iterator<E> {
    int expectedModCount = modCount;
    public E next() {
        checkForComodification();
    }
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();//抛出并发修改异常
    }
}

      这里我们就能看到modCount了,他把值赋给了expectedModCount,用处是什么?我们发现每当调用迭代器的next()方法取数据时,会先调用checkForComodification()来检测,检测什么?该方法内判断了modCount与expectedModCount值是否相等,如果不相等则抛出并发修改异常ConcurrentModificationException,看到这里,再回想一下刚刚什么时候对modCount进行了修改,如果修改了,又会造成什么后果?

      我们首先举一个例子:遍历一个长度为3的ArrayList,因其调用了3次add方法,所以modCount = 3,进入iterator()方法后将modCount赋给expectedModCount,此时两者大小相等。如果在迭代中途add添加了元素,则modCount = 4,但此时expectedModCount不等于4,就会抛出异常。

      我们可以这样理解:你虽然向集合中添加了元素,但迭代器不知道,所以如果想在遍历过程中add()添加、remove()删除元素,只能使用for循环遍历。但修改元素时不会受影响,因为它没有对modCount进行修改。

  • 总结:
  • 集合迭代器只能遍历集合,如果在迭代过程中对集合进行add增加、remove删除元素,则会抛出ConcurrentModificationException异常(并发修改异常),不仅ArrayList如此,HashMap等等也如此。
  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Sadness°

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

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

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

打赏作者

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

抵扣说明:

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

余额充值