分析目的
接触了 java 两年,虽然在2015年阅读了一遍 Collection 接口下面的各种实现。但是最近还是想重温一下 jdk 的源代码,同时也想用笔记的方式记录和分享一下学习历程。
目前初步想把 Collection 和 Map 的源代码都重新阅读分析一遍,以便加深印象理解。其实这也是面试中最常见的问题之一,例如ArratList是如何实现的,ArrayList 和 LinkedList 有什么区别,常见数组扩容的深拷贝和浅拷贝等知识点。本文先讲 ArrayList ,主要围绕着实现原理,然后从构造函数,到增删是如何实现的。
简介:
1. ArrayList 是基于数组实现的一个队列,因为基于数组,但是相对来说,他比数组更为灵活,因为可以动态扩容,但是效率不如数组(知识点)。
2. ArrayList 实现了 RandmoAccess 接口,即提供了随机访问功能。RandmoAccess 是 java 中用来被List 实现,为 List 提供快速访问功能的。在 ArrayList 中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。其实就是快速从索引来访问。
3.ArrayList 实现了Cloneable接口,即覆盖了函数clone(),能被克隆。注意:这里有个知识点是java的深拷贝和浅拷贝,这里需要追踪到Arrays.copy方法。不过这个方法是个native的,需要最终到c++的实现。这点应该也是大厂的面试题之一(知识点)。
4.ArrayList 实现java.io.Serializable接口,这意味着ArrayList支持序列化,能通过序列化去传输。
5.ArrayList是线程不安全的,这里经常有对比的就是同一家族下的vector(知识点)。
首先看结构:
构造函数
1.无参构造
public ArrayList() {
this.elementData =DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
这里说明,默认构造容量为10的一个arraylist,但是要注意,实际上这里的长度是空的,继续往下看,稍后分析。
2.初始化指定容量的List
首先如果制定的长度大于0,那么初始化一个指定构造函数内,参数大小容量的 object 数组。
如果参数为0,那么设置list置为长度为0的 object 数组。
如果指定长度小于0,那么 new IllegalArgumentException()异常。
3.接收一个 Collection 作为参数,构造一个List(),这里也说明,只要实现了 Collection 接口的容器,都可以作为参数,传递给 ArrayList() ;
如果当前集合长度不为0,那么调用 Arrays.copyOf()方法,将参数,复制给当前数组。
否则,将当前数组赋值为空的 object 数组。
这是基础的三个构造函数。
add方法
add 方法,共有两个 public 方法,供我们调用。方法一接受指定类型的一个元素,方法二在指定的元素索引处增加元素,然后后续的元素索引分别增加1。
方法1
首先分析方法1,增加一个指定类型的元素。(这里也可以称为范型,是 jdk1.5 新增加的特性)。
这里调用私有的 private add()方法,首先判断当前的size,和 List 内的元素长度是否相等,这里假如是第一次添加时,如果条件成立那么进行扩容,或者说是容器的长度存储到数组容量的当前最大索引数时。
接下来看扩容操作,grow()方法做了哪些事情。
首先当前容器的 size 增加1。
然后进行数组的 copy 方法,
但是 copy 方法要注意的就是另一个方法,newCapacity(),
这里照应了,第一个无参的构造函数,是在添加元素时,才会选择初始化10个长度的数组,这也是优化代码的一个小技巧。同时 newCapacity()方法,返回新的数组长度。
下面的return之前会判断长度是否为数组的最大长度-8,这里以前的jdk是没有-8操作的,后续可以解释这里为什么-8,当然也可以百度这个问题。最主要的就是要记住,ArrayList的最大长度就是Integer.MAX_VALUE-8,就是整型最大长度-8。
再扩容完之后,接下来介绍 Arrays.copyOf()方法。
最终会跟到 native 方法,arraycopy()
arraycopy,共需要五个参数
Object src : 原数组
int srcPos : 从元数据的起始位置开始
Object dest : 目标数组
int destPos : 目标数组的开始起始位置
int length : 要copy的数组的长度
例如 :
有一个数组数据 byte[] srcBytes = new byte[]{1,2,3,4,5,6,7,8,9,10}; // 源数组
byte[] destBytes = new byte[5]; // 目标数组
我们使用 System.arraycopy 进行转换 (copy) ,System.arrayCopy(srcBytes,0,destBytes ,0,5)
上面这段代码就是 : 创建一个一维空数组,数组的总长度为 10位,然后将srcBytes源数组中 从0位 到 第5位之间的数值 copy 到 destBytes 目标数组中,在目标数组的第0位开始放置.
那么这行代码的运行效果应该是 1,2,3,4,5 。
注意:这里 ArrayList 中目标数组的长度的长度,是永远大于源数组的,因为需要将源数组的所有元素copy到目标数组中,然后返回新的数组。
接下来,将新的元素填充返回新的数组结构中,然后 size+1 。如果添加中没有任何异常发生,则默认返回boolean 为 true 的结果。
方法2
方法2为,添加元素在数组的指定索引位置。共有两个参数,参数1,int 值为 List 的索引,参数二,为需要添加的元素。
这里可以理解为插队。比如你去火车站排队,假如你的车要走了,你很急需要插队。这时候你可以直接跑到队伍前面,这时候后面的所有人就都要往后排一个人出去。
首先这里判断数组内的实际元素数量,和数组容量的size是否相等。如果相等,则进行扩容操作。和上面讲解的扩容操作,做了一件事情。
这里注意一点就是不要把,size 和 this.elementData.length 混淆。
比如:new object()[10]
那么 size 为10,但是假如你add 5个元素之后,this.elementData.length 为5
在前面的基础一些校验和判断相关操作完成后,开始真的数组添加操作,实际上就是这一句话做了实际的事情。
这里的 arraycopy 就是说的插队操作,每个元素,从 elementData 的 index 需要插入的位置开始截取,然后从 index+1 处开始复制。在复制完之后,将新的元素插入到 index。
remove方法
方法1
首先 Objects.checkIndex(index, size); 这里要校验,删除的index是否合法,例如数组长度为10,你要删除的 index 为12,这里就会抛出异常。
然后将要删除的元素保存起来。在删除成功之后,会把旧的 value return 回去。
开始真正的删除动作
这里继续调用 System.arraycopy()方法,从原来需要删除的 index+1 开始截取,复制到原来 index的位置。通俗点理解,就是排队时候,前面有一个人因为有事情走了,这样你们后面每个人则可以向前多走一个人的距离,然后队伍最后面设置为 null 。就完成了删除操作。
方法2
删除容器内指定的元素,
首先这里 remove 的元素是否为空,如果为空。则找到数组内为空的元素,并且 beak 出去,,同时i变量记录下了索引。注意:这里也说明,ArrayList 是可以存储 null 的。其实在数组没有填充满时,存储的就是 null ,这里只是加以说明一下。此时,else 内只是找到相同的元素,同样返回索引。然后进行删除。
接下来看具体的删除操作
这里的 fastRemove(快速删除),是怎样快速删除的呢?具体看代码来分析。
其实是同样的 System.arraycopy() 方法,将当前元素的后面所有元素,从当前指定索引数开始复制。并将最后一个元素内从置为null。
总结:
ArrayList 大部分都围绕着 arraycopy 方法来实现。其实就是对数组的一种快速操作的方法。只要围绕着这一点,理解 ArrayList 其实还是不难的,只要对方法进行足够细致的拆分。这样就像堆积木一样,得以足够的复用。这里我也只是做到抛砖引玉的作用,因为还有很多的方法没有讲解到,但是道理都是相似的。一切都是围绕着一个主题,就是数组的快速操作,这样理解起来就不难了。
想要学习的透彻,一定要自己动手根据理解写一份代码出来!!!
附注:
我一直觉得自己是个小白,从自己的角度尽可能细致的去分析。希望能给大家还有我自己带来一些收获。