Java 代码中如果显式的用 throw
关键字抛出异常,那么在该分支中其后的语句不可到达,并且即使对于有返回值的函数也不必写 return
语句了。像下面的代码
1234567 private static int foo(int num) {if (num == 0) {throw new RuntimeException("");} else {return num + 1;}}
以上代码是合法的。要清洁代码的话,最后的 return num + 1
不必写在 else
条件中,这样写只是为了验证抛出异常后不必有返回值。
比如我们想对该代码进行重构,把 throw
语句抽取到一个方法中,以便于在该方法中集中处理错误信息,于是变成了
1 2 3 4 5 6 7 8 9 10 11 |
private static int foo(int num) { if (num == 0) { panic(-1, "something", "wrong"); } else { return num + 1; } } private static void panic(int errorCode, String... message) { throw new RuntimeException("errorCode: %s, message: %s".formatted(errorCode, String.join(" ", message))); } |
看起来是把整条 throw
语句置换成的 panic()
方法调用, 咋一看没什么问题,可是上面代码无法编译通过
javac Test.java
Test.java:12: error: missing return statement
}
^
1 error
因为 Java 编译器只看在当前代码行有没有 throw
关键字,才会认定后面的语句会否到达或可省略 return
语句,把 throw
关键字藏到一个方法中(即使该方法直接 throw 异常) 就不能理解了。
这时候保证方法只有一个出口就有成效了,修改后的代码
1 2 3 4 5 6 |
private static int foo(int num) { if (num == 0) { panic(-1, "something", "wrong"); } return num + 1; } |
如此就和显式 throw
异常一样的效果了,但会给阅读代码带来困惑,必须清楚 panic()
方法必定是抛出了异常,而不能在某种逻辑下不抛异常,而正常执行了 if
语句后续的代码。
panic
是借鉴了 Go, Rust 语言中的用法,panic
是 Go 语言的关键字,panic!
是 Rust
的宏,它们都能立即抛出异常。
// Go language
panic("something went wrong!")
// Rust language
panic!("something went wrong!");
在 panic()
或 panic!()
的语句都不可到达。panic
不是 Java 的关键字,但如果在 Java 中也约定用 panic()
表明此处必抛出异常的,就必须调整后代码中的 if/else 条件语句,以保证 panic()
与 throw 语句有同等效果。
另一个问题是,在显式 throw 后的代码是不可到达的,像以下代码
1 2 3 4 5 6 7 |
private static int foo(int num) { if (num == 0) { throw new RuntimeException(""); return num * 2; } return num + 1; } |
是无法通过编译的,因为代码行 return num * 2
紧接在 throw
语句之后,不可到达,用 javac
编译得到的错误是
Test.java:9: error: unreachable statement
return num * 2;
^
1 error
而且在 IntelliJ IDEA 中也能标识出来
同样的,把该 throw
语句换成 panic()
方法调用, 改成下面的样子
1 2 3 4 5 6 7 8 9 10 11 |
private static int foo(int num) { if (num == 0) { panic(-1, ""); return num * 2; } return num + 1; } private static void panic(int errorCode, String... message) { throw new RuntimeException("errorCode: %s, message: %s".formatted(errorCode, String.join(" ", message))); } |
则可通过编译,虽然实际行为并没有变,语句 return num * 2
同样是永远不会被执行。
而且在 IntelliJ IDEA 中也无法识别出来动态抛出异常后的代码是不可到达的
要让 IntelliJ IDEA 能作一定动态分析的话,需要把 IntelliJ IDEA 的 Editor/Inspections/Java/Probable bugs/Unreachable code 选项勾选上,默认未选择。
现在 IntelliJ IDEA 就会用灰色标识出来 return num * 2
是不可到达的,同时我们可以测试一下,如果把 panic() 函数改成不抛出或只是有条件的抛出异常,则 return num * 2
就会被认为是可到达的(正常色彩显示)。
看相关的介绍说用 IntelliJ jetbrains 的 @Contract 标识为 fail
的方法可让 IntelliJ IDEA 用红色标识 panic() 后的代码不可到达,可实测未达到预期,仍然是只有灰色提示。
最后的总结
- 在 IntelliJ IDEA 中识别永远抛异常方法调用后代码不可到达的方式只有勾选上 IntelliJ IDEA 的 Editor/Inspections/Java/Probable bugs/Unreachable code
- 确保
panic()
方法只抛出异常,安排好方法的 if/else 的结构,对于有返回值的方法,在调用了 panic() 之后保证也有一个返回出口,以通过编译 - 在感觉使用 panic() 模式有些不适的话,考虑回归传统的
throw
异常的方式, 省得影响代码阅读,也太考验 Java IDE 和编译器。比如 myError 是静态引入的一个函数,throw myError(-1, "something went wrong")
比panic(-1, "something went wrong")
多了几个字符 - 至少我本文写到这里就打算直接用
throw myError(...)
的,不想再折腾panic(...)
了,在未得到 Java 编译器支持的话,这样的panic(...)
并优雅