非空Kotlin ArrayList为何会在索引0处抛出越界异常?
问题
这段Kotlin Android代码偶尔会抛出IndexOutOfBoundsException:
if (mPointQueue.isNotEmpty()) { mPointRecycle.add(mPointQueue.removeAt(0)) }
已知mPointQueue是ArrayList,且未在多线程间共享。按逻辑非空列表的索引0必然存在元素,为何会出现无元素的情况?该错误在用户快速切换屏幕时触发,是否是代码连续执行两次引发的竞态条件(比如调用isNotEmpty()时底层数组在后台复制导致判断结果错误)?除捕获并忽略异常外,还有哪些解决办法?
原因解析
- 单线程内的竞态条件:快速切换屏幕会触发Android的配置变更(比如横竖屏切换),此时Activity会经历销毁重建,或多个生命周期回调(如
onPause、onResume)被快速触发。如果这段代码在这些回调中被调用,就可能出现同一线程内连续两次执行的情况:第一次判断isNotEmpty()为true,还没执行removeAt(0),第二次又进入判断(此时列表仍非空),结果第一次执行删除后列表为空,第二次执行removeAt(0)就会抛出异常。 - 判断与操作非原子:ArrayList的
isNotEmpty()和removeAt()是两个独立的方法,中间存在时间窗口。哪怕是单线程,只要在这个窗口内其他代码(比如其他生命周期回调里的逻辑)修改了mPointQueue,就会出现“判断时非空,执行时为空”的矛盾。你提到的底层数组复制不是核心原因——ArrayList扩容仅在添加元素时发生,删除元素不会触发数组复制,问题本质是状态判断和实际操作的分离。
可行的解决办法
- 使用原子性的安全API:直接用Kotlin提供的
removeFirstOrNull()方法,它会在列表为空时返回null,无需提前判断,从根源避免判断和操作分离的问题:mPointQueue.removeFirstOrNull()?.let { mPointRecycle.add(it) } - 用同步块保证原子性:如果必须保留判断逻辑,可以用
synchronized包裹整个操作,确保判断和删除是一个不可分割的原子操作(哪怕是单线程内重复调用也能避免冲突):synchronized(mPointQueue) { if (mPointQueue.isNotEmpty()) { mPointRecycle.add(mPointQueue.removeAt(0)) } } - 统一操作入口并避免重复触发:把元素转移的逻辑封装到一个单独的方法里,所有需要执行该操作的地方都调用这个方法。同时可以加个标志位,防止短时间内重复执行:
private var isTransferring = false fun transferPointToRecycle() { if (isTransferring) return isTransferring = true if (mPointQueue.isNotEmpty()) { mPointRecycle.add(mPointQueue.removeAt(0)) } isTransferring = false }
内容的提问来源于stack exchange,提问作者John
相关产品推荐
相关产品推荐

