文章系作者原创,如有转载请注明出处,如有雷同,那就雷同吧~(who care!)
一、写在前面
这是源码分析计划的第一篇,博主准备把一些常用的集合源码过一遍,比如:ArrayList、HashMap及其对应的线程安全实现,此文章作为自己相关学习的一个小结,记录学习成果的同时,也希望对有缘的朋友提供些许帮助。
当然,能力所限,难免有纰漏,希望发现的朋友能够予以指出,不胜感激,以免误导了大家!
二、稳扎稳打过源码
首先,是源码内部的成员变量定义以及构造方法:
参数项
构造方法
进阶分析,集合常用操作,先从简单入手:
按位次获取单个集合元素
按位次修改单个集合元素
如前所述,单个元素按位次获取操作很简单,只要你了解到arrayList内部是数组存放数据,那上述操作完全可以想得到。
下面继续,集合的添加和删除操作,没看过源码的朋友可以思考下,添加和删除操作同上述两个操作主要的区别在哪里?为什么会复杂些?
因为添加元素的时候可能位置不够嘛,那怎么办?位置不够就加位置,也就是所谓的“扩容”!我们自己思考下实现思路,然后同作者的思路对比下:
1.上面我们提到,默认构造方法使用的lazy_init模式,添加元素时才init。那第一步判断是否需要init,需要则init。
2.判断是否需要扩容(通俗点说就是当前数组有没有空位置?)需要就扩容(创建个容量更大的新数组),但是扩容也不是无限的,超过上限就报错(Integer.Max_Value),并把原数据copy到新数组,否则什么都不做
3.在指定位置添加元素,并size++
单个添加集合元素
对比后发现,总体思路是没问题的,但是作者为什么要加入下面这样的容量上限处理逻辑?朋友们可以思考下,ArrayList的容量上限到底是Integer.MAX_VALUE还是MAX_ARRAY_SIZE?
答案是:Integer.MAX_VALUE,依然还是它,并不是源码中作者定义的MAX_ARRAY_SIZE,那就引申出一个问题了-MAX_ARRAY_SIZE存在的意义。
我的理解是,这个上限的存在,减小了内存溢出的概率。首先注释中也提到了“Some VMs reserve some header words in an array”,意思就是有些虚拟机把头信息保留在数组中,毫无疑问保存信息是要占空间的,也就是我实际数组空间可能是不足Integer.MAX_VALUE的,作者应该是这样的思路(我猜的~)“我可以基本确定实际空间不会小于MAX_ARRAY_SIZE(可能是分析了主流虚拟机的实现而得出的结论),因此设置个阈值,来确保不会内存溢出,但如果这个容量还是不够,那把剩下的风险很高的8也给你好了,至于是不是会溢出,天知道,反正我已经尽力了!”
if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity);private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // overflow throw new OutOfMemoryError(); return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; }
单个删除元素
批量删除
序列化与反序列化
迭代器
三、 ArrayList使用建议
1.当你需要确定长度的ArrayList时,推荐使用该长度初始化,理由是防止频繁扩容,扩容相对而言是对性能影响相当大的操作(请注意是相对而言,相对于常规的增删改查等操作)。
2.谨慎使用clone、toArray,对于源对象和方法返回对象,数据修改操作会相互影响。他们最终都执行了System.arrayCopy(),都是“浅拷贝”:只copy了引用,所有对于其中一个引用的修改操作会传递到其他引用上。java的copy应该都是“浅拷贝”,测试如下:
测试clone
3.谨慎使用subList,深入分析后会发现,本质原因同上面类似,都是相同的引用,所以数据修改操作会相互影响,如下例子:
测试subList
4.细心的朋友可能发现了,arrayList没有提供对数组的构造方法,但是我们知道array->arrayList是比较常见的需求,那如何做呢?办法不止一种,选择你喜欢的即可,如下:
arrayToList
四、源码相关扩展(我能想到的可以深入思考的一些地方)
1.快速失败(fail fast)机制:
我们知道,ArrayList不是线程安全的集合类。意味着在并发操作时,很容易发生错误。按照常规思路,发现结果出错,抛出异常即可。但在实际应用中,我们并不满足于此,有没有一种检测机制,在并发操作前进行校验,提前告诉我们可能发生的错误呢?如你所料,就是我们提到的快速失败机制。它是如何实现的呢?其实很简单,我们定义一个计数器modCount,这个计数器记录所有集合结构变动的次数,比如增加两个元素就modCount+=2,再比如删除3个元素就modCount+=3,这个计数器只能增加不能减少,然后我们在执行一些需要快速失败的操作时(比如:迭代器、序列化等等),执行之前记录下当前modCount为expectedModCount,在适当的时候判断expectedModCount、modCount是否相等就可以判断这期间是否有过集合结构的变动。
2.lambda表达式:读源码我们发现,ArrayList中加入了许多支持lambda的方法,作为JDK8的亮点之一,应该能够熟练使用,然后再详细分析lambda的实现机制我觉得也很有意思。
3.jdk中常用JNI方法的实现:上文我们在对clone方法做分析的时候,最终只分析到Object的native方法,我觉得有机会去看下常用的一些native方法的实现也是很有意思的,待研究。
五、总结一下
老实说,分析完ArrayList发现,所花的时间大出我预计,我一度认为我已经理解的很透彻了,但是在写这篇文章的途中我又把差不多一半的源码回顾了一遍(这真是个悲伤的故事),不过不管怎么说,这是个好的开始,后面一篇解析hashMap。