Java提供了Collection这个集合接口,可以用来作为数据的容器,其子接口分为单列集合List和双列集合Map,本文初略探索一下List集合下ArrayList的扩容原理。

创建时的elementData数组

首先,ArrayList的底层是用数组来实现的,看一下ArrayList的源码:

        可以看到当我们创建一个ArrayList对象的时候,它会在底层创建一个名叫elementData的数组,并把DEFAULTCAPACITY_EMPTY_ELEMENTDATA这个用final修饰的空数组赋值给它——可见最开始创建ArrayList的时候是在底层创建了一个空数组的;源码可见,DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一个空数组:

 size的作用

        

        随着ArrayList的实例化,类中的私有成员变量size被初始化为0,size是一个非常重要的变量,它有两个作用:1.记录当前集合的大小(长度);2.是下一个元素应该加入集合的索引(ArrayList是有索引的)。所以说创建ArrayList对象,size=0,集合此时的大小也是0,同时,下一个(也就是第一个元素)应该加入集合的索引也是0。当这些都准备好了之后,就可以开始加入元素了——

第一次加入元素

        调用ArrayList中的add方法,传入想要加入集合的元素,然后接收到返回值(但ArrayList返回值没什么意义),元素就成功加入集合了。但数组的长度发生了什么变化?还是进入源码:

        首先modCount这个变量会自增一次(这个变量是用来记录集合的操作次数的,对于扩容原理用处不大。)然后调用add的重载方法,这个方法需要三个参数——第1个参数是需要加入集合的元素,第2个参数是底层的element数组,第3个参数是size,size十分重要,代表了元素应该加入的索引和现在的集合大小(扩容的判断条件),然后就返回true了(可以看到add方法总是返回true,这也间接证明了add的返回值没有意义),现在进入add重载方法的源码:

                这个add重载的方法,第1个参数是需要加入集合的元素,第2个参数是底层的element数组,第3个参数是size,进入方法,if语句判断size是否和现在的elementData的长度相等,因为是第一次加入元素,所以说size的值是0,elementData的长度也是0,所以说if会执行,会调用grow方法,而grow方法就是用来给elementList扩容的方法,

        可以看见grow方法也是有重载方法的,给这个重载的grow方法传递的参数是size+1,那么继续跟进到grow的重载方法中:

        这个重载的grow方法中的参数是minCapacity,其意义是“数组扩容后的最小容量”,现在数组扩容后的最小容量是1(size+1),有一个叫oldCapcity变量,用来记录ArrayList底层数组的长度,顾名思义,也就是老(数组)的容量,然后开始判断老容量(oldCapcity)是否大于0或者elementData数组是否不是一个空数组,很显然,是第一次加入集合,if不会执行,那么就会执行else中的语句:会返回一个新的数组,其容量是DEFAULT_CAPACITY和minCapacity中的较大值(DEFAULT_CAPACITY是一个常量,值为10),很显然,这次会返回一个大小为10的数组。 然后可以回到add方法中,当扩容结束后,就可以在size处加入元素了,加入之后,size会+1,因为此时的集合长度已经是1了,而且下一个要加入集合的元素应该加入的索引也是1。

后续添加的扩容

        现在ArrayList中已经有了1个元素了,size也指向了1,elementData的大小也来到了10,所以说当我们再调用9次add方法,都是直接添加元素就成功了(此时是添加到第10个元素),不会涉及到扩容,但是当添加第11个元素的时候,此时的size等于10(相当于arrayList中有10个元素了,并且这次的元素应该添加到索引10),但是回顾add方法——此时的size和elementData的长度又相等了,所以说应该再次用grow方法扩容。

