Java 异常处理的最佳实践
在 Java 开发中,异常处理是一个非常重要的环节。良好的异常处理实践可以提高代码的健壮性、可读性和可维护性。本文将介绍 20 个异常处理的最佳实践,帮助你在实际开发中避免常见的陷阱。
1. 尽量不要捕获 RuntimeException
阿里出品的 Java 开发手册上规定:尽量不要捕获 RuntimeException
,比如 NullPointerException
、IndexOutOfBoundsException
等,应该用预检查的方式来规避。
正例:
if (obj != null) {//...
}
反例:
try { obj.method();
} catch (NullPointerException e) {//...
}
2. 尽量使用 try-with-resource 来关闭资源
当需要关闭资源时,尽量不要使用 try-catch-finally
,禁止在 try
块中直接关闭资源。
反例:
public void doNotCloseResourceInTry() {FileInputStream inputStream = null;try {File file = new File("./tmp.txt");inputStream = new FileInputStream(file);inputStream.close();} catch (FileNotFoundException e) {log.error(e);} catch (IOException e) {log.error(e);}
}
正例:
public void automaticallyCloseResource() {File file = new File("./tmp.txt");try (FileInputStream inputStream = new FileInputStream(file)) {} catch (FileNotFoundException e) {log.error(e);} catch (IOException e) {log.error(e);}
}
3. 不要捕获 Throwable
Throwable
是 exception
和 error
的父类,如果在 catch
子句中捕获了 Throwable
,可能会把超出程序处理能力之外的错误也捕获了。
反例:
public void doNotCatchThrowable() {try {} catch (Throwable t) {// 不要这样做}
}
4. 不要省略异常信息的记录
很多时候,由于疏忽大意,我们很容易捕获了异常却没有记录异常信息,导致程序上线后真的出现了问题却没有记录可查。
反例:
public void doNotIgnoreExceptions() {try {} catch (NumberFormatException e) {// 没有记录异常}
}
正例:
public void logAnException() {try {} catch (NumberFormatException e) {log.error("哦,错误竟然发生了: " + e);}
}
5. 不要记录了异常又抛出了异常
这纯属画蛇添足,并且容易造成错误信息的混乱。
反例:
try {
} catch (NumberFormatException e) {log.error(e);throw e;
}
正例:
try {
} catch (NumberFormatException e) {throw e;
}
6. 不要在 finally 块中使用 return
try
块中的 return
语句执行成功后,并不会马上返回,而是继续执行 finally
块中的语句,如果 finally
块中也存在 return
语句,那么 try
块中的 return
就将被覆盖。
反例:
private int x = 0;
public int checkReturn() {try {return ++x;} finally {return ++x;}
}
7. 抛出具体定义的检查性异常而不是 Exception
一定要避免出现下面的代码,它破坏了检查性(checked)异常的目的。声明的方法应该尽可能抛出具体的检查性异常。
反例:
public void foo() throws Exception { //错误方式
}
正例:
public void foo() throws SQLException { //正确方式
}
8. 捕获具体的子类而不是捕获 Exception 类
如果在 catch
块中捕获 Exception
类型的异常,会将所有异常都捕获,从而可能会给程序带来不必要的麻烦。
反例:
try {someMethod();
} catch (Exception e) { //错误方式LOGGER.error("method has failed", e);
}
正例:
try {// 读取数据的代码
} catch (FileNotFoundException e) {// 处理文件未找到异常的代码
} catch (IOException e) {// 处理输入输出异常的代码
}
9. 自定义异常时不要丢失堆栈跟踪
捕获异常时,不要破坏原始异常的堆栈跟踪。
反例:
catch (NoSuchMethodException e) {throw new MyServiceException("Some information: " + e.getMessage()); //错误方式
}
正例:
catch (NoSuchMethodException e) {throw new MyServiceException("Some information: " , e); //正确方式
}
10. finally 块中不要抛出任何异常
finally
块用于定义一段代码,无论 try
块中是否出现异常,都会被执行。如果在 finally
块中抛出异常,可能会导致原始异常被掩盖。
反例:
try {someMethod(); //Throws exceptionOne
} finally {cleanUp(); //如果finally还抛出异常,那么exceptionOne将永远丢失
}
11. 不要在生产环境中使用 printStackTrace()
在生产环境中,应该使用日志系统来记录异常信息,例如 log4j
、slf4j
、logback
等。
反例:
try {// some code
} catch (Exception e) {e.printStackTrace();
}
正例:
try {// some code
} catch (Exception e) {logger.error("An error occurred: ", e);
}
12. 对于不打算处理的异常,直接使用 try-finally,不用 catch
如果 method1
正在访问 Method 2
,而 Method 2
抛出一些你不想在 Method 1
中处理的异常,但是仍然希望在发生异常时进行一些清理,可以直接在 finally
块中进行清理,不要使用 catch
块。
正例:
try {method1(); // 会调用 Method 2
} finally {cleanUp(); //do cleanup here
}
13. 记住早 throw 晚 catch 原则
“早 throw, 晚 catch” 是 Java 中的一种异常处理原则。这个原则指的是在代码中尽可能早地抛出异常,以便在异常发生时能够及时地处理异常。同时,在 catch
块中尽可能晚地捕获异常,以便在捕获异常时能够获得更多的上下文信息,从而更好地处理异常。
正例:
public static int parseInt(String str) {if (str == null || "".equals(str)) {throw new NullPointerException("字符串为空");}if (!str.matches("\\d+")) {throw new NumberFormatException("字符串不是数字");}return Integer.parseInt(str);
}
14. 只抛出和方法相关的异常
相关性对于保持代码的整洁非常重要。一种尝试读取文件的方法,如果抛出 NullPointerException
,那么它不会给用户提供有价值的信息。相反,如果这种异常被包裹在自定义异常中,则会更好。NoSuchFileFoundException
则对该方法的用户更有用。
正例:
public static int divide(int a, int b) throws ArithmeticException {if (b == 0) {throw new ArithmeticException("Division by zero");}return a / b;
}
15. 切勿在代码中使用异常来进行流程控制
在代码中使用异常来进行流程控制会导致代码的可读性、可维护性和性能出现问题。
反例:
public class Demo {public static void main(String[] args) {String input = "1,2,3,a,5";String[] values = input.split(",");for (String value : values) {try {int num = Integer.parseInt(value);System.out.println(num);} catch (NumberFormatException e) {System.err.println(value + " is not a valid number");}}}
}
16. 尽早验证用户输入以在请求处理的早期捕获异常
在用户注册的业务中,如果按照这样来做:
验证用户
插入用户
验证地址
插入地址
如果出问题回滚一切
这是不正确的做法,它会使数据库在各种情况下处于不一致的状态,应该首先验证所有内容,然后再进行数据库更新。正确的做法是:
验证用户
验证地址
插入用户
插入地址
如果问题回滚一切
正例:
Connection conn = null;
try {// Connect to the databaseconn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydatabase", "username", "password");// Start a transactionconn.setAutoCommit(false);// Validate user inputvalidateUserInput();// Insert user datainsertUserData(conn);// Validate address inputvalidateAddressInput();// Insert address datainsertAddressData(conn);// Commit the transaction if everything is successfulconn.commit();} catch (SQLException e) {// Rollback the transaction if there is an errorif (conn != null) {try {conn.rollback();} catch (SQLException ex) {System.err.println("Error: " + ex.getMessage());}}System.err.println("Error: " + e.getMessage());
} finally {// Close the database connectionif (conn != null) {try {conn.close();} catch (SQLException e) {System.err.println("Error: " + e.getMessage());}}
}
17. 一个异常只能包含在一个日志中
不要这样做:
log.debug("Using cache sector A");
log.debug("Using retry sector B");
在单线程环境中,这样看起来没什么问题,但如果在多线程环境中,这两行紧挨着的代码中间可能会输出很多其他的内容,导致问题查起来会很难受。应该这样做:
LOGGER.debug("Using cache sector A, using retry sector B");
18. 将所有相关信息尽可能地传递给异常
有用的异常消息和堆栈跟踪非常重要,如果你的日志不能定位异常位置,那要日志有什么用呢?
正例:
LOGGER.debug("Error reading file", e);
应该尽量把 String message, Throwable cause
异常信息和堆栈都输出。
19. 终止掉被中断线程
InterruptedException
提示应该停止程序正在做的事情,比如事务超时或线程池被关闭等。
反例:
while (true) {try {Thread.sleep(100000);} catch (InterruptedException e) {} //别这样做doSomethingCool();
}
正例:
while (true) {try {Thread.sleep(100000);} catch (InterruptedException e) {break;}
}
doSomethingCool();
20. 对于重复的 try-catch,使用模板方法
类似的 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);}
}
总结
异常处理是 Java 开发中不可或缺的一部分。通过遵循上述最佳实践,你可以编写出更加健壮、可读性更高、易于维护的代码。在实际开发中,不断积累经验,灵活运用这些原则,将有助于你更好地处理异常情况,提高代码的质量和稳定性。
思维导图
参考链接
Java异常处理的20个最佳实践,别再踩坑了!