

ArrayList分析1-循环、扩容、版本 - funnyZpC
source link: https://www.cnblogs.com/funnyzpc/p/16407733.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

ArrayList分析1-循环、扩容、版本
转载请注明出处 https://www.cnblogs.com/funnyzpc/p/16407733.html
前段时间抽空看了下ArrayList
的源码,发现了一些有意思的东东,真的是大受裨益哈,尤其是版本问题😊
所以,本篇博客开始我将大概分三篇讲讲ArrayList
里面一些有意思的点哈,由于源码大概一千八百逾行,里面大多代码都很通俗,也有些部分存在重复的(Itr
以及SubList
的内部方法),因为大多通俗遂这里不会逐行的分析哈,好了,现在开始~😂
一.关于循环的一个问题
首先,我给出一个很easy的循环
:
public static void main(String[] args) { for(int i = 0;i<8;i++){ System.out.print(i+"\t"); // 0 1 2 3 4 5 6 7 } }
看起来很简单吧,哈哈,这时我会问:各位有没试过将i
提到for
循环外边呢,像下面这样:
public static void main(String[] args) { int i; for(i = 0;i<8;i++){ System.out.println(i); } System.out.println(i);// ? }
上面第六行的i
会输出什么呢?真是个有意思的问题,这真是一个微小而有意思的问题,我们经常使用,却很少利用for
去做一些别样的事儿,ArrayList
就有一骚操作,
原本我是准备臆测出来,却发现怎么也理解不了,当然啦,这个问题接下来我会说到:探究这个问题我们先看看一个普通的for循环的结构
for(定义1;定义2;定义3){ //定义4 :循环内的语句块}
个人文采拙劣,这里就用定义
一词哈😳
定义1: 这个地方我们经常会用int i=0;
这样一个语句,其实这个地方是对循环的变量做一次定义
,这个地方的定义是一次性的,而且是第一次循环的时候会执行。
定义2: 这里一般是个判断性的表达式,而且这个地方的整体必须返回一个boolean
,这个很重要,既然这个地方只需要返回一个布尔的结果,那么你想过没有,如果这个地方 我写的是 (i<10 && i>=0)
会不会抛错呢 🤣
定义4: 这是循环内语句块,通常我们会取到当前循环到的i
进行某些逻辑处理,这里不是重点哈。
定义3: 这个地方是重点,一般我们会说每次循环后我们会将i--
或者i++
, 这种循环变量变化我们一般都会写在这个位置,这是_very very normal_的,但问题是每次执行完定义4
的部分 就一定会执行定义3这个地方嘛? 答案是:一定会的!,为什么呢,看看生成的字节码指令就知道了哈🌹 :
0 iconst_01 istore_12 iload_13 bipush 85 if_icmpge 21 (+16)8 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>11 iload_112 invokevirtual #3 <java/io/PrintStream.println : (I)V> //打印i15 iinc 1 by 118 goto 2 (-16)21 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>24 iload_125 invokevirtual #3 <java/io/PrintStream.println : (I)V> //打印i28 return
以上是main函数内的完整字节码内容(jdk=java8), 可以看到指令内有两处println
,自然第一个println
即是for循环
内的(标号12
处的),下面一行就很重要了,官方描述是:将局部栈帧的索引+1(see: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.iinc),说明白些也就是将i加一,然后就到了标号18
这个位置,goto
是将当前语句指向标号2
这个位置 将store
的i
加载...到这里也就很明白了, goto
指令是在i自增1
之后,可以完全确认循环外的println
打印的就一定是 8
!
看似简单的操作 ArrayList 则时常使用,比如可以用i
循环,循环完成后,数组的大小不就是这个i
了?以下ArrayList
->Itr
内的一段代码:
// 循环每个剩余操作 // 这是java8提供给iterator的函数式循环接口,其使用方式如下 // ArrayList arr = new ArrayList(); // arr.add("a"); // arr.add("b"); // arr.add("c"); // System.out.println(arr); // Iterator iterator = arr.iterator(); // iterator.next(); // a // iterator.forEachRemaining(item-> System.out.println(item)); // b c @Override @SuppressWarnings("unchecked") public void forEachRemaining(Consumer<? super E> consumer) { // 检查是否为null,否则抛出错误 Objects.requireNonNull(consumer); // 获取当前数组大小并检查迭代器的游标位置是否大于数组大小 final int size = ArrayList.this.size; int i = cursor; if (i >= size) { return; } // 老实说 elementData.length 与 ArrayList.this.size 是一对一关联的,这里这样做似乎多余 final Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) { throw new ConcurrentModificationException(); } while (i != size && modCount == expectedModCount) { // 消费这个元素,同时将游标位置+1 consumer.accept((E) elementData[i++]); } // update once at end of iteration to reduce heap write traffic 在迭代结束时更新一次以减少堆写入流量 // 因为i在以上已经+1了,所以这里直接赋值以及重置当前迭代的索引位置(lastRet) cursor = i; lastRet = i - 1; checkForComodification(); }
上面这是迭代器内的部分迭代方法的定义,可以看到有句 cursor = i;
这个地方的i
其实也就是size
哈,稍不注意就理解错了,小问题让大家见笑了...😂
二.ArrayList
扩容
ArrayList
内部其实也就是维护了一个 Object
类型的数组
,它具体是这样定义的transient Object[] elementData;
,看起来是不是超简单呢?呵呵呵,如果将ArrayList
看作成一个大水缸的话,这个elementData
就是水缸的本体,ArrayList
可以看做一个用户态的接口,简单理解它其实就是是水缸外层刷的油漆或盖子抑或是漏斗。
打比方,如果这个水缸 (elementData )能装四桶水,那么这四桶水我们用 initialCapacity 这个变量来表示,当前实际上如果只有两桶水,则这个水缸实际储水容量(两桶水)我们用size来表示,这样就好理解了吧?
好了,如果预先知道将有8桶水
倒入缸内,那我们就要准备一个能容纳8桶水
的水缸,对于代码就是这样的:public ArrayList(int initialCapacity) {....}
当然,如果我们只允许用一个水缸
来储存水的话,这个水缸当前如果是满载
(8桶水),第9桶水
的时候就需要准备一个至少能容纳9桶水的水缸,对于ArrayList
来说这时候就需要扩容了,代码是这样的:
// 確保顯式容量(官方直译,不懂直接看代码) private void ensureExplicitCapacity(int minCapacity) { // 这个变量记录的是当前活动数组被修改的次数 // 每添加一个(准确的说是一次)元素修改次数+1,如果是addAll也算做是+1 modCount++; // overflow-conscious code 判断是否溢出 // 可以简单的理解 minCapacity 为当前需要保证的最小容量(具体大小为当前容量+1:这是对于当前add元素的个数而定的),elementData.length则为当前活动数组的容量 // minCapacity 也为添加元素后所需数组容量大小,如果(所需容量)大于当前(添加前)数组容量即需要<b>扩容</b> if (minCapacity - elementData.length > 0) grow(minCapacity); //增长(扩容) }
当然这时本着少腾挪多储水的原则
,我们一般不会准备一个只能容纳9桶水
的水缸,水缸太大也不好,容易浪费缸的容量维护也麻烦😓,所以对于ArrayList
这个水缸,我们每次增长为现有容量的1.5
倍(多了50%左右,如果当前Capacity是10->增长到15,9->增长到13),具体对应到ArrayList的代码就是:
/** * Increases the capacity to ensure that it can hold at least the * number of elements specified by the minimum capacity argument. * 增加容量以確保它至少可以容納最小容量參數指定的元素數量。 * @param minCapacity the desired minimum capacity 所需的最小容量(也即当前需要的容量大小) */ private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; // 这里的增长策略是 oldCapacity=10 -> newCapacity=15 oldCapacity=9 -> newCapacity=14 // 即 每一次增长的为上一次的一半 int newCapacity = oldCapacity + (oldCapacity >> 1); // 这里个人觉得只是一个保险,对于类似addAll这样的操作 newCapacity 可能小于一次add的数量 // 比如当前容量是10[oldCapacity:10->newCapacity:15],addAll(100)后所需的容量还是不够 这时就会出现[newCapacity:100] if (newCapacity - minCapacity < 0) newCapacity = minCapacity; // 这里也是一个保险,对于待扩容后的大小比数组最大(MAX_ARRAY_SIZE)还要大的时候启用hugeCapacity(minCapacity) // 这里调用 hugeCapacity 后顶多扩容8个大小 MAX_ARRAY_SIZE=2147483639(Integer.MAX_VALUE-8) if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: // 翻译: minCapacity 通常接近 size,所以這是一個勝利 // Arrays.copyOf: // 複製指定的數組,截斷或填充空值(如有必要),使副本具有指定的長度。對於在原始數組和副本中都有效的所有索引, // 這兩個數組將包含相同的值。對於在副本中有效但在原始副本中無效的任何索引,副本將包含 null。 // 當且僅當指定長度大於原始數組的長度時,此類索引才會存在。結果數組與原始數組的類完全相同。 elementData = Arrays.copyOf(elementData, newCapacity); }
上面有一句很重要 : int newCapacity = oldCapacity + (oldCapacity >> 1);
,每次换用更大的水缸时都会将之前缸内的水兑过来
,对应ArrayList
就是这句:elementData = Arrays.copyOf(elementData, newCapacity);
算是很形象吧😅,这只是简单的理解,扩容
也还有很多内容,比如什么时候扩容,扩容一个单位不够怎么办,满了怎么办???等等问题....
三.ArrayList
中的版本管理
一开始大家会觉得这是个奇怪的问题,ArrayList中为啥会有版本,版本做什么用?
首先,我详细解答第一个问题:ArrayList
中为什么有版本?,首先先看一段ArrayList
的源码,关于Iterator
的:
(以下为第一段代码)
public Iterator<E> iterator() { // 虽然与都会返回一个迭代器,但是iterator只能单向循环,且不能实现增删改查 // 详见: https://www.cnblogs.com/tjudzj/p/4459443.html return new Itr(); }
(以下为第二段代码)
public E next() { // 版本检查 checkForComodification(); // 这个将游标赋予i,然后检查是否i是否超出当前数组索引位置(size) // 我暂时没看出以下三行跟 hasNext() 有多少区别。。。,而且checkForComodification内也是做了安全检查了的 // 总结就是:十分没必要啊... int i = cursor; if (i >= size) throw new NoSuchElementException(); // 不大明白为啥要再整个引用 ,通过这个新引用索引返回数组值 Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) throw new ConcurrentModificationException(); // 因为迭代器每次循环前都会调用 hasNext ,故此推测这里也应该将游标+1 cursor = i + 1; // 需要说明的是这个 lastRet 是个成员变量,而i只是个方法内临时变量而已 // 所以每循环一次这个 lastRet 需要记录为当前返值前的当前索引位置 return (E) elementData[lastRet = i]; }
(以下为第三段代码)
//// example: // ArrayList arr = new ArrayList(); // arr.add("a"); // arr.add("b"); // arr.add("c"); // ListIterator listIterator = arr.listIterator(); // arr.remove("a");// throw error // Object previous = listIterator.previous(); final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
额,由于只是截取了部分代码,我先简单讲讲上面三段
代码,第一段
代码很明显,我们使用迭代器的时候首先调用的就是集合类的 iterator()
方法,而 iterator()
内部 只做了一件事儿:new Itr()
,现在知道迭代器是一个类对象
,这点很重要。
继续看第二段
:next()
方法,这个方法内部第一行代码 是 checkForComodification()
, 这是一个较为特殊的存在,点进去会看到以上第三段
代码,if判断
内有两个参数 ,一个是 modCount
(直译为修改次数),第二个参数为 expectedModCount
(预期修改的次数),点到这个参数定义,它只有一句很简单的定义int expectedModCount = modCount;
,是不是很迷糊😂?
在next()
内还有一句也很重要 Object[] elementData = ArrayList.this.elementData;
,这句估计很好懂了,Itr
迭代器内使用的数组其实也就是ArrayList
中维护的数组对象(elementData
),倒退一步,再往回思考下 checkForComodification()
看...
不知读者老爷有没恍然大悟,其实很简单啦: Itr
对象不希望你在使用Itr
迭代器的过程中修改(主要是增删)ArrayList
中的(elementData
)元素,不然在迭代的时候源数组少了个元素会直接抛错
的,Itr
内的expectedModCount
只会在 new Itr()
时被赋值一次,这就是很好的证明啦~Itr
是Iterator
的实现,里面只有迭代
的操作,如果有更复杂的操作,比如ListItr
(是Itr
以及ListIterator
的继承实现) 里面更是对迭代器增加了增删改查
方法,以及SubList
这个对象内部也是,内部均是对ArrayList
维护elementData
直接操作(他们并未拷贝elementData
),所以里面的增删操作不仅仅要比较ArrayList
的elementData
版本,也要在操作(增删)之后同步ArrayList
的modCount
版本,以下是ListItr
内的add(E e)
函数的源码:
public void add(E e) { checkForComodification(); try { // ArrayList的添加方法,在游标位置添加元素,游标及之后的元素往后移动, int i = cursor; // 这里还需要注意的是这个插入是在当前元素之后插入元素,ArrayList则是在元素之前,这主要是游标是当前位置+1 ArrayList.this.add(i, e); // 因为增加了个元素,所以游标的位置要+1,当前位置lastRet会在下一次调用next或previous时会被重置 cursor = i + 1; lastRet = -1; // 这个其实类似于版本的概念,主要由于ArrayList与Iterator修改不同步,expectedModCount,这个会在 checkForComodification() 中进行校验 expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } }
以上代码中有这样一句 expectedModCount = modCount;
不知读者们明白否。。。😂
ArrayList
中为啥有版本管理,版本管理怎么用?
好了,我总结下本小节开头的的两个问题
首先版本管理就是在增删元素的时候对 modCount
自增1
因为对ArrayList
的迭代器 Itr
、ListItr
以及SubList
(截取类) 他们是单独类对象同时内部也是直接操作的ArrayList
的源elementData
数组对象,所以在ArrayList
添加元素时这三个类内部方法均不知道数组元素个数
已发生变化,所以在操作elementData
时候均需要判读版本
是否一致,这就是为啥有版本;
他解决的是:这几个类在操作 elementData
(ArrayList
的)时 ArrayList
可能对其的增删导致的版本
不一致的问题,总结似乎臭长了些,但就是这么个意思😂
理解这个很重要,不然你在读 ArrayList
的 add
、remove
这类方法时不知modCount
作甚云云,哈哈。。。
Recommend
-
109
ArrayList部分一共五篇文章了,并且引入了时间复杂度来分析,强烈建议大家一定要按顺序阅读,本文是第2篇,相关文章分别是:1、ArrayList初始化 - Java那些事儿专栏再次强调,ArrayList是一个普通的类,如果我们开心,可以自己写一个。Arr
-
59
ArrayList实现分析(二)——常用操作alexwu592019.08.20 15:42:01字数 1,605阅读 187上一篇文章
-
9
写在最前面 这个项目是从20年末就立好的 flag,经过几年的学习,回过头再去看很多知识点又有新的理解。所以趁着找实习的准备,结合以前的学习储备,创建一个主要针对应届生和初学者的 Java 开源知识项目,专注 Java 后端面试题 + 解析...
-
4
ArrayList扩容机制分析 首先是看 ArrayList 的构造函数,在 JDK8 中,ArrayList 有三种方式来初始化无参构造,创建空数组带初始容量的有参构造
-
6
ArrayList是我们开发中最常用到的集合,但是很多人对它的源码并不了解,导致面试时,面试官问的稍微深入的问题,就无法作答,今天我们一起来探究一下ArrayList源码。 ArrayList底层是数组,允许元素是null,能够动态扩容 size、isEmpty、get...
-
4
性能测试:springboot-2.x vs actix-web-4.x benchmark 转载请注明出处 https://www.cnblogs.com/funnyzpc/p/15956465....
-
5
ArrayList分析2 : Itr、ListIterator以及SubList中的坑 转载请注明出处:ht...
-
5
dolphinscheduler简单任务定义及跨节点传参 转载请注明出处 https://www.cnblogs.com/funnyzpc/p/16395094.html ...
-
7
datax开启hana支持以及dolphinscheduler开启datax任务 前面(@,@) 前段时间因为要做异构数据导入导出,所以搜了下,发现这类工具收费的居多,使用起来未必趁手~ 于是我找...
-
4
dolphinscheduler添加hana支持 转载请注明出处: https://www.cnblogs.com/funnyzpc/p/16395092.html 上一节有讲datax
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK