ArrayList动态数组精讲

区别于array,数组为基本的数据结构为定死的. ArrayList 则是动态扩容的数组相当于C++或Python的List.

1. 数组的底层原理

在这里插入图片描述
内存管理器在你申请一个新数组的时候开辟了一段新的内存地址:
注意一个int类型为32位, 一个内存地址只能存储8位, 因此数组中的一个数据对应的是一个地址块地址块中包含4个连续的地址, 地址是32位的. 两个相邻的数据相差一个地址块即32位, 因此数组指针的单位为4Bytes-每次加一相当于加了32bits.

2. 动态数组的操作

2.1 数组对元素的访问

通过指针可以迅速访问到任何一个元素, 这时查询只进行一次复杂度是常数O(1).

2.2 数组的增添

数组增添操作的复杂度

数组增加元素都涉及到插入位置之下元素的平移, 因此复杂度与插入位置成线性关系所以为线性复杂度O(n).
在这里插入图片描述
接下来会深究源码:

a. 向末尾添加-Public boolean add(E e){}

在这里插入图片描述
上图是调用addFunction的时候实际的Function的访问顺序
在这里插入图片描述上述代码是在末尾添加, 若数组容量不够会复制扩容, 再在数组末尾加上该元素. 注意其中的ensureCapacityInternal(size+1) 这是用来确保数组当前容量够用的函数.

