Java 学习之路 之 异常处理机制(四十五)

Java 的异常处理机制可以让程序具有极好的容错性,让程序更加健壮。当程序运行出现意外情形时,系统会自动生成一个 Exception 对象来通知程序,从而实现将“业务功能实现代码”和“错误处理代码”分离,提供更好的可读性。

1,使用 try...catch 捕获异常

正如前一节代码所提示的,我们希望有一种非常强大的“if块”,可以表示所有的错误情况,让程序可以一次处理所有的错误,也就是希望将错误集中处理。

出于这种考虑,我们试图把“错误处理代码”从“业务实现代码”中分离出来。将上面最后一段伪码改为如下所示伪码。

if(一切正常)
{
  //业务实现代码
  ...
}
else
{
  alert 输入不合法
  goto retry
}

上面代码中的“if块”依然不可表示——一切正常是很抽象的,无法转换为计算机可识别的代码,在这种情形下,Java 提出了一种假设:如果程序可以顺利完成,那就“一切正常”,把系统的业务实现代码放在 try 块中定义,所有的异常处理逻辑放在 catch 块中进行处理。下面是 Java 异常处理机制的语法结构。

try
{
  //业务实现代码
  ...
}
catch (Exception e)
{
  alert 输入不合法
  goto retry
}

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

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

不管程序代码块是否处于 try 块中,甚至包括 catch 块中的代码,只要执行该代码块出现了异常,系统总会自动生成一个异常对象。如果程序没有为这段代码定义任何的 catch 块,则 Java 运行时环境无法找到处理该异常的 catch 块,程序就在此退出,这就是前面看到的例子程序在遇到异常时退出的情形。

下面使用异常处理机制来改写前面五子棋游戏中用户下棋部分的代码。

String inputStr = null;
// br.readLine(); 每当在键盘上输入一行内容时按回车键
// 刚输入的内容将 br 读取到
while ((inputStr = br.readLine()) != null)
{
  try{
    //将用户输入的字符串以逗号作为分隔符,分解成 2 个字符串
    String[] posStrArr = inputStr.split(",");
    // 将 2 个字符串转换成用户下棋的坐标
    int xPos = Integer.parseInt(posStrArr[0]);
    int yPos = Integer.parseInt(posStrArr[1]);
    // 把对应的数组元素赋为“·”
    if (!gb.board[xPos -1][yPos -1].equals("+")){
      System.out.println("您输入的坐标已有棋子," + "请冲洗输入");
      continue;
    }
    gb.board[xPos - 1][yPos - 1] = "·";
  }
  catch (Exception e){
    System.out.println("您输入的坐标不合法,请重新输入," + "下棋坐标应为 x,y 的格式");
    continue;
  }
  ...
}

上面程序把处理用户输入字符串的代码都放在 try 块里进行,只要用户输入的字符串不是有效的坐标值(包括字母不能正确解析,没有逗号不能正确解析,解析出来的坐标引起数组越界……),系统都将抛出一个异常对象,并把这个异常对象交给对应的 catch 块处理,catch 块的处理方式是向用户提示坐标不合法,然后使用 continue 忽略本次循环剩下的代码,开始执行下一次循环,这就保证了该五子棋游戏有足够的容错性——用户可以随意输出,程序不会因为用户输入不合法而突然退出,程序会向用户提示合法输入,让用户再次输入。

2,异常类的继承体系

当 Java 运行时环境接收到异常对象时,如何为该异常对象寻找 catch 块呢?注意上面 Gobang 程序中 catch 关键字的形式:(Exception e),这意昧着每个 catch 块都是专门用于处理该异常类及其子类的异常实例。

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

当程序进入负责异常处理的 catch 块时,系统生成的异常对象 ex 将会传给 catch 块后的异常形参,从而允许 catch 块通过该对象来获得异常的详细信息。

从图 10.1 中可以看出,try 块后可以有多个 catch 块.这是为了针对不同的异常类提供不同的异常处理方式。当系统发生不同的意外情况时,系统会生成不同的异常对象,Java 运行时就会根据该异常对象所属的异常类来决定使用哪个 catch 块来处理该异常。

通过在 try 块后提供多个 catch 块可以无须在异常处理块中使用 if、switch 判断异常类型,但依然可以针对不同的异常类型提供相应的处理逻辑,从而提供更细致、更有条理的异常处理逻辑。

