【2021-07-12】你对 Java 的异常处理有哪些心得?
请移步至:
每日一题 查看更多的题目 ~
本题是一道开放性问答题,答案并不唯一。面试官旨在考察面试者对 Java 异常的理解,本回答为我个人对异常处理的心得体会,并非标准答案,如果大家有更好的回答,可以评论提醒我进行查漏补缺。
答:
原则一:使用 try-with-resources 来关闭资源
《Effective Java》中给出的一条最佳实践是:Prefer try-with-resources to try-finally 。
我们知道,Java 类库中包含许多必须通过调用 close 方法手动关闭资源的类,比如:InputStream,OutputStream,java.sql.Connection 等等。在 JDK 1.7 以前,try-finally 语句是保证资源正确关闭的最佳实践。
不过,try-finally 带来的最大问题有两点:
- 有一些资源需要保证按顺序关闭
- 当我们的代码中引入了很多需要关闭的资源时,代码就会变得冗长难以维护
从 JDK 1.7 开始,便引入了 try-with-resources 语句,这些问题一下子都得到了解决。使用 try-with-resouces 这个构造的前提是,资源必须实现了 AutoCloseable 接口。Java 类库和第三方类库中的许多类和接口现在都实现或继承了 AutoCloseable 接口。
所以,我们应该使用 try-with-resources 代替 try-finally 来关闭资源。
原则二:如果你需要使用到 finally,那么请避免在 finally 块中使用 return 语句
我们来看两个示例程序
程序一:
package com.github.test;
public class Test {
public static int test() {
int i = 1;
try {
Integer.valueOf("abc");
} catch (NumberFormatException e) {
i++;
return i;
} finally {
i++;
return i;
}
}
public static void main(String[] args) {
System.out.println(test());
}
}
程序二:
package com.github.test;
public class Test {
public static int test() {
int i = 1;
try {
Integer.valueOf("abc");
i++;
} catch (NumberFormatException e) {
i++;
return i;
} finally {
i++;
}
return i;
}
public static void main(String[] args) {
System.out.println(test());
}
}
程序一的输出结果为:
3
程序二的输出结果为:
2
导致两个程序输出不同结果的原因在于:程序一,我们将 return 语句写在了 finally 块中;而程序二则是将 return 语句写在了代码的最后部分。
在 finally 块中写 return 语句是一种非常不好的实践,因为程序会将 try-catch 块里面的语句,或者是抛出的异常全部丢弃掉。如上面的代码,Integer.valueOf("abc");
会抛出一个 NumberFormatException ,该异常被 catch 捕获处理,我们的本意是,在 catch 块中将异常处理并返回,但是由于示例一 finally 块中有 return 语句,导致 catch 块的返回值被丢弃。
我们需要铭记一点,如果 finally 代码块中有 return 语句,那么程序会优先返回 finally 块中 return 的结果。
为了避免这样的事情发生,我们应该避免在 finally 块中使用 return 语句。
原则三:Throw early,Catch late
关于异常处理,有一个非常著名的原则叫做:Throw early,Catch late。
Remember “Throw early catch late” principle. This is probably the most famous principle about Exception handling. It basically says that you should throw an exception as soon as you can, and catch it late as much as possible. You should wait until you have all the information to handle it properly.
This principle implicitly says that you will be more likely to throw it in the low-level methods, where you will be checking if single values are null or not appropriate. And you will be making the exception climb the stack trace for quite several levels until you reach a sufficient level of abstraction to be able to handle the problem.
上文的含义是,遇到异常,你应该尽早地抛出,并且尽可能晚地捕获它。如果当前方法会抛出一个异常,我们应该判断,该异常是否应该交给这个方法处理,如果不是,那么最好的选择是将这个异常向上抛出,交给更高的调用级去处理它。
这样做的好处是,我们可以打印出更多的异常栈轨迹(Stacktrace),从最顶层的逻辑开始逐步向下,清楚地看到方法调用关系,以便我们理清报错原因。
原则四:捕获具体的异常,而不是它的父类
如果某个被调用的模块抛出了多个异常,那么只捕获这些异常的父类是非常不好的实践。
例如,某一个模块抛出了 FileNotFoundException 和 IOException ,那么调用这个模块的代码最好使用 catch 语句的级联分别捕获这两个异常,而不是只写一个 Exception 的 catch 块。
try {
// ...
}catch(FileNotFoundException e) {
// handle
}catch(IOException e) {
// handle
}
原则五:OutOfMemoryError 与 StackOverflowError
StackOverflowError,即栈溢出错误,一般无限制地递归调用会导致 StackOverflowError 的发生,所以,再一次提醒大家,在写递归函数的时候一定要写最基本的 base case,如果递归没有触底条件,就会导致栈溢出错误的发生。
OutOfMemoryError,即堆内存溢出错误,导致 OutOfMemoryError 可能有如下几点原因:
JVM启动参数内存值设定过小
代码中存在死循环导致产生过多对象实体
内存中加载的数据量过于庞大,比如使用 IO 流读取大文件,一次性的从数据库取出过多的数据也会导致堆溢出
集合类中有对对象的引用,使用完后未清空,使得JVM无法回收