这次的grow方法传递的是size+1,也就是11。

        重载的grow方法得到的minCapactiy是11,oldCapacity是10,然后开始判断if——显然oldCapacity是大于0的(这次的elementData也不是空数组,但是因为是||所以说直接就进入if了)。if里面会调用一个newLength方法给数组扩容(实际上是创建了一个新数组并且将原来的元素拷贝过去)里面的参数分别是oldCapacity(老容量);minGrowth(需要扩容的最小容量)——这个参数是数组扩容后的最小容量减去老容量;和oldCapacity>>1(将老容量右移1位)相当于将老容量/2。本次的参数分别是oldCapacity:10;minGrowth:11-10=11;;oldGrowth>>1:5,然后进入newLength的源码分析:

        方法中的三个参数上文已经提及,这里不过多的赘述,prefLength(也就是第三个参数)默认是将老容量右移1位的值——也就是刚才传递的值5,但方法一开始就更新这个变量,更新为老容量加上minGrowth和prefGrowth的更大的值,显然prefGrowth更大,那么prefGrowth就被更新为15。进入if判断——prefGrowth>0(这个判断应该是为了防止扩容的大小是0而导致扩容失败)并且prefGrowth<SOFT_MAX_ARRAY_LENGTH这个常量(Integer的最大值-8),if条件满足,直接返回prefCapcity,此时elementData的长度就变为15了,然后再加入元素,“移动”size。

还有一件事

        那么还有一件事,假如同时加入100个元素,是否还会将数组扩容成15?通过逻辑思考,这显然不现实,那么同时加入100个元素是如何扩容的呢?其实在newLength第一行就告诉了我们答案——方法第一步就是更新prefCapacity,将它变为老容量加上需要扩容的最小容量和理论上需要扩容的容量(oldCapacity>>1)的最大值,假如同时需要添加100个元素,那么需要扩容的最小容量就是100,而现在的elementData的oldCapacity是10,所以说理论上扩容的容量就是5,显然100远>5,所以说现在扩容的量就是10(oldCapacity)+100(minGrowth)=110

        还有一个用于处理数组长度可能溢出的方法hugeLength(oldLength, minGrowth),当prefLenght的大小大于了SOFT_MAX_ARRAY_LENGTH这个常量(Integer的最大值-8)就会进入else,也就是进入了hugeLength(oldLength, minGrowth)方法(但是基本上不可能真用到这个方法吧?):

        这个方法主要做了以下几件事:

  1. 计算最小所需长度minLength,即当前长度oldLength加上最小增长量minGrowth。
  2. 如果minLength小于0,说明发生了溢出,抛出OutOfMemoryError异常。
  3. 如果minLength小于等于SOFT_MAX_ARRAY_LENGTH,返回SOFT_MAX_ARRAY_LENGTH。
  4. 否则,返回minLength

总结

        创建一个ArrayList对象时,底层先创建了一个长度为0的数组elementDate,创建变量size,size有两个作用:集合的长度(元素的个数)和下一个元素应该添加的位置添加一个元素其实有点复杂:如果现在的size≠数组的长度(数组没有存满),则直接在size所指的位置添加元素,然后size++但是若size=数组的长度(相当于已经存满了),那么就会调用ArrayList中的grow方法,先对数组进行扩容然后再存入元素。

        数组扩容的原理:第一次添加元素的时候,数组长度是0,会用grow进行扩容,这时的扩容长度是10(设定的第一次扩容10),但是当10个元素存满之后,想要添加第11个元素,也会进行grow扩容,此刻算出至少需要扩容的长度是1,然后grow方法中可以计算出需要扩容1,然后grow有个默认扩容机制——将老容量左移1位作为扩容的大小,当需要扩容的大小小于默认扩容机制的时候,将使用默认的扩容机制扩容——新数组是原来的1.5倍(长度是原来的1.5倍);但是当需要扩容的长度大于了默认扩容的长度, 则以实际的长度为准。

        举个例子:当默认长度为10的数组已经存满了,再想存元素,按理来说默认的扩容是将10扩容为15,但是假如想一次添加100个元素,15显然不够,这时就会以实际需要添加的元素为准,将其扩容为长度为110的数组,而不是默认的1.5倍长度的15的数组。

 

 

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