Kotlin刨根问底(二):for循环引起的一起“血案”(中)

简介: 本文灵感来源于:群友遍历列表时remove元素引发异常,后对for循环的实现原理进行一系列的探究~

0x2、数组越界问题解析


原因其实很简单「循环条件不一样」,Java中是:



先判断是否满足条件执行循环体自增 ,打断点跟下i、ls.size(),记录如下:


0→5、1→4、2→3、3(此时size=3,判断条件不成立,不会自增,走循环体),所以上面看到只打印了3次


而Kotlin则不一样,点开 for(i in l.indices) 里的indices



噢,indices是一个扩展属性,值为:0..size - 1,一个范围(Range),比如现在有5个元素,这个值就是:0..(5-1) → 0..4,你可以简单地把Range看成一个List(列表),那么这里的 循环条件 就变成了:遍历这个范围,所以


0→5、1→4、2→3、3→3(这里是 直接进循环体,数组长度为3,访问3个,所以就引发了异常!)


到此:普通for循环的“血案(数组越界)”就此水落石出,接着看下增强for循环~


0x3、增强for循环异常分析


有读者可能有疑问,上述的Kotlin代码用的是 forEach 扩展方法,增强for循环不是应该这样写吗?



其实,forEach也是用的增强for循环,点开源码就知道了:



在增强for循环的基础上,传入一个处理函数,不了解函数式编程的可移步至:《8、Kotlin高阶函数与lambda表达式》


① 增强for循环的原理


反编译下增强for循环字节码,如下:



逐行源码解析:


  • Iterator var → 定义迭代器类型的临时变量var1


  • l.iterator() → 获取列表l的迭代器


  • var1.hasNext() → 判断迭代器中是否有未遍历的元素


  • Integer i = (Integer)var1.next() → 获取第一个未遍历元素,赋值给临时变量i


  • l.remove(i) → 移除列表中的i元素


不难看出底层是:while循环 + Iterator(迭代器)


② 迭代器的设计“哲学”


设计模式中有一种模式「迭代器模式


  • 针对 =>「容器对象中元素的迭代访问」;


  • 核心思想 => 将「遍历行为」与「被遍历对象」分离;


  • 好处 => 无需关心容器的底层结构,拿到对象,使用迭代器即可遍历对象内部;

打开 Collection.kt,文件结构如下:



这里要对「Iterable」和「Iterator」进行区分:


  • Iterable接口:实现此接口的集合对象支持迭代(可配合foreach使用),定义了一个iterator()函数,返回一个Iterator迭代器对象。


  • Iterator:迭代器,提供迭代机制的对象,代码如下:



两个函数:next() => 返回当前迭代元素;hasNext() => next前调用此方法判断是否迭代到终点


可以跟下实现看看:ListIteratorAbstractList



IteratorImpl



以上就是 迭代器的设计“哲学” 的简单讲解。


③ 寻根问底


回到增强for循环中remove元素引起的「ConcurrentModificationException」,此异常直接翻译:并发修改异常


但是问题是:我们并没有用多线程修改集合,却引起这样的异常?跟一波异常,定位到:


java.util.ArrayList$Itr.checkForComodification



直译下变量名:modCount(修改次数),expectedModCount(预计修改次数),em…这是判断两值不等就直接抛异常?往上看:



modCount赋值给expectedModCount,说明两者一开始是相等的,那modCount是在哪里赋初值的呢?


点进去来到ArrayList的父类AbstractList



Tips:transient关键字 用来标记的成员变量不参与序列化过程)


AbstractList中搜了下modCount,没发现有值变化的操作;


回到ArrayList,观察源码可以发现「在涉及结构变化的方法中modCount都会自增1



而add(),addAll() 方法虽然没有明写modCount++,但暗地里调用ensureCapacityInternal()完成自增:



到此,我们知道了modCount →「会在ArrayList的结构发生变化时变化

接着,到expectedModCount,定位到内部类 Itr



类的组成比较简单:实现了Iterator迭代器接口,重写next()和hasNext()方法;定义了两个变量:cursor(下一个元素的下标),lastRet(上一个元素的下标);读者有疑问的可能是next()里的:ArrayList.this.elementData,跟一下:



噢,就是一个缓存数组,长度为列表的容量,当第一个元素添加进来,会扩展至DEFAULT_CAPACITY



看回remove()方法,调用ArrayList.this.remove()移除元素后,把modCount的赋值给expectedModCount


继续往下走,因为此时两者相等,hasNext()执行checkForComodification()没抛出异常,按流程走是没问题的。


但:我们上面跳过了迭代器,直接对列表进行了remove,而此时modCount已经+1,但expectedModCount没变,所以执行到checkForComodification就抛出异常了!


嗯,你有个大胆的想法?直接用迭代器进行remove会抛异常吗?那就试试:



行吧,用迭代器的方式remove,但此时已经不是增强for循环了,哈哈!


相关文章
|
25天前
|
Java Kotlin 索引
Kotlin教程笔记(9) - 分支与循环
Kotlin教程笔记(9) - 分支与循环
|
1月前
|
Java Kotlin 索引
Kotlin - 分支与循环
Kotlin - 分支与循环
|
1月前
|
Java Kotlin 索引
Kotlin - 分支与循环
Kotlin - 分支与循环
|
1月前
|
Java Kotlin 索引
Kotlin - 分支与循环
Kotlin - 分支与循环
26 2
|
1月前
|
Java Kotlin 索引
Kotlin教程笔记(9) - 分支与循环
Kotlin教程笔记(9) - 分支与循环
34 5
|
1月前
|
Java Kotlin 索引
Kotlin教程笔记(9) - 分支与循环
Kotlin教程笔记(9) - 分支与循环
34 2
|
1月前
|
Java Kotlin 索引
Kotlin - 分支与循环
Kotlin - 分支与循环
48 4
|
2月前
|
Java 开发者 Kotlin
Kotlin开发笔记- 分支与循环
本系列教程详细讲解了Kotlin语法,适合需要深入了解Kotlin的开发者。若需快速学习Kotlin,可参考“简洁”系列教程。本文重点介绍了Kotlin中的分支语句(if...else 和 when)及循环语句(for 和 while),并提供了丰富的示例代码,帮助读者掌握这些核心语法。
29 1
|
1月前
|
Java Kotlin 索引
Kotlin - 分支与循环
Kotlin - 分支与循环
31 0
|
2月前
|
Java Kotlin 索引
Kotlin12 - 分支与循环
Kotlin 12- 分支与循环
26 2