9.1 异常处理机制

当程序运行出现以外的情形时,系统将自动生成一个Exception对象来通知程序,从而实现“业务功能实现代码”和“错误处理代码”分类,提供更好的可读性。

一、使用try...catch捕获异常

为了将“错误处理代码”从“业务实现代码”中分离出来,考虑下面的伪代码:

1 if(一切正常)
2 {
3      //业务实现代码  
4 }
5 else
6 {
7     alert 输入不合法
8     goto retry
9 }

 java提出了一种假设:如果程序可以顺利完成,那么”一切正常”,把系统的业务实现代码放在try块中定义,所有异常处理逻辑放在catch块中处理。下面是Java异常处理机制的语法结构:

1 try
2 {
3     //业务实现代码
4 }
5 catch(Exception e)//捕获(catch)异常
6 {
7     alert 输入不合法
8     goto retry
9 }

  如果执行try块里的业务代码时出现异常,系统将会自动生成一个异常对象,该异常对象被提交给Java运行环境,这个过程被称为抛出(throw)异常

  当Java运行时环境收到异常对象时,会寻找能处理该异常对象的catch块,如果找到合适的catch块,则把该异常对象将给catch块处理,这个过程被称为捕获(catch)异常;如果Java运行时环境找不到捕获异常的catch块,则运行环境终止,Java程序也将退出。

 1 import java.io.*;
 2 public class Gobang
 3 {
 4     // 定义一个二维数组来充当棋盘
 5     private String[][] board;
 6     // 定义棋盘的大小
 7     private static int BOARD_SIZE = 15;
 8     public void initBoard()
 9     {
10         // 初始化棋盘数组
11         board = new String[BOARD_SIZE][BOARD_SIZE];
12         // 把每个元素赋为"╋",用于在控制台画出棋盘
13         for (var i = 0; i < BOARD_SIZE; i++)
14         {
15             for (var j = 0; j < BOARD_SIZE; j++)
16             {
17                 board[i][j] = "╋";
18             }
19         }
20     }
21     // 在控制台输出棋盘的方法
22     public void printBoard()
23     {
24         // 打印每个数组元素
25         for (var i = 0; i < BOARD_SIZE; i++)
26         {
27             for (var j = 0; j < BOARD_SIZE; j++)
28             {
29                 // 打印数组元素后不换行
30                 System.out.print(board[i][j]);
31             }
32             // 每打印完一行数组元素后输出一个换行符
33             System.out.print("\n");
34         }
35     }
36     public static void main(String[] args) throws Exception
37     {
38         var gb = new Gobang();
39         gb.initBoard();
40         gb.printBoard();
41         // 这是用于获取键盘输入的方法
42         var br = new BufferedReader(
43             new InputStreamReader(System.in));
44         String inputStr = null;
45         // br.readLine():每当在键盘上输入一行内容按回车,
46         // 用户刚刚输入的内容将被br读取到。
47         while ((inputStr = br.readLine()) != null)
48         {
49             try
50             {
51                 // 将用户输入的字符串以逗号作为分隔符,分解成2个字符串
52                 String[] posStrArr = inputStr.split(",");
53                 // 将2个字符串转换成用户下棋的坐标
54                 var xPos = Integer.parseInt(posStrArr[0]);
55                 var yPos = Integer.parseInt(posStrArr[1]);
56                 // 把对应的数组元素赋为"●"。
57                 if (!gb.board[xPos - 1][yPos - 1].equals("╋"))
58                 {
59                     System.out.println("您输入的坐标点已有棋子了,"
60                         + "请重新输入");
61                     continue;
62                 }
63                 gb.board[xPos - 1][yPos - 1] = "●";
64             }
65             catch (Exception e)
66             {
67                 System.out.println("您输入的坐标不合法,请重新输入,"
68                     + "下棋坐标应以x,y的格式");
69                 continue;
70             }
71 
72             gb.printBoard();
73             System.out.println("请输入您下棋的坐标,应以x,y的格式:");
74         }
75     }
76 }
View Code

上面的代码把处理用户输入字符串的代码都放在try块里进行,只要用户输入的字符串不是有效的坐标值,系统将会抛出一个异常,并把这个异常对象交给对应的catch块处理。

二、异常类的继承关系

  当Java运行时环境收到异常对象后,会依次判断该异常对象是否是catch块后异常类或其子类的实例,如果是,Java运行时环境将调用该catch块来处理异常;否则再次拿该对象和下一个catch块里的异常类进行比较。Java异常捕获流程示意图:

 Java把所有的非正常情况分为两种:异常(Exception)和错误(Error),它们都继承Throwable父类。

