阿里出品的嵩山版 Java 开发手册上这样规定:
尽量不要 catch RuntimeException,比如 NullPointerException、IndexOutOfBoundsException 等等,应该用预检查的方式来规避。
正例:
if (obj != null) {
// ...
}
反例:
try {
obj.mwthod();
} catch (NullPointerException e) {
// ...
}
但是有些情况下的异常是预检查不出来的,比如说 NumberFormatException,虽然也属于 RuntimeException,但是没办法预检查,这时还是应该使用 catch 捕获处理。
当需要关闭资源时,尽量不要使用 try-catch-finally,禁止在 try 块中直接关闭资源。
反例:
public void doNotCloseResourceInTry() {
FileInputStream inputStream = null;
try {
File file = new File("./qhj.txt");
inputStream = new FileInputStream(file);
inputStream.close();
} catch (FileNotFoundException e) {
log.error(e);
} catch (IOException e) {
log.error(e);
}
}
原因也很简单,因为一旦 close() 之前发生了异常,那么资源就无法关闭,直接使用 try-with-resources 来处理是最佳方式(前提是资源实现了 AutoCloseable 接口):
public void automaticallyCloseResource() {
File file = new File("./qhj.txt");
try (FileInputStream inputStream = new FileInputStream(file);) {
} catch (FileNotFoundException e) {
log.error(e);
} catch (IOException e) {
log.error(e);
}
}
但是,如果资源没有实现 AutoCloseable 接口,就要在 finally 块中关闭流:
public void closeResourceInFinally() {
FileInputStream inputStream = null;
try {
File file = new File("./qhj.txt");
inputStream = new FileInputStream(file);
} catch (FileNotFoundException e) {
log.error(e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
log.error(e);
}
}
}
}
Throwable 是 exception 和 error 的父类,如果在 catch 子句中捕获了 Throwable,很可能把超出程序处理能力之外的错误也捕获了。
所以,不要这样做:
public void doNotCatchThrowable() {
try {
// ...
} catch (Throwable t) {
// 不要这样做
}
}
这是因为有些 error 是不需要程序来处理的,程序可能也处理不了。比如说 OutOfMemoryError 或者 StackOverflowError,前者是因为 Java 虚拟机无法申请到足够的内存空间时出现的非正常的错误,后者是因为线程申请的栈深度超过了允许的最大深度出现的非正常错误,如果捕获了,就掩盖了程序应该被发现的严重错误。
异常信息的记录,也就是需要在开发时给必要的代码打上日志:
public void logAnException() {
try {
} catch (NumberFormatException e) {
log.error("这是错误信息: " + e);
}
}
这样把错误信息记录下来,如果后期上线时真的出现了问题,可以查询得到记录。
反例:
try {
} catch (NumberFormatException e) {
log.error(e);
throw e;
}
要抛出异常就抛出,不要记录。这样记录了又抛出,等于多此一举。
反例:
public void wrapException(String input) throws MyBusinessException {
try {
} catch (NumberFormatException e) {
throw new MyBusinessException("错误信息描述:", e);
}
}
这个也是同样的,既然已经捕获了异常,就不需要再方法签名上抛出了。
阿里出品的嵩山版 Java 开发手册上这样规定:
try 块中的 return 语句执行成功后,并不会马上返回,而是继续执行 finally 块中的语句,如果 finally 块中也存在 return 语句,那么 try 块中的 return 就将被覆盖。
反例:
private int x = 0;
public int checkReturn() {
try {
return ++x;
} finally {
return ++x;
}
}
像这段代码,try 块中 x 返回的值为 1,到 finally 块中执行完后就返回 2 了,相当于多执行一次 ++ 操作。
反例:
public void foo() throws Exception { // 错误方式
}
一定要避免出现上面的反例,它破坏了检查性异常的目的。
声明的方法上可能要抛出具体的检查性异常,如果有太多这样的检查性异常,应该把它们包装在自己的异常中,并在异常消息中添加信息。
正例:
public void foo() throws SpecificException1, SpecificException2 { // 正确方式
}
反例:
try {
someMethod();
} catch (Exception e) { // 错误方式
LOGGER.error("method has failed", e);
}
捕获异常的问题是,如果稍后调用的方法为其方法声明添加了新的检查性异常,则开发人员的意图是应该处理具体的新异常。
但是,如果你的代码只是捕获异常(或 Throwable),你就永远不会知道这个变化,并且你的代码现在是错误的,很可能会在运行时的任何时候中断。
反例:
catch (NoSuchMethodException e) {
throw new MyServiceException("Some information: " + e.getMessage()); // 错误方式
}
这破坏了原始异常的堆栈跟踪,并且始终是错误的。正确的做法是:
catch (NoSuchMethodException e) {
throw new MyServiceException("Some information: " , e); // 正确方式
}
try {
someMethod(); // Throws exceptionOne
} finally {
cleanUp(); // 如果finally还抛出异常,那么exceptionOne将永远丢失
}
只要 cleanUp() 永远不会抛出任何异常,上面的代码是没有问题的。但是如果 someMethod() 抛出一个异常,并且在 finally 块中,cleanUp() 也抛出另一个异常,那么程序只会把第二个异常抛出来,原来的第一个异常(正确的原因)将永远丢失。如果在 finally 块中调用的代码可能会引发异常,请确保要么处理它,要么将其记录下来。永远不要让它从 finally 块中抛出来。
完成代码后,切勿忽略 printStackTrace(),最终别人可能会得到这些堆栈,并且对于如何处理它完全没有任何方法,因为它不会附加任何上下文信息。
try {
someMethod(); // Method 2
} finally {
cleanUp(); // do cleanup here
}
这是一个很好的做法,如果你正在访问 Method 2,而 Method 2 抛出一些你不想在 Method 2 中处理的异常,但是仍然希望在发生异常时进行一些清理,可以在 finally 块中进行清理,不要使用 catch 块。
这可能是关于异常处理最著名的原则。简单说,应该尽快抛出(throw)异常,并尽可能晚地捕获(catch)它。应该等到有足够的信息来妥善处理它。
这个原则隐含地说,你将更有可能把它放在低级方法中,在那里你将检查单个值是否为空或不适合。而且你会让异常堆栈跟踪上升好几个级别,直到达到足够的抽象级别才能处理问题。
相关性对于保持应用程序清洁非常重要。一种尝试读取文件的方法,如果抛出 NullPointerException,那么它不会给用户任何相关的信息。相反,如果这种异常被包裹在自定义异常中,则会更好。NoSuchFileFoundException 则对该方法的用户更有用。
不要在项目中出现使用异常来处理应用程序逻辑。永远不要这样做,它会使代码很难阅读和理解。
始终要在非常早的阶段验证用户输入,甚至在达到 controller 之前,它将帮助你把核心应用程序逻辑中的异常处理代码量降到最低。如果用户输入出现错误,还可以保证与应用程序一致。
例如:如果在用户注册应用程序中,遵循以下逻辑:
这是不正确的做法,它会使数据库在各种情况下处于不一致的状态,应该首先验证所有内容,然后将用户数据置于 dao 层并进行数据库更新。正确的做法是:
反例:
LOGGER.debug("Using cache sector A");
LOGGER.debug("Using retry sector B");
不要像上面这样做,对于多个 LOGGER.debug() 调用使用多行日志消息可能在测试用例中看起来不错,但是当它在具有 100 个并行运行的线程的应用程序服务器的日志文件中显示时,所有信息都输出到相同的日志文件,即使它们在实际代码中为前后行,但是在日志文件中这两个日志消息可能会间隔 100 多行。所以应该这样做:
LOGGER.debug("Using cache sector A, using retry sector B");
有用的异常消息和堆栈跟踪非常重要,日志的作用就是要定位异常位置。如果你的日志不能定位异常位置,那要日志有什么用呢?
反例:
while (true) {
try {
Thread.sleep(100000);
} catch (InterruptedException e) {} // 不要这样做
doSomethingCool();
}
InterruptedException 异常提示应该停止程序正在做的事情,比如事务超时或线程池被关闭等。
应该尽最大努力完成正在做的事情,并完成当前执行的线程,而不是忽略 InterruptedException。应该这样做:
while (true) {
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
break;
}
}
doSomethingCool();
在代码中有许多类似的 catch 块是无用的,只会增加代码的重复性,针对这样的问题可以使用模板方法。
例如,在尝试关闭数据库连接时的异常处理:
class DBUtil{
public static void closeConnection(Connection conn){
try{
conn.close();
} catch(Exception ex){
// Log Exception - Cannot close connection
}
}
}
这类的方法将在应用程序中的很多地方使用。但是也不要把这块代码放的到处都是,而是定义一个方法,然后再调用它:
public void dataAccessCode() {
Connection conn = null;
try{
conn = getConnection();
....
} finally{
DBUtil.closeConnection(conn);
}
}
把用 JavaDoc 记录运行时可能抛出的所有异常作为一种习惯,其中也尽量包括用户应该遵循的操作,以防这些异常发生。