异常触发条件分析

各位,咱们今天来聊聊最常见的那几个异常触发场景。

你想想看,代码写再多,最后跑起来出问题的地方,翻来覆去就那几种。空指针、下标越界、类型转换失败、IO异常。说白了,这就是程序的「四大坑」。

我个人习惯是,写代码之前先想清楚:“这段逻辑跑起来,最可能在哪个环节炸?” 把这个想明白了,你的健壮性起码提升一半。

空指针异常 (NullPointerException)

我觉得这是Java世界里最常见的「老朋友」了。为什么会这样?因为你把一个压根没实例化的对象拿出来用了。

我在项目中遇到过一件印象挺深的事。一个支付接口,上游返回了一大串JSON,我们用链式调用去拿深层字段。

String userId = orderJson.getJSONObject("data")
                       .getJSONObject("user")
                       .getString("id");

看到这段代码熟悉不?我当时检查日志,发现接口偶尔报NPE,但重启一下又好了。查了一天,最后发现是上游某个极端场景下「user」字段是null。链式调用里只要有一环是null,整条链直接崩。

注意:链式调用里的每一个点,都可能是一颗雷。不要相信上游一定会返回你预期的结构。

那怎么防?我个人建议分三步:

  • 明确约定:接口文档写清楚哪些字段可能为null。
  • 防御性检查:关键节点用if判断,或者用Optional优雅兜底。
  • 快速失败:如果null就是非法的,那就在入口处直接抛异常,别让脏数据往里传。

下标越界异常 (IndexOutOfBoundsException)

数组或列表越界,嗯,这里要注意。很多时候是循环边界没算对,也就是经典的「差一错误」(Off-by-one)。

我排查过一个比较诡异的OOM问题。当时服务内存飙升,dump下来一看,有个ArrayList一直在增长。找来找去,发现是一个while循环里用了动态的索引。

int i = 0;
while (i <= list.size()) {
    list.add(i * 2);  // 每添加一个元素,size就变大一点
    i++;
}

你想想看,这`list.size()`在循环体里一直在涨,`i`永远追不上它,结果就是个死循环,直到内存撑爆。

经验之谈:遍历集合时,如果会动态修改集合结构,要么用迭代器,要么用for-each。别自己手写索引,容易出问题。

对于下标越界,我总结了几条规矩:

  • 遍历数组时,用`for-each`或下标`i < arr.length`,别用`i <= arr.length`。
  • 用`List.subList()`或`String.substring()`时,一定要检查起始和结束位置。
  • 从列表取元素前,先看一眼`size()`。

类型转换失败 (ClassCastException)

类型转换异常,说白了就是你把一个苹果当成了梨。编译时看着没问题,运行时一暴露真身就炸。

我记得很多年前重构一个遗留系统,老代码用了大量的原生`List`,没有泛型。

List userList = getUserList(); // 老代码返回的原始List
for (Object obj : userList) {
    String name = (String) obj;  // 假设所有元素都是String
    // ...
}

结果后来数据源改了,某个字段变成了`Integer`,ClassCastException刷了一屏。

为什么会这样?因为向下转型时,JVM发现对象的真实类型跟你要转的类型不匹配。你想想看,这就像你对着一个程序员喊“那个产品经理过来一下”,人家能不翻白眼么?

建议:尽量用泛型来约束集合,避免使用原生类型。如果实在需要向下转型,先用instanceof探探路。
if (obj instanceof String) {
    String name = (String) obj;
    // 安全处理
} else {
    // 异常分支或告警
}

IO/资源异常 (IOException, SQLException等)

IO异常是个大家族,文件找不到、权限不够、磁盘满了、网络断了、数据库连接超时……这些都是运行时可能遇到的。

我遇到过最典型的一个问题,是生产环境的日志库把磁盘IO打满了。所有业务线程都在等文件锁释放,结果系统就假死了。你想想看,业务代码本身没什么问题,但底层资源不够用了,照样崩。

处理这类异常,我建议遵循几个原则:

  • 资源必须释放:用`try-with-resources`,让JVM帮你自动关闭流。
  • 设置超时:网络连接、数据库查询一定要设超时,不能无限等下去。
  • 做好降级:如果IO资源不可用,业务逻辑要有兜底方案,比如返回缓存数据或友好提示。

这里给你们看一个典型的错误写法:

InputStream is = new FileInputStream("test.txt");
int data = is.read();
// 忘记在finally中关闭is
// 如果中间抛出异常,这个文件句柄就漏了

正确的写法大家应该都会:

try (InputStream is = new FileInputStream("test.txt")) {
    int data = is.read();
    // 业务处理
} catch (IOException e) {
    log.error("文件读取失败", e);
    // 降级处理
}
重点:资源泄漏是生产环境的头号杀手。文件句柄、数据库连接、网络连接,用完了必须还回去。记性靠不住,交给try-with-resources最靠谱。

四种典型异常的对比

为了清晰对比,我把这几个异常整理成了表格,方便大家查阅:

异常类型 触发条件 检测时机 常用处理模式
NullPointerException 对null对象调用方法/属性 运行时 防御性检查、Optional
IndexOutOfBoundsException 索引超过集合/数组边界 运行时 循环边界控制、for-each
ClassCastException 不安全的向下转型 运行时 泛型约束、instanceof校验
IOException / SQLException IO资源不可用、网络断连等 运行时 try-with-resources、超时降级

下面这张图展示了它们的核心关系与防范思路:

典型异常触发条件与思路 异常触发条件 空指针 (NPE) 未初始化对象 链式调用 下标越界 边界错误 并发修改 类型转换失败 ClassCastException IO / 资源异常 IOException / SQL 核心对策:了解触发条件,针对性防御

上面这张图,把四种典型异常和它们的触发场景串在了一起。你从头到尾看一遍,其实套路都一样:先搞清楚“哪些资源可能为null/越界/类型不对/不可用”,然后针对这些风险点做校验或降级处理。