异常不应用于流程控制,而应仅处理意外情况;业务状态应通过返回值表达,避免滥用RuntimeException,合理使用预判方法、结果封装类和防御性校验。

在Java中,用异常来控制程序流程是一种常见但危险的习惯。异常本意是处理“意外情况”,而非替代正常的条件判断。滥用会导致性能下降、逻辑混乱、调试困难,甚至掩盖真正的问题。
别用异常做流程控制
比如检查文件是否存在时,调用 file.delete() 后靠捕获 SecurityException 或 IOException 来判断权限或路径有效性,这是错的。应先用 file.exists()、file.canWrite() 等方法预判。
明确区分“错误”与“预期分支”
用户输入格式错误、网络超时、数据库连接失败——这些属于需要响应的异常场景;而“用户名已存在”、“库存不足”、“订单状态不支持此操作”——这些是业务规则下的合法状态,应通过返回值(如 Optional、自定义结果类、枚举状态码)表达,而非抛出 RuntimeException。
- 定义像 Result
或 ApiResponse 这样的封装类,统一承载成功/失败、数据、提示信息 - 对可预期的失败场景,避免继承 RuntimeException 自造“业务异常”,除非该异常确实需跨多层中断流程并被顶层统一处理
- 使用 Objects.requireNonNull()、Preconditions.checkArgument() 等仅用于防御性校验,不是替代if判断的捷径
谨慎使用 try-with-resources 和 finally 的边界
资源管理要精准:只在真正持有外部资源(IO、DB连接、锁)时才用 try-with-resources;不要为普通对象或临时计算加一层无意义的 try。finally 中避免抛出新异常,否则可能吞掉原始异常。
立即学习“Java免费学习笔记(深入)”;
- 流式操作(如 Stream.filter().map().collect())内部出错应尽早暴露,而不是包裹成异常再层层上抛
- 日志记录、指标上报等副作用操作放在 catch 块里,但不要在 finally 里重试或修改业务状态
- 关闭资源前先判空,防止 NullPointerException 掩盖原始异常
用单元测试暴露异常滥用
写测试时,不仅验证正常路径,还要覆盖边界输入,并断言是否抛出了不该抛的异常。例如:
- 给一个解析JSON的方法传空字符串,它应该返回 Optional.empty() 或含错误码的 Result,而不是抛 JsonParseException
- 用 @Test(expected = IllegalArgumentException.class) 只适用于真正该由参数校验触发的异常,而非业务逻辑分支
- 结合 Mockito 模拟下游失败,验证上层是否用异常驱动了分支跳转
不复杂但容易忽略:把“会不会发生”交给条件判断,把“发生了怎么办”留给异常处理。代码健壮性不来自拼命 catch,而来自清晰的契约和克制的异常使用。











