当程序运行出现以外的情形时,系统将自动生成一个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(" "); 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 }
上面的代码把处理用户输入字符串的代码都放在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 数组格式异常:程序只能接受整数参数
空指针异常:
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 输出:空指针异常
上面程序试图调用一个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 异常处理机制>
上面程序中使用了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 秒) - 正常终止
五、使用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 }
上面程序增加了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流,由于BufferedReaderPrintStream流都实现了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 秒) - 正常终止