程序中总有些意想不到的状况所引发的错误,Java中的错误也以对象方式呈现,为java.lang.Throwable的各种子类实例。只要你能捕捉包装错误的对象,就可以针对该错误做一些处理,例如,试回复正常流程、进行日志(Logging)记录、以某种形式提醒用户等。

1. 语法和异常的继承结构

1.1 使用try、catch

来看一个简单的程序,用户可以连续输入整数,最后输入0结束后会显示输入数的平均值:

import java.util.Scanner;

public class Average {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        double sum = 0;
        int count = 0;
        int number;
        while(true) {
            number = scanner.nextInt();
            if(number == 0) {
                break;
            }
            sum += number;
            count++;
        }
        System.out.printf("平均 %.2f%n", sum / count);
        scanner.close();
    }
}

如果我们输入整数,程序会如预期地显示平均。

如果不小心输入错误,那就会出现奇怪的信息,例如第三个数输入为3o,而不是30了:

10
20
3o
Exception in thread "main" java.util.InputMismatchException
	at java.base/java.util.Scanner.throwFor(Scanner.java:939)
	at java.base/java.util.Scanner.next(Scanner.java:1594)
	at java.base/java.util.Scanner.nextInt(Scanner.java:2258)
	at java.base/java.util.Scanner.nextInt(Scanner.java:2212)
	at com.ripjava.exception.Average.main(Average.java:12)

这段错误信息对除错是很有价值的,不过先看到错误信息的第一行:

Exception in thread "main" java.util.InputMismatchException

Scanner对象的nextInt()方法,可以将用户输入的下一个字符串剖析为int值,如果出现InputMismatchException错误信息,表示不符合Scanner对象预期,因为Scanner对象预期下一个字符串本身要代表数字。

Java中所有错误都会被打包为对象,如果愿意,可以尝试(try)捕捉(catch)代表错误的对象后做一些处理。

例如:

import java.util.InputMismatchException;
import java.util.Scanner;

public class Average2 {

    public static void main(String[] args) {
        try {
            Scanner scanner = new Scanner(System.in);
            double sum = 0;
            int count = 0;
            int number;
            while (true) {
                number = scanner.nextInt();
                if (number == 0) {
                    break;
                }
                sum += number;
                count++;
            }
            System.out.printf("平均 %.2f%n", sum / count);
            scanner.close();
        } catch (InputMismatchException ex) {
            System.out.println("请输入整数");
        } 
        
    }
}

这里使用了try、catch语法,JVM会尝试执行try区块中的程序代码。如果发生错误,执行流程会跳离错误发生点,然后比较catch括号中声明的类型,是否符合被抛出的错误对象类型,如果是的话,就执行catch区块中的程序代码。

但是呢。一般如果发生错误的话,我们都捕捉处理之后,尝试回复程序正常执行流程。

那应该怎么做的呢?

import java.util.*;

public class Average3 {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        double sum = 0;
        int count = 0;
        int number;
        while (true) {
            try {
                number = scanner.nextInt();
                if (number == 0) {
                    break;
                }
                sum += number;
                count++;
            } catch (InputMismatchException ex) {
                System.out.printf("请重新输入整数,本次输入被跳过:%s%n", scanner.next());
            }
        }
        System.out.printf("平均 %.2f%n", sum / count);
        scanner.close();
    }
}

如果nextInt()发生了InputMismatchException错误,执行流程就会跳到catch区块,执行完catch区块之后,由于还在while循环中,所以还可继续下一个循环流程。

1.2 异常继承架构

刚才我们已经知道了,如何去处理异常了。但是呢,还有另外一种情况。

public class ThrowException {
	public static void main(String[] args) {
		int read = System.in.read();
		System.out.println(read);
	}
}

IDE会提示我们

Unhandled exception type IOException

简单来说,编译程序认为调用System.in.read()时有可能发生错误,要求你一定要在程序中明确处理错误。

我们先来解决上面的IDE的提示

