Java 异常

编程难免会遇到

是程序就少不了异常。异常的正确处理是保证程序健壮性的关键。

异常的分类


  • 所有异常源自共同的祖先Throwable
  • 异常分为两大类Error和Exception
  • Error比较严重,一般是没救了
  • Exception可以捕获进行处理,try catch块针对的是这类异常
  • Exception中有一类编译器不检查的RuntimeException,一般情况是不该捕获的,应当修正逻辑去避免

JDK中常见的异常,如果没有特殊需求,优先使用自带的异常类

异常原则


  • 提早抛出:检测出异常行为立刻抛出,避免继续执行无用的逻辑,也有利于精确定位
  • 延迟捕获:当前层面无法处理就不要捕获,交给上级处理
  • 检查规避:主动提前检查优于被动捕捉运行时异常,除非无法检查,比如NumberFormatException

异常的负担


如果函数声明了会抛出可被检查的异常,那么所有调用此函数的地方都必须用try-catch捕获
可能的话尽量搭配提前检查的方法避免异常抛出,API设计中最好不要声明可检查异常

Sample
1
2
3
4
5
6
7
8
9
try {
callWithException(arg);
} catch (Exception e) {
e.printStackTrace();
}

if(isValid(arg)){
call(arg);
}

底层函数可能会抛出多种异常,都返给上层的话不光暴露了底层细节,而且也会让上层混乱
应该对异常进行抽象,上层只处理一种类型的异常

Sample
1
2
3
4
5
try {
callWithException(arg);
} catch (LowLevelExceptionA |LowLevelExceptionB e) {
throw new HighLevelException(e);
}

如果构造函数声明了可检查异常,而且作为内部变量直接初始化赋值,那么外部构造函数也必须抛异常

Sample
1
2
3
4
5
6
7
8
9
10
11
12
class Sample {
private SampleWithException s = new SampleWithException();
public Sample() throws Exception { //必须

}
}

class SampleWithException {
public SampleWithException() throws Exception {

}
}

异常捕获注意点


  • 不要在循环中进行try-catch,降低执行效率
  • 即使try中进行return,finally也会执行,但是不会对返回值造成影响
Sample
1
2
3
4
5
6
7
8
9
10
11
12
// return foo
private static String test() {
String s = "foo";
try {
return s;
} catch (Exception e) {
e.printStackTrace();
} finally {
s = "bar";
}
return null;
}
  • finally中不应该有return,这造成finally块没有正常执行,如果有会覆盖try中结果
Sample
1
2
3
4
5
6
7
8
9
10
11
12
// return bar
private static String test() {
String s = "foo";
try {
return s;
} catch (Exception e) {
e.printStackTrace();
} finally {
s = "bar";
return s;
}
}
  • Java7语法支持捕获多个异常。
    遇到抛出多种异常的代码,要写很多catch块。如果处理逻辑一样,会造成代码重复。
Sample
1
2
3
4
5
6
7
try{
//code throws exception
}catch(Exception1 e){
//handle
}catch(Exception2 e) {
//handle
}

偷懒直接捕获根异常Exception不仅丧失了清晰性,并且连运行时异常也包括,变得不可控。可以使用新语法同时捕获多种类型,进行统一处理

Sample
1
2
3
4
5
try{
//code throws exception
}catch(Exception1 | Exception2 e){
//handle
}
  • exit退出时不会执行finally
    JVM字节码层面不存在finally特殊机制只是一段普通代码,编译时会把finally块复制插入所有执行路径中
    如果有System.exit(0),那说明已经中断了程序字节码执行,执行不到finally逻辑
    如果在退出情况下还想执行逻辑,应该使用Runtime.getRuntime().addShutdownHook()