Error错误,一般是指与虚拟机相关的问题,如系统崩溃、虚拟机错误、动态链接失败等,这种错误无法恢复或不可能捕捉。通常程序无法处理这些错误,因此应用程序不应该尝试使用catch来捕捉Error对象,也无须在其throws子句中声明该方法可能抛出Error及其任何子类。

Java常见异常类之间的继承关系:

 程序示例:

 1 public class DivTest 
 2 {
 3     public static void main(String[] args) 
 4     {
 5         try
 6         {
 7             {
 8                 var a=Integer.parseInt(args[0]);
 9                 var b=Integer.parseInt(args[1]);
10                 var c=a/b;
11                 System.out.println("你输入的两个数相除的结果是:"+c);
12             }
13         }
14         catch (IndexOutOfBoundsException ie)
15         {
16             System.out.println("数组越界:运行程序时输入的参数个数不够");
17         }
18         catch (NumberFormatException ne)
19         {
20             System.out.println("数组格式异常:程序只能接受整数参数");
21         }
22         catch (ArithmeticException ae)
23         {
24             System.out.println("算术异常");
25         }
26         catch (Exception e)
27         {
28             System.out.println("未知异常");
29         }
30     }
31 }
32 运行测试:
33 E:\>cd E:\Java\第十章 异常处理\10.2 异常处理机制
34 
35 E:\Java\第十章 异常处理\10.2 异常处理机制>javac DivTest.java
36 
37 E:\Java\第十章 异常处理\10.2 异常处理机制>java DivTest 1 2
38 你输入的两个数相除的结果是:0
39 
40 E:\Java\第十章 异常处理\10.2 异常处理机制>java DivTest 5
41 数组越界:运行程序时输入的参数个数不够
42 
43 E:\Java\第十章 异常处理\10.2 异常处理机制>java DivTest 8 4
44 你输入的两个数相除的结果是:2
45 
46 E:\Java\第十章 异常处理\10.2 异常处理机制>java DivTest 5 0
47 算术异常
48 
49 E:\Java\第十章 异常处理\10.2 异常处理机制>java DivTest 1.2 1
50 数组格式异常:程序只能接受整数参数
View Code

空指针异常:

 1 import java.util.Date;
 2 public class NullTest 
 3 {
 4     public static void main(String[] args) 
 5     {
 6         Date d=null;
 7         try
 8         {
 9             System.out.println(d.after(new Date()));
10         }
11         catch (NullPointerException ne)
12         {
13             System.out.println("空指针异常");
14         }
15         catch (Exception e)
16         {
17             System.out.println("未知异常");
18         }
19     }
20 }
21 输出:空指针异常
View Code

上面程序试图调用一个null对象的after()方法,这将引发NullPointerException异常(当试图调用一个null对象的实例方法或实例变量时,就会引发NullPointerException异常),Java运行时会调用NullPointerException对应的catch块处理该异常。

实际上,进行异常捕获时不仅应该把Exception类对应的catch块放在最后,而且所有父类异常的catch块都应该排在子类异常catch块的后面(简称:先处理小异常,再处理大异常),否则编译器将出现错误。

三、多异常捕获

在Java 7以前,每个catch块只能捕获一种类型的异常;但从Java 7开始,一个catch块可以捕获多种类型的异常。

使用一个catch块多种类型的异常需要注意:

1、捕获多种类型的异常时,多种异常类型之间用竖线(|)隔开;

2、捕获多种类型的异常时,异常变量有隐式的final修饰,程序不能对异常变量重写赋值。

 1 public class MultiExceptionTest
 2 {
 3     public static void main(String[] args)
 4     {
 5         try
 6         {
 7             var a = Integer.parseInt(args[0]);
 8             var b = Integer.parseInt(args[1]);
 9             var c = a / b;
10             System.out.println("您输入的两个数相除的结果是:" + c );
11         }
12         catch (IndexOutOfBoundsException|NumberFormatException
13             |ArithmeticException ie)
14         {
15             System.out.println("程序发生了数组越界、数字格式异常、算术异常之一");
16             // 捕捉多异常时,异常变量默认有final修饰,
17             // 所以下面代码有错:
18 //            ie = new ArithmeticException("test");  //
19         }
20         catch (Exception e)
21         {
22             System.out.println("未知异常");
23             // 捕捉一个类型的异常时,异常变量没有final修饰
24             // 所以下面代码完全正确。
25             e = new RuntimeException("test");    //
26         }
27     }
28 }
29 
30 E:\Java\第十章 异常处理\10.2 异常处理机制>javac MultiExceptionTest.java
31 
32 E:\Java\第十章 异常处理\10.2 异常处理机制>java MultiExceptionTest 1 2.2
33 程序发生了数组越界、数字格式异常、算术异常之一
34 
35 E:\Java\第十章 异常处理\10.2 异常处理机制>
View Code

 上面程序中使用了IndexOutOfBoundsException|NumberFormatException|ArithmeticException来定义异常类型,这就是表明该catch块可以同时捕获这三种异常类型。捕获多种类型的异常时,异常变量使用隐式final修饰,因此上面程序中①号粗体代码将会产生编译错误;捕获一种类型的异常时,异常变量没有final修饰,因此上面程序中②号代码可以通过。

