Java面向对象系列[v1.0.0][异常处理机制]

异常和测试岗位的理念紧密相关,所以关于异常应该好好说一说,实际上异常机制是判断一门编程语言是否成熟的标准,主流的编程语言中都提供了健全的异常处理机制,请看清楚这里说的是处理机制,它可以使程序中的异常处理代码和正常业务代码分离,保证程序代码更加优雅的展现,而其本质是大大提高了程序的健壮性,反映到客户层面更多的感知就是稳定性

对于计算机语言来说情况相当复杂,没人能保证得了程序永远不会出错,就算程序没有错误,谁又能保证客户是按你的预期来输入的?就算客户是非常聪明而且配合的,谁能保证运行该程序会永久稳定?谁能保证运行程序的物理资源永久配合?谁能保证网络条件永远合适?。。。太多无法保证的东西,人是否要覆盖住?

程序员喜欢的永远是解决问题以及开发带来的创造快感,都不愿意当个堵漏洞的工人,而这才是漏洞的真正概念,而这些也是一个程序员是否成熟的标准

以Java为例,它的异常处理主要依赖于try、catch、finally、throw和throws五个关键字:

  • try代码块中放置可能引发异常的代码,因此程序员判断代码是否会有异常情况,是否需要try代码块来处理就成了关键,try只是工具,关键在人;
  • catch代码块对应异常类型以及该类型的处理方式;
  • 多个catch代码块后可以跟一个finally代码块它与try代码块相呼应,主要用于回收try代码块里打开的物理资源,而异常处理机制会保证finally代码块总会被执行;
  • throws关键字主要是用于方法签名,声明该方法可能抛出的异常,而throw用于抛出一个实际的异常

Java的异常分为两种,Checked异常和Runtime异常,Checked异常和Runtime异常,Checked都是可以在编译阶段被处理的异常,因此它强制程序处理所有的Checked异常Runtime异常则无需处理,程序员处理异常是一个繁琐的事情,因此程序的健壮性在人而非try代码块

使用try…catch捕获异常

try
{
	// 业务实现代码
	...
}
catch (Exception e)
{
	// 处理异常的代码块
}

如果程序可以顺利完成,那就一切正常,如果try块里的业务逻辑代码出现异常,系统会自动生成一个异常对象,该异常对象会被提交给Java runtime环境,而这个过程就被称为抛出异常,当抛出异常发生的时候,Java会寻找能够处理该异常对象的catch块,如果找到合适的catch块,则把该异常对象交给该catch块处理,这个过程被称为捕获异常,如果找不到捕获异常的catch块,则Java runtime环境终止,Java程序也会退出
无论程序是否出现在try代码块中,只要执行该代码块出现了异常,系统总会自动生成异常对象,如果不处理则程序直接退出

import java.io.*;

public class Gobang
{
	// 定义一个二维数组来充当棋盘
	private String[][] board;
	// 定义棋盘的大小
	private static int BOARD_SIZE = 15;
	public void initBoard()
	{
		// 初始化棋盘数组
		board = new String[BOARD_SIZE][BOARD_SIZE];
		// 把每个元素赋为"╋",用于在控制台画出棋盘
		for (var i = 0; i < BOARD_SIZE; i++)
		{
			for (var j = 0; j < BOARD_SIZE; j++)
			{
				board[i][j] = "╋";
			}
		}
	}
	// 在控制台输出棋盘的方法
	public void printBoard()
	{
		// 打印每个数组元素
		for (var i = 0; i < BOARD_SIZE; i++)
		{
			for (var j = 0; j < BOARD_SIZE; j++)
			{
				// 打印数组元素后不换行
				System.out.print(board[i][j]);
			}
			// 每打印完一行数组元素后输出一个换行符
			System.out.print("\n");
		}
	}
	public static void main(String[] args) throws Exception
	{
		var gb = new Gobang();
		gb.initBoard();
		gb.printBoard();
		// 这是用于获取键盘输入的方法
		var br = new BufferedReader(
			new InputStreamReader(System.in));
		String inputStr = null;
		// br.readLine():每当在键盘上输入一行内容按回车,
		// 用户刚刚输入的内容将被br读取到。
		while ((inputStr = br.readLine()) != null)
		{
			try
			{
				// 将用户输入的字符串以逗号作为分隔符,分解成2个字符串
				String[] posStrArr = inputStr.split(",");
				// 将2个字符串转换成用户下棋的坐标
				var xPos = Integer.parseInt(posStrArr[0]);
				var 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;
			}

			gb.printBoard();
			System.out.println("请输入您下棋的坐标,应以x,y的格式:");
		}
	}
}

程序中catch代码块处理了异常后,使用continue忽略本次循环剩下的代码,开始执行下一次循环,这就保证了足够的兼容性,用户可以随意输入,程序不会因为用户输入不合法而突然退出

查找catch代码块

当Java runtime环境接收到异常对象的时候,会依次判断该异常对象是否是catch块里的异常类或者其子类的实例,如果是Java runtime将调用该catch块来处理该异常,否则再次拿该异常对象与下一个catch块里的异常类进行比较,如下图所示
在这里插入图片描述

Java异常类的继承体系

