异常和测试岗位的理念紧密相关,所以关于异常应该好好说一说,实际上异常机制是判断一门编程语言是否成熟的标准,主流的编程语言中都提供了健全的异常处理机制,请看清楚这里说的是处理机制,它可以使程序中的异常处理代码和正常业务代码分离,保证程序代码更加优雅的展现,而其本质是大大提高了程序的健壮性,反映到客户层面更多的感知就是稳定性
对于计算机语言来说情况相当复杂,没人能保证得了程序永远不会出错,就算程序没有错误,谁又能保证客户是按你的预期来输入的?就算客户是非常聪明而且配合的,谁能保证运行该程序会永久稳定?谁能保证运行程序的物理资源永久配合?谁能保证网络条件永远合适?。。。太多无法保证的东西,人是否要覆盖住?
程序员喜欢的永远是解决问题以及开发带来的创造快感,都不愿意当个堵漏洞的工人,而这才是漏洞的真正概念,而这些也是一个程序员是否成熟的标准
以Java为例,它的异常处理主要依赖于try、catch、finally、throw和throws五个关键字:
Java的异常分为两种,Checked异常和Runtime异常,Checked异常和Runtime异常,Checked都是可以在编译阶段被处理的异常,因此它强制程序处理所有的Checked异常Runtime异常则无需处理,程序员处理异常是一个繁琐的事情,因此程序的健壮性在人而非try代码块
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忽略本次循环剩下的代码,开始执行下一次循环,这就保证了足够的兼容性,用户可以随意输入,程序不会因为用户输入不合法而突然退出
当Java runtime环境接收到异常对象的时候,会依次判断该异常对象是否是catch块里的异常类或者其子类的实例,如果是Java runtime将调用该catch块来处理该异常,否则再次拿该异常对象与下一个catch块里的异常类进行比较,如下图所示
如图所示,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运行时的异常处理逻辑可能有如下几种情形:
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块后的异常参数,程序即可通过该参数来获得异常的相关信息
所有的异常对象都包含了如下几个常用方法:
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();
}
}
}
Java的垃圾回收机制不会回收任何物理资源,垃圾回收机制只能回收堆内存中对象所占用的内存,而程序在try代码块里打开的物理资源如数据库连接、网络资源、磁盘文件等,这些资源都必须显示的回收,而这些显示的回收应该在finally块中做,因为无论try和catch执行的什么,finally代码块总会被执行
try
{
// 业务代码
...
}
catch (SubException e)
{
// 异常处理代码块
}
catch (SubException e)
{
// 异常处理代码块
}
...
finally
{
// 资源回收代码块
...
}
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块里的资源回收!");
}
}
}
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代码块里的任何代码了
当程序使用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("庄生晓梦迷蝴蝶");
}
}
}