👨🎓作者简介:一位大四、研0学生,正在努力准备大四暑假的实习
🌌上期文章:JAVASE进阶:Collection高级(1)——源码分析contains方法、lambda遍历集合
📚订阅专栏:JAVASE进阶
希望文章对你们有所帮助
ArrayList的底层其实还是比较复杂的,如果你去尝试阅读源码的话,但是这又是面试常考的问题,网上有些面经会说当创建ArrayList的时候会在底层创建长度为10的数组,后续会以1.5倍扩容,但是实际上这既不完整,也不准确。因为ArrayList除了用add方法添加元素,还有用addAll方法添加元素。
在这里剖析一下ArrayList,另外剖析一下LinkedList和迭代器的底层。
源码剖析ArrayList、LinkedList
- ArrayList
- 原理
- 源码剖析ArrayList
- LinkedList
- LinkedList方法
- 源码剖析LinkedList
- 源码剖析迭代器
ArrayList
原理
新手还是先大致讲解一下原理有个印象再去看源码会比较好:
1、利用空参创建的集合,在底层创建一个默认长度为0的数组
elementData
,并用size=0记录元素个数/元素下次存入位置
2、添加第一个元素时
,底层会创建一个新的长度为10的数组,把元素添加进去,size=1
3、存满时,将会扩容1.5倍(其实就是直接再创建一个长度15的数组并把之前数组的元素拷贝进来)
4、若一次添加了多个元素,扩容1.5倍还是放不下,则创建新数组的长度以实际为准
注意点:
1、创建ArrayList的时候底层默认长度为0,添加第一个元素时才会创建出长度为10的数组
2、不是每次存满都扩容1.5倍,因为ArrayList添加元素的方法还有addAll,添加元素过于多就要以实际为准
源码剖析ArrayList
1、Ctrl+N
输入ArrayList:
2、进入后就是ArrayList的空参构造,定位一下DEFAULTCAPACITY_EMPTY_ELEMENTDATA
可以发现其一开始创建的确实是空集合,并且size初始值为0:
3、Alt+7
找到这个类下面的所有成员变量、方法:
4、右边栏点击add方法
,可以看到它又调用了另一个add方法,传入3个参数(当前要添加的元素、集合底层的数组名、集合长度/当前元素应存入的位置):
5、跟踪这个add方法,可以看到,在添加元素之前会先判断这个容器满了没有,如果满了就会调用grow()函数扩容,然后再增加元素并更新size。
6、跟踪grow()方法,它会将size+1传入调用另一个grow做扩容,所以当添加了第一个元素的时候,方法体就是return grow(1)
7、接着跟踪grow,可以看到函数体先记录了原来数组的老容量,然后进行逻辑判断,若是第一次添加元素,老容量还是0,因此会走else:
先比较了DEFAULT_CAPACITY(10)和minCapacity谁更大,若minCapacity更大则以此为标准,但是由于这里是用add方法而不是addAll方法批量添加,在这里显然是创建了长度为10的数组:
8、下一次走到该扩容函数的时候,长度是已经大于10了,参数符合if条件,此时oldCapacity=10,minCapacity=11,进入if条件,调用了newLength方法去计算新容量的大小,传入的参数为(老容量,最小扩容长度、老容量/2)
9、跟踪进入newLength方法,minGrowth和prefGrowth可以视为至少要新增的容量大小
和默认新增容量大小
,取最大值后加上oldLegth,默认的情况即为老容量 + 老容量/2 = 1.5 * 老容量
,若minGrowth更大,说明默认的1.5倍扩容还是不够大,这时候容量大小就需要以实际长度为准了,这种情况会出现在ArrayList的addAll函数中:
10、若不满足if情况,则说明此时超过了限定的大小,就需要用hugeLength函数进行处理,跟踪查看该函数,基本上只会执行else if,返回最大限定值:
至此,底层源码剖析完毕,其实这部分的原理跟之前的StringBuilder有点像,只不过StringBuilder一创建的默认长度就是16,扩容的时候是以原容量 * 2 + 2
的方式进行的,也同时拥有grow()方法中的预防机制即插入元素数量过多,导致长度超过扩容后大小,则容量以实际长度为准
,所以阅读源码起来感觉很熟悉。
LinkedList
LinkedList方法
LinkedList底层数据结构是双向链表,查询慢但是增删快,操作首尾元素的速度极快。
每一个结点都有3个部分:前一个结点地址值、当前结点的value、后一个结点的地址值。LinkedList本身就多了很多直接操作首尾元素的特有API。
函数名 |
---|
public void addFirst(E e) |
public void addLast(E e) |
public E getFirst() |
public E getLast() |
public E removeFirst() |
public E removeLast() |
这些方法全部都是针对首尾的,但是List类都是具有根据索引添加、修改和删除元素的操作的,上面的一些操作相对使用的会比较少。
源码剖析LinkedList
1、Ctrl+N
选中LinkedList函数,进入:
2、查看LinkedList中的内部类Node,这就表示了链表中的每个结点:
3、找到add方法,其调用了addLast函数,前面讲过addLast是专门封装的给LinkedList操作尾部的API:
4、跟踪LinkedList,可以看出,LinkedList除了插入元素的时候需要把之前末尾结点中的next赋值新增结点,把新增结点的pre赋值为前一个尾结点,还单独记录了头结点和尾结点的位置,其实就是头插法+尾插法
:
源码剖析迭代器
一般是这么使用迭代器的:
ArrayList<String> list = new ArrayList<>();
Iterator<String> it = new list.Iterator();
while(it.hasNext()){
String str = it.next();
System.out.println(str);
}
接下来跟踪器源码:
1、跟踪Iterator方法,其底层是创建了迭代器对象(Itr)并返回:
2、跟踪进入Itr,其实它就是一个ArrayList的内部类而已(我们创建的迭代器就是ArrayList对象的迭代器):
(1)cursor表示游标,也就是迭代器中的指针,空参构造默认初始化为0,因此迭代器对象创建后,指针默认指向0索引
(2)lastRet表示之前操作的索引的位置
(3)expectedModCount与并发修改异常有关系
(4)hasNext判断是否有下一个元素(迭代完了没有)
(5)next会获取当前元素并且将指针移动一次
(6)checkForComodification是一种安全性检测
3、跟踪进入checkForComodification方法,其中modCount变量,这个变量的意思是集合变化的次数
,因为删除、修改和添加元素都会使得modCount+1。
这个函数体中先判断当前集合中最新的变化次数和一开始记录的变化次数是否相同,不相同就说明出现了并发修改,此时会报并发修改异常。
因此,无论我们使用迭代器、增强for还是lambda表达式(底层遍历方式都是迭代器),遍历集合的过程中,不要去使用集合中的方法去增加或删除元素。