




Java异常默认向上抛出:运行时异常自动传播,受检异常须声明或捕获;应封装异常链而非吞掉或重复包装;finally中抛异常会覆盖主异常;异步异常需显式获取,资源关闭推荐try-with-resources。
Java的异常传播机制决定了:只要方法里没用 try-catch 捕获,且该异常是 RuntimeException 及其子类(运行时异常),或声明了 throws 的受检异常,它就会沿调用栈逐层向上传递,直到被处理或终止线程。
这意味着你不需要在每一层都写 throw e —— 它默认就“冒泡”上去。但关键点在于:受检异常(如 IOException、SQLException)必须显式声明或捕获,否则编译失败;而 NullPointerException 这类运行时异常完全跳过编译检查。
IllegalArgumentException(运行时异常),A 不写任何 try-catch 也能编译通过,异常最终由 JVM 处理并打印堆栈FileNotFoundException(受检异常),那么 B 必须要么 try-catch,要么在方法签名加 throws FileNotFoundException;同理,A 也得对这个异常做同样选择catch 吞掉异常再“静默返回”,这会让上层完全无法感知失败,调试时只剩 NullPointerException 这种二次异常原始异常信息常含敏感路径或底层实现细节(比如数据库连接串、文件绝对路径),直接抛给上层不安全也不友好。更常见的做法是用 throw new BusinessException("订单创建失败", e) 这类方式封装。
这种“异常链”保留了原始根因(可通过 e.getCause() 获取),又提供了业务语义。但要注意:不要重复包装同一异常,否则堆栈里出现多层相同类型,干扰问题定位。
T
hrowable cause 参数的子类,如 IllegalArgumentException(String msg, Throwable cause)
throw new RuntimeException(e) —— 这会丢失原始异常类型,让上层无法用 instanceof 区分处理逻辑@ExceptionHandler 统一处理顶层异常,此时异常是否被包装,直接影响响应码和错误消息的生成逻辑这是最容易踩坑的地方:当 try 块已抛出异常,而 finally 块里又发生新异常(比如关闭资源时 IO 失败),JVM 会丢弃 try 中的原始异常,只抛出 finally 里的那个。
例如下面代码:
public void readFile() throws IOException {
FileInputStream fis = null;
try {
fis = new FileInputStream("missing.txt"); // 抛出 FileNotFoundException
return;
} finally {
if (fis != null) fis.close(); // close() 抛出 IOException
}
}
调用方看到的是 IOException,而真正的 FileNotFoundException 被吞掉了。
try-with-resources 自动管理资源,它会在多个异常同时发生时把次要异常作为 suppressed exception 附加到主异常上(可用 e.getSuppressed() 查看)finally,关闭资源时要用 try-catch 包裹,并记录日志,而不是再次 throw
close()、数据库连接池的 close()、HTTP client 的 shutdown(),它们都可能抛异常在 CompletableFuture.supplyAsync() 或新启线程中抛出的异常,默认不会传回主线程,而是被吃掉或仅打印到 stderr。主线程继续执行,像什么都没发生。
比如:
CompletableFuture.runAsync(() -> {
throw new RuntimeException("后台任务炸了");
}); // 这个异常永远不会到达主线程
.join() 或 .get() 才能触发异常传播;.get() 会把原始异常包进 ExecutionException,需用 e.getCause() 解包.exceptionally() 或 .handle() 注册回调来捕获异常,适合做降级或告警,但不能替代同步传播Thread.UncaughtExceptionHandler,别依赖默认行为——它通常只打日志,不中断流程也不通知上游