文章目录
一、引入
先来我们来学习一下List集合中的第一个实习类:ArrayList
。
首先我们先来看一下 ArrayList
在整个集合体系中的位置。
通过这个位置我们就知道了,在之前我们学习过的 Collection
和 List
中常见的方法在 ArrayList
中都可以拿过来直接使用,因此在 ArrayList集合
中我们就不需要再去学习常见的方法。
因此本节课就是来讲解一下 ArrayList
的底层原理,然后再去扒一扒它的源代码,看看Java是怎么去写 ArrayList
的。
有部分同学在某些资料中看过ArrayList的底层原理,大概知道两句话:
① ArrayList底层是数组结构的,数组默认长度为10
② 当数组添加满了之后,会自动扩容为1.5倍。
这两句话真正正确的只有第一句的前半段,剩下来的不完整。
正确的情况我会给你一一道来。
二、底层原理分析
1)引入
我们先来看一段代码:首先创建 ArrayList集合
的对象,再调用 add()
添加一些数据,这样我们可以一次添加一个数据。
但是在 ArrayList
中,还有一个 addAll()
方法,可以一次批量添加多个数据。
例如 list2.addAll(list1)
中,它就可以将 list1
集合中的数据全都放到 list2
中。
因此我们在看源码的时候需要将这两种情况都考虑进去:① 一次添加一个;② 一次添加多个
2)情况1:一次添加一个
① 当我们利用空参构造创建ArrayList对象的时候,Java在底层会创建一个默认长度为0的数组。
并且数组有一个名字:elementData
,与此同时,Java在底层还会有一个成员变量 size
,它可以用来记录元素的个数。
② 当调用 add()
添加第一个元素时,底层会创建一个新的长度为10的数组。
数组中默认初始化值为 null
,因此我们也会认为 ArrayList
底层的数组默认长度是 10
。
PS:不是创建集合的时候这个数组就有了,而是添加第一个元素的时候这个数组才出现。
假设现在添加的第一个元素是 "a"
,我们就可以将 "a"
放到 0索引
的位置,添加完成后 size++
,此时 size
的值就是 1
。
有很多人会认为此时集合的长度就是 1
,因为现在集合中只有一个元素。
确实是 1
,但是 size变量
其实有两层含义:1、表示元素个数;2、下一个存入位置。
例如我现在要添加元素 "b"
,那么 "b"
就应该放在 1索引
,与此同时 size++
变成 2
。
此时这个 size
就表示:1、集合中现在有两个元素;2、再添加下一个元素的时候就应该放在2索引。
当我们不断的添加,数组里面的元素存满了,这个时候如果需要继续再存,在底层就需要自动扩容。
③ 存满时,会扩容1.5倍
当原来10个元素的数组存满时,它会创建一个新的数组,新数组长度是原来的1.5倍,也就是长度为15。
然后再将旧数组中所有的元素全部拷贝到新数组中。
当我们还要添加新元素时,就在 10索引
往后加就ok。
但如果现在这个长度为15的数组也加满了,就会继续扩容为原来的 1.5倍。
3)情况二:一次添加多个
如果不是一个一个加的,而是一次添加多个数据,1.5倍还放不下,则新创建数组的长度为实际为准。
假设集合第一次新建的长度为10的数组已经装满了,然后此时我需要一次性添加100个数据,很显然,长度为10的数组扩容1.5倍为15个数据的数组,肯定不够,此时新数组的长度就以实际情况为准:110(原来的10个 + 新增加的100个数据)。
三、查看源码
打开IDEA,ctrl + N 搜索 java.lang包
下的 ArrayList
。
这个类的源码比较多,看起来不太方便,因此我们需要将它的大纲视图罗列出来,有两种办法
① alt + 数字7 ,此时在左边就会有 ArrayList
的大纲。你想要看哪个方法直接用鼠标点就行了。
但是在左边感觉看着有点不舒服,因此我们可以将它移动到右边去
② ctrl + F12 ,也可以出现这个大纲视图,并且还可以输入一些字母进行搜索。
例如输入 add()
,那么它的相关方法也都出现了。
这里我们采用的是 alt + 数字7 将整个大纲都罗列出来的方式。
首先来看一下ArrayList的空参构造。
1)空参构造
在右边点击一下空参构造,然后IDEA就会自动跳转到空参构造这里。
空参构造里面它只有一行代码。
选中 elemetDate
ctrl + b 可以发现它是一个数组。
在刚刚我们说了,ArrayList
底层其实就是数组,数组的名字就叫做 elementDate
。
如果我们使用空参创建,你认为是将什么东西赋值给了 elementDate
呢?
选中 DEFAULTCAPACITY_EMPTY_ELEMENTDATA
ctrl + b 跟进,此时就会跳转到 130行
,此时就发现了,它就是一个 长度为0
的数组!
因此就可以得出刚刚的结论:当我们用空参构造创建对象的时候,它在底层创建了一个长度为 0
的数组。
与此同时,我们还需要来看一下 size
成员变量,默认初始化值也是 0
,它就表示集合中现在元素的个数。
2)boolean add(E e)
在右边找到 add(E e)
,此时它就会跳转到这个方法。
这个 add方法
中又调用了另一个 add方法
,选中它继续跟进。
可以发现这个方法里面只有四行代码,但并不是就这么简单
首先它会去判断你当前元素的位置 if (s == elementData.length)
,如果数组满了,就需要调用 grow()
进行扩容,继续跟进 grow()
。
此时它又调用了另一个有参的 grow()
方法,继续跟进
在扩容的时候还需要计算新数组的长度 newCapacity
,最后再调用 Arrays
中的 copyOf()
进行复制。
这个过程来讲稍微有点麻烦,如果我们用眼睛一个个去看,几乎是不可能看懂的。
因此这里将里面的代码都罗列出来了,我们一个一个去讲解。
下面过程还是听视频讲解比较好:集合进阶-06-ArrayList源码分析_哔哩哔哩_bilibili,定位到 12:04
情况2:第一次添加数据
情况2:当长度的数组已经装满了。
PS:下面 newLength()
中默认新增的容量为 oldCapacity >> 1
,即 除以2
,原来的容量加上默认新增的容量,合起来就是 1.5倍
。