从图 10.1 中可以看出,在通常情况下,如果 try 块被执行一次,则 try 块后只有一个 catch 块会被执行,绝不可能有多个 catch 块被执行。除非在循环中使用了 continue 开始下一次循环,下一次循环又重新运行了 try 块,这才可能导致多个 catch 块被执行。

try 块与 if 语句不一样, try 块后的花括号({...})不可以省略,即使 try 块里只有一行代码,也不可省略这个花括号。与之类似的是,catch 块后的花括号({...})也不可以省略,还有一点需要指出:try 块里声明的变量是代码块局部变量,它只在 try 块内有限,在 catch 块中不能访问该变量。

Java 提供了丰富的异常类,这些异常类之问有严格的继承关系,图 10.2 显示了 Java 常见的异常类之间的继承关系。


从图 10.2 中可以看出,Java 把所有的非正常情况分成两种:异常(Exception)和错误(Error).它们都继承 Throwable 父类。

Error 错误,一般是指与虚拟机相关的问题,如系统崩溃、虚拟机错误、动态链接失败等,这种错误无法恢复或不可能捕获,将导致应用程序中断。通常应用程序无法处理这些错误,因此应用程序不应该试图使用 catch 块来捕获 Error 对象。在定义该方法时,也无须在其 throws 子句中声明该方法可能抛出 Error 及=其任何子类。下面看几个简单的异常捕获例子。

package com.sym.demo5;

public class DivTest {
	public static void main(String[] args) {
		try {
			int a = Integer.parseInt(args[0]);
			int b = Integer.parseInt(args[1]);
			int c = a / b;
			System.out.println("您输入的两个数相除的结果是:" + c);
		} catch (IndexOutOfBoundsException e) {
			System.out.println("数组越界:运行程序时输入的参数个数不够");
		} catch (NumberFormatException e) {
			System.out.println("数字格式异常:程序只能接收整数参数");
		} catch (ArithmeticException e) {
			System.out.println("算术异常");
		} catch (Exception e) {
			System.err.println("未知异常");
		}
	}
}

上面程序针对 IndexOutOfBoundsException、NumberFormatException、ArithmeticException 类型的异常,提供了专门的异常处理逻辑。Java 运行时的异常处理逻辑可能有如下几种情形。

如果运行该程序时输入的参数不够,将会发生数组越界异常,Java运行时将调用 IndexOutOfBoundsException 对应的 catch 块处理该异常。

如果运行该程序时输入的参数不是数字,而是字母,将发生数字格式异常,Java 运行时将调用 NumberFormatException 对应的 catch 块处理该异常。

如果运行该程序时输入的第二个参数是 0,将发生除 0 异常,Java 运行时将调用 AritbmeticException 对应的 catch 块处理该异常。

如果程序运行时出现其他异常,该异常对象总是 Exception 类或其子类的实例,Java 运行时将调用 Exception 对应的 catch 块处理该异常。

上面程序中的 3 种异常,都是非常常见的运行时异常,读者应该记住这些异常,并掌握在哪些情况下可能出现这些异常。

package com.sym.demo5;

import java.util.Date;

public class NullTest {
	public static void main(String[] args) {
		Date d = null;
		try {
			System.out.println(d.after(new Date()));
		} catch (NullPointerException e) {
			System.out.println("空指针异常");
		} catch (Exception e) {
			System.out.println("未知异常");
		}
	}
}

上面程序针对 NulIPointerException 异常提供了专门的异常处理块。上面程序调用一个 null 对象的 after() 方法,这将引发 NuIIPointerException 异常(当试图调用一个 null 对象的实例方法或实例变量时,就会引发 NuIIPointerExceptron 异常),Java 运行时将会调用 NuIIPointerException 对应的 catch 块来处理该异常;如果程序遇到其他异常,Java 运行时将会调用最后的 catch 块来处理异常。

正如前面程序所看到的,我们总是把对应 Exception 类的 catch 块放在最后,这是为什么呢?想一下图 10.1 所示的 Java 异常捕获流程,读者可能明白原因:如果我们把 Exception 类对应的 catch 块排在其他 catch 块的前面,Java 运行时将直接进入该 catch 块(因为所有的异常对象都是 Exception或其子类的实例),而排在它后面的 catch 块将永远也不会获得执行的机会。

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

try
{
   statements...
}
catch(RuntimeException e)//1
{
  System.out.println("运行时异常");
}
catch(NullPointerException ne)//2
{
  System.out.println("空指针异常");
}

