You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

非空Kotlin ArrayList为何会在索引0处抛出越界异常?

问题

这段Kotlin Android代码偶尔会抛出IndexOutOfBoundsException:

if (mPointQueue.isNotEmpty()) {
   mPointRecycle.add(mPointQueue.removeAt(0))
}

已知mPointQueue是ArrayList,且未在多线程间共享。按逻辑非空列表的索引0必然存在元素,为何会出现无元素的情况?该错误在用户快速切换屏幕时触发,是否是代码连续执行两次引发的竞态条件(比如调用isNotEmpty()时底层数组在后台复制导致判断结果错误)?除捕获并忽略异常外,还有哪些解决办法?

原因解析

  1. 单线程内的竞态条件:快速切换屏幕会触发Android的配置变更(比如横竖屏切换),此时Activity会经历销毁重建,或多个生命周期回调(如onPause、onResume)被快速触发。如果这段代码在这些回调中被调用,就可能出现同一线程内连续两次执行的情况:第一次判断isNotEmpty()为true,还没执行removeAt(0),第二次又进入判断(此时列表仍非空),结果第一次执行删除后列表为空,第二次执行removeAt(0)就会抛出异常。
  2. 判断与操作非原子: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 17:42:51