0. 前言
Java异常该怎么正确的使用,于是有了这篇关于Java异常的基本知识和最佳实践方式的整理。
1. Exception和Error
共同点
Exception 和Error 都是继承了Throwable类,
在Java中只有Throwable类型的实例才可以被抛出或者捕获,它是异常处理机制的基本类型。
不同点
Exception和Error体现了Java平台设计者对不同异常情况的分类。
- Exception是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应处理。
- Exception又分为可检查(checked)异常和不可检查(unchecked)异常。
可检查异常在源代码里必须显式的进行捕获处理,这是编译期检查的一部分,像InterruptedException、ClassNotFoundException,
不可检查时异常是指运行时异常,像NullPointerException、ArrayIndexOutOfBoundsException之类,
通常是可以编码避免的逻辑错误,具体根据需要来判断是否需要捕获,并不会在编译期强制要求。 - Error是指正常情况下,不大可能出现的情况,绝大部分的Error都会导致程序处于非正常的、不可恢复的状态。既然是非正常情况,不便于也不需要捕获。常见的比如OutOfMemoryError之类都是Error的子类。
其中有一个比较经典的面试题目, 就是 NoClassDefFoundError 和 ClassNotFoundException 有什么区别?1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18区别一: NoClassDefFoundError它是Error,ClassNotFoundException是
Exception。
区别二:还有一个区别在于NoClassDefFoundError是JVM运行时通过classpath加载类
时,找不到对应的类而抛出的错误。ClassNotFoundException是在编译过程中如果可能出现此异常,在编译过程中必须将ClassNotFoundException异常抛出!
NoClassDefFoundError发生场景如下:
1、类依赖的class或者jar不存在 (简单说就是maven生成运行包后被篡改)
2、类文件存在,但是存在不同的域中 (简单说就是引入的类不在对应的包下)
3、大小写问题,javac编译的时候是无视大小的,很有可能你编译出来的class文件就与想要的不一样!这个没有做验证
ClassNotFoundException发生场景如下:
1、调用class的forName方法时,找不到指定的类
2、ClassLoader 中的 findSystemClass() 方法时,找不到指定的类
举例说明如下:
Class.forName("abc"); 比如abc这个类不存项目中,代码编写时,就会提示此异常是检查性异常,比如将此异常抛出。
从性能角度审视Java的异常处理机制
try-catch 代码段会产生额外的性能开销,或者换个角度说,它往往会影响 JVM 对代码进行优化,
所以建议仅捕获有必要的代码段,尽量不要一个大的 try 包住整段的代码;与此同时,利用异常控制代码流程,
也不是一个好主意,远比我们通常意义上的条件语句(if/else、switch)要低效。
Java 每实例化一个 Exception,都会对当时的栈进行快照,这是一个相对比较重的操作。
如果发生的非常频繁,这个开销可就不能被忽略了。
关于 Java 中引入的 Checked Exceptions,目前存在着很多反对意见。
- 正方的观点是引入 Checked Exceptions,可以增加程度的鲁棒性。
- 反方的观点是 Checked Exceptions 很少被开发人员正确使用过,并且降低了程序开发的生产率和代码的执行效率。
2. 设计异常的最佳实践(Best Practises for Designing the API)
1. 当要决定是采用checked exception还是Unchecked exception的时候,你要问自己一个问题,“如果这种异常一旦抛出,客户端会做怎样的补救?(When deciding on checked exceptions vs. unchecked exceptions, ask yourself, “What action can the client code take when the exception occurs?)”
如果客户端可以通过其他的方法恢复异常,那么这种异常就是checked exception;如果客户端对出现的这种异常无能为力,那么这种异常就是Unchecked exception;从使用上讲,当异常出现的时候要做一些试图恢复它的动作而不要仅仅的打印它的信息。
此外,尽量使用unchecked exception来处理编程错误:因为unchecked exception不用使客户端代码显示的处理它们,它们自己会在出现的地方挂起程序并打印出异常信息。Java API中提供了丰富的unchecked excetpion,譬如:NullPointerException , IllegalArgumentException 和 IllegalStateException等,因此一般使用这些标准的异常类而不愿亲自创建新的异常类,这样使代码易于理解并避免的过多的消耗内存。
2. 保护封装性(Preserve encapsulation)
不要让你要抛出的checked exception升级到较高的层次。例如,不要让SQLException延伸到业务层。业务层并不需要(不关心?)SQLException。你有两种方法来解决这种问题:
- 转变SQLException为另外一个checked exception,如果客户端并不需要恢复这种异常的话;
- 转变SQLException为一个unchecked exception,如果客户端对这种异常无能为力的话;
多数情况下,客户端代码都是对SQLException无能为力的,因此你要毫不犹豫的把它转变为一个unchecked exception,看看下边的代码:1
2
3
4
5
6
7public void dataAccessCode(){
try {
..some code that throws SQLException
} catch (SQLException ex) {
throw new RuntimeException(ex);
}
}
上边的做法是把SQLException转换为RuntimeException,一旦SQLException被抛出,那么程序将抛出RuntimeException,此时程序被挂起并返回客户端异常信息。Spring在底层就是把SQLException封装成了unchecked exception,以便于回滚事务。
如果你有足够的信心恢复它当SQLException被抛出的时候,那么你也可以把它转换为一个有意义的checked exception, 但是我发现在大多时候抛出RuntimeException已经足够用了。
3. 不要创建没有意义的异常(Try not to create new custom exceptions if they do not have useful information for client code.)
看看下面的代码有什么问题?1
public class DuplicateUsernameException extends Exception {}
它除了有一个“意义明确”的名字以外没有任何有用的信息了。不要忘记Exception跟其他的Java类一样,客户端可以调用其中的方法来得到更多的信息。
我们可以为其添加一些必要的方法,如下:1
2
3
4
5public class DuplicateUsernameException extends Exception {
public DuplicateUsernameException (String username){....}
public String requestedUsername(){...}
public String[] availableNames(){...}
}
在新的代码中有两个有用的方法:reqeuestedUsername(),客户但可以通过它得到请求的名称;availableNames(),客户端可以通过它得到一组有用的usernames。这样客户端在得到其返回的信息来明确自己的操作失败的原因。但是如果你不想添加更多的信息,那么你可以抛出一个标准的Exception:1
throw new Exception("Username already taken");
很有必要再重申一下,checked exception应该让客户端从中得到丰富的信息。要想让你的代码更加易读,请倾向于用unchecked excetpion来处理程序中的错误(Prefer unchecked exceptions for all programmatic errors)
4. Document exceptions
可以通过Javadoc’s @throws
标签来说明(document)你的API中要抛出checked exception或者unchecked exception
3. 使用异常的最佳实践(Best Practices for Using Exceptions)
1. 总是要做一些清理工作(Always clean up after yourself)
如果你使用一些资源例如数据库连接或者网络连接,请记住要做一些清理工作(如关闭数据库连接或者网络连接):1
2
3
4
5
6
7
8
9
10
11public void dataAccessCode(){
Connection conn = null;
try {
conn = getConnection();
..some code that throws SQLException
} catch (SQLException ex) {
ex.printStacktrace();
} finally {
DBUtil.closeConnection(conn);
}
}
如果你的API抛出Unchecked exception,那么你要用try-finally来做必要的清理工作:1
2
3
4
5
6
7
8
9
10
11
12Lock lock = new ReentrantLock();
public void syn() {
try {
lock.lock();
// ...do something that needs synchronization
} catch (RuntimeException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
2. 不要使用异常来控制流程(Never use exceptions for flow control)
下边代码中,MaximumCountReachedException被用于控制流程:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public void useExceptionsForFlowControl() {
try {
while (true) {
increaseCount();
}
} catch (MaximumCountReachedException ex) {
}
//Continue execution
}
public void increaseCount() throws MaximumCountReachedException {
if (count >= 5000)
throw new MaximumCountReachedException();
}
上边的useExceptionsForFlowControl()用一个无限循环来增加count直到抛出异常,这种做法并没有说让代码不易读,但是它是程序执行效率降低。
记住,只在要会抛出异常的地方进行异常处理。
3.不要忽略异常
当有异常被抛出的时候,如果你不想恢复它,那么你要毫不犹豫的将其转换为unchecked exception,而不是用一个空的catch块或者什么也不做来忽略它,以至于从表面来看象是什么也没有发生一样。
4. 不要捕获顶层的Exception
unchecked exception都是RuntimeException的子类,RuntimeException又继承Exception,因此,如果单纯的捕获Exception,那么你同样也捕获了RuntimeException,如下代码:1
2
3
4
5try{
...
} catch (Exception ex) {
}
一旦你写出了上边的代码(注意catch块是空的),它将忽略所有的异常,包括unchecked exception.
5.Log exceptions just once
Logging the same exception stack trace more than once can confuse the programmer examining the stack trace about the original source of exception. So just log it once.
4. 个人观点
何时使用Exception?个人认为:
- 当业务逻辑代码遇到处理异常的情况时,如果业务范围较小,且考虑到效率问题,直接返回附带异常信息的处理结果而不使用异常,而当业务范围比较大,或者考虑到代码的鲁棒性和可读性,选择自定义异常将异常从Service层抛向Controller层的方式比较好。
- 当在编写框架、API或者通用工具类的时候,应该选择抛异常的方式给调用方(或使用方),让调用方去处理异常,使调用方可以灵活的处理,若不想上层强制处理异常,可以抛出unchecked exception。
参考: