JDK源码之 ArrayList
作者丨乔二爷
来源丨乔二爷(hellozhouq)
00 前言
ArrayList 是我们日常开发中使用非常频繁的一个集合,今儿我们通过源码来看看它底层是怎么来实现的,了解了解它的优缺点和真正适合的场景。01 内部的组成
//默认容量
privatestaticfinalint DEFAULT_CAPACITY = 10;
// 用于空实例的共享空数组实例。在初始化时,你传进去的初始化大小为0,那么就用这个来做的处理。
privatestaticfinalObject[] EMPTY_ELEMENTDATA = {};
//共享的空数组实例,用于默认大小的空实例,默认构造器使用的。
privatestaticfinalObject[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 用来存储元素的数组
transientObject[] elementData;
//数组的大小
privateint size;
02 构造函数
先来看ArrayList 的构造函数。如果我们直接 new ArrayList() 使用默认的构造函数来创建它,那么它会直接使用一个空的 Object [],初始大小是0,它有一个默认的初始化大小参数 DEFAULT_CAPACITY 是 10,当添加第一个元素时,这个空数据的大小才是10,我们也可以认为它给我们创建的默认的数据大小就是10 。平常我们一般在使用 ArrayList 的时候,会使用另外一个构造函数,可以带初始容量进去的。一般来说是可以预估出来数据的大小,比如几十个,一百个,那么我们在初始化的时候就把这个数字带进去,避免数据容量太小在后续插入数据的时候进行频繁的扩容,创建新的数组。扩容这块放在后面说,先看它里面比较核心的几个方法。03 add() 方法
add() 方法很简单,先去校验需不需要扩容,然后把数据的 size 位置设置为元素,最后返回true;举个例子,比如我现在有这么一段代码需要往数据中插入3个元素:List<String> list = newArrayList<>();
list.add("张三");
list.add("李四");
list.add("王二");
04 set() 方法
如果你现在进行 set(3,"麻子") ,首先先进行 rangeCheck(index),检查给的索引是否越界,此时数组的个数是3,但是索引下标还是2,没有索引为 3的 这个位置,就会判定为越界。如果是 set(1,"麻子") 是没问题的,E oldValue = elementData(index) 这段代码会先把索引为 1 这个位置的数据 “李四” 取出来,那么 oldValue = “李四”;然后把 elementData[1] = “麻子”,此时 elementData = ["张三",“麻子”,“王二”],最后把 oldValue 的数据 “李四” 返回。05 add(index,element) 方法
比如我们现在要在下标为 1 的位置添加“光头强”这条数据list.add(1,"光头强");
System.arraycopy(elementData, index, elementData, index + 1, size - index);
System.arraycopy(elementData, 1, elementData, 2, 2);
06 get() 方法
get() 方法应该是这里面最简单的了,首先先校验index 是否越界,然后直接调用 elementData(index) 方法,这个方法就是调用数组的取值。由于 elementData[index] 是直接通过数组直接定位这个元素,然后直接获取这个元素的,这个也是ArrayList 性能最好的一个操作,优点。
07 remove(index) 方法
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 开始,进行拷贝。拷贝前的 elementData = ["张三","光头强","麻子","王二"]
拷贝后的 elementData = ["张三","麻子","王二","王二"]
08 数组扩容
ArrayList 里面最关键的一块儿,就是数据装满以后如何进行扩容的。假设说我们现在用的数组是默认的大小,也就是10 ,现在数据里面已经装满了10个元素,此时数组的size =10,capacity = 10。此时我们要调用 add() 方法 插入一个元素,插入第11个元素的时候是不行的,因为数据已经满了,此时就需要扩容。我们在 add( ) 方法中可以看到这样一行代码 :ensureCapacityInternal(size + 1);此时 ensureCapacityInternal(11),minCapacity = 11。调用 calculateCapacity(elementData, minCapacity) 后得到 minCapacity = 11进入到 ensureExplicitCapacity(),会发现 minCapacity - elementData.length 是大于 0 的,所以会进入到 grow() 方法里面,这里面就会进行扩容。grow() 方法代码首先 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源码课的一篇输出总结,结合老师的教案文档看了一遍源码,然后自己做了些补充,希望对你有用。近期精彩内容推荐:
在看点这里好文分享给更多人↓↓
评论