在平时编程中,基本对异常处理这部分都不是很关心。重点都是在实现程序的主要功能,遇到需要处理或抛出异常的时候也是一味的往上抛出异常。异常可以说是作为编程中缺陷的一种弥补,在编程中我们无法确保写出的代码不会出现问题,所以引进了异常处理,当然这样说也不是很确切。
1、异常概述
java的异常分为Checked异常和Runtime异常。
Checked异常都是可以在编译阶段被处理的异常,所以程序会强制要求处理所有的Checked异常。Runtime异常则是在运行时期才会出现的异常,则无须处理。
java的异常类体系结构如下图
其中Error称为错误,其无法恢复也不可捕获,其将导致应用程序中断。所以编程过程中也无法使用catch块来捕获Error对象,也无法通过throws来抛出Error及其子类对象。
2、try、catch、finally、throws、throw
一提到上述几个关键字,就会让人想起java的异常处理。确实在编程工程中,这几个关键字也是用得最多的。
2.1、try、catch
在平时编码中,我们将会抛出异常的代码块放在try块中,然后使用catch来捕获该异常,并对异常进行处理。那么当try块中的代码出现异常时,系统会自动生成一个异常对象,该对象会被提交给jvm,该过程称为抛出异常。抛出异常后,jvm会去寻找对应的catch块,将该异常对象交给对应的catch块进行处理,所以一个try块后必须至少跟一个catch块或一个finally块。
比如下面一段简单的代码,在使用FileInputStream时,可能存在的情况是e.txt文件并不存在。这个问题在程序开始之前我们是可以知道的,要么该文件存在,要么该文件不存在。如果该文件存在程序继续执行,如果文件不存在就需要进行处理。所以使用try块来捕获该异常,如果文件不存在抛出该异常,并交给catch块来处理。
import java.io.FileInputStream;
import java.io.FileNotFoundException;
public class ExceptionTest_1 {
public static void main(String[] args) {
try {
FileInputStream fis = new FileInputStream("e.txt");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
在上述代码中,我们也可以catch FileNotFoundException的父类,但是一般要求catch对应的异常类型。
2.2、finally
我们一般使用finally都是为了能在异常发生以后去做一些资源对象的关闭动作。比如打开了InputStream流,但是异常发生之后如果不进行处理该资源是不会关闭的。jvm可以做堆内存的垃圾回收,但是它无法估计到比如数据库连接、磁盘文件等一些物理资源。所以这些物理资源都需要进行关闭处理。finally代码块会保证finally中的代码一定会执行,即try块中抛出或者不抛出异常,finally中的代码都会执行。其中代码的执行流程如下图所示:
出现异常之后,try块异常后面的代码不会再执行,程序直接跳转到catch块中,然后跳到finally中直到结束。如果不出现异常,try块中的代码执行结束后会跳到finally中直到结束。
如下代码就是使用finally进行处理的代码。这里catch改为了捕获Exception类型(不建议用)。如果后面其他逻辑功能抛出了异常,catch会捕捉该异常,然后再在finally中关闭fis流。注意这里是指后面的逻辑功能出现异常,不是指创建fis出现异常。因为如果创建fis出现异常,finally中的代码也就没有意义了。
import java.io.FileInputStream;
import java.io.IOException;
public class ExceptionTest_1 {
public static void main(String[] args) {
FileInputStream fis = null;
try {
fis = new FileInputStream("e.txt");
// 其他逻辑功能
} catch (Exception e) {
e.printStackTrace();
} finally {
if(fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
我们知道,finally中的代码再程序中一定会被执行。这是正确的吗?运行下面的代码我们会发现finally块并没有被执行。
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class ExceptionTest_1 {
public static void main(String[] args) throws IOException {
FileInputStream fis = null;
try {
fis = new FileInputStream("d:axis.log");
System.exit(1);
} catch (FileNotFoundException e) {
e.printStackTrace();
}finally {
System.out.println("finally被执行");
fis.close();
}
}
}
注意:虽然我们可以在finally中使用return返回值,或者使用throw抛出异常。但是平时一般不允许这样做,因为finally中的return或throw会覆盖try和catch中的return和throw,严重时函数会返回非期望结果。
2.3、throws
在编程过程中如果不知道如何处理代码中的异常可以使用throws关键字抛出该异常,让该方法的调用者去处理该异常。但是如果main方法中也使用throws关键字抛出该异常,该异常会交给JVM进行处理,JVM对异常处理的方式比较简单:打印异常的详细信息,同时终止程序运行(程序会退出)。
比如如下代码,test方法中使用throws抛出了异常,在main可以使用try处理异常,也可以使用throws来抛出该异常给JVM处理。
import java.io.FileInputStream;
import java.io.FileNotFoundException;
public class ExceptionTest_1 {
public static void main(String[] args) throws FileNotFoundException {
test();
}
public static void test() throws FileNotFoundException {
FileInputStream fis = new FileInputStream("a.txt");
// 后续逻辑代码
}
}
注意
如果方法存在重写,子类中重写父类方法如果存在抛出异常,抛出异常的类型应该是父类抛出异常类型的子类或相同类型,不能是父类中该方法抛出异常类型的父类。同时子类中抛出异常不允许比父类抛出异常多。
2.4、throw
在编写程序的过程中,在遇到一些问题时,我们也可以自行抛出异常。比如如下代码,当输入不符合代码要求时,我们可以自行抛出异常。
import java.util.Scanner;
public class ExceptionTest_1 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int num = sc.nextInt();
try {
if (num < 0) {
throw new Exception("输入的数字不符合要求");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
需要注意,如果抛出的是Checked异常类型,需要对该异常进行try catch捕获。如果不使用try catch,也可以使用throws抛出该异常。
3、java异常体系的特点与改进
3.1、java7新增多异常捕获
多异常捕获是指在一个catch中可以捕获多种类型的异常。
public class ExceptionTest_1 {
public static void main(String[] args) {
method(new int[] { 1, 2 });
}
public static void method(int[] num) {
try {
int a = num[1];
int b = num[2];
int res = a / b;
System.out.println(res);
} catch (IndexOutOfBoundsException | ArithmeticException e1) {
e.printStackTrace();
} catch (Exception e2) {
e.printStackTrace();
}
}
}
需要注意的是,多异常捕获中变量e1为final修饰。同时上述代码的一个隐含概念为:捕获异常时从上往下一定要先捕获小异常,再捕获大异常。
3.2、java7和java9对关闭资源的改进
在以前,我们通常使用finally关闭资源。
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class ExceptionTest_1 {
public static void main(String[] args) throws IOException {
FileInputStream fis = null;
try {
fis = new FileInputStream("d:axis.log");
} catch (FileNotFoundException e) {
e.printStackTrace();
}finally {
System.out.println("finally被执行");
fis.close();
}
}
}
java7对该种方式进行了改进,将待关闭的资源放在try后面的小括号中,该资源会在try块执行完毕后自动关闭。如下代码将输入流放入了try中,我们发现该代码只有一个try块,跟2.1节第一段的结论不太符合。其实并不然,这里的代码就相当于包含了一个隐式的finally块用于关闭输入流。
import java.io.FileInputStream;
import java.io.IOException;
public class ExceptionTest_1 {
public static void main(String[] args) throws IOException {
try (FileInputStream fis = new FileInputStream("d:axis.log")) {
int a = 0;
int b = 1;
System.out.println(a + b);
}
}
}
能放入try中的资源必须实现了AutoCloseAble或Closeable(查看api发现现在使用的大部分可关闭资源都实现了这两个接口)。
java9对该功能进行了改进,如果资源被final修饰,或者不被final修饰,但是其引用一直不被改变,可以在try外部定义,然后将资源放到try后的小括号中即可。
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class ExceptionTest_1 {
public static void main(String[] args) throws IOException {
final FileInputStream fis = new FileInputStream("d:axis.log");
FileOutputStream fos = new FileOutputStream("d:axis.log");
try (fis;fos) {
int a = 0;
int b = 1;
System.out.println(a + b);
}
}
}
3.3、java7改进的throw语句
如下代码,try中抛出的异常其实为FileNotFoundException,在java7之前,throws会抛出Exception,不会进行更细致的检查。在java7中,代码会进行细致检查,throws后面可以抛出try块中更小的异常。
import java.io.FileInputStream;
import java.io.FileNotFoundException;
public class ExceptionTest_1 {
public static void main(String[] args) throws FileNotFoundException{
try {
FileInputStream fis = new FileInputStream("d:axis.log");
}catch(Exception e) {
e.printStackTrace();
throw e;
}
}
}
3.4、catch和throw同时使用
在进行异常处理时,如果当前的catch块无法对异常完全处理。可以在catch中继续抛出异常。
4、自定义异常类
如果java提供的异常不符合自己的要求,我们可以通过继承Exception来自定义异常类。
class MyException extends Exception{
public MyException() {}
public MyException(String message) {
super(message);
}
}
5、总结
我们在学习java编程语言时,最多忽略的可能就是异常这部分。大部分的人认为这部分不重要,能抛就抛,能不解决就不解决。其实这种想法是错误的,代码再出现异常后,我们应该对异常进行捕获。同时在捕获之后提出解决方案对该异常进行妥善的解决,而不是一味的抛给JVM处理。