IndexOutOfBoundsException是Java集合操作中最常见的运行时异常之一。它的出现往往意味着程序试图访问一个不存在的位置,比如一个只有5个元素的List却去取第6个元素。这个异常虽然看起来简单,但在实际项目中引发的线上故障并不少见,尤其是在索引由外部输入或动态计算得出的场景下。本文将从异常的抛出原理讲起,逐个分析典型的越界场景,并给出可落地的防御性编码方案。

一、IndexOutOfBoundsException的抛出原理
在JDK的集合体系中,这个异常的检查逻辑集中在具体的实现类里。以ArrayList为例,get(int index)方法的源码中有一个rangeCheck私有方法,它会将传入的index与当前的实际元素个数size做比较,一旦index大于等于size,就立即抛出IndexOutOfBoundsException。注意这里比较的是size而不是内部数组的length,这是一个容易混淆的点。
ArrayList内部维护的数组容量往往大于实际元素数量。比如调用new ArrayList<>()后添加3个元素,内部数组长度可能是10,但size只有3。此时访问index为5的位置,虽然数组本身有这个槽位,但从集合语义上讲它并不存在有效元素,因此依然会抛出异常。这个设计保证了集合的封装性,外部无法触及未使用的区域。
除了List,String的charAt、substring方法,数组的直接下标访问,以及Vector、Stack等类都有类似机制。不同的是,数组访问越界抛出的是ArrayIndexOutOfBoundsException,字符串则是StringIndexOutOfBoundsException,这两者其实都是IndexOutOfBoundsException的子类,统一归在一个异常家族下。
二、五种典型的越界场景分析
第一种是负数索引。有些开发者认为负数可以用作从尾部计数,类似于Python的语法,但Java并不支持这种语义,任何小于0的索引都会直接触发异常。第二种是循环边界写错,典型错误是for (int i = 0; i <= list.size(); i++),条件用了小于等于,导致最后一次访问的index等于size,恰好越界。
第三种是并发修改问题。多线程环境下,一个线程刚检查完size() > 0,另一个线程就把元素移除了,随后调用get(0)就会失败。这种问题的隐蔽性在于测试环境单线程下完全正常。第四种是遍历过程中删除元素。用普通for循环边遍历边remove,size在不断缩小,但循环变量还在按原计划递增,很容易访问到已收缩区域之外的位置。
第五种是subList的误用。subList(fromIndex, toIndex)返回的是原列表的视图,fromIndex不能小于0,toIndex不能大于size,否则立刻抛出异常。此外subList返回的视图与原列表共享结构,对原列表做结构性修改后再操作视图,会抛出ConcurrentModificationException,这也是一大隐患来源。
三、防御性编码的正确姿势
最基础的防御是访问前检查边界。推荐用isEmpty()判断集合是否为空,语义比size() == 0更清晰。取尾部元素时,用list.get(list.size() - 1)之前必须先确认size大于0。下面是一段修复前后的对比代码:
// 错误写法:直接取尾部元素,空列表会抛异常
public String last(List<String> list) {
return list.get(list.size() - 1);
}
// 正确写法:先判空,语义明确且安全
public String last(List<String> list) {
if (list == null || list.isEmpty()) {
return null; // 或者抛出业务异常
}
return list.get(list.size() - 1);
}遍历时删除元素要改用迭代器。Iterator的remove()方法会在删除后同步调整内部游标,不会产生索引错位。如果使用JDK 8及以上版本,removeIf是更简洁的选择,它内部封装了安全的删除逻辑:
List<Integer> numbers = new ArrayList<>(
Arrays.asList(1, 2, 3, 4, 5));
// 方式一:迭代器安全删除
Iterator<Integer> it = numbers.iterator();
while (it.hasNext()) {
if (it.next() % 2 == 0) {
it.remove();
}
}
// 方式二:removeIf一行搞定
numbers.removeIf(n -> n % 2 == 0);对于索引来自外部输入的场景,比如分页查询的page和pageSize参数,一定要在进入业务逻辑前做参数校验。负数页码、超过总页数的请求都应该被拦截并返回友好提示,而不是让底层的集合访问直接崩溃。可以封装一个通用的安全索引方法:
public static <T> T safeGet(List<T> list, int index) {
if (list == null || index < 0 || index >= list.size()) {
return null;
}
return list.get(index);
}多线程场景下,要么使用Collections.synchronizedList包装,要么改用CopyOnWriteArrayList,要么用synchronized块把检查和访问合并成一个原子操作,避免检查与使用之间出现时间窗口。
四、快速定位异常的排查技巧
当异常真的发生时,堆栈信息是最直接的线索。异常信息通常包含具体的越界索引值,比如Index: 10, Size: 5,这个数字对比能立刻告诉你差了多少。再结合堆栈中第一个属于自己业务包的类名和行号,就能定位到出问题的访问语句。
如果越界是偶发且难以复现的,大概率是并发问题或数据依赖问题。这时可以在访问点加上日志,记录size、index以及集合内容的摘要,或者临时使用断言把隐含的前提条件暴露出来。对于长期方案,建议在代码评审时重点关注所有出现get(、subList(、charAt(调用的位置,确认索引来源是否可信。
最后提醒一点,捕获IndexOutOfBoundsException然后吞掉异常是一种糟糕的做法。它只是把显性的崩溃变成了隐性的数据错误,问题依然存在,只是更难排查了。正确的思路是保证索引合法,让异常根本不发生,而不是等它发生后再补救。
IndexOutOfBoundsExceptionJava集合越界访问修改时间:2026-09-08 14:15:04