Java ArrayList扩容初探

这篇文章为了记录我在初学ArrayList的时候对于类内扩容机制的理解

ArrayList类变量:

private static final int DEFAULT_CAPACITY = 10;

    /**
     * Shared empty array instance used for empty instances.
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * Shared empty array instance used for default sized empty instances. We
     * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
     * first element is added.
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * The array buffer into which the elements of the ArrayList are stored.
     * The capacity of the ArrayList is the length of this array buffer. Any
     * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
     * will be expanded to DEFAULT_CAPACITY when the first element is added.
     */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * The size of the ArrayList (the number of elements it contains).
     *
     * @serial
     */
    private int size;

第一个DEFAULT_CAPACITY变量是一个静态变量,看名字好像是指定数组的一个基本大小,但是具体怎么指定的我不知道,具体要看实现逻辑,而第二个和第三个都给是两个空数组,一个叫EMP...另一个叫DEFA...什么什么的,两个都是空数组,具体用来干什么的也不怎么知道,具体要看下面的实现

剩下的是一个elementData,这个数组用于储存ArrayList里面的元素,而size准确的表述是数组中的有效元素的大小

我们继续来看他的构造方法,一共有三个构造方法:

这算是第一个构造方法,很明显的是,这是一个指定创建大小的构造方法,传入整形值initialCapacity来指定ArrayList的大小是多少,如果大于零就正常创建,字面意思,如果等于零的话就用这个类里面自带的这个EMP...这个static final空数组,如果是小于零的话,就扔出去异常,没什么问题,过

这个相当于是没有传入任何的参数,是一个不带参数的构造方法,此时ArrayList里面的储存数据的数组被指定为DEF...这个空数组,这个数组和上面的那个传入0之后的空数组是不一样的,没什么问题,区分开来也挺好的,因为上一种构造方法传入0更像是指代这个ArrayList的数组大小就要为0,感觉是刻意为0的,是一种强制的行为,而这种不指定数组大小的很大可能用户并不是想让他的大小是0,而是想用你这个类里面默认的大小罢了,事实上也确实是这样子的,但是到这里,这个数组的大小为空,实际大小并不是上面的那个字面变量DEFAULT_CAPACITY所指代的10,这种设计可能在初学的时候会让人感觉困惑,但是深入理解了后续的一些方法,其实这种设计是为了不让这些刚创建的这个数组就占用默认大小的空间,而是让他们在添加元素的时候识别到了这是一个默认的空数组,按照一定的逻辑扩容之后,在扩容的时候把这个默认的大小给你了,你要是不往里面放东西,我就不给他分配默认大小,非常合理

这是第三个构造方法,旨在让一个实现了Collection接口的类来初始化一个数组,但这和文章无关,不赘述

什么时候需要扩容,那当然是当数组大小不够用,我想往里面放东西但是迫于数组大小不能再往里面放东西的时候需要合理扩容,就比如我在调用ArrayList的add或者addAll方法的时候,总会遇到有时候可能会有,或者是我要在这些方法中去检查的,所以为了研究扩容,我们要首先把视角转向为add方法或者addAll方法,但是为了方便举例,我们用最简单的尾插add来看看:

modCount代表着数组被修改的次数,和我们要研究的无关,不用管他

内部重载了一个add方法,来看看这个add方法:

写的很明确了,就是如果说传入的s(传入的参数是size,就是数组的用小元素个数)和数组大小相等了,意味着在这里,elementData到头了,elementData要接收grow方法给出的扩容后的数组了,这个grow是没有参数的,那我们再把视角转换为没有参数的grow那里:

grow方法内部委托的是一个重载方法,是带了一个参数的grow方法,这个参数的名字叫minCapacity,通过后面的深入我们清楚这个意思是这个数组最小需要被扩容到哪里,而这个参数在这个是size+1,那么我来解释一下,因为我们的那个add毕竟是尾插,我们的调用逻辑是add(E e)->add(E e,Object[] ekementData,int s)->grow()->grow(size + 1),实际上add的意思就是尾插,后面的重载的方法也是尾插,所以是仅仅增加了一个元素,然是再增加一个元素之前,被重载的多参数add察觉到了数组已经满了,所以说数组最小需要被扩容的量就是size + 1,也就是有效元素(在这里也等于数组元素个数)的个数,所以说要比原来多扩容1,也没什么问题,这里我们真正的主角就来了,就是带有一个参数的grow:

