前言
之前学过C++的异常,现在又学Java中的异常,大同小异
1、异常机制
在说异常之前,我有必要回顾一下之前学的Linux中的信号。当一个进程意外终止,一定是收到了某个信号。而收到了信号的处理动作有三种:
1.忽略该信号
2.执行默认的处理动作(一般都是终止该进程)
3.执行自定义动作(由程序员自己决定到底执行什么)
个人认为,无论是C++还是Java的异常机制,都是对底层信号的封装。
异常机制是指当程序出现错误后,程序如何处理。具体来说,异常机制提供了程序退出的安全通道。当出现错误后,程序执行的流程发生改变,程序的控制权转移到异常处理器。
程序的错误分为三种:
- 编译时错误:编译错误是因为程序没有遵循语法规则,编译程序能够自己发现并且提示我们错误的原因和位置(红色波浪下划线)
- 运行时错误:程序在执行过程中,遇到了无法执行的指令或操作,例如空指针异常,数组越界等
- 逻辑错误:程序没有按照预期的逻辑顺序执行
2、异常结构
Throwable:是异常体系的顶层类,其派生出两个重要的子类,Error(错误) 和 Exception(异常),二者都是 Java 异常处理的重要子类。
Error 和 Exception的区别:
Error:是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现OutOfMemoryError
这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如Java虚拟机运行错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在 Java中,错误通过Error的子类描述。
Exception:是程序本身可以处理的异常。Exception 类有一个重要的子类 RuntimeException。RuntimeException 类及其子类表示“JVM 常用操作”引发的错误。例如,空指针异常、除数为零或数组越界,则分别引发运行时异常(NullPointerException、ArithmeticException)和 ArrayIndexOutOfBoundException。
Exception又分为两类:一类是受查异常,另一类是非受查异常
非受查异常(编译器不要求强制处置的异常):包括运行时异常(RuntimeException与其子类)和错误(Error)。RuntimeException表示编译器不会检查程序是否对RuntimeException作了处理,在程序中不必捕获RuntimException类型的异常,也不必在方法体声明抛出RuntimeException类。RuntimeException发生的时候,表示程序中出现了编程错误,所以应该找出错误修改程序,而不是去捕获RuntimeException
受查异常(编译器要求必须处置的异常):正确的程序在运行中,很容易出现的、情理可容的异常状况。除了Exception中的RuntimeException及RuntimeException的子类以外,其他的Exception类及其子类(例如:IOException和ClassNotFoundException)都属于可查异常。这种异常的特点是Java编译器会检查它,也就是说,当程序中可能出现这类异常,要么用try-catch语句捕获它,要么用throws子句声明抛出它,否则编译不会通过。
3、异常的处理机制
在 Java 应用程序中,异常处理机制为:抛出异常,捕获异常。
1. 抛出异常:当一个方法出现错误引发异常时,方法创建异常对象并交付运行时系统,异常对象中包含了异常类型和异常出现时的程序状态等异常信息。运行时系统负责寻找处置异常的代码并执行。
2. 捕获异常:在方法抛出异常之后,运行时系统将转为寻找合适的异常处理器(exception handler)。潜在的异常处理器是异常发生时依次存留在调用栈中的方法的集合。当异常处理器所能处理的异常类型与方法抛出的异常类型相符时,即为合适 的异常处理器。运行时系统从发生异常的方法开始,依次回查调用栈中的方法,直至找到含有合适异常处理器的方法并执行。当运行时系统遍历调用栈而未找到合适 的异常处理器,则运行时系统终止。同时,意味着Java程序的终止
对于错误、运行时异常、受查异常,Java技术所要求的异常处理方式有所不同。
- 错误:对于方法运行中可能出现的Error,当运行方法不欲捕捉时,Java允许该方法不做任何抛出声明。因为,大多数Error异常属于永远不能被允许发生的状况,也属于合理的应用程序不该捕捉的异常。
- 运行时异常:由于运行时异常的不可查性,为了更合理、更容易地实现应用程序,Java规定,运行时异常将由Java运行时系统自动抛出,允许应用程序忽略运行时异常。
- 受查异常:对于所有的可查异常,Java规定:一个方法必须捕捉,或者声明抛出方法之外。也就是说,当一个方法选择不捕捉可查异常时,它必须声明将抛出异常。
能够捕捉异常的方法,需要提供相符类型的异常处理器。所捕捉的异常,可能是由于自身语句所引发并抛出的异常,也可能是由某个调用的方法或者Java运行时 系统等抛出的异常。也就是说,一个方法所能捕捉的异常,一定是Java代码在某处所抛出的异常。简单地说,异常总是先被抛出,后被捕捉的。
4、异常的基本语法及其使用
try{
有可能出现异常的语句 ;
}[catch (异常类型 异常对象) {
} ... ]
[finally {
异常的出口
}]
try 代码块中放的是可能出现异常的代码
catch 代码块中放的是出现异常后的处理行为
finally 代码块中的代码用于处理善后工作。会在最后执行
其中 catch 和 finally 都可以根据情况选择加或者不加
例1:不处理异常
public class Test8 {
public static void main(String[] args) {
int[] arr = {1, 2, 3};
System.out.println("before");
System.out.println(arr[100]);
System.out.println("after");
}
}
我们发现一旦出现异常,程序就终止了"after"没有正确输出
例2:使用 try catch 后的程序执行过程
public class Test8 {
public static void main(String[] args) {
int[] arr = {1, 2, 3};
try {
System.out.println("before");
System.out.println(arr[100]);
System.out.println("after");
} catch (ArrayIndexOutOfBoundsException e) {
// 打印出现异常的调用栈
e.printStackTrace();
}
System.out.println("after try catch");
}
}
我们发现,一旦 try 中出现异常,那么 try 代码块中的程序就不会继续执行,而是交给 catch 中的代码来执行。catch 执行完毕会继续往下执行。
虽然这里的打印顺序不同,但是程序一定是先catch,再System.out.println(“after try catch”)
关于异常的处理方式
异常的种类有很多,我们要根据不同的业务场景来决定
对于比较严重的问题(例如和算钱相关的场景),应该让程序直接崩溃,防止造成更严重的后果
对于不太严重的问题(大多数场景),可以记录错误日志,并通过监控报警程序及时通知程序员
对于可能会恢复的问题(和网络相关的场景),可以尝试进行重试.
在我们当前的代码中采取的是经过简化的第二种方式. 我们记录的错误日志是出现异常的方法调用信息, 能很快速的让我们找到出现异常的位置. 以后在实际工作中我们会采取更完备的方式来记录异常信息
关于 “调用栈”
方法之间是存在相互调用关系的,这种调用关系我们可以用 “调用栈” 来描述。在 JVM 中有一块内存空间称为 “虚拟机栈” 专门存储方法之间的调用关系。当代码中出现异常的时候,我们就可以使用 e.printStackTrace()。的方式查看出现异常代码的调用栈
例3: catch 只能处理对应种类的异常
修改了代码,让代码抛出的是空指针异常
public class Test8 {
public static void main(String[] args) {
int[] arr = {1, 2, 3};
try {
System.out.println("before");
arr = null;
System.out.println(arr[100]);
System.out.println("after");
} catch (ArrayIndexOutOfBoundsException e) {
e.printStackTrace();
}
System.out.println("after try catch");
}
}
catch 语句不能捕获到刚才的空指针异常,因为异常类型不匹配
例4:catch 可以有多个
public class Test8 {
public static void main(String[] args) {
int[] arr = {1, 2, 3};
try {
System.out.println("before");
arr = null;
System.out.println(arr[100]);
System.out.println("after");
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("这是个数组下标越界异常");
e.printStackTrace();
} catch (NullPointerException e) {
System.out.println("这是个空指针异常");
e.printStackTrace();
}
System.out.println("after try catch");
}
}
一段代码可能会抛出多种不同的异常,不同的异常有不同的处理方式,因此可以搭配多个 catch 代码块
如果多个异常的处理方式是完全相同,也可以写成这样
catch (ArrayIndexOutOfBoundsException | NullPointerException e) {
...
}
例5:可以用一个 catch 捕获所有异常(不推荐)
public class Test8 {
public static void main(String[] args) {
int[] arr = {1, 2, 3};
try {
System.out.println("before");
arr = null;
System.out.println(arr[100]);
System.out.println("after");
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("after try catch");
}
}
由于 Exception 类是所有异常类的父类,因此可以用这个类型表示捕捉所有异常
注意:catch 进行类型匹配的时候,不光会匹配相同类型的异常对象,也会捕捉目标异常类型的子类对象
如刚才的代码,NullPointerException 和 ArrayIndexOutOfBoundsException 都是 Exception 的子类,因此都能被捕获到
例6:finally 表示最后的善后工作, 例如释放资源
public class Test8 {
public static void main(String[] args) {
int[] arr = {1, 2, 3};
try {
System.out.println("before");
arr = null;
System.out.println(arr[100]);
System.out.println("after");
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("finally code");
}
}
}
无论是否存在异常,finally 中的代码一定都会执行到,保证最终一定会执行到 Scanner 的 close 方法
例7:使用 try 负责回收资源
try (Scanner sc = new Scanner(System.in)) {
int num = sc.nextInt();
System.out.println("num = " + num);
} catch (Exception e) {
e.printStackTrace();
}
例6可以有一种等价写法, 将 Scanner 对象在 try 的 ( ) 中创建, 就能保证在 try 执行完毕后自动调用 Scanner的 close 方法
例8:如果本方法中没有合适的处理异常的方式,就会沿着调用栈向上传递
public class Test8 {
public static void main(String[] args) {
try {
func();
} catch (ArrayIndexOutOfBoundsException e) {
e.printStackTrace();
}
System.out.println("after try catch");
}
public static void func() {
int[] arr = {1, 2, 3};
System.out.println(arr[100]);
}
}
例9:如果向上一直传递都没有合适的方法处理异常,最终就会交给 JVM 处理,程序就会异常终止
public class Test8 {
public static void main(String[] args) {
func();
System.out.println("after try catch");
}
public static void func() {
int[] arr = {1, 2, 3};
System.out.println(arr[100]);
}
}
可以看到,程序已经异常终止了,没有执行到 System.out.println(“after try catch”); 这一行
**抛出异常** 除了 Java 内置的类会抛出一些异常之外,程序猿也可以手动抛出某个异常,使用 throw 关键字完成这个操作
public class Test8 {
public static void main(String[] args) {
System.out.println(divide(10, 0));
}
public static int divide(int x, int y) {
if (y == 0) {
throw new ArithmeticException("抛出除 0 异常");
}
return x / y;
}
}
在这个代码中,我们可以根据实际情况来抛出需要的异常,在构造异常对象同时可以指定一些描述性信息
异常说明
我们在处理异常的时候,通常希望知道这段代码中究竟会出现哪些可能的异常
可以使用 throws 关键字,把可能抛出的异常显式的标注在方法定义的位置,从而提醒调用者要注意捕获这些异常
public static int divide(int x, int y) throws ArithmeticException {
if (y == 0) {
throw new ArithmeticException("抛出除 0 异常");
}
return x / y;
}
关于 finally 的注意事项
finally 中的代码保证一定会执行到,这也会带来一些麻烦
finally 执行的时机是在方法返回之前(try 或者 catch 中如果有 return 会在这个 return 之前执行 finally)。但是如果finally 中也存在 return 语句,那么就会执行 finally 中的 return,从而不会执行到 try 中原有的 return
一般我们不建议在 finally 中写 return