6.0 第6章 异常 6.1 初识异常

第6章 异常

之前我们介绍的基本类型、类、接口、枚举都是在表示和操作数据,操作的过程中可能有很多出错的情况,出错的原因可能是多方面的,有的是不可控的内部原因,比如内存不够了、磁盘满了,有的是不可控的外部原因,比如网络连接有问题,更多的可能是程序的编写错误,比如引用变量未初始化就直接调用实例方法。

这些非正常情况在Java中统一被认为是异常,Java使用异常机制来统一处理。本章就来详细讨论Java中的异常机制,首先介绍异常的初步概念,以及异常类本身,然后主要介绍异常的处理。

6.1 初识异常

我们先来看两个具体的异常:NullPointerException和NumberFormatException。

6.1.1 NullPointerException(空指针异常)

我们来看段代码:

1
2
3
4
5
6
7
public class ExceptionTest {
public static void main(String[] args) {
String s = null;
s.indexOf("a");
System.out.println("end");
}
}

变量s没有初始化就调用其实例方法indexOf,运行,屏幕输出为:

1
2
Exception in thread "main" java.lang.NullPointerException
at ExceptionTest.main(ExceptionTest.java:5)

输出是告诉我们:在ExceptionTest类的main函数中,代码第5行,出现了空指针异常(java.lang.NullPointerException)。

但,具体发生了什么呢?当执行s.indexOf(“a”)的时候,Java虚拟机发现s的值为null,没有办法继续执行了,这时就启用异常处理机制,首先创建一个异常对象,这里是类NullPointerException的对象,然后查找看谁能处理这个异常,在示例代码中,没有代码能处理这个异常,因此Java启用默认处理机制,即打印异常栈信息到屏幕,并退出程序。

在介绍函数调用原理的时候,我们介绍过栈,异常栈信息就包括了从异常发生点到最上层调用者的轨迹,还包括行号,可以说,这个栈信息是分析异常最为重要的信息。

Java的默认异常处理机制是退出程序,异常发生点后的代码都不会执行,所以示例代码中的System.out.println(“end”)不会执行。

6.1.2 NumberFormatException(数字格式异常)

我们再来看一个例子,代码如下:

1
2
3
4
5
6
7
8
9
10
public class ExceptionTest {
public static void main(String[] args) {
if(args.length<1){
System.out.println("请输入数字");
return;
}
int num = Integer.parseInt(args[0]);
System.out.println(num);
}
}

args表示命令行参数,这段代码要求参数为一个数字,它通过Integer.parseInt将参数转换为一个整数,并输出这个整数。参数是用户输入的,我们没有办法强制用户输入什么,如果用户输入的是数字,比如123,屏幕会输出123,但如果用户输的不是数字而是字母,比如abc,屏幕会输出:

1
2
3
4
5
Exception in thread "main" java.lang.NumberFormatException: For input string: "abc"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Integer.parseInt(Integer.java:492)
at java.lang.Integer.parseInt(Integer.java:527)
at ExceptionTest.main(ExceptionTest.java:7)

出现了异常NumberFormatException。这个异常是怎么产生的呢?根据异常栈信息,我们看相关代码。NumberFormatException类65行附近代码如下:

1
2
3
64 static NumberFormatException forInputString(String s) {
65 return new NumberFormatException("For input string: \"" + s + "\"");
66 }

Integer类492行附近代码如下:

1
2
3
4
5
6
7
490 digit = Character.digit(s.charAt(i++), radix);
491 if (digit < 0) {
492 throw NumberFormatException.forInputString(s);
493 }
494 if (result < multmin) {
495 throw NumberFormatException.forInputString(s);
496 }

将这两处合为一行,主要代码就是:

1
throw new NumberFormatException(...)

new NumberFormatException是容易理解的,含义是创建了一个类的对象,只是这个类是一个异常类。throw是什么意思呢?就是抛出异常,它会触发Java的异常处理机制。在之前的空指针异常中,我们没有看到throw的代码,可以认为throw是由Java虚拟机自己实现的。

throw关键字可以与return关键字进行对比。return代表正常退出,throw代表异常退出;return的返回位置是确定的,就是上一级调用者,而throw后执行哪行代码则经常是不确定的,由异常处理机制动态确定。

异常处理机制会从当前函数开始查找看谁“捕获”了这个异常,当前函数没有就查看上一层,直到主函数,如果主函数也没有,就使用默认机制,即输出异常栈信息并退出,这正是我们在屏幕输出中看到的。

对于屏幕输出中的异常栈信息,程序员是可以理解的,但普通用户无法理解,也不知道该怎么办,我们需要给用户一个更为友好的信息,告诉用户,他应该输入的是数字,要做到这一点,需要自己“捕获”异常。“捕获”是指使用try/catch关键字,如代码清单6-1所示。

代码清单6-1 捕获异常示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ExceptionTest {
public static void main(String[] args) {
if(args.length<1){
System.out.println("请输入数字");
return;
}
try{
int num = Integer.parseInt(args[0]);
System.out.println(num);
}catch(NumberFormatException e){
System.err.println("参数" + args[0] + "不是有效的数字,请输入数字");
}
}
}

上述代码使用try/catch捕获并处理了异常,try后面的花括号{}内包含可能抛出异常的代码,括号后的catch语句包含能捕获的异常和处理代码,catch后面括号内是异常信息,包括异常类型和变量名,这里是NumberFormatException e,通过它可以获取更多异常信息,花括号{}内是处理代码,这里输出了一个更为友好的提示信息。

捕获异常后,程序就不会异常退出了,但try语句内异常点之后的其他代码就不会执行了,执行完catch内的语句后,程序会继续执行catch花括号外的代码。

至此,我们就对异常有了一个初步的了解。异常是相对于return的一种退出机制,可以由系统触发,也可以由程序通过throw语句触发,异常可以通过try/catch语句进行捕获并处理,如果没有捕获,则会导致程序退出并输出异常栈信息。异常有不同的类型,接下来,我们来认识一下。