在这里插入图片描述
上述代码中的elementData是ArrayList类中数组(基础数据类型)存储的当前动态数组内容.
从这里我们可以看出动态数组的底层实现还是数组只不过用了复制等方法封装了函数扩容.
这一部分中的minCapacity其实就是上面传进来的size+1也很形象啊满足需求所需的最小值.
这一步进行了数组是否为空的判断为空不用想了一定不够用, 因此当数组为空时将输入的size+1 与 类里定义的最小容量10作比较取最大值(如果size+1<10 也取10因为这是动态数组的最小值. 这一步的目的是明确最小所需容量到底是多少引入了是否为空的判断. 揭晓来进入确保明确的容量, 很明显当明确的容量值得到后就该确保有这么多了.
在这里插入图片描述
这一步的功能十分简单就是判断上面得到的所需要的容量是否满足, 注意此时minSize有两种可能性

  1. 此时minSize=10, 这是elementData=[]的情况, 因为此情况下size+1=0肯定小于10.
  2. 此时minSize=size+1, 有可能小于10因为可能初始化的时候小于10.
    接下来进入了ArrayList的核心方法-grow()-实现动态扩容的方法.

在这里插入图片描述
其中第二句新的容量等于旧的容量的1.5倍, >>1代表右移1位(2进制)比如旧容量为8就是1000那么右移以为就变成0100就是4, 所以这一步其实是新容量=旧容量+0.5旧容量. 接下来再进行判断, 如果说还是不够就不费事了就直接让新容量=最小所需容量(注意这里不一定是10或者size+1, 很有可能是直接调用了这个方法. 接下来在判断新容量和最大容量的关系, 最大容量的定义是最大整数值-4. 如果新的容量大于了最大容量则返回能给的最大值. 接下来这一步很重要就是把旧的数组拷贝到新的容量为newCapacity的数组里(基本类型).
注意这里处理完后即有了充分的容量后会回到add Function 去执行下一句即添加元素.
在这里插入图片描述
这个便是返回最大容量的函数了, 按理说传到这步不可能再等0了可能是为了防止直接被调用出错, 又加入了一个minSize和0的判断, 若小于0直接扔出异常, 判断minCapacity是否大于最大容量, 大于返回整数的最大值, 否则返回定义好的最大容量
在这里插入图片描述
注意这里的minCapacity是上一步传进来的最小容量而不是新的容量, 因为新的容量扩展为两倍大于最大容量, 理所当然应该传进来最小的满足容量. 换句话说grow() 倾向于将容量扩充为两倍当扩充为两倍后大于了最大容量是不是应该把最小满足条件传过去判断能给的最大值是多少? 因为很有可能最小满足容量不大于最大值呀. 但是这个函数的处理有些问题如果可能的话应该尽可能地返回最小满足容量.
在这里插入图片描述
这是最后一步再确定容量后在数组末尾加上元素并且吧size+1.

两个例子:
在这里插入图片描述
这里可以很明显的看到list.add 中有一条分线使用来保证容量的, 在容量保证后返回add.
上面之所以为10 是因为ensure中把所需的1和default的10取了最大值(因为此时数组为空)
在这里插入图片描述
注意这里数组是不为空的第一步画错了 应该是ArrayList < Intege r>(6).
因此在ensure中没有进入数组为空的判断条件因此最小容量为1.

b. 按指针位置插入- Public void add(int index, E element){}

在这里插入图片描述上述代码是在指定位置插入元素, 并且将后续元素后移, 本质是通过数组的复制实现的. 在这里插入图片描述
第一位为原始的数组, 第三位是最终复制后得到的数组, 复制的内容是由第2位和第5位决定的, 第二位决定的是复制的起始位置, 第5位确定的是复制的从复制起点开始的长度, 第四位决定的是复制的内容在最后得到的数组中的位置.

a, b 方法不同处分析
  • 这里的话选择了直接复制数组, 上面当然也可以这么做因为其实上面就是这个的特例, 不过不用说我们也能感觉出来数组的复制至少是个线性复杂度(要用到循环或者遍历器) 接着空间的复杂度和上面的其实一样.
  • 然而为什么上面的方法先判断, 因为最理想的情况是数组长度够用那我们直接末尾加1, 很方便快捷, 复杂度为常数复杂度, 然而很有可能数组长度不够因此必须要进行上述的判断.

2. 3 数组的删除

a. Public E remove (int index)根据指针删除

这一步是有返回值的返回的是被删除的元素.

在这里插入图片描述
这里比上面方便多了是因为一定涉及数组的复制因此用了上面b的方法.
这个因为一定有数组的复制因此复杂度为线性复杂度Linear Complexity.

b. Public boolean remove (Object o){}

这一步也是有返回值的是是否删除成功和add a方法一致.
在这里插入图片描述
这是根据传入的元素删除元素, 传入的元素可以为空效果为删除所有空值.
这个函数的复杂度还是线性复杂度.
如果传入的元素为空则遍历整个elementData去删除为空的元素.
如若不是空的则还是遍历整个数组去寻找是否相等并删除.
这里用两个if本质是因为为null的相等判断和对象的相等判断不同:
1. elementData[index]==null
2. o.equals(elementData[index])

c. Protected void removeRange(int fromIndex, int toIndex)

这个可以根据传入的参数删除指定的区间内容:[fromIndex, toIndex)没错toIndex删不掉.

 1 protected void removeRange(int fromIndex, int toIndex) {  
 2      modCount++;  
 3      int numMoved = size - toIndex;  
 4          System.arraycopy(elementData, toIndex, elementData, fromIndex,  
 5                           numMoved);  
 6    
 7      // Let gc do its work  
 8      int newSize = size - (toIndex-fromIndex);  
 9      while (size != newSize)  
10          elementData[--size] = null;  
11 }  

从这里面看这个方法的实现还是建立在System.arraycopy()上的, 之前也介绍过了fromIndex是复制块最后到的位置, 复制块是从toIndex 到size-toIndex(数组后面剩余的元素).
但是注意了这个方法是protected方法那么有3个办法去实现removeRange的使用.

(1). 用继承类去调用
Protected 只能同类同包以及不同包的子类使用

public class JavaApplication5<E> extends ArrayList<E> {

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        JavaApplication5<Integer> a=new JavaApplication5<Integer>();
        a.add(0);
        a.add(1);
        a.add(2);
        a.add(2);
        a.add(2);
        a.add(5);
        a.add(null);
        a.removeRange(0,4);
        }
        }

这里的输出是[2,5,null] 可以看到toIndex指向的元素没有被删除.

(2). System.arraycopy(elementData, toIndex, elementData, fromIndex, numMoved)

        int[] c= {0,1,2,3,0};
        System.arraycopy(c, 2, c, 0, 2);

输出为什么呢输出为 2, 3, 2, 3, 0
之所以是这样因为从toIndex到toIndex后面两位是2,3 把这两位拷贝到了fromIndex的位置就是0.

(3). 用.subset(fromIndex, toIndex).clear()的方法来实现清除哈

        ArrayList <Integer> a=new ArrayList<Integer>();
        a.add(0);
        a.add(1);
        a.add(2);
        a.add(2);
        a.add(2);
        a.add(5);
        a.add(null);
        a.subList(0, 4).clear(); 
        System.out.println(a);

.subset是ArrayList父类abstractArrayList的一个非抽象方法. 运行结果和上面一样.

d. Public void clear(){}

这个函数用来清空整个数组等待回收机制.

在这里插入图片描述
直接遍历整个数组并把里面的所有元素赋值为null.

e. Public boolean removeAll(Collection<?> c){}

这是个批量删除的函数

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
从上面的代码可以看除a会删除其中所有被b包含的元素.

2.4 其它比较重要的操作

1. Public void ensureCapacity(int minCapacity){}

这个函数是用来一次性扩充容量的可以减少时间复杂度, 比如很有可能在增加过程中增加了
多次, 这种情况下可以先用这个函数一次性扩充一下, 扩充的是elementData的容量, 注意容量
可不等于size, 容量是ArrayList的底层数组容器的长度array.length(), 而size是其有效含量数目,
所以就算你ensure(100000), 你调用size(), 里面该有多少还是多少但是elementData长度变了.
在这里插入图片描述
这里又调用了ensureExplicitCapacity(), 这个Function其实就是ensureCapacityInternal(),
只不过那个是私有的访问不到所以用这个提供个访问接口.

2. Public int indexof(Object o){}

这个函数是根据传入的对象检索这个对象所在的位置并返回其对应指针如若不含则返回-1,

在这里插入图片描述

同样哈这个跟上边的那个remove(Object 0), 除了返回值不一样其他完全一模一样, 拆成两个的目的也说了是为了处理不同的判断语句.

3. Public boolean contains(Object o){}

在这里插入图片描述

这个是用来判断是否包含所含的对象的, 这个十分简单调用indexOf(o)看返回值是否为-1.

4. Public E set (int index, E element){}

这个函数通过传入index和新元素来将索引位置处的元素改为新元素同时返回旧元素.
在这里插入图片描述

5. Public E get(int index){}

这个也十分简单用了数组的查询方式这不过封装了一层get函数目的是检查index是否合理.
在这里插入图片描述

6. Public boolean retainALL(Collention<?> c){}

想必大家也发现了这个代码传入参数和removeAll很像, 没错不仅名字像, 他们调用了相同函数,
就是上面的batchRemove(), 这个batchRemove()有两个作用一个是去重一个是检查是否包含,
这样是通过传入的flag一样的参数complement实现的, complement为false为true对应具体的.

在这里插入图片描述
这里面的返回值是boolean, 但其实通过batchRemove是可以得到重复值和重复数的.
这个返回值没有什么意义, 但有意义的是调用这个函数的数组会被改写和remove一样, 只是
更改完以后数组里面试两个数组的交集.
在这里插入图片描述

3. Conclusion 和 Improvement:

  1. ArrayList的底层就是array-一种基本类型(没有具体的方法和类). 说的再过分点就是ArrayList牺牲了大量的时间效率封装了array, 本质上就是elementData数组.
  2. ArrayList可以存放null, 但其不占size打印也不会显示(除非在中间)
  3. ArrayList有着高效的查询和和低效的插入删除操作, 但是末尾加入不一定低效.

目前没写构造器, batchDelete的处理, Iterator, sort 和继承关系图.

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值