参考资料
[1]. 疯狂Java讲义(第三版) 李刚
[2]. The try-with-resources Statement,
https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html
异常处理机制
异常关键字
Java的异常机制主要依赖于try、catch、finally、throw、throws五个关键字
- try关键字后紧跟一个花括号括起来的代码块(花括号不可省略),简称try块,它里面放置可能引发异常的代码。
- catch后对应异常类型和一个代码块,用于表名该catch块用于处理这种类型的代码块,catch块可以有多个,catch的异常范围应该是从小到大。
- finally块在catch块后面,用于显式回收在try块里打开的物理资源,异常机制会保证finally块一定被执行。
- throws 关键字主要在方法签名中使用,用于声明该方法可能抛出的异常。
- throw 可以单独作为语句使用,抛出一个具体的异常对象。
异常的继承体系
Java把所有的非正常情况分成两种:异常(Exception)和错误(Error),它们都继承了Throwable父类。
Error错误,一般是指与虚拟机相关的问题,如系统崩溃、虚拟机错误、动态链接失败等,所以我们的关注对象是Exception异常。
Exception异常
进行异常捕获时不仅应该把Exception类对应的catch块放在最后,而且所有父类异常的catch块都应该排在子类异常catch块的后面,简单的说就是,先处理小异常,再处理大异常,否则会出现编译错误。
示例1
int[] a = {1,2,3};
try
{
int c = a[0] / a[3];
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("未知异常");
}
捕获空指针异常和其他异常
Date d = null;
try
{
System.out.println(d.after(new Date()));
}
catch (NullPointerException ne)
{
System.out.println("空指针异常");
}
catch(Exception e)
{
System.out.println("未知异常");
}
Java 7 提供的多异常捕获
在Java 7 以前,每个catch块只能捕获一种类型的异常;但从Java 7 开始,一个catch块可以捕获多种类型的异常。
注意事项:
1. 多种异常类型之间用竖线(|)隔开。
2. 异常变量有隐式的final修饰,因此程序不能对异常变量重新赋值。
示例程序:
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"); // ①
}
catch (Exception e)
{
System.out.println("未知异常");
// 捕捉一个类型的异常时,异常变量没有final修饰
// 所以下面代码完全正确。
e = new RuntimeException("test"); // ②
}
访问异常信息
所有的异常对象都包含了如下几个常用方法:
1. getMessage():返回该异常的详细描述字符串。
2. printStackTrace():将该异常的跟踪栈信息输出到标准错误输出。
3. printStackTrace(PrintSream s):将该异常的跟踪栈信息输出到标准错误输出。
4. getStackTrace():返回该异常的跟踪栈信息。
try
{
FileInputStream fis = new FileInputStream("a.txt");
}
catch (IOException ioe)
{
System.out.println(ioe.getMessage());
ioe.printStackTrace();
}
使用finally回收资源
由于Java的垃圾回收机制不会回收任何物理资源,垃圾回收机制只能回收堆内存中对象所占用的内存。所以需要显示回收例如数据库连接、网络连接和磁盘文件等物理资源。
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块里的资源回收!");
}
在通常情况下,不要在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;
}
}
}
Java 7 的自动关闭资源的try语句
Java 7 增强了try语句的功能,它允许在try关键字后紧跟一对圆括号,圆括号可以声明、初始化一个或多个资源,此处的资源指的是那些必须在程序结束时显示关闭的资源(比如数据库连接、网络连接等),try语句在该语句结束时自动关闭这些资源。
实现条件
Closeable是AutoCloseable的子接口,可以被自动关闭的资源类要么实现AutoCloseable接口,要么实现Closeable接口。
Closeable接口里的close()方法声明抛出了IOException,因此它的实现类在实现close()方法时只能声明抛出IOException或其子类。
AutoCloseable接口里的close()方法声明抛出了Exception,因此它的实现类在实现close()方法时可以声明抛出任何异常。
代码示例1:
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.println("庄生晓梦迷蝴蝶");
}
}
}
BufferedReader类和PrintStream类都实现了Closeable接口,它们放在try语句中声明、初始化的话,try语句会自动关闭它们。
自动关闭资源的try语句相当于包含了隐式的finally块(这个finally块用于关闭资源),因此这个try语句可以既没有catch块,也没有finally块。如果有需要,也可以带catch和finally块。
Checked异常和Runtime异常体系
Java的异常被分为两大类,Checked异常和Runtime异常(运行时异常)。
使用throws声明抛出异常
throws声明用在方法签名之后,一旦使用throws语句声明抛出该异常,程序就无须使用try..catch块来捕获该异常了。
语法如下:
throws ExceptionClass1, ExceptionClass1...
最简单的示例
public class ThrowsTest
{
public static void main(String[] args)
throws IOException
{
FileInputStream fis = new FileInputStream("a.txt");
}
}
调用一个带throws的方法
public class ThrowsTest2
{
public static void main(String[] args)
throws Exception
{
// 因为test()方法声明抛出IOException异常,
// 所以调用该方法的代码要么处于try...catch块中,
// 要么处于另一个带throws声明抛出的方法中。
test();
}
public static void test()throws IOException
{
// 因为FileInputStream的构造器声明抛出IOException异常,
// 所以调用FileInputStream的代码要么处于try...catch块中,
// 要么处于另一个带throws声明抛出的方法中。
FileInputStream fis = new FileInputStream("a.txt");
}
}
子类方法抛出的异常不能超过父类
public class OverrideThrows
{
public void test()throws IOException
{
FileInputStream fis = new FileInputStream("a.txt");
}
}
class Sub extends OverrideThrows
{
// 子类方法声明抛出了比父类方法更大的异常
// 所以下面方法出错
public void test()throws Exception
{
}
}
使用throw抛出异常
抛出异常
如果需要在程序中自行抛出异常,则应使用throw语句,throw语句可以单独使用,throw语句抛出的不是异常类,而是一个异常实例,而且每次只能抛出一个异常实例。
如果throw语句抛出的异常是Checked异常,则该throw语句要么处于try块,显式捕获该异常,要么放在一个带throws声明抛出的方法中,即把该异常交给该方法的调用者处理;如果throw语句抛出的异常是Runtime异常,则无限制。
public class ThrowTest
{
public static void main(String[] args)
{
try
{
// 调用声明抛出Checked异常的方法,要么显式捕获该异常
// 要么在main方法中再次声明抛出
throwChecked(-3);
}
catch (Exception e)
{
// 调用后处理异常
System.out.println(e.getMessage());
}
// 调用声明抛出Runtime异常的方法既可以显式捕获该异常,
// 也可不理会该异常
throwRuntime(3);
}
public static void throwChecked(int a)throws Exception
{
if (a > 0)
{
// 自行抛出Exception异常
// 该代码必须处于try块里,或处于带throws声明的方法中
throw new Exception("a的值大于0,不符合要求");
}
}
public static void throwRuntime(int a)
{
if (a > 0)
{
// 自行抛出RuntimeException异常,既可以显式捕获该异常
// 也可完全不理会该异常,把该异常交给该方法调用者处理
throw new RuntimeException("a的值大于0,不符合要求");
}
}
}
自定义异常类
自定义异常都应该继承Exception基类,如果希望自定义Runtime异常,则应该继承RuntimeException基类。
public class AuctionException extends Exception
{
// 无参数的构造器
public AuctionException(){} //①
// 带一个字符串参数的构造器
public AuctionException(String msg) //②
{
super(msg);
}
}
catch和throw同时使用
异常处理方式有如下两种:
1. 在出现异常的方法内捕获并处理异常,该方法的调用者将不能再次捕获该异常。
2. 该方法签名中声明抛出该异常,将该异常完全交给方法调用者来处理。
现在结合两者一起使用,这样既能处理异常,又能继续抛出一个异常告诉调用者。
public class AuctionTest
{
private double initPrice = 30.0;
// 因为该方法中显式抛出了AuctionException异常,
// 所以此处需要声明抛出AuctionException异常
public void bid(String bidPrice)
throws AuctionException
{
double d = 0.0;
try
{
d = Double.parseDouble(bidPrice);
}
catch (Exception e)
{
// 此处完成本方法中可以对异常执行的修复处理,
// 此处仅仅是在控制台打印异常跟踪栈信息。
e.printStackTrace();
// 再次抛出自定义异常
throw new AuctionException("竞拍价必须是数值,"
+ "不能包含其他字符!");
}
if (initPrice > d)
{
throw new AuctionException("竞拍价比起拍价低,"
+ "不允许竞拍!");
}
initPrice = d;
}
public static void main(String[] args)
{
AuctionTest at = new AuctionTest();
try
{
at.bid("df");
}
catch (AuctionException ae)
{
// 再次捕捉到bid方法中的异常。并对该异常进行处理
System.err.println(ae.getMessage());
}
}
}
Java 7 增强的throw语句
Java 7以后Java编译器可以自我辨识抛出的异常类型
public class ThrowTest2
{
public static void main(String[] args)
// Java 6认为①号代码可能抛出Exception,
// 所以此处声明抛出Exception
// throws Exception
// Java 7会检查①号代码可能抛出异常的实际类型,
// 因此此处只需声明抛出FileNotFoundException即可。
throws FileNotFoundException
{
try
{
new FileOutputStream("a.txt");
}
catch (Exception ex)
{
ex.printStackTrace();
throw ex; // ①
}
}
}
异常链
String构造器用来向外传递异常,Throwable 构造器用来向内传递异常。这样用户和调用程序的程序员都可以看到不同的异常。
public class SalException extends Exception
{
public SalException(){}
public SalException(String msg)
{
super(msg);
}
// 创建一个可以接受Throwable参数的构造器
public SalException(Throwable t)
{
super(t);
}
}