上面代码中有两个 catch 块,前一个 catch 块捕获 RuntimeException 异常,后一个 catch 块捕获 NuIIPointerException 异常.编译上面代码时将会在②处出现已捕获到异常 java.lang.NuIIPointerException 的错误,因为①处的 RuntimeException 已经包括了 NuIIPointerException 异常,所以②处的 catch 块永远也不会获得执行的机会。

进行异常捕获时,一定要记住先捕获小异常,再捕获大异常。

3,Java 7 提供的多异常捕获

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

使用一个 catch 块捕获多种类型的异常时需要注意如下两个地方。

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

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

下面程序示范了 Java 7 提供的多异常捕获。

package com.sym.demo5;

public class MultiExceptionTest {
	public static void main(String[] args) {
		try {
			int a = Integer.parseInt(args[0]);
			int b = Integer.parseInt(args[1]);
			int c = a / b;
			System.out.println("您输入的两个数相除的结果是:" + c);
		} catch (IndexOutOfBoundsException|NumberFormatException|ArithmeticException ie) {
			System.out.println("程序发生了数组越界、数字格式异常、算术异常之一");
			// 捕获多异常时,异常变量默认有 final 修饰
			// 所以下面代码有错
			ie = new ArithmeticException("test");//1
		} catch (Exception e) {
			System.out.println("未知异常");
			// 捕获一种类型的异常时,异常变量没有 final 修饰
			// 所有下面代码完全正确
			e = new RuntimeException("test");//2
		}
	}
}

上面程序中使用了 IndexOutofBoundsExceptionINumberFormatExceptionlArithmeticException 来定义异常类型,这就表明该 catch 块可以同时捕获这 3 种类型的异常。捕获多种类型的异常时,异常变量使用隐式的 final 修饰,因此上面程序中①号代码将产生编译错误:捕获一种类型的异常时,异常变量没有 final 修饰,因此上面程序中②号代码完全正确。

4,访问异常信息

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

所有的异常对象都包含了如下几个常用方法。

getMessage(): 返回该异常的详细描述字符串。

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

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

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

下面例子程序演示了程序如何访问异常信息。

package com.sym.demo5;

import java.io.FileInputStream;
import java.io.IOException;

public class AccessExceptionMsg {
	public static void main(String[] args) {
		try {
			FileInputStream fis = new FileInputStream("a.text");
		} catch (IOException ioe) {
			System.out.println(ioe.getMessage());
			ioe.printStackTrace();
		}
	}
}

上面程序调用了 Exception 对象的 getMessage() 方法来得到异常对象的详细信息,也使用了 prinStackTrace() 方法来打印该异常的跟踪信息。

上面程序中使用的 FileInputStream 是 Java IO 体系中一个文件输入流,用于读取磁盘文件的内容。

5,使用 finally 回收资源

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

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

在哪里回收这些物理资源呢?在 try 块里回收?还是在 catch 块中进行回收?假设程序在 try 块里进行资源回收,根据 10.1 所示的异常捕获流程----如果 try 块的某条语句引起了异常,该语句后的其他语句通常不会获得执行的机会,则将导致位于该语句之后的资源回收语句得不到执行。如果在 catch 块里进行资源回收,但 catch 块完全有可能得不到执行,则将导致不能及时回收这些物理资源。

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

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

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

package com.sym.demo5;

import java.io.FileInputStream;
import java.io.IOException;

public class FinallyTest {
	public static void main(String[] args){
		FileInputStream fis = null;
		try {
			fis = new FileInputStream("a.txt");
		} catch (IOException ioe) {
			System.out.println(ioe.getMessage());
		    // return 语句强制方法返回
			return;//1
		    //使用exit退出虚报帆
			//System.exit(1);//2
		} finally {
			//关闭磁盘文件,回收资源
			if (fis != null){
				try {
					fis.close();
				} catch (IOException ioe) {
					ioe.printStackTrace();
				}
			}
			System.out.println("执行 finally 块里的资源回收!");
		}
	}
}

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

a.txt (The system cannot find the file specified)
执行 finally 块里的资源回收!

上面运行结果表明方法返回之前还是执行了 finally 块的代码。将①处的 return 语句注释掉,取消②处代码的注释,即在异常处理的 catch 块中使用 System.exit(1) 语句来退出虚拟机。执行上面代码,看到如下结果:

a.txt (The system cannot find the file specified)

上面执行结果表明 finally 块没有被执行。如果在异常处理代码中使用 System.exit(1) 语句来退出虚拟机,则 finally 块将失去执行的机会。

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