四、访问异常类型

如果程序需要在catch块中访问异常对象的相关信息,则可以通过访问catch块的后异常形参来获得。当Java运行时决定调用某个catch块来处理该异常对象时,会将异常对象赋给catch块后的异常参数,程序可以通过该参数来获得异常的相关信息。

所有异常都包含如下几个方法:
1、getMessage():返回该异常的详细描述字符串。

2、printStackTrace():将该异常的跟踪栈信息输出到标准错误输出。

3、printStackTrace(PrintStream s):将异常的跟踪栈信息输出到指定输出流。

4、getStackTrace():返回异常的跟踪栈信息。

 1 import java.io.FileInputStream;
 2 import java.io.IOException;
 3 public class AccessExceptionMsg 
 4 {
 5     public static void main(String[] args) 
 6     {
 7         try
 8         {
 9             var fis=new FileInputStream("a.txt");
10         }
11         catch (IOException ioe)
12         {
13             System.out.println(ioe.getMessage());
14             ioe.printStackTrace();
15         }
16     }
17 }
18 ---------- 运行Java捕获输出窗 ----------
19 a.txt (系统找不到指定的文件。)//异常信息描述
20 
21 //异常根据栈信息
22 java.io.FileNotFoundException: a.txt (系统找不到指定的文件。)
23     at java.base/java.io.FileInputStream.open0(Native Method)
24     at java.base/java.io.FileInputStream.open(FileInputStream.java:213)
25     at java.base/java.io.FileInputStream.<init>(FileInputStream.java:155)
26     at java.base/java.io.FileInputStream.<init>(FileInputStream.java:110)
27     at AccessExceptionMsg.main(AccessExceptionMsg.java:9)
28 
29 输出完成 (耗时 0 秒) - 正常终止
View Code

五、使用final回收资源

 程序在try块打开了一些物理资源(例如数据库连接、网络连接和磁盘文件等),这些物理资源必须显式回收。

注意:Java的回收机制不会回收任何物理资源,垃圾回收机制只能回收堆内存中对象所占用的内存。

 为了一定能回收try块中打开的物理资源,异常处理机制提供了finally块。不管try块中的代码是否出现异常,也不管哪一个catch块会被执行,甚至try块或catch块中执行了return语句,finally块总会被执行。完整的Java异常处理处理语法结构:

try
{
    //业务实现代码
    ...
}
catch (SubException1 e)
{
    //异常处理块1
    ...
}
catch (SubException2 e)
{
    //异常处理块2
    ...
}
finally
{
    //资源回收块
    ...
}

异常处理语法结构中的try块是必需的,即没有try块,则不能有后面的catch块和finally块;catch块和finally块都是可选的,但catch块和fianlly块至少出现其中之一,也可以同时出现;可以有多个catch块,捕获父类异常的catch块必须位于捕获子类异常的后面;但不能只有try块,既没有catch块,也没有finally块;多个catch块位于try块之后,finally块必须位于所有的catch块之后。例如下面的程序: 

 1 import java.io.*;
 2 public class FinallyTest
 3 {
 4     public static void main(String[] args)
 5     {
 6         FileInputStream fis=null;
 7         try
 8         {
 9             fis=new FileInputStream("a.txt");
10         }
11         catch (IOException ioe)
12         {
13             System.out.println(ioe.getMessage());
14             //return语句强制方法返回
15             return;//16             //使用exit退出虚拟机
17             //System.exit(1);//
18         }
19         finally
20         {
21             //关闭磁盘文件,回收资源
22             if(fis!=null)
23             {
24                 try
25                 {
26                     fis.close();
27                 }
28                 catch (IOException ioe)
29                 {
30                     ioe.printStackTrace();
31                 }
32             }
33             System.out.println("回收finally块里的资源回收!");
34         }
35     }
36 }
View Code

上面程序增加了finally块,用于回收在try块中打开的物理资源。上面程序的catch块中①处有一条return语句,该语句强制方法返回。通常情况下,一旦方法执行到return语句的地方,程序将会立即结束该方法;现在不会,虽然return语句也强制方法结束,但一定会先执行到finally块里的代码。