有两种方式,

  • 使用try、catch打包System.in.read(),
  • main()方法旁声明throws java.io.IOException

那你可能会好奇,为什么我在写scanner.nextInt()却没有写。

首先要了解错误会被包装为对象,这些对象都是可抛出的(稍后介绍throw语法,就会了解如何抛出错误对象),因此设计错误对象都继承自java.lang.Throwable类,Throwable定义了取得错误信息、堆栈追踪(Stack Trace)等方法,它有两个子类:

  • java.lang.Error

Error与其子类实例代表严重系统错误,如硬件层面错误、JVM错误或内存不足等问题。虽然也可以使用try、catch来处理Error对象,但并不建议,发生严重系统错误时,Java应用程序本身是无力回复的。

举例来说,如果JVM所需内存不足,我们通过Java没有办法要求操作系统给予JVM更多内存。

Error对象抛出时,基本上不用处理,任其传播至JVM为止,或者是最多留下日志信息。

  • java.lang.Exception

通常程序中会使用try、catch加以处理的错误,都是Exception或其子类实例,所以通常称错误处理为异常处理,对于某些异常,可以用try、catch语法尝试将应用程序回复至可执行状态。

下面给大家说明一下,为什么我在写scanner.nextInt()却没有写。

如果某个方法声明会抛出Throwable、Exception或子类实例,但又不属于java.lang.RuntimeException或其子类实例,就必须明确使用try、catch语法加以处理,或者用throws声明这个方法会抛出异常,否则会编译失败。

下面我们来看一下异常继承架构

image-20190614205713661

这里有几个需要大家了解的地方

  • 我们可以使用try、catch可以去捕捉多个异常。

