用throw代替if判断是反模式,因异常本应表示真正错误而非流程控制,滥用会破坏调用栈、干扰调试监控、带来性能损耗且易引发异常掩盖等问题。

用 throw 代替 if 判断就是反模式
Java 中把异常当流程控制用,本质是用 throw 模拟 goto —— 看似绕开了嵌套,实则破坏了调用栈的语义。比如用 IllegalArgumentException 跳过校验失败后的逻辑,或靠 catch 捕获“正常业务分支”,这会让 IDE、调试器和监控系统完全失焦。
常见错误现象:catch 块里只写 return 或空语句;try 块里全是条件判断;日志里高频出现 java.lang.IllegalArgumentException: invalid state 却不对应真实错误场景。
- 真正该抛异常的情况:输入违反契约(如传
null给非空参数)、资源不可用(如数据库连接中断)、状态非法(如对已关闭的流调用read()) - 该用
if的情况:用户输入格式不对、业务规则不满足(如余额不足)、可预期的分支选择(如根据类型走不同处理路径) - JVM 对异常构造开销敏感——每次
new RuntimeException()都要填充栈帧,比普通分支慢 10–100 倍(取决于栈深度)
自定义异常不带业务含义就是白定义
只继承 RuntimeException 并起个模糊名字(如 BusinessException),等于没定义。调用方无法区分“库存扣减失败”和“支付超时”,只能靠 getMessage() 字符串匹配,脆弱且不可维护。
使用场景:需要让上游明确感知并差异化处理时,才值得定义新异常。例如订单服务中,InsufficientStockException 和 PaymentTimeoutException 应该由不同模块捕获并重试/降级。
立即学习“Java免费学习笔记(深入)”;
- 必须重写
toString()或提供getErrorCode()方法,返回稳定码值(如STOCK_SHORTAGE_409),别依赖消息文本 - 避免在异常中塞复杂对象或大字段——序列化和日志打印时可能引发
StackOverflowError或泄露敏感数据 - 如果异常只在本类内 throw/catch,优先用布尔返回值或 Optional,而不是自定义异常
catch (Exception e) 吞掉异常最危险
这是最隐蔽的反模式:代码看似“健壮”,实则把 NullPointerException、OutOfMemoryError 全部吃掉,连日志都不打。线上出问题时,连第一个报错点都找不到。
性能影响不大,但调试成本爆炸。JVM 在抛出 Error 类型(如 StackOverflowError)时已处于不稳定状态,强行 catch 可能导致后续行为不可预测。
- 永远不要 catch
Throwable或裸Exception,除非你在写顶层守护线程(如Thread.setDefaultUncaughtExceptionHandler) - 必须记录日志时,至少保留原始堆栈:
log.error("failed to process order", e),而不是e.getMessage() - 如果真要忽略某种异常(如
InterruptedException),也要显式恢复中断状态:Thread.currentThread().interrupt()
在 finally 里抛异常会掩盖主异常
当 try 块已抛出异常,而 finally 块又抛出另一个异常,JVM 会丢弃前者,只向上抛后者。结果就是你看到的错误和实际问题完全无关。
典型场景:关闭资源时 IO 失败(如 socket.close() 抛 IOException),但原始异常可能是 SQL 执行失败,却被彻底覆盖。
- JDK 7+ 优先用 try-with-resources,它自动抑制次要异常(通过
addSuppressed()),主异常仍可追溯 - 手写
finally时,所有操作必须包裹在try/catch内,且不能抛出检查异常;非必要不调用可能失败的方法 - 若必须手动关资源,检查是否为
null再调用close(),避免NullPointerException掩盖主异常
最难察觉的是多层嵌套 try-catch 中的异常压制,尤其在异步回调或代理增强后。上线前用字节码工具扫一遍 finally 块里的方法调用,比等故障时翻三天日志更省事。