a.txt (系统找不到指定的文件。)
回收finally块里的资源回收!

如果使用System.exit(1)语句退出虚拟机,则finally块将失去执行的机会。执行上面的代码,将会看到如下结果:

1 a.txt (系统找不到指定的文件。)

 注意:除非在try块、catch块中调用了退出虚拟机的方法,否则不管在try块、catch块中执行怎样的代码,出现增氧的情况,异常处理finally块总会被执行。

在异常情况下,不要在finally块中使用如return或throw语句等导致方法终止的语句,一旦在finally块中使用了return或throw语句,将导致try块、catch块中的return或throw语句失效。例如:

public class FinallyFlowTest
{
    public static void main(String[] args)
        throws Exception
    {
        boolean a=test();
        System.out.println(a);
    }
    public static boolean test()
    {
        try
        {
            //因为finally块中return语句
            //所以下面的return语句失去作用
            return true;
        }
        finally
        {
            return false;
        }
    }
}
输出:false

六、异常处理的嵌套

异常处理流程代码可以放在任何可执行代码的地方,因此完整的异常处理流程既可以放在try块里,也可放在catch块里,还可以放在finally块里。这种情形叫做异常处理嵌套。异常处理的深度没有任何限制,但通常没有必要超过两层嵌套异常处理,因为层次太深将降低程序的可读性。

七、Java 9增强的自动关闭资源的try语句

回顾以前在finally块中关闭资源时,程序显得异常臃肿:

 1 public class FinallyTest
 2 {
 3     public static void main(String[] args)
 4     {
 5         FileInputStream fis=null;
 6         try
 7         {
 8             fis=new FileInputStream("a.txt");
 9         }
10 ......
11         finally
12         {
13             //关闭磁盘文件,回收资源
14             if(fis!=null)
15             {
16                 try
17                 {
18                     fis.close();
19                 }
20                 catch (IOException ioe)
21                 {
22                     ioe.printStackTrace();
23                 }
24             }
25             System.out.println("回收finally块里的资源回收!");
26         }
27     }
28 }

java 7增强了try语句的功能——它允许在try关键字后紧跟一对圆括号,圆括号可以声明、初始化一个或多个资源,此处的资源指那些必须在程序结束时显式关闭的资源(比如数据库连接、网络连接),try语句在该语句结束时自动关闭这些资源。

需要注意的是,为了保证try语句可以正常关闭资源,这些资源类,必须实现AutoCloseable或Closeable接口,实现这两个接口就必须实现close。

提示:

 1 import java.io.*;
 2 public class AutoCloseTest
 3 {
 4     public static void main(String[] args)
 5         throws IOException
 6     {
 7         try (
 8             // 声明、初始化两个可关闭的资源
 9             // try语句会自动关闭这两个资源。
10             var br = new BufferedReader(
11                 new FileReader("AutoCloseTest.java"));
12             var ps = new PrintStream(new
13                 FileOutputStream("a.txt")))
14         {
15             // 使用两个资源
16             System.out.println(br.readLine());
17             ps.println("庄生晓梦迷蝴蝶");
18         }
19     }
20 }

上面代码分别声明、初始化了两个IO流,由于BufferedReader\PrintStream流都实现了Closeable子接口,而且它们放在try语句中声明、初始化,所以try语句会自动关闭它们。

自动关闭资源的try相当于包含了隐式的finally块(这个finally块用于关闭资源),因此这个try语句既没有catch块,也没有finally块。

Java 9再次增强了这种try语句,Java 9不要求在try后的圆括号内声明并创建资源,只需要自动关闭的资源有final修饰或则时有效的final,Java 9允许将资源变量放在try后的圆括号内。

 1 import java.io.*;
 2 public class AutoCloseTest2
 3 {
 4     public static void main(String[] args)
 5         throws IOException
 6     {
 7         //有final修饰的资源
 8         final var br=new BufferedReader(new FileReader("AutoCloseTest2.java"));
 9         //没有显式使用final修饰,但只要不对该变量重新赋值,该变量就是有效的final,隐式修饰的
10         //捕获多种类型异常时,异常变量有final修饰
11         var ps=new PrintStream(new FileOutputStream("a.txt"));
12         //只要将两个资源放在try后的圆括号即可
13         try(br;ps)
14         {
15             //使用这两个资源
16             System.out.println(br.readLine());
17             ps.println("庄生晓梦迷蝴蝶");
18         }
19     }
20 }
21 ---------- 运行Java捕获输出窗 ----------
22 import java.io.*;
23 
24 输出完成 (耗时 0 秒) - 正常终止
posted @ 2020-04-02 10:53  小新和风间  阅读(224)  评论(0编辑  收藏  举报