前面忘了说 博客对于ArrayList的源码剖析 针对的是JDK8
ArrayList的常用方法还没有测试,这次来测试阅读ArrayList的核心方法,当我们添加元素,超过了容器的初始容量底层会发生什么,然后测试这些核心方法。
其实之前已经看过这种情况的源码了,其实就是底层的ensureExplicitCapacity()来判断是否扩容,这个方法翻译成中文意思是:确保精确容量。
这次再复习一下吧。
点击下一步,这里这里容量是2,前面添加的两个元素 不会超出容器的容量,所以下图中的这个逻辑判断是false,
不会执行grow() 扩容函数。这三个add()前两个底层执行过程一模一样,第三个跟前面在ensuerExplicitCapacity()这个函数 只有一处区别 就是if 判断为true 执行了grow()函数,进行扩容,因为此时添加了一个元素,最小容量也得是3,已经超出了初始容量2.
现在直接看第三个add()的底层执行
点击下一步,此时minCapacity是3
再次点击下一步。判断一下,这个容器使用的是不是无参构造。这里肯定没有
再次点击下一步,如果最小容量比容器容量大就开始执行扩容函数
再次点击下一步,这里把旧的底层数据容量2 拿给oldCapacity这个变量
然后这个变量自身加上自身的值右移一位 等于3
然后判断新容量是否小于最小容量
这里都不会执行 直接跳到最后一步,把底层数据扩容到newCapacity。然后弹栈,最后一句add()方法执行结束
这里可以看到add()方法有两个,而且都是重写父类AbstractList的add()方法
这里写上一段,测试程序。add(int index,E element) 这个函数是在一个索引位插入一个元素,这个索引位后面的元素依次往后移。这里我们先猜测一下底层源码,插入元素,应该判断插入位置是否合法以及这个数组的容量还够不够。
学过数据结构的应该知道,数组适合查找,但是如果是增加(插入)或者删除,效率非常低,因为在中间插入一个元素,这个索引位后面的依次往后移动。
点击下一步,开始调试,正如我们所想,一开始判断下标是否越界
我们这里再点下一步,进这个函数看看,这里判断索引index 不能大于size 也不能小于0
也就是说下标不能为负,如果没有元素只能往0号添加,如果有元素只能往这些元素的中间或者最后添加,不能添加到比这些元素的最后还最后,比如这里容量为30. 里面有2个元素,不能添加到索引位3及以后的位置
再点击下一步,这里遇到个非常熟悉的函数ensureCapacityInternal();此时最小容量是3 没有超过容器的容量
所以不会执行ensureapacityInternal()里面的ensureExplicitCapacity()里面的grow()函数
点击下一步,会到add()方法,
这里可以看到这里并没有让索引位后面的数依次往后移,而是数组的自拷贝。
从element数据的index位置开始复制 ,复制到element数组的index+1位置开始
而要知道index这个索引位 在这个索引位开始都往后移完成了之后,这个索引位后面有多少元素
就是size-index。
这里执行数组的拷贝后还剩下0号索引,刚好让我们能往这个位置插入元素,然后容器的实际数据大小加1.
添加还有两个方法addAll(),之后再阅读他的源码,现在先把重要的删除等核心方法看看。
现在开始阅读remove()方法。可以看到这里有5个remove类似的方法。
这里我们就只暂时只测试remove(int index)
点击下一步,remove()这个函数会移除指定位置的元素,并且index之后的元素依次左移一位,如果不补空位
会导致浪费空间
点击下一步,先看索引位是否合法,这个rangecheck()跟之前的不一样,这里index不能等于size,否则就是 删除数组里面最后一个数据的下一位,这显然是不行的,既然是最后一个元素下一位肯定是没有元素的。
点击下一步,这里执行删除方法会影响数组的数据的数据量大小,所以执行modcount++
点击下一步,这个函数也很熟悉了,传个索引位进去,返回对应索引位的数据
然后把这个返回值赋值给E(这里是Integer)类型的oldValue变量。
再次点击下一步,如果要想知道一个索引位后面有多少个数,那么就是数据的实际长度 - 索引位 - 1
这里得到要移动的长度
然后判断要移动的长度,决定是否调用system.arraycopy(),这里的逻辑判断其实就是如果不是删除的最后一个,就会去自拷贝
system.arraycopy() 这个函数是JVM底层由c++实现了,这里不好看源码,知道作用就行
然后点击下一步,因为索引位后面的全部往前移一位,所以最后面肯定会空一位
所以把最后一个赋为null,然后直接返回这个被删除的元素
运行结果