本篇讲讲异常。

异常处理

 程序在运行时 ,我们往往会默认用户会按照我们的设想去做,但事实是,用户往往不知道我们要做什么。例如,我要求成绩需要是字符型,但是用户可能就会输入数字型。所以,我需要在一开始对各种意外作出考虑。例如,输入了数字怎么办,输入了英文怎么办。但是,各种异常是很难穷举完的。那么就反过来作出假设,只分为两种,一种有错,一种没错。即:

1
2
3
4
5
if(用户输入不正常){
//抛出错误
}else{
//业务实现代码
}

异常处理机制

使用try…catch捕获异常

 问题是如何判定是否是对的或错的呢?最简单的就是直接把代码跑一遍。当程序出现错误时,系统会自动生成一个Exception对象,称为抛出(throw)。直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.util.Scanner;

public class test {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int i = scanner.nextInt();
int[] Array = { 2, 3, 5, 6, 8, 9 };
try {
System.out.println(Array[i]);
} catch (Exception e) {
// TODO: handle exception
System.out.println("arrayoverflow!");
}
}
}

 当Java运行时环境收到异常对象时,会寻找能处理异常对象的catch块,将这个异常对象交给catch处理,称为捕获(catch)。如果找不到合适的catch块,运行时环境就会终止,程序退出。不论程序代码块是否在try中,甚至在catch中,只要程序出现了异常,系统都会生成一个异常对象,但是只要找不到catch块,就会推出。

异常类的继承体系

1
2
3
4
catch (Exception e) {
// TODO: handle exception
System.out.println("arrayoverflow!");
}

 注意这里的Exception e,这说明每个catch块都是接受特定异常对象的,是处理这个异常类及其子类的。如果存在一个try和多个catch,那么就会根据抛出的异常类型找符合的catch。当异常进入catch块后,异常对象将被传给形参,以便获得具体的异常信息。一个异常对象只会引起catch执行一次。

 Java提供了丰富的异常类,这些类有严格的继承关系,看下图:
exception
 从图上来看,主要分为两个类,Exception和Error。Error一般是虚拟机相关的问题,如系统崩溃,虚拟机错误,动态链接失败,这种错误无法恢复或不可能捕获,将导致程序中断。因此程序也不应该试图用catch捕获。至于Exception,看具体的名字基本上就能猜出来是什么了,下面看看简答的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 ie) {
System.out.println("数组越界");
} catch (NumberFormatException ne) {
System.out.println("数字格式异常");
} catch (ArithmeticException ae) {
System.out.println("算术异常");
} catch (Exception e) {
System.out.println("未知异常");
}
}
}

 根据继承关系,Exception异常应该放在最后,如果放在了最前面,程序将直接进入这个异常,而后面的将被忽略。所以,所有的父异常都应该排在子异常的后面。

Java7提供的多异常捕获

 从Java7开始,一个catch块可以捕获多个类型的异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 ie) {
System.out.println("数组越界");
} catch (ArithmeticException | NumberFormatException ae) {
System.out.println("算术异常");
} catch (Exception e) {
System.out.println("未知异常");
}
}
}

 捕获多种类型的异常时,异常变量隐式地用final修饰,所以是不能修改的。

访问异常信息

 都包含啊了下面几个方法,用于访问异常的详细信息:

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

使用finally回收资源

 在一个程序中,不仅要用try处理异常,还应该回收资源。内存资源由垃圾回收机制管理,而物理资源,例如打开的文件,数据库链接,网络链接等等,都需要显式地回收。但是在哪回收呢,try里和catch里都有可能执行不到,所以有一个新的finally。

 异常处理语法结构中,只有try是必须的,catch和finally都是可选的,但是必须出现一个。finally必须位于所有catch之后。finally的作用就是,不论try和catch的执行情况怎样,finally部分都会被执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.io.FileInputStream;
import java.io.IOException;

public class Divtest {
public static void main(String[] args) {
FileInputStream fis = null;
try {
fis = new FileInputStream("a.txt");
} catch (IOException ioe) {
System.out.println(ioe.getMessage());
return;
// System.exit(1);
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
System.out.println("执行资源回收");
}
}
}

 对于方法来说,如果遇到return;就会强制返回,结束方法,虽然这里也是如此,但是根据结果可以看出,仍旧是执行了finally语句。但是如果我把注释去掉,执行System.exit(1)这句,直接强制退出虚拟机,那么finally就没有机会运行了。

 对于finally这种特性,有个注意点:应该避免在finally中使用return语句,这会影响到try中的return语句。当执行了finally中的return语句后,方法就直接推出了,try中的是不会执行的。