使用try、catch捕捉异常对象时也要注意,如果父类异常对象在子类异常对象前被捕捉,则catch子类异常对象的区块将永远不会被执行,编译程序会提示错误

	public static void main(String[] args) {
		try {
			System.in.read();
		} catch (Exception e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

错误

Unreachable catch block for IOException. It is already handled by the catch block for Exception
  • JDK 7 之后, 可以使用多重捕捉(multi-cath)语法:

JDK 7 之前

	public static void main(String[] args) {
		try {
			doSome("two");
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (EOFException e) {
			e.printStackTrace();
		}
	}

JDK 7 之后

	public static void main(String[] args) {
		try {
			doSome("two");
		} catch (FileNotFoundException | EOFException e) {
			e.printStackTrace();
		}
	}

1.3 要抓还是要抛

下面我们举一个例子

假设开发一个类库,其中有个功能是读取纯文本文档,并以字符串返回文档中所有文字,你也许会这么写:

public class FileUtil {
    public static String readFile(String name)  {
        StringBuilder builder = new StringBuilder();
        try {
            Scanner scanner = new Scanner(new FileInputStream(name));
            while(scanner.hasNext()) {
                builder.append(scanner.nextLine());
                builder.append('\n');
            }
            scanner.close();
        } catch (FileNotFoundException ex) {
            ex.printStackTrace();
        }
        return builder.toString();
    }
}

由于创建FileInputStream时会抛出FileNotFoundException,根据目前学到的异常处理语法,于是你捕捉FileNotFoundException并在控制台中显示错误信息。

但是类库的使用时候一定是控制台吗?

如果是在一个WEB程序中,我们怎么把错误返回给用户。

如果方法设计流程中发生异常,而你设计时并没有充足的信息知道该如何处理(例如不知道类库会用在什么环境),那么可以抛出异常,让调用方法的客户端来处理。

比如

public class FileUtil2 {
    public static String readFile(String name) throws FileNotFoundException {
        StringBuilder builder = new StringBuilder();
        try {
            Scanner scanner = new Scanner(new FileInputStream(name));
            while(scanner.hasNext()) {
                builder.append(scanner.nextLine());
                builder.append('\n');
            }
            scanner.close();
        } catch (FileNotFoundException ex) {
            ex.printStackTrace();
            throw  ex;
		}
        return builder.toString();
    }
}

1.4 认识堆栈追踪

在多重方法调用下,异常发生点可能是在某个方法之中,若想得知异常发生的根源,以及多重方法调用下异常的堆栈传播,可以利用异常对象自动收集的堆栈追踪(Stack Trace)来取得相关信息。

查看堆栈追踪最简单的方法,就是直接调用异常对象的printStackTrace()。

public class StackTraceDemo {
	public static String a() {
        String text = null;
        return text.toUpperCase();
    }
    public static void b() {
        a();
    }
    public static void c() {
        b();
    }
    public static void main(String[] args) {
        try {
            c();
        } catch(NullPointerException ex) {
            ex.printStackTrace();
        }
    }
}

我们执行一下

java.lang.NullPointerException
	at com.ripjava.exception.StackTraceDemo.a(StackTraceDemo.java:6)
	at com.ripjava.exception.StackTraceDemo.b(StackTraceDemo.java:9)
	at com.ripjava.exception.StackTraceDemo.c(StackTraceDemo.java:12)
	at com.ripjava.exception.StackTraceDemo.main(StackTraceDemo.java:16)

堆栈追踪信息中显示了异常类型,最顶层是异常的根源,以下是调用方法的顺序,程序代码行数是对应于源代码,如果使用IDE,单击行数就会直接跳至对应行数(如果有源代码的话)。

一般情况下,我们可以通过堆栈追踪信息定位问题。

但是有时候,可能会遇到程序代码中私吞异常的行为。

try{
  
} catch(Exception e){
   // 什么也没有做。
}

这样的程序代码会对应用程序维护造成严重伤害,因为异常信息没有打印也没有处理,之后调用此片段程序代码的客户端,完全不知道发生了什么事,造成除错异常困难,甚至找不出错误根源。

2. 异常与资源管理

程序中因错误而抛出异常时,原本的执行流程就会中断,抛出异常处之后的程序代码就不会被执行,如果程序开启了相关资源,使用完毕后你是否考虑到关闭资源呢?

若因错误而抛出异常,你的设计是否还能正确地关闭资源呢?

2.1 使用finally

FileUtil的实例并不是很正确。

如果scanner.close()前发生了任何异常,执行流程就会中断,因此scanner.close()就可能不会执行,因此Scanner及搭配的FileInputStream就不会被关闭。

最后一定要执行关闭资源的动作,try、catch语法还可以搭配finally,无论try区块中有无发生异常,若撰写有finally区块,则finally区块一定会被执行。

public class FileUtil3 {
    public static String readFile(String name) throws FileNotFoundException {
        StringBuilder builder = new StringBuilder();
        Scanner scanner = null;
        try {
            scanner = new Scanner(new FileInputStream(name));
            while(scanner.hasNext()) {
                builder.append(scanner.nextLine());
                builder.append('\n');
            }
        } catch (FileNotFoundException ex) {
            ex.printStackTrace();
            throw  ex;
        } finally {
          if(scanner != null) {
            scanner.close();
         	}
				}
        return builder.toString();
    }
}

由于finally区块一定会被执行,scanner原先是null,若FileInputStream创建失败,则scanner就有可能还是null,因此在finally区块中必须先检查scanner是否有引用对象,有的话才进一步调用close()方法,否则scanner参考至null又打算调用close()方法,反而会抛出NullPointerException。

如果程序的流程中先return了,而且也有finally区块,那finally区块会先执行完后,再将值返回。

我们测试一下

public class TryCatchFinallyReturn {
	public static  int test(boolean flag) {
		try {
			if(flag) {
				return 1;
			}
		} finally {
			System.out.println("finally ");
		}
		
		return 0;
	}
	
	public static void main(String[] args) {
		System.out.println(test(true));
	}
}

2.2 自动尝试关闭资源

2.3 java.lang.AutoCloseable接口

这两个内容我们在学习IO的时候一起讲。