如图所示,Java把所有的非正常情况非为两种即:异常(Exception)和错误(Error),他们都继承Throwable父类
Error:一般是指与虚拟机相关的问题,如系统崩溃、虚拟机错误、动态链接失败等,这种错误无法恢复或不可能捕获,将导致程序中断,一般情况下应用程序无法处理这些错误,因此应用程序不应该试图使用catch块来捕获Error对象,在定义该方法时,无须在其throws子句中声明该方法可能抛出Error及其任何子类
在这里插入图片描述

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

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

  • 如果运行该程序时输入的参数不够,将会发生数组越界异常,Java运行时将调用IndexOutOfBoundsException对应的catch块处理该异常
  • 如果运行该程序时输入的参数不是数字,而是字母,将发生数字格式异常, Java运行时将会调用NumberFormatException对应的catch块处理该异常
  • 如果运行该程序时输入的第二个参数是0,将会发生除0异常,Java运行时将会调用ArithmeticException对应的catch块处理该异常
  • 如果程序运行时出现其他异常,该异常对象总是Exception类或其子类的实例,Java运行时将调用Exception对象的catch块处理该异常
import java.util.*;

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

当试图调用一个null对象的实例方法或实例变量的时,就会引发NullPointerException异常,Java运行时会调用NullPointerException对应的catch块来处理该异常,如果遇到其他异常则调用最后的catch块来处理异常

注意:Exception类的catch块必须方法最后,因为所有的异常对象都是Exception或其子类的实例,如果Exception类的catch块在前边,那么它后边的catch块将永远不会获得执行

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

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

因为RuntimeException已经包括了NullPointerException异常,所以catch (NullPointerException ne)处的catch块永远不会获得执行的机会

多异常捕获

在Java7之后,一个catch块可以捕获多种类型的异常,只需要在多种异常类型之间使用竖线(|)隔开,并且异常变量有隐式的final修饰,因此程序不能对异常变量重新赋值

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

访问异常类信息

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

  • getMessage():返回该异常的详细描述字符串
  • printStackTrace():将该异常的跟踪栈信息输出到标准错误输出
  • printStackTrace(PrintStream s): 将该异常的跟踪栈信息输出到指定输出流
  • getStackTrace():返回该异常的跟踪栈信息
import java.io.*;

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

finally块的作用

Java的垃圾回收机制不会回收任何物理资源,垃圾回收机制只能回收堆内存中对象所占用的内存,而程序在try代码块里打开的物理资源如数据库连接、网络资源、磁盘文件等,这些资源都必须显示的回收,而这些显示的回收应该在finally块中做,因为无论try和catch执行的什么,finally代码块总会被执行

try
{
	// 业务代码
	...
}
catch (SubException e)
{
	// 异常处理代码块
}
catch (SubException e)
{
	// 异常处理代码块
}
...
finally
{
	// 资源回收代码块
	...
}
  • 异常处理机制中必须有try代码块,catch和finally都是可选的,但catch和finally必须有其一,也可以同时有
  • 可以有多个catch块,捕获父类异常的catch块必须位于捕获子类异常的后面
  • 不能只有try代码块,既没有catch也没有finally
  • 多个catch代码块必须位于try代码块之后,finally块必须位于所有的catch块之后
import java.io.*;

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;       
			// 使用exit来退出虚拟机
			System.exit(1);    
		}
		finally
		{
			// 关闭磁盘文件,回收资源
			if (fis != null)
			{
				try
				{
					fis.close();
				}
				catch (IOException ioe)
				{
					ioe.printStackTrace();
				}
			}
			System.out.println("执行finally块里的资源回收!");
		}
	}
}
  • 即便有return强制方法返回,但仍旧会执行finally代码块里的代码
  • System.exit(1)强制退出JVM,这种退出的话finally就没机会执行了
  • 如果在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;
		}
	}
}

当系统遇到try和catch里的return或者throw语句的时候,都会立即终止执行当前方法,当方法执行并未结束,且return和throw语句也未执行,然后程序去寻找finally代码块,如果没有finally代码块程序立即执行return或throw语句方法终止,如果有finally代码块,系统立即执行finally代码块,当finally代码块执行完毕后系统才会跳回去执行try代码块、catch代码块里的return或throw语句,如果finally里也使用了return或throw等导致方法终止的语句,finally代码块就终止了系统也不会跳回去执行try代码块、catch代码块里的任何代码了

自动关闭资源的try语句

当程序使用finally代码块关闭资源时,显得非常臃肿,Java7之后允许try关键字后跟一对圆括号用于声明、初始化一个或多个资源,然后try语句在该语句结束时自动关闭这些资源,从而降低了代码的臃肿
需要说明的是,要保证try语句可以正常关闭资源,这些资源实现类必须实现AutoCloseable或Closeable接口,实现这两个接口就必须实现close()方法

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

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

自动关闭资源的try语句相当于包含了隐式的finally代码块,因此这个代码既没有catch也没有finally,Java7之后几乎所有的资源类,包括文件IO的各种类、JDBC的Connection、Statement等接口进行了改写,改写后资源类都实现了AutoCloseable或Closeable接口

Java9之后不要求在try后的圆括号内声明并创建资源,只需要自动关闭的资源有final修饰或者是有效的final,在Java9之后改写上面的代码

import java.io.*;

public class AutoCloseTest2
{
	public static void main(String[] args)
		throws IOException
	{
		// 有final修饰的资源
		final var br = new BufferedReader(
			new FileReader("AutoCloseTest.java"));
		// 没有显式使用final修饰,但只要不对该变量重新赋值,按该变量就是有效的final
		var ps = new PrintStream(new
			FileOutputStream("a.txt"));
		// 只要将两个资源放在try后的圆括号内即可
		try (br; ps)
		{
			// 使用两个资源
			System.out.println(br.readLine());
			ps.println("庄生晓梦迷蝴蝶");
		}
	}
}

猜你喜欢

转载自blog.csdn.net/dawei_yang000000/article/details/106038440
今日推荐