JDK源码由浅入深(二)从源码分析ArrayList和FastList

上一期分析了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只保留了最关键的核心功能,全心全意向持久层服务,因此抛弃了许多不需要的功能……

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值