【Java面向对象】Exception 异常处理(捕获异常)
简单介绍这篇blog
先用代码举个异常的例子,再结合例子进行理论介绍。
对异常有大致了解后,再对代码进行优化。
理论和举例分析
什么是异常(Exception)?
异常是在程序中导致程序中断的一种指令流。
举个例子:程序中有100行代码,那么它需要按顺序执行第1行到第100行。如果走到第5行的时候出现了bug,从第五行以后的内容就不再执行了,程序崩溃了。那么这个导致程序崩溃(程序中断)的东西就是我们所说的异常。
例如,有如下操作的代码:
代码一:程序中无异常处理
import java.util.Scanner;
public class Demo {
public static void main(String[] args){
test();
System.out.println("程序执行完毕 , 正常结束");
}
private static void test() {
Scanner input = new Scanner(System.in);
System.out.println("请输入一个数字");
int x = input.nextInt();
System.out.println("请再输入一个数字");
int y = input.nextInt();
System.out.println(x / y);
}
}
代码一运行时可能会出现的异常
程序一可能会出现以下两种异常。
异常一:输入参数类型错误导致的异常
该程序需要接收的值是int类型的。如果用户输入为字母,就会出现如下异常:
异常二:除数不能为零的算术异常
我们都知道除数不能为零。那么,如果在程序一中,用户输入的除数为0,就会出现如下异常:
确定异常出现的位置
以上两个截屏中,红色字体第一行描述的是代码出现异常的原因。
那如何确定一场具体出现在哪个指令呢?可以看到图上有描述一些文件的行数。其中灰色表示的文件来自于依赖库,与我们自己写的代码无关,而蓝色的才是自己写的代码最初发生异常的位置。
异常是怎么发生的?
这里用异常二来举例分析。
分析异常二
代码一中第15行出错,也就是x除以y的时候出错。
首先,要知道main方法的调用是JVM在执行,且代码出错以后是被JVM发现的。
当JVM在进行程序员所指定的指令执行时,发现这个指令不合理。那么它就需要告诉程序员:这条指令不合理。
然后,JVM就会通过程序中断(不再执行后续代码)的方式告诉程序员代码出现异常。
- Java官方也对异常问题进行一些整理,列了很多的异常的类型(异常体系结构部分会举例)。为什么是类型呢?因为Java是一门纯面向对象的语言,它是通过对象来告诉程序员程序出现异常。再来解释一下异常发生的过程:
首先,JVM发现程序出现了除数为零这样的一个异常情况。
在执行第15行指令时,JVM会创造一个对象,叫算术异常对象。这个异常对象里面描述的信息就是除数不能为零。
然后,JVM就会中断程序,并显示上面图片上的红色信息,也就是把这个对象给到程序员。
这就是面向对象的一个异常处理的流程(附上图便于理解)。
如果再具体一点来描述。
JVM创建这个异常对象,就相当于这个第15行代码的x除y这句话,变成了一个new对象的操作,new出来的是一个异常对象,因为JVM在执行这句指令的时候出错了。这个异常对象被创建以后,如果在这个main方法里并没有被处理掉,就又被通过类似返回的方式(类似返回,不是返回)抛回给调用这个main方法的人,也就是抛回给JVM。
- 相当于JVM产生了一个异常对象给程序员,但没有被程序员处理,就又通过类似返回的方式给回了JVM。
JVM发现这个异常对象回到它这以后,它的处理方案就是前面所说的中断程序。
那么我们怎么才能不让这个异常对象要返回JVM,让程序正常运行?这个拦截异常对象返回JVM的过程就是接下来要讲的异常处理。
异常处理
程序员对异常处理的拦截操作就叫做捕获异常,可以通过try…catch…来实现。标准异常处理格式如下:
try{
// 可能会发生异常的一段代码
}catch(异常类型1 对象名1){
// 异常类型1的处理操作
}catch(异常类型2 对象名2){
// 异常类型2的处理操作
} ...
finally{
// 异常的统一出口
}
try…catch…处理流程
- 一旦产生异常,则系统会自动产生一个异常类的实例化对象。
- 那么,此时如果异常发生在try语句,则会自动找到匹配的catch语句执行,如果没有在try语句中,则会将异常抛出.
- 所有的catch根据方法的参数匹配异常类的实例化对象,如果匹配成功,则表示由此catch进行处理。
也就是说,这里如果try{…}中出现异常,不会像前面说的那样抛给JVM,而是在对应异常类型catch{…}里面进行异常处理做补救操作。在try{…}这块运行的过程中,一旦发生某个异常被程序员捕获后,try{…}瞬间结束执行,直接跳入catch{…}开始执行。但是程序员必须先预料到哪个异常可能会发生,才能对这个异常进行处理让操作。
那具体都有哪些异常呢?后面异常体系结构部分会描述。
finally
在进行异常的处理之后,在异常的处理格式中还有一个finally语句,那么此语句将作为异常的统一出口,不管是否产生了异常,最终都要执行此段代码。
无论是否发生异常,finally必然执行。
只有在执行finally的指令前,电脑关机了,没内存了,或者已经通过代码
System.exit(status:0);
退出了JVM等特殊情况下,finally才不会执行。
(在最后补充部分举例)
异常体系结构
异常指的是Exception, Exception类, 在Java中存在一个父类Throwable(可能的抛出)。
Throwable存在两个子类:
- Error:表示的是错误,是JVM发出的错误操作,只能尽量避免,无法用代码处理。
- Exception:一般表示所有程序中的错误,所以一般在程序中将进行try…catch…的处理。
Exception类
Exception类中有一个比较特殊的异常子类叫做执行时异常(RuntimeException)。它又被称为非受检异常,指的是这个代码在执行过程中因为参数问题有可能发生的错误。
比如,前面的异常一和异常二。
总的来说,Exception类分两种:受检异常和非受检异常。( 面试可能会问到:Java异常类型分哪些 )
受检异常,即除RuntimeException的其他子类。代码写出来的代码会飘红。
非受检异常RuntimeException,在代码运行时才有可能发生问题。
补充
代码一的优化及其他例子
代码一的优化及其他例子。
代码二(代码一改进):加上异常一和异常二处理的代码
import java.util.Scanner;
public class Demo2 {
public static void main(String[] args){
test();
System.out.println("程序执行完毕 , 正常结束");
}
private static void test() {
Scanner input = new Scanner(System.in);
System.out.println("请输入一个数字");
int x = input.nextInt();
System.out.println("请再输入一个数字");
int y = input.nextInt();
try {
System.out.println(x / y);
System.out.println("处理完毕");
}catch(InputMismatchException e){
System.out.println("请输入输入数字");
}catch(ArithmeticException e) {
System.out.println("除数不能为0");
}
}
}
程序运行:
这里对比代码一的运行异常结果,可以看出,在出现异常后程序中断,
System.out.println("程序执行完毕 , 正常结束");
这句指令没有执行。代码二优化后,该指令能够被执行。
如果希望两个异常都执行同意指令,也可以将两个catch合并成一个。
catch(InputMismatchException|ArithmeticException e) {
System.out.println("出现异常");
}
代码三:一个处理异常更好的例子
当遇到异常时,不是直接打印出错,而是及时补救。
import java.util.InputMismatchException;
import java.util.Scanner;
public class Demo3 {
public static void main(String[] args) {
int num = menu();
System.out.println("你输入的是:"+num);
}
public static int menu(){
System.out.println("1. 增加XX");
System.out.println("2. 删除XX");
System.out.println("3. 修改XX");
System.out.println("0. 退出");
System.out.println("请根据提示,选择功能序号:");
Scanner input = new Scanner(System.in);
int num = -1;
try{
num = input.nextInt();
if(num<0 || num>3){
//程序有问题 , 输入有误
System.out.println("功能序号必须是: 0/1/2/3");
return menu();
}
return num;
}catch(InputMismatchException e){
//补救
System.out.println("必须输入数字哦");
return menu();
}
}
}
这样可以让用户再次输入,直到输入正确。
finally的几个的例子
代码四:try中的return是否会执行finally?
public class Demo4 {
public static void main(String[] args) {
test();
}
public static void test(){
try{
System.out.println("1");
System.out.println("2");
System.out.println("3");
System.out.println("4");
return;
}catch(Exception e){
}finally {
System.out.println("finally语句执行");
}
}
}
运行结果:
1
2
3
4
finally语句执行
简单解释一下:会执行finally,finally在return准备到方法真正结束的中间执行。
代码五:在finally改变某个值时,可能会出现什么结果?
public class Demo5 {
public static void main(String[] args) {
Person p = test();
System.out.println(p.age);
}
public static Person test(){
Person p = new Person(); //赋值了person的引用地址
try{
p.age = 18;
return p;
}catch(Exception e){
return null;
}finally {
p.age = 28;
}
}
static class Person{
int age;
}
}
运行结果:
28
简单来说就是pass-by-reference。这里p储存的是地址,返回的是地址。
代码六:在finally改变某个值时,还可能会出现什么结果?
public class Demo6 {
public static void main(String[] args) {
int a = test();
System.out.println(a);
}
public static int test(){
int a = 18;
try{
return a;
}catch(Exception e){
}finally {
a = 28;
}
return 0;
}
static class Person{
int age;
}
}
运行结果:
18
简单来说就是pass-by-value。在执行try时,return的就是18。
代码五和代码六的区别其实就是 pass-by-value 和 pass-by-reference 的区别。