Java7的自动关闭资源的try语句

 在Java7中允许在try关键字后面加上一对圆括号,里面是声明,初始化的一个或多个资源,try结束时将会自动关闭这些资源。为了保证能关闭这些资源,,这些资源实现类必须实现接口AutoCloseable或子接口Closeable,实现这两个接口就必须实现close()方法。AutoCloseable接口中的close方法要求抛出Exception,所以实现类中抛出什么异常都可以,但是Closeable接口的close方法声明的是IOException,因此在实现这个方法时必须抛出IOException类或其子类的异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package test6;

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 (BufferedReader br = new BufferedReader(new FileReader("AutoCloseTest"));
PrintStream ps = new PrintStream(new FileOutputStream("a.txt"))) {
System.out.println(br.readLine());
ps.println("蛤");
}
}
}

Checked异常和Runtime异常体系

 Java的异常被分为两大类,Checked和Runtime异常。所有RuntimeException类及其子类的实例都被称作Runtime异常,其余称为Checked异常。Java认为checked异常都是可以被修复的异常,因此必须显式地处理这种异常。对于这种异常有两种处理方式:

  • 当前方法知道如何处理异常,用try…catch来捕获该异常,然后在catch中修复,例如,提示输入的不合法并要求重新输入
  • 不知道如何让处理,应该在定义方法时声明抛出该异常,如上面的代码

 总之,对于checked异常要么捕获它并处理它,要么显式声明抛出。

用throws声明抛出异常

 使用throws的思路是,当前方法不知道如何处理异常,应该交给上一级处理,如果main方法也不知道如何让处理,则用throws声明抛出异常,交给JVM处理。JVM的处理方式是,打印异常的跟踪栈信息,并终止运行。throws声明抛出只能在方法签名中使用,可以抛出多个异常种类,用逗号隔开。对于继承中的方法重写,要记得”两小“原则,子类方法声明中抛出的异常类型应该是父类方法声明抛出的异常类型的子类或相同,子类抛出的异常声明不能比父类抛出的多。

 对于checked异常,处理起来是比较麻烦的,要显式地声明,还要注意父类和子类的限制。所以更加推荐Runtime异常,他不需要声明,一旦发生了自定义错误,程序只管抛出Runtime异常即可,一样也可以用try…catch捕获并处理它们。

用throw抛出异常

 在前面讲的东西中,不论有什么区别,异常都是系统自动抛出的,除此之外,Java也允许程序自行抛出异常,这种异常是由throw抛出,与throws不一样。对于JVM来说,age=-100,不是一种异常,但是对于业务逻辑来说,这就是一种异常,所以要用到throw抛出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ThrowTest {
public static void main(String[] args) {
try {
throwChecked(-3);
} catch (Exception e) {
System.out.println(e.getMessage());
}
throwRuntime(3);
}

public static void throwChecked(int a) throws Exception {
if (a > 0) {
throw new Exception("a大于0,不符合要求1");
}
}

public static void throwRuntime(int a) {
if (a > 0) {
throw new RuntimeException("a大于0,不符合要求2");
}
}
}

 这段代码有写非常有意思的地方,首先按上面的代码运行结果是:

1
2
3
Exception in thread "main" java.lang.RuntimeException: a大于0,不符合要求2
at test6.ThrowTest.throwRuntime(ThrowTest.java:21)
at test6.ThrowTest.main(ThrowTest.java:10)

 对于Runtime异常,无需放在try…catch中,也不要用throw抛出,可以进行捕获处理,也可以不理会它,交给该方法的调用者处理。这里并没有对这种异常的处理,所以交由JVM处理,JVM会打印跟踪栈信息和异常的详细信息。而看到的a大于0,不符合要求2这句就是由JVM调用getMAssage()方法的结果,可以看图:
异常
 上面的代码中3和-3替换一下,结果又不同

1
a大于0,不符合要求1

 就这简单一句,在try…catch中接收到了用户自行抛出的异常,随后用catch来处理,调用了getMassage()方法,所以并没有打印跟踪栈信息。这就能很明显的看出checked和Runtime的区别,明显后者更灵活。

 throws和throw的区别,参看链接:https://www.cnblogs.com/liuyaozhi/p/5812700.html

自定义异常类

 上一节讲了程序员自己决定什么情况抛出异常,现在再看看抛出何种异常。

 要自定义异常,需要继承Exception基类。如果是Runtime异常,则要继承RuntimeException基类。需要提供两个构造器,一个无参数,一个参数为字符串,用于getMassage()方法。大部分情况下下面的代码可以用作自定义异常的代码,只要改个名字。

