ArrayList 解析

基于JDK1.8,版本不同源码有些许不同

1. ArrayList 介绍

image-20210626152410077

我们可以看到 ArrayList是 Collection 集合框架(家族很大)的一个实现类,以及有 LinkedList 和 Vector 这些兄弟等等,然后还实现了 RandomAccess 接口和 List 接口等等,那么自然他会继承一些特点,方法。

ArrayList 就是一个可变数组(底层是 object 数组),可以随机访问,长度可变,存入取出有序,可以存重复元素,可以存 null ,效率高,线程不安全,查询快增删慢(数组的特点)。

2. 常用方法

先看一下 ArrayList 的属性和方法

image-20210626154259816

方法实在太多了,有兴趣可以进源码看看,上面的几个属性很重要,后面会讲到!!

2.1 Constructs()

2.1.1 无参构造

image-20210626161821189

image-20210626162003900

无参构造,通过上图我们可以看到elementData是一个属性,使用无参构造时,初始化了一个默认容量空数组,此时属性size肯定为0,所以ArrayList1.8版本默认初始化长度为0。

2.1.2 有参构造

image-20210626162459584

image-20210626163132457

初始化时指定容量,逻辑也很简单,大于0就new 一个Object数组,等于就赋一个空元素数组

这个时候肯定有人在想这两个都是空数组,有啥区别,不着急,后面会说到

还有一个参数是集合的构造方法这里就先不讲了,都一样

2.2 add()

使用无参构造创建一个集合,并add一个元素,这时候如果是基本数据类型会自动装箱

image-20210626163737591

image-20210626163827964

集合元素都是Object类型的,add时添加的是一个泛型元素,最后都会转成Object类型,而基本数据类型无法转成Object,基本类型在栈中,引用类型在堆中

我们可以看到在add时会先调用ensureCapacityInternal方法来确保内部容量,容量不够就要扩容。

image-20210626164852873

image-20210626164922669

可以看到这边又去调用了一个方法去计算容量,当我们的elementData=DEFAULTCAPACITY_EMPTY_ELEMENTDATA的时候就代表我们是使用无参构造来初始化(不理解的看前面)的,然后在默认容量DEFAULT_CAPACITY和我们需要的最小容量中取一个最大值,当我们元素小于等于10个时,那么最小容量就是10,否则就是我们需要的最小容量,比如添加第11个元素时,那么最大值就是minCapacity了,如果是有参构造那就直接返回了一个minCapacity,比如初始化容量为5,add后需要的最小容量就是6了。

DEFAULTCAPACITY_EMPTY_ELEMENTDATA的作用就是确保无参构造初始化集合在第一次add时初始化容量为10,也解释了前面为什么要有两个空数组。

继续往下走

image-20210626171538213

这时minCapacity肯定是比我们的元素数组长度要大的,那么就需要扩容了

modConut是它父类的一个记录修改次数的变量,以后会讲到

image-20210626172224821

先是去取出了老容量长度,然后定义了一个新的容量,等于老的长度加老的长度右移一位,那就是等于原来的1.5倍了(位运算,右移一位高位补0,相当于除以2[1]),这里就是ArrayList的扩容机制,当我们使用无参构造初始化时,元素数组的长度为0,那么老容量长度就为0,新容量长度也为0,如果说新容量比最小容量还小,那么新容量就为10了,最后调用的Arrays.copyOf方法对数组进行扩容,此时的``已经不是原来的那个空数组了,最后得到的结论是无参构造初始化容量为0,第一次add后容量为10

image-20210626174116386

最后在数组的第0个位置放入远处,并且size+1,由此可见ArrayList是可以存重复元素的,并且索引从0开始

第二次添加元素时由于我们的elementData已经不是原来的空元素数组了,那么就会直接返回最小容量

image-20210626174527700

image-20210626174650670

image-20210626175002932

这时最小容量比元素数组的长度要小,说明放得下,就不需要扩容了

有些人可能还会有疑问,扩容后,没有元素的位置都是为null,那可以add多个null吗,下图可以看出是可以的

image-20210626175537735


上面是关于无参构造的,下面就开始有参构造

image-20210626191156628

初始化长度为5 ,然后我们一样添加元素

image-20210626191501195

此时size为0,那么最小容量就为1

image-20210626191740925

一样的确保容量,此时最小容量小于元素数组的长度,那么便不需要扩容,后面add第六个元素时才会扩容,扩容后的长度为5+5>>2=7

image-20210626192206597

image-20210626192302151

2.3 remove()

有新增自然有删除,接下来看一下删除

image-20210626192534033

这边有四个重载的删除方法,第一个是删除指定元素,第二个是根据索引删除,第三个是根据一个集合来进行删除,第四个是根据一个函数式接口[2]来进行删除,我这边就讲一下第一个和第四个,其他两个也是一样的道理

2.3.1 remove(Object o)

首先new一个集合,然后添加十个元素,然后删除2这个元素

image-20210626193235039

注意,参数如果是基本数据类型那么以索引进行删除的,所以我这里使用包装类

image-20210626193655928

可以看到这里走的是参数为o的方法,先是判断元素为不为空,为空走上面,否则下面,然后找到匹配的第一个元素索引

image-20210626200905399

然后调用fastRemove方法

image-20210626201627031

这边代码有点意思,先是算出要移动的元素个数,然后把当前元素后面的元素移到前面来,最后删除当前元素(设置为null大小减1,让gc去回收),所以增删慢

size最少比index大1,所以要减1,如果是最后一个元素,那么直接把最后一个元素设为null,大小减1即可,否则就要移动元素

image-20210626202414075

这边是调用了System本地方法,就是把元素数组第3到10个元素复制到元素数组的第2到9个上面,效果图

image-20210626203730752

最后效果,没毛病(内存地址不同是因为重来了一遍…)

image-20210626204729384

2.3.2 remove(Predicate<? super E> filter)

这边还是创建一个集合image-20210627100942119

我们删除小于3的元素

image-20210627101227672

前面add时遇到过这个modCount,就是记录集合被修改的次数,然后这边还有个预期修改次数,下面的循环也以这个作为了一个判断条件,以及最下面还有一个判断,不等抛出异常,这是为了防止并发修改的

可以看到循环里面就是取出每一个元素,看符不符合我们的filter条件,符合的记录下索引,以及删除次数+1

image-20210627101824462

这边的anyToRemove的意思就很明显了,需要删除就执行,到了下面,声明了一个新的size,然后又有一个循环

removeSet.nextClearBit(i)这个方法就是返回要移除的下标+1,就是要把保留的元素前移

image-20210627102952140

这是第一次循环,i=0,没什么变化,i++,j++

image-20210627104214209

第二次循环,i=1,因为remove里面存储了索引为1的元素是要被删除的,所以返回2,最后把第三个元素放在了第二个上面,那么第二个元素就消失了,i++,j++

下一步就是把第4个元素放在第三个上面,最后i++,i=5不小于size,j=3不小于新size,循环结束,最后索引为3和4的应该被删除,于是下面又有一个循环去赋值为null,帮助gc回收

然后修改新的size,modCount+1,不得不说设计的太合理了

后面有空再讲下System.arraycopy是深复制还是浅复制。

3. 参考


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!