上一期分析了Object,这一期分析ArrayList,也是用的最多的List。
当然有人会问,FastList是什么?
FastList是由SpringBoot提供的默认数据库连接池----->hikari连接池util包下的集合类,起名字叫做FastList可以看出来这个List有多快。。
本人膨胀的表示我能手写ArrayList,虽然人家手写的叫做FastList,但是我也不赖啊,手写SlowList写的麻溜麻溜的~
好了,不扯淡,开始我们的源码解读,如果对ArrayList有了解的,不妨看看FastList,看看人家是如何针对自己的业务优化代码的~
如果实在没耐心看源码,那就看看总结再走吧。
码字不易,点个赞再走呗。
一、分析ArrayList数据结构
1)如何存储
ArrayList不是空穴来风凭空生成,它也是由我们的Java8个基本数据类型加上一系列算法与逻辑,产生的工具类。人如其名,ArrayList的底层也就是Array---->数组。
//无法被序列化的 Object类型的 数组
transient Object[] elementData; // non-private to simplify nested class access
这个地方有人会问了,ArrayList的数组为什么无法序列化呢?(不知道啥叫序列化的同学请回去补知识~)
这个问题会结合后面的知识,在后面提到。
2)初始化
虽然说是数组,但是基础扎实的人都会问:数组的长度不是不可变化吗?那ArrayList是如何存储的?
首先咱们来看看elementData这个数组默认的初始长度是多少:
//这是带参数的构造方法~
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
//可以看到,如果构造参数长度大于0,那么elementData的初始长度就是构造参数的大小~
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
//如果是0,那就是一个长度为0的数组(而并非是null)
this.elementData = EMPTY_ELEMENTDATA;
} else {
//小于0,抛出异常。
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//这是不带参数的构造方法,居然也是空的???
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
无参构造方法居然是空的确实出乎意料,但是实际上,在JDK1.8版本之后,基本上所有的工具类都改成了这样,也就是在无参构造方法中不初始化,而是改在第一次添加元素的时候,初始化大小(类似的有HashMap)。
这样修改之后,在大量创建空数组(sql语句查询出来的结果就经常可能是空数组)时,效率会变高。
但是如果对数组进行大量的增加操作的时候,因为每次都要判断这个数组是不是空数组,反而效率变低。
其他情况,效率基本不变。
//跳过第一次初始化的代码逻辑,其实初始大小已经定义在这里了,也就是10
private static final int DEFAULT_CAPACITY = 10;
3)如何添加
ArrayList提供了两个添加方法。
一个是指定添加的位置,一个则不指定。
咱们先看看不指定位置的代码:
//不指定位置添加
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 这一步是在判断能否添加,先不管~
//size是集合已经添加的元素个数,这里有一个经典的i++和++i的问题不知大家是否看粗来
elementData[size++] = e;
return true;
}
size是集合中已经添加的元素个数,因此elementData[size]就是数组中存放的最后一个元素?错了,数组是从0开始的,因此elementData[size]就是数组最后一个元素的后一位。
所以elementData[size++]就是把添加进来的e放在数组最后一个元素的末尾,然后size++。
看不懂的去面壁。
接着再来看看复杂一点的,指定元素位置添加。
//指定位置添加元素~
public void add(int index, E element) {
//检查添加位置是否符合逻辑
rangeCheckForAdd(index);
//和上面一样的检查数组是否超出,不管他。
ensureCapacityInternal(size + 1);
//这里就很麻烦了,要将插入位置之后的元素全部向后复制一位。。。
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
//最后在指定位置插入元素
elementData[index] = element;
//插入完毕,数组大小+1
size++;
}
//检查添加的位置是否符合逻辑
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
可以看到,指定位置添加就很麻烦,每个元素都要向后移动一位,这个大家做冒泡排序算法的时候应该都写过,实现起来不难,但是麻烦。
4)存不下了的问题
既然底层是数组,那么ArrayList就必定会遇到很尴尬的问题,数组存不下了怎么办。
回过头来看看这一行代码:
ensureCapacityInternal(size + 1);//这是数组检测是否需要扩容时调用的方法
//----->跟踪之后里面是这样子的
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
这里嵌套了两个函数,别急,咱们慢慢看下去。
首先说内部的方法:
calculateCapacity(elementData, minCapacity)
其实这个方法就是判断elementData是否没有初始化,因为调用无参构造方法时,elementData={},存不下任何东西。
因为每次执行add()方法的时候,都会检测数组是否需要扩容,因此延迟加载的思路,反而限制了add方法的添加速度,所以,如果你需要的List需要经常添加修改的话,建议换一个List哦~(LinkedList:没错,就是在下。)
最后看看外部的函数:
//参数 minCapacity:目前至少需要多大的空间,也就是 size+1(如果还没有初始化,minCapacity=10)
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 出现了,如果当前至少需要空间>数组长度,说明数组存不下了,调用grow方法进行扩容。
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
接着看看grow方法:
private void grow(int minCapacity) {
// overflow-conscious code
//旧的空间大小,也就是数组的长度
int oldCapacity = elementData.length;
//新的空间大小,也就是 = oldCapacity/2+oldCapacity(jdk1.7之后使用位运算,更快)
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
//复制旧数组的数据到新的数组~
elementData = Arrays.copyOf(elementData, newCapacity);
}
咋一眼看上去脑壳晕,不过人家起名规范,仔细读的话还是很容易读懂的,至少比某某实习生大学生的代码好读(说着说着泪水就留下来了)
从上面咱们也看出来了,ArrayList如果存不下了,会扩容成原来的1.5倍,并且会扩容的数组是一个新的数组(请看arrays.copyof方法),它不会像hashMap一样,没存满就扩容,而是要完全存满。
5)如何删除
其实删除和添加基本上是一个道理,这里不过多赘述。
不过需要注意的是,ArrayList是一个死胖子,只长胖不变瘦,一个100斤的ArrayList,你就算把他remove的干干净净,它还是一百斤(捂脸痛哭)。
建议在座的各位,疫情期间不要吃饱了睡睡饱了吃,小心变成ArrayList。
二、总结ArrayList
1)ArrayList底层是一个Object类型的数组。
2)当添加时,如果不指定位置则按照数组排列的顺序添加到末尾。否则则会添加到指定位置,并且把指定位置开始的元素全都往后挪动一位。
3)因为数组存放有上限,所以ArrayList在数组完全存不下任何数据的时候才会进行扩容,每次扩容都是1.5倍。注意扩容不可逆。
三、FastList为何Fast
FastList作为目前最快的连接池,光连接池下的工具类,其优化策略可以说是相当粗暴,建议各位大哥大姐没事的时候不要乱用他,否则满屏幕的红叉叉不要说是我教的~
//直接对比add方法吧
public boolean add(T element) {
//FastList添加时需要先比较数组大小是否足够,这里只用了一个判断,而不会像ArrayList那么臃肿
if (this.size < this.elementData.length) {
this.elementData[this.size++] = element;
}
else {
//扩容策略也很简单,直接翻倍,而且少了函数之间的调用嵌套,快。
int oldCapacity = this.elementData.length;
int newCapacity = oldCapacity << 1;
T[] newElementData = (Object[])((Object[])Array.newInstance(this.clazz, newCapacity));
System.arraycopy(this.elementData, 0, newElementData, 0, oldCapacity);
newElementData[this.size++] = element;
this.elementData = newElementData;
}
return true;
}
我们在写代码的时候,有时候为了代码的严谨性,经常要加一堆的限制条件,比如检测是否大于0啊,是否小于0啊,参数是否符合逻辑啊……
虽然逻辑性是做编程必不可少的东西,但是如果这个代码只是给自己用呢?
如果我能够保证我每次输入的参数都符合规范,那么我还要检测这些参数干什么?
因此FastList就给出了这样的代码,看看它的remove方法吧:
public T remove(int index) {
if (this.size == 0) {
return null;
} else {
T old = this.elementData[index];
int numMoved = this.size - index - 1;
if (numMoved > 0) {
System.arraycopy(this.elementData, index + 1, this.elementData, index, numMoved);
}
this.elementData[--this.size] = null;
return old;
}
}
从头到尾,它都没有对这个index参数是否规范进行校验,因此当我们使用FastList的时候,如果输入的index超过数组的长度,会报出OutOfBoundsEception,可问题来了,就算使用ArrayList,如果输入的index超过数组的长度,你不照样报错吗,只不过换了个异常类而已啊(掩面)
所以我们如果嫌弃jdk提供的工具类不符合自己的需求的时候,也可以自己手写一个FastList这样的工具类,当然,千万别写成了SlowList……
但是使用也需要谨慎~,因为:
有没有被吓到……
实际上FastList只保留了最关键的核心功能,全心全意向持久层服务,因此抛弃了许多不需要的功能……