1
2
3
4
5
6
public class AuctionException extends Exception{
public AuctionException(){}
public AuctionException(String msg){
super(msg);
}
}

throw和catch同时使用

 前面写的对异常的处理方式,要么在catch中处理,要么用throw或throws抛出,交给上一级处理。但是在实际中,可能更复杂,当前方法的catch完成不了处理,全部交给上一级也处理不了,所以需要同时使用两种方法协作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class AuctionTest {
private double initPrice = 30.0;

public void bid(String bidPrice) throws AuctionException {
double d = 0.0;
try {
d = Double.parseDouble(bidPrice);
} catch (Exception e) {
e.printStackTrace(); //1
throw new AuctionException("竞拍价必须是数字"); //2
}
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) { //3
System.err.println(ae.getMessage());
}
}
}

 这段代码中,先打印了跟踪栈信息(1),然后抛出了一个异常对象(2),然后catch捕获这个异常对象(3)。

Java7中增强的throw语句

 首先看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
import java.io.FileOutputStream;

public class ThrowTest {
public static void main(String[] args) throws Exception {
try {
new FileOutputStream("a.txt");
} catch (Exception ex) {
ex.printStackTrace();
throw ex;
}
}
}

 这段代码将catch捕获到的异常再次抛出,由于Exception ex这样的声明,所以throws后面跟的是Exception。但是这里面的问题是,原本try中调用了FileOutStream构造器,这种构造器只是抛出FileNotFoundException异常。但是后面这种抛出导致了异常的放大,不利于异常的处理。在Java7中,不在这么”粗暴“,请看代码:

1
2
3
4
5
6
7
8
9
10
11
12
import java.io.FileOutputStream;

public class ThrowTest {
public static void main(String[] args) throws FileNotFoundException {
try {
new FileOutputStream("a.txt");
} catch (Exception ex) {
ex.printStackTrace();
throw ex;
}
}
}

 编译器会进行更细致的检查,检查throw语句抛出异常的实际种类,这样在方法签名处只要抛出FileNotFoundException异常就行。

异常链

 假设下面的情况,在对数据库进行操作时出现了异常,但是这种异常是不应该返回给用户的,因为对于他们来说这没有任何用处,同时增加了把程序缺陷暴露给恶意用户的风险。正确的做法是,将原始异常记录下来,将异常进行一定的转变再传给用户层。

1
2
3
4
5
6
7
8
9
public calSal() throws SalException{
try{
//实现业务逻辑
}catch(SQLException sqle){
//把原始异常记录下来
//抛出给用户的异常提示
throw new SalException("访问底层数据出现错误")
}
}

 这种捕获一个异常接着抛出另一个异常,并把原始异常信息保存下来是一种典型的链式处理模式(设计模式:职责链模式)。在Java1.4之前,需要自己写代码将原始异常保存下来,但是在之后,所有的Throwable的子类在构造器中都可以接受一个cause对象作为参数。这个cause就用来表示原始异常:

1
2
3
4
5
6
7
8
9
public calSal() throws SalException{
try{
//实现业务逻辑
}catch(SQLException sqle){
//把原始异常记录下来
//将原始异常作为参数传递出去
throw new SalException(sqle)
}
}
1
2
3
4
5
6
7
8
9
10
11
public class SalException extends Exception {
public SqlException(){}

public SalException(String msg) {
super(t);
}

public SqlException(Throwable t){
super(t);
}
}

异常处理规则

 异常处理应该实现一下四个目标:

  • 使程序代码混乱最小化
  • 捕获并保留诊断信息
  • 通知何时的人员
  • 采用何时的方式结束异常活动

不要过度使用异常

  • 把异常和普通错误混在一起,不在编写任何处理错误的代码,全部一刀切的用抛出异常处理
  • 使用异常处理来代替流程控制

&上面两点应该避免,例如,下棋时,用户重复在一点落子,这种情况是完全能预料到的,直接用个判断语句比抛出异常更加简单合适。

避免过大的try块

 try块过大的话,内部的代码过多,产生异常的可能就越多,导致分析异常产生原因时会很难。针对一个try块中的各种异常,还要用多个catch来处理,这种分类也会很复杂。所以,应该将代码拆开,方便寻找异常和处理异常。

避免使用Catch All语句

 所谓的Catch All语句就是用一个异常种类,例如用Throwable来捕获异常。这样子模糊了异常的种类,不利于做出针对性的处理,在catch中还有用流程控制语句分别处理,得不偿失。

不要忽略异常

 对于异常不能视而不见,然catch为空,企图蒙混过关,或者打印跟踪栈信息就了事。应该尽量修复异常,或者将异常转译包装为当前层的异常抛给上一层。