里面首先定义了个oldCapacity用来确认当前数组的长度,没什么问题,我们继续往下读,第一个if有点意思:

oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA

先说结论:这个代表着你使用了不带参数的构造方法构造的ArrayList的话,你进不去这个逻辑,因为即使是指定数组大小为0不满足第一个条件,但是第二个条件下,迫于两种构造方式的空数组是两个不同的数组,你也能满足这个条件进入正常扩容逻辑

如果要理解这个东西,我们需要更新一下我们的视角,就比如我们最开始的时候是使用的是不带参数的构造函数构造出来的一个ArrayList,此时我们想要add尾插一个,但是检测到了数组元素个数是0,有效元素个数也是0,说明这个数组已经满了(或者理解为这个数组空着的,我需要扩容),我需要扩容,所以说我按照文章上述的路径我慢慢慢慢慢的往下爬,爬着爬着我进入到了这个函数里面,非常倒霉,第一个if我进不去,我进入到了else逻辑里面,遇到了DEFAULT_CAPACITY,自此,默认初始化才完成了一个闭环

但是我们现在假设我们是有20个元素的数组满了,进来了第一个if,我们的扩容逻辑是怎么样的呢?传统的1.5倍扩容又体现在了哪里呢?

我们看if内的逻辑

newCapacity的意思就是新的容量,代表扩容后的容量

而这个容量,都被传入计算新的长度newLength方法中的三个元素决定:

第一个:oldCapacity,这个指的是旧的数组长度,不是“增量部分”仅仅是基类部分,不是重点

第二个:minGrowth:minCapacity - oldCapacity:这个指的是我们最小需要被扩容的长度,因为我们调用的是add方法,也就是一次加一个,这里的minCapacity-oldCpacity其实就是(size + 1)- (size)(这里因为length已经等于size了),也就是1,也就是字面意思的minGrowth

第三个:prefGrowth : oldCapacity >> 1,这个在即将要被调用的newLength函数就叫prefGrowth,至于为什么没展示在图片里面我也不知道qwq,oldCapacity恒正,那么oldCapacity >> 1就是oldCapacity / 2,在这里就可以看到端倪了,这个被叫做prefGrowth(偏好增长量)是0.5倍的oldCapacity,加上原来的就是1.5倍的Capacity,端倪已经出现了

细心的人已经看出来,接下来的newLength函数就是在比较到底是minGrowth充当增长量好还是prefGrowth充当增长量好,这个号或者不好的算法,我们要看newLength函数里面的具体实现了:

其实也没那么复杂的“好不好”算法,这个算法原来就是简单的Mah.max(minGrowth,prefGrowth),就是看两者谁大一些,哪个就是被选择要被扩容的量,但是最后这个接收最终长度的变量名叫prefLength,也算是真的有脸了,但是至少能说,1.5倍扩容不一定真的是偏好扩容

但是前面来说,minGrowth在这里就是1,怎么也比较不了prefGrowth啊?

是这样的,在这里我们仅仅添加了一个元素,minGrowth理应就是1,而prefGrowth却不一样,是0.5倍的数组长度,那么后者岂不是永远大于前者,而永远是1.5倍扩容了?

是这样的,但是如果说minGroth不是1呢?

也就是add并不全面,如果看看addAll方法,你会清楚,可能数组20个元素,addAll一次性要放100个元素,100远远大于元素个数的一半10个元素,所以说在这里会选择倾向于minGroth的扩容方式,不信可以看addAll方法的调用链:
你可以清楚的看到直接调用了带有一个参数的grow,意味着我要增长的大概率不只是1而已
 

小做总结:

prefLength(实际被扩容后的长度)取决于minGrowth和prefGrowth哪个更大一些,后者大于前者,那就是经典的1.5倍扩容,前者大于后者,那就是最少增长量扩容逻辑扩容

自此,疑问被解开,我也真正了解了扩容逻辑,但还是有一些没说的留了下来,比如newLength的else逻辑等等..代表着处理大数的逻辑,和本文无关,我也不说了,就这样吧,打字都打累了:(

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值