在我们日常编程中,异常处理是必不可少的,异常处理是否得当关系到程序的健壮性和后续维护成本。
试想一下,如果一个项目从头到尾没有考虑过异常处理,当程序出错从哪里寻找出错的根源?但是如果一个项目异常处理设计地过多,又会严重影响到代码质量以及程序的性能。因此,如何高效简洁地设计异常处理是一门艺术,今天我们来讲解一下。
1、什么是异常
异常Exception,就是非正常情况。比如常见的:数组越界异常、内存溢出异常、空指针异常等。在我们编写程序的过程中错误会时常发生,包括编译期间和运行期间的错误,在编译期间出现的错误有编译器帮助我们一起修正,然而运行期间的错误便不是编译器力所能及了,并且运行期间的错误往往是难以预料的。假若程序在运行期间出现了错误,如果置之不理,程序便会终止或直接导致系统崩溃,显然这是不行的。
那么,我们如何对运行期间出现的错误进行处理和补救呢?Java提供了一套异常处理机制,通过异常机制来处理程序运行期间出现的错误。通过异常机制,我们可以更好地提升程序的健壮性。
我们看下Jdk文档:
总结下,异常类的结构层次图如下图所示:
在Java中异常被当做对象来处理,根类是java.lang.Throwable类,在Java中定义了很多异常类(如OutOfMemoryError、NullPointerException、IndexOutOfBoundsException等),这些异常类分为两大类:Error和Exception。
Error是无法处理的异常,比如OutOfMemoryError,一般发生这种异常,JVM会选择终止程序。因此我们编写程序时不需要关心这类异常。
Exception,也就是我们经常见到的一些异常情况,比如NullPointerException、IndexOutOfBoundsException,这些异常是我们可以处理的异常。Exception类的异常包括checked exception和unchecked exception。
unchecked exception(非检查异常),也称运行时异常:都是RuntimeException类及其子类异常,如NullPointerException、IndexOutOfBoundsException等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。运行时Java编译器不会去检查该类异常,也就是说,当程序中可能出现这类异常,即使没有用try-catch语句捕获它,也没有用throws子句声明抛出它,也会编译通过。
checked exception(检查异常),也成非运行时异常 (编译异常):是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等。
2、Java中如何处理异常
① 用try、catch捕获异常
try {
File f = new File("d:/1.txt");
if(!f.exists()) {
f.createNewFile();
}
} catch (IOException e) {
e.printStackTrace();
}
可能发生异常的代码块用try{}括上,一旦发生异常,异常便会被catch捕获到(异常需要和catch块中的参数匹配上,发生的异常为catch块中的参数子类或本身则匹配上),然后在catch块中进行异常处理。
② 用throws向上层抛出异常
public static void main(String[] args) {
try {
createFile();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void createFile() throws IOException {
File f = new File("d:/1.txt");
if(!f.exists()) {
f.createNewFile();
}
}
createFile方法中并没有捕获异常,而是用throws关键字声明抛出异常,即告知这个方法的调用者此方法可能会抛出IOException。那么在main方法中调用createFile方法的时候,采用try…catch块进行了异常捕获处理。
注:如果抛出的异常是非运行时异常(checked exception),此方法的调用者必须显示地用try…catch块进行捕获或者继续向上层抛出异常。如果抛出的异常是运行时异常,此方法的调用者可以选择地进行异常捕获处理。
③ 手动用throw向上层抛出异常
public static void main(String[] args) {
int[] array = {1, 2, 3};
try {
getData(0, array);
} catch (Exception e) {
e.printStackTrace();
}
}
public static int getData(int i, int[] array) {
if(i<0 || i>=array.length) {
throw new ArrayIndexOutOfBoundsException("数组索引越界");
}
return array[i];
}
手动抛出了一个ArrayIndexOutOfBoundsException,上层调用的方法main来进行处理这个异常。
注:如果抛出的异常是非运行时异常(checked exception),此方法的调用者必须显示地用try…catch块进行捕获或者继续向上层抛出异常。如果抛出的异常是运行时异常,此方法的调用者可以选择地进行异常捕获处理。
3、运用try,catch,finally,throws,throw五个关键字
① try,catch,finally
try关键字用来包围可能会出现异常的逻辑代码,它单独无法使用,必须配合catch或者finally使用。Java编译器允许的组合使用形式只有以下三种形式:
try…catch…;
try…finally…;
try…catch…finally…
当然catch块可以有多个,注意try块只能有一个,finally块是可选的(但是最多只能有一个finally块)。三个块执行的顺序为try—>catch—>finally。
当然如果没有发生异常,则catch块不会执行。但是finally块无论在什么情况下都是会执行的(这点要非常注意,因此多数情况下,都会将释放资源的操作放在finally块中进行)。
在有多个catch块的时候,是按照catch块的先后顺序进行匹配的,一旦异常类型被一个catch块匹配,则不会与后面的catch块进行匹配。
在使用try…catch…finally块的时候,注意千万不要在finally块中使用return,因为finally中的return会覆盖已有的返回值。看下面例子:
public static void main(String[] args) {
String s = readFile();
System.out.println(s);
}
public static String readFile() {
try {
FileInputStream f = new FileInputStream("d:/1.txt");
f.read();
return "1";
} catch (FileNotFoundException e) {
System.out.println("FileNotFoundException...");
return "2";
} catch (IOException e) {
System.out.println("IOException...");
return "3";
} finally {
System.out.println("finally block...");
//return "4";
}
}
输出结果,文件没有找到,抛出FileNotFoundException异常,执行finally块,正常返回2。
FileNotFoundException...
finally block...
2
接下来把//return “4”;的注释去掉,再次运行,输出结果,我们看到finally的返回值4覆盖了catch块的返回值2。因此如果方法有返回值,切忌不要在finally中使用return,这样会使得程序结构变得混乱。
FileNotFoundException...
finally block...
4
② throws和thow关键字
1)throws出现在方法的声明中,表示该方法可能会抛出的异常,然后交给上层调用它的方法程序处理,throws后面允许跟着多个异常类型;
2)一般会用于程序出现某种逻辑时程序员主动抛出某种特定类型的异常。throw只会出现在方法体中,当方法在执行过程中遇到异常情况时,将异常信息封装为异常对象,然后throw出去。
4、子类进行方法重写的时候,如何声明抛出异常
看代码:
从上面四个图,可以得出:
如果抛出的是RuntimeException类型的异常,Java编译器不会去检查它,所以在子类和父类中使用它都没有关系。
如果不是RuntimeException类型的异常,子类不能抛出比父类范围更广的异常。
5、几点建议
① 只在必要使用异常的地方才使用异常,不要用异常去控制程序的流程
谨慎地使用异常,异常捕获的代价非常高昂,异常使用过多会严重影响程序的性能。如果在程序中能够用if语句和Boolean变量来进行逻辑判断,那么尽量减少异常的使用,从而避免不必要的异常捕获和处理。
② 切忌使用空catch块
在捕获了异常之后什么都不做,相当于忽略了这个异常。千万不要使用空的catch块,空的catch块意味着你在程序中隐藏了错误和异常,并且很可能导致程序出现不可控的执行结果。最好用Log日志将该异常进行记录,以便日后方便更新和维护。
③ 有多个catch块时,需要注意catch块之间的顺序
多个catch块应该按照先子类后父类的顺序排列。如果先父类后子类排列,则后面的子类catch块不会执行到。
④ 异常处理尽量放在高层进行
尽量将异常统一抛给上层调用者,由上层调用者统一之时如何进行处理。如果在每个出现异常的地方都直接进行处理,会导致程序异常处理流程混乱,不利于后期维护和异常错误排查。由上层统一进行处理会使得整个程序的流程清晰易懂。
⑤ 在finally中释放资源
如果有使用文件读取、网络操作以及数据库操作等,记得在finally中释放资源。这样不仅会使得程序占用更少的资源,也会避免不必要的由于资源未释放而发生的异常情况。
欢迎小伙伴们留言交流~~
浏览更多文章可关注微信公众号:diggkr