工作有几年了,说来惭愧,从来没认真仔细的分析过JDK里面的源码,从今天开始分析下JDK中集合部分源码,学习下大神的思路,如有错误,大家尽管指出。
JDK版本 JDK_1.8.0_201
编辑器:idea 2019.3
首先我们看一下Collection接口
看一下Collection接口的各种关系,idea中 ctrl + h
我们只看其中的Set与List
用一个UML图画一下
这里面有我们最常用到的ArrayList与LinkedList,以及HashSet 与LinkedHashSet
先看ArrayList类
实现了四个接口、一个抽象类
四个不可修改的变量
serialVersionUID:序列化需要的值
DEFAULT_CAPACITY :默认初始容量
EMPTY_ELEMENTDATA:用于空实例的共享空数组实例
DEFAULTCAPACITY_EMPTY_ELEMENTDATA:也是一个空的数组
elementData:真正存放元素的数组,用transient 关键字修饰?
size:元素个数
问题:为啥会有两个空的数组?transient 关键字?后续说明吧。
先看构造方法
1.指定大小的初始化方法,如果初始化数量等于0,elementData初始化为EMPTY_ELEMENTDATA数组,如果初始化入参小于0则抛出异常
2.不指定大小,默认空数组,elementData初始化为DEFAULTCAPACITY_EMPTY_ELEMENTDATA
3.将集合转为ArrayList
4.trimToSize方法
首先说明下modCount的作用,唉,看个源码有这么多东西需要学习。。。。。
看下官方解释,来自AbstractList中
噼里啪啦一大堆,看滴脑阔痛。
原文大概意思:
这个字段表示list被结构性更改的次数,结构性更改是指改变了list的长度大小或者以别的方式干扰了这个值,会导致在迭代的过程中产生不正确的结果。
此字段由迭代器和列表迭代器实现使用,在next、add、remove、previous、set操作中如果此字段的值意外更改,则迭代器(或列表迭代器)将在其中抛出{ConcurrentModificationException}异常(阿里巴巴java开发手册关于list的遍历删除的规范有说明)面对迭代期间的并发修改,这提供了快速失败的行为,而不是不确定的行为
子类对于此字段的使用是可选择的,如果子类希望提供快速失败的迭代器(和列表迭代器)只需要在add、remove、等有可能结构性更改list的方法中增加这个字段的值。单线程调用add、remove方法不能删除正在遍历的对象。否则将可能抛出ConcurrentModificationException异常,如果子类不希望支持快速失败,该字段可以直接忽略。
阿里java开发手册
言归正传,这个方法平时基本上不会用到,第一次看到这个
可以在debug的过程中查看这个变化过程
初始化一个大小为10 的list,在list中添加了十个元素,这个时候可以看到elementData的数组大小是10,然后继续向下走,添加一个新元素,这个时候发现elementData的数组大小变为了15,这是它自动扩容了,这个时候的size的大小就不等于数组的大小了。继续走,把trimtosize方法走完,发现size的大小变的跟数组elementData的大小一样了。这就是这个方法的作用。
既然讲到这了,先看一下它的add方法吧
整体看起来比较简单,就是第一行调用了一个方法,我们来看看这个方法的内容,从字面的意思看是“确保内部容量”的意思。
先看一下这个calculateCapacity计算容量的方法,在add之后会传size+1的值过来做计算
再看ensureExplicitCapacity方法,字面意思:“确保准确的容量”
扩容方法
数组最大值,为啥是int最大值-8?
16进制数据,转成10进制为2147483647,也就是2的31次方-1的值,也就是int的最大值
看看hugeCapacity方法对所需的最小容量值干了什么吧
所以最大容量理论上可以达到Integer.MAX_VALUE的值,这已经是一个很大的数字了。
所以add一个元素的时候,如果实际元素数量大于数组的真实长度,那么就会执行扩容方法。
既然add讲完了,那么顺手看看get方法
这个其实很简单啊,就是返回指定位置的数组的值就行。当然在返回值之前有一个判断方法rangeCheck
也就是如果指定的数组位置大于总大小,会抛出越界的异常
这个indexOf方法也很简单,返回指定元素在ArrayList中第一次出现的位置,同理,lastIndexOf返回最后一次出现的位置。
返回一个副本的ArrayList集合
上面这个也比较简单,给指定位置的ArrayList设置值
在判断是否需要扩容的同时将数组的length+1
arraycopy方法,顾名思义,数组复制方法。做一个简单的演示
System.arraycopy(elementData, index, elementData, index + 1, size - index);
假如elementData现在存放的数据是{1,2,3,4,5},由于在判断是否扩容方法中重新构建过数组,这时的length是6,index = 3,由于那么执行这个方法的入参就是System.arraycopy(elementData, 3, elementData, 4, 2);最后得到结果是{1,2,3,4,4,5}。就是将数组的3位开始复制2位数据,从数组的第四位开始写进去,也就是{1,2,3,4}+{4,5}。
下面是删除方法,删除第几个元素
下面看删除指定元素方法
阅读起来应该没有压力,看fastRemove方法
与remove(int index) 方法大同小异,但是没有越界的校验,并且也没有返回要删除的值。(前面用到这个方法的删除方法,元素肯定在数组中,也就无需校验)
数组清空方法
将数组中是所有元素置为NULL,并且size置为0
下面看addAll方法,也比较简单
从指定位置开始addAll,与addAll方法大同小异,不再详细说明,就贴个代码吧
范围删除,从第几个元素开始删到第几个元素,也比较容易
两个校验方法,这两个异常初学者应该见的比较多
删除集合方法
这个方法里面设计的很巧妙,r!=size那块我觉得还是单独拿出来说明。正常流程r=size的流程应该还比较好懂
注释里面说了,如果c.contains()抛出异常,会导致r!=size
我们来做个实际例子,elementData={2,2,3,4,5,6} Collection c = {2,5,9},size = 6
假如现在在判断c.contains(4)的时候报错,那么现在elementData={3,2,3,4,5,6} ,w = 1, r = 3,size=6
那么现在进入到r!=size 的if中,System.arraycopy(elementData, 3, elementData, 1, 3)的结果就是{3,4,5,6,5,6},w= w+ size -r = 4
然后继续执行w!=size的中的if内容 w = 4 从4开始的数据置为nul,修改size,修改modeifed = true。
总的来说这个方法看起来还是有点麻烦,但是用起来却没啥困难。写这个代码的人实在是大牛中的大牛。
retainAll方法也是用的batchRemove方法来实现的,这里就不再讲一遍了
还有常用的截取的方法
public List<E> subList(int fromIndex, int toIndex)
这个是一个截取的方法,但是要注意下,这个截取后返回的是sublist类,并不是Arraylist类型。
阿里的手册里面有写道
最后看一看这个forEach循环遍历
最终实现其实还是普通的for循环,只是要注意在使用的过程中不要对原list进行结构性改变,否则有可能会抛出异常。
最常用的这些方法我大致都列出了,欢迎大家来补充,有问题直接指出就行。互相学习!