Java异常沿调用栈自动向上抛出,未捕获则终止程序;受检异常必须声明或处理,运行时异常需主动预防而非忽略;跨层传递应保留原始堆栈,拦截决策取决于业务语义。

Java多层调用中抛异常,本质是异常沿调用栈向上传播的过程。只要某一层没捕获,它就会自动“冒泡”到上一层方法,直到被处理或到达main线程由JVM终止程序。关键不是“怎么拦住”,而是“在哪拦、怎么拦、拦完做什么”。
异常传播的默认路径
当方法A调用B,B调用C,C中抛出NullPointerException:
- C未catch → 异常对象立即中断C的执行,控制权交还给B
- B若也没声明
throws且没catch → 异常继续上抛至A - A同样未处理 → 最终传到main方法;main也不捕获 → JVM打印堆栈并退出
这个过程不依赖return或显式传递,是JVM内置机制,靠字节码中的异常表(Exception Table)驱动。
受检异常必须显式声明或处理
比如FileReader构造可能抛FileNotFoundException(受检异常):
立即学习“Java免费学习笔记(深入)”;
- 如果C里用了
new FileReader("x.txt"),但没try-catch,编译直接报错 - C必须要么用try-catch包住,要么在方法签名加
throws FileNotFoundException - 若C选择
throws,那B调用C时,也得处理——要么自己catch,要么继续throws向上推
这种强制链路设计,是为了提醒开发者:外部依赖(IO/DB/网络)出问题很常见,不能假装看不见。
运行时异常可以不处理,但不该放任不管
NullPointerException、ArrayIndexOutOfBoundsException这类非受检异常,编译器不管,但它们暴露的是代码缺陷:
- 别写
catch (RuntimeException e) { }空吞异常——等于掩盖bug - 真正该做的是:提前校验(如判空、范围检查)、用Optional替代null、单元测试覆盖边界
- 如果真要捕获(比如统一日志+降级),建议catch具体子类,而不是笼统的RuntimeException
跨层传递时保留原始上下文
避免用throw new RuntimeException("出错了")掩盖根源。推荐方式:
- 直接抛出原异常:
throw e;(不丢失堆栈) - 封装再抛:
throw new ServiceException("订单创建失败", e);,构造时传入cause - 用
e.getCause()和e.printStackTrace()可逐层追溯原始错误点
日志记录时务必打全堆栈,否则排查多层调用问题会卡在“不知道谁先错的”。
基本上就这些。传播本身很简单,难的是判断哪一层该拦截、拦截后是恢复、降级还是告警,这取决于异常类型和业务语义。