在通常情况下,不要在 finally 块中使用如 return 或 throw 等导致方法终止的语句,(throw 语句将在后面介绍),一旦在 finally 块中使用了 return 或 throw 语句,将会导致 try 块 catch 块中的 return、throw语句失效。看如下程序。

package com.sym.demo5;

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;
		}
	}
}

上面程序在 finally 块中定义了一个 return false 语句,这将导致 try 块中的 return true 失去作用。运行上面程序,将打印出 false 的结果。

当 Java 程序执行 try 块、catch 块时遇到了 return 或 throw 语句,这两个语句都会导致该方法立即结束,但是系统执行这两个语句并不会结束该方法,而是去寻找该异常处理流程中是否包含 finally 块,如果没有 finally 块,程序立即执行 return 或 throw 语句,方法终止;如果有 finally 块,系统立即开始执行 finally 块----只有当 finally 块执行完成后,系统才会再次跳回来执行 try 块、catch 块里的 return 或 throw语句;如果 finally 块里也使用了 return 或 throw 等导致方法终止的语句,finally 块已经终止了方法.系统将不会跳回去执行 try 块、catch 块里的任何代码。

尽量避免在 finally 块里使用 return 或 throw 等导致方法终止的语句,否则可能会出现一些很奇怪的情况。

6,异常处理的嵌套

正如 FinallyTest.java 程序所示了 finally 块中也包含了一个完整的异常处理流程,这种在 try 块、catch 块或 finally 块中包含完整的异常处理流程的情形被称为异常处理的嵌套。

异常处理流程代码可以放在任何能放可执行性代码的地方,因此完整的异常处理流程既可放在 try 块里,也可放在 catch 块里,还可放在 finally 块里。

异常处理嵌套的深度没有很明确的限制,但通常没有必要使用超过两层的嵌套异常处理,层次太深的嵌套异常处理没有太大必要,而且导致程序可读性降低。

7,Java 7 的自动关闭资源的 try 语句

在前面程序中看到,当程序使用 finally 块关闭资源时,程序显得异常臃肿。

FileInputStream fis = null;
try
{
  fis = new FileInputStream("a.txt");
}
...
finally
{
  //关闭磁盘文件,回收资源
  if (fis != null)
  {
    fis.close();
  }
}

在 Java 7 以前,上面程序中 finally 代码是不得不写的”臃肿代码“’,Java 7 的出现改变了这种局面。Java 7 增强了 try 语句的功能——它允许在 try 关键字后紧跟一对圆括号,圆括号可以声明、初始化一个或多个资源,此处的资源指的是那些必须在程序结束时显式关闭的资源(比如数据库连接、网络连接等),try 语句在该语句结束时自动关闭这些资源。

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

Closeable 是 AutoCloseable 的子接口,可以被自动关闭的资源类要么实现 AutoCloseable 接口,要么实现 Closeable 接口.Closeable 接口里的 close() 方法声明抛出了 IOException,因此它的实现类在实现 close() 方法时只能声明抛出 IOException 及其子类;AutoCloseable 接口里的 close() 方法声明抛出了 Exception,因此它的实现在实现 close()方法时可以声明抛出任何异常。

下面程序示范了如何使用自动关闭资源的 try 语句。

<span style="font-size: 12px;">package com.sym.demo5;

import java.io.BufferedReader;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintStream;

public class AutoCloseTest {
	public static void main(String[] args) throws IOException {
		try (
			//声明、初始化两个可以关闭的资源
			// try 语句会自动关闭这两个资源
			BufferedReader br = new BufferedReader(new FileReader("AutoCloseTest.java"));
			PrintStream ps = new PrintStream(new FileOutputStream("a.txt")))
		{
			// 使用两个资源
			System.out.println(br.readLine());
			ps.print("庄生晓梦迷蝴蝶");
		} 
	}
}</span>

上面程序中 try 圆括号里的分别声明、初始化了两个 IO 流,由于 BufferedReader、PrintStream 都实现了 Closeable 接口,而且它们放在 try 语句中声明、初始化,所以 try 语句会自动关闭它们。因此上面程序是安全的。

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

Java 7 几乎把所有的“资源类”(包括文件 IO 的各种类,JDBC 编程的 Connection、Statement 等接口...)进行了改写,改写后资源类都实现了 AutoCloseable 或 Closeable 接口。

如果程序需要,自动关闭资源的 try 语句后也可以带多个 catch 块和一个 finally 块。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值