JDK源码之 ArrayList

Posted 程序员专栏

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JDK源码之 ArrayList相关的知识,希望对你有一定的参考价值。

作者丨乔二爷 
来源丨乔二爷(hellozhouq


00 前言

ArrayList 是我们日常开发中使用非常频繁的一个集合,今儿我们通过源码来看看它底层是怎么来实现的,了解了解它的优缺点和真正适合的场景。

01 内部的组成

 
   
   
 

  1. //默认容量



  2. privatestaticfinalint DEFAULT_CAPACITY = 10;




  3. // 用于空实例的共享空数组实例。在初始化时,你传进去的初始化大小为0,那么就用这个来做的处理。



  4. privatestaticfinalObject[] EMPTY_ELEMENTDATA = {};




  5. //共享的空数组实例,用于默认大小的空实例,默认构造器使用的。



  6. privatestaticfinalObject[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};




  7. // 用来存储元素的数组



  8. transientObject[] elementData;




  9. //数组的大小



  10. privateint size;


02 构造函数

先来看ArrayList 的构造函数。
JDK源码之 ArrayList
如果我们直接 new ArrayList() 使用默认的构造函数来创建它,那么它会直接使用一个空的 Object [],初始大小是0,它有一个默认的初始化大小参数 DEFAULT_CAPACITY 是 10,当添加第一个元素时,这个空数据的大小才是10,我们也可以认为它给我们创建的默认的数据大小就是10 。
平常我们一般在使用 ArrayList 的时候,会使用另外一个构造函数,可以带初始容量进去的。一般来说是可以预估出来数据的大小,比如几十个,一百个,那么我们在初始化的时候就把这个数字带进去,避免数据容量太小在后续插入数据的时候进行频繁的扩容,创建新的数组。
扩容这块放在后面说,先看它里面比较核心的几个方法。

03 add() 方法

JDK源码之 ArrayList
add() 方法很简单,先去校验需不需要扩容,然后把数据的 size 位置设置为元素,最后返回true;举个例子,比如我现在有这么一段代码需要往数据中插入3个元素:
 
   
   
 

  1. List<String> list = newArrayList<>();



  2. list.add("张三");



  3. list.add("李四");



  4. list.add("王二");


最开始 elementData = [],是一个空的数组,size = 0,当 list.add("张三") 时,elementData[0] = "张三",size++ 后,size 的值就为 1 。
然后 list.add("李四"),elementData[1] = "李四",当size++ 后,size 的值为 2,此时 elementData 的数据就是 ["张三","李四"] 。
依次类推,当 list.add("王二") ,elementData[2] = "王二",添加了王二这个数据后,elementData = ["张三","李四","王二"] ,size = 3。

04 set() 方法

JDK源码之 ArrayList
如果你现在进行 set(3,"麻子") ,首先先进行 rangeCheck(index),检查给的索引是否越界,此时数组的个数是3,但是索引下标还是2,没有索引为 3的 这个位置,就会判定为越界。
如果是 set(1,"麻子") 是没问题的,E oldValue = elementData(index) 这段代码会先把索引为 1 这个位置的数据 “李四” 取出来,那么 oldValue = “李四”;
然后把 elementData[1] = “麻子”,此时 elementData = ["张三",“麻子”,“王二”],最后把 oldValue 的数据 “李四” 返回。

05 add(index,element) 方法

JDK源码之 ArrayList
比如我们现在要在下标为 1 的位置添加“光头强”这条数据
 
   
   
 

  1. list.add(1,"光头强");


首先 rangeCheckForAdd(1) 会对传进来的 index 为 1 进行是否越界的处理 。接下来执行 ensureCapacityInternal(size + 1) 操作,是做数组扩容的,后面说。
然后来到了比较重要的代码  ,对数组的数据进行移动
 
   
   
 

  1. System.arraycopy(elementData, index, elementData, index + 1, size - index);


此时的index = 1 ,size = 3 , 我们来转化一下:
 
   
   
 

  1. System.arraycopy(elementData, 1, elementData, 2, 2);


那么这段代码表示的意思就是说 :elementData 这个数组,从第1位开始(第二个元素)拷贝,拷贝2个元素,把他们拷贝到 elementData 这个数组(还是原来的这个数组)从第2位开始(第三个元素)。
然后 elementData[index] = element ,把 elementData[1] 设置成 “光头强”,最后  elementData 的数据就是  ["张三","光头强","麻子","王二"]。
最后再 size++ 后,size的值 为 4。 

06 get() 方法

JDK源码之 ArrayList
get() 方法应该是这里面最简单的了,首先先校验index 是否越界,然后直接调用 elementData(index) 方法,这个方法就是调用数组的取值。
由于 elementData[index] 是直接通过数组直接定位这个元素,然后直接获取这个元素的,这个也是ArrayList 性能最好的一个操作,优点。

07 remove(index) 方法

JDK源码之 ArrayList
remove() 方法首先还是会校验索引是否越界,然后取出索引位置的数据,然后根据删除位置来计算需要移动元素的数量,如果移动需要移动元素的数量大于0 ,则使用 System.arraycopy 进行元素的移动,然后再把最后一个元素设置为 null,最终返回 删除的元素。
例如 我们要删除 index = 1 的元素 ,remove(1),当前数组的长度 size 是 4 , oldValue = elementData(1) = "光头强"。
numMoved = size - index - 1,numMoved = 4 - 1 -1 = 2,相当于要把后面的 “麻子”,“王二” 都要往前面移动一位。
接下来是 System.arraycopy(elementData, index + 1, elementData, index, numMoved) ,转换过来则为 System.arraycopy(elementData, 2, elementData, 1, 2)。
意思是说把 elementData 数组中,从 index = 2 的位置开始的元素,一共有 2 个,拷贝到 elementData (还是原来的数组),从 index = 1 开始,进行拷贝。
 
   
   
 

  1. 拷贝前的 elementData = ["张三""光头强""麻子""王二"]



  2. 拷贝后的 elementData = ["张三""麻子""王二""王二"]


然后再执行 elementData[--size] = null ,转化过来就是 elementData[3] = null ,把数组最后一个位置设置为 null。最后得到的 elementData = ["张三","麻子","王二"]。
最后再返回 oldValue “光头强”。

08 数组扩容

ArrayList 里面最关键的一块儿,就是数据装满以后如何进行扩容的。
假设说我们现在用的数组是默认的大小,也就是10 ,现在数据里面已经装满了10个元素,此时数组的size =10,capacity = 10。
此时我们要调用 add() 方法 插入一个元素,插入第11个元素的时候是不行的,因为数据已经满了,此时就需要扩容。
我们在 add( ) 方法中可以看到这样一行代码 :ensureCapacityInternal(size + 1);
JDK源码之 ArrayList
此时 ensureCapacityInternal(11),minCapacity = 11。
JDK源码之 ArrayList
调用 calculateCapacity(elementData, minCapacity) 后得到 minCapacity = 11
JDK源码之 ArrayList
进入到 ensureExplicitCapacity(),会发现 minCapacity - elementData.length 是大于 0 的,所以会进入到 grow() 方法里面,这里面就会进行扩容。
JDK源码之 ArrayList
grow() 方法代码
JDK源码之 ArrayList
首先 oldCapacity = 10,newCapacity = oldCapacity + (oldCapacity >> 1),oldCapacity >> 1 相当于 oldCapacity/2 也就是 10 / 2 =5,那么 newCapacity = 15。
此时新的数组 就变成了可以容纳15个元素的数据,老的数组是 10个。最后使用 Arrays.copyOf() 工具方法来完成老数组到新数组的拷贝。
看到这里你就会发现数组扩容它是怎么来扩的,是老数据的大小+ 老数组大小 >> 1(相当于除以2) 来计算出来新数组的 大小,再使用 Arrays.copyOf() 来实现数据的拷贝。

09 总结

缺点一、若我们要频繁的向ArrayList 里面塞数据,会导致它频繁的数据扩容,会导致系统性能下降,所以说不要频繁的向ArrayList 插入数据。
缺点二、ArrayList 基于数组来实现的,数组你要想往中间插入一个元素,那么你需把插入位置后面大量的元素都向后面挪动一位,性能较差。
再说优点、基于数组实现,非常适合的是随机读取,你可以随机读取任意一个位置的元素,它是基于索引,下标来直接定位元素在内存中的位置的。
我们在开发系统的时候还是会大量用到ArrayList 的,很多时候我们需要一个集合按照顺序来放一些数据,它最主要的功能就是它里面的元素是有顺序的,会按照我们插入的顺序来排列。
本文是学习石杉老师JDK源码课的一篇输出总结,结合老师的教案文档看了一遍源码,然后自己做了些补充,希望对你有用。

JDK源码之 ArrayList

近期精彩内容推荐:  

JDK源码之 ArrayList 

JDK源码之 ArrayList 

JDK源码之 ArrayList 

JDK源码之 ArrayList 


JDK源码之 ArrayList



在看点这里好文分享给更多人↓↓

以上是关于JDK源码之 ArrayList的主要内容,如果未能解决你的问题,请参考以下文章

JDK 源码解读之 ArrayList

死磕JDK源码之ArrayList

JDK源码之 ArrayList

JDK源码之ArrayList

JDK源码集合源码目录,冲冲冲

JDK源码那些事儿之常用的ArrayList