JAVASE进阶:Collection高级(2)——源码剖析ArrayList、LinkedList、迭代器

👨‍🎓作者简介:一位大四、研0学生,正在努力准备大四暑假的实习

🌌上期文章:JAVASE进阶:Collection高级(1)------源码分析contains方法、lambda遍历集合

📚订阅专栏:JAVASE进阶

希望文章对你们有所帮助

ArrayList的底层其实还是比较复杂的,如果你去尝试阅读源码的话,但是这又是面试常考的问题,网上有些面经会说当创建ArrayList的时候会在底层创建长度为10的数组,后续会以1.5倍扩容,但是实际上这既不完整,也不准确。因为ArrayList除了用add方法添加元素,还有用addAll方法添加元素。

在这里剖析一下ArrayList,另外剖析一下LinkedList和迭代器的底层。

源码剖析ArrayList、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赋值为前一个尾结点,还单独记录了头结点和尾结点的位置,其实就是头插法+尾插法

源码剖析迭代器

一般是这么使用迭代器的:

java 复制代码
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表达式(底层遍历方式都是迭代器),遍历集合的过程中,不要去使用集合中的方法去增加或删除元素。

相关推荐
Wyang_XXX40 分钟前
CSS 选择器和优先级权重计算这么简单,你还没掌握?一篇文章让你轻松通关面试!(下)
面试
星河梦瑾42 分钟前
SpringBoot相关漏洞学习资料
java·经验分享·spring boot·安全
黄名富1 小时前
Redis 附加功能(二)— 自动过期、流水线与事务及Lua脚本
java·数据库·redis·lua
love静思冥想1 小时前
JMeter 使用详解
java·jmeter
言、雲1 小时前
从tryLock()源码来出发,解析Redisson的重试机制和看门狗机制
java·开发语言·数据库
1nullptr1 小时前
三次翻转实现数组元素的旋转
数据结构
TT哇1 小时前
【数据结构练习题】链表与LinkedList
java·数据结构·链表
java1234_小锋1 小时前
JVM对象分配内存如何保证线程安全?
jvm
A懿轩A1 小时前
C/C++ 数据结构与算法【栈和队列】 栈+队列详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·栈和队列
Yvemil71 小时前
《开启微服务之旅:Spring Boot 从入门到实践》(三)
java