异常:
指的是程序运行过程中,因为用户的误操作、代码的BUG等等一系列原因,引起的程序奔溃的现象,被称为异常。
有一些错误是用户造成的,比如,希望用户输入一个 int 类型的年龄,但是用户的输入是 abc :
// 假设用户输入了abc:
String s = "abc";
int n = Integer.parseInt(s); // NumberFormatException!
程序想要读写某个文件的内容,但是用户已经把它删除了:
// 用户删除了该文件:
String t = readFile("C:\\abc.txt"); // FileNotFoundException!
还有一些错误是随机出现,并且永远不可能避免的。
比如:
- 网络突然断了,连接不到远程服务器;
- 内存耗尽,程序崩溃了;
- 用户点“打印”,但根本没有打印机;
- ……
所以,一个健壮的程序必须处理各种各样的错误。
所谓错误,就是程序调用某个函数的时候,如果失败了,就表示出错。
调用方如何获知调用失败的信息?
Java内置了一套异常处理机制,总是使用异常来表示错误。
异常是一种 class ,因此它本身带有类型信息。异常可以在任何地方抛出,但只需要在上层捕 获,这样就和方法调用分离了:
try {
String s = processFile(“C:\\test.txt”);
// ok:
} catch (FileNotFoundException e) {
// file not found:
} catch (SecurityException e) {
// no read permission:
} catch (IOException e) {
// io error:
} catch (Exception e) {
// other error:
}
因为Java的异常是 class ,它的继承关系如下:
从继承关系可知: Throwable 是异常体系的根,它继承自 Object 。 Throwable 有两个体 系: Error 和 Exception , Error 表示严重的错误,程序对此一般无能为力,例如:
- OutOfMemoryError :内存耗尽
- NoClassDefFoundError :无法加载某个Class
- StackOverflowError :栈溢出
而 Exception 则是运行时的错误,它可以被捕获并处理。 某些异常是应用程序逻辑处理的一部分,应该捕获并处理。例如:
- NumberFormatException :数值类型的格式错误
- FileNotFoundException :未找到文件
- SocketException :读取网络失败
还有一些异常是程序逻辑编写不对造成的,应该修复程序本身。例如:
- NullPointerException :对某个 null 的对象调用方法或字段
- IndexOutOfBoundsException :数组索引越界
Exception 又分为两大类:
1. RuntimeException 以及它的子类;
2. 非 RuntimeException (包括 IOException 、 ReflectiveOperationException 等 等)
Java规定:
- 必须捕获的异常,包括 Exception 及其子类,但不包括 RuntimeException 及其子类,这 种类型的异常称为Checked Exception。
- 不需要捕获的异常,包括 Error 及其子类, RuntimeException 及其子类。
捕获异常:
捕获异常使用 try...catch 语句,把可能发生异常的代码放到 try {...} 中,然后使用 catch 捕获对应的 Exception 及其子类:
try {
} catch(XxxException e) {
// 异常处理
} catch(XxxxException e) {
// 异常处理
} catch(Exception e) {
// 异常处理
} finally {
// 必须要执行的代码}
Java的 try ... catch 机制提供的 finally 语句, finally 语句块保证有无错误都会执行。
注意 finally 有几个特点:
1. finally 语句不是必须的,可写可不写;
2. finally 总是最后执行。
如果没有发生异常,就正常执行 try { ... } 语句块,然后执行 finally 。如果发生了异常, 就中断执行 try { ... } 语句块,然后跳转执行匹配的 catch 语句块,最后执行 finally 。
可见, finally 是用来保证一些代码必须执行的。
捕获多种异常:
如果某些异常的处理逻辑相同,但是异常本身不存在继承关系,那么就得编写多条 catch 子句:
public static void main(String[] args) {
try {
process1();
process2();
process3();
} catch (IOException e)
{
System.out.println("Bad input");
} catch (NumberFormatException e) {
System.out.println("Bad input");
} catch (Exception e) {
System.out.println("Unknown error");
}
}
抛出异常:
异常的传播
当某个方法抛出了异常时,如果当前方法没有捕获异常,异常就会被抛到上层调用方法,直到遇到 某个 try ... catch 被捕获为止:
public class Main {
public static void main(String[] args) {
try {
process1();
} catch (Exception e) {
e.printStackTrace();
}
}
static void process1() {
process2();
}
static void process2() {
Integer.parseInt(null); // 会抛出NumberFormatException
}
}
通过 printStackTrace() 可以打印出方法的调用栈,类似:
java.lang.NumberFormatException: null
at java.base/java.lang.Integer.parseInt(Integer.java:614)
at java.base/java.lang.Integer.parseInt(Integer.java:770)
at Main.process2(Main.java:16) at Main.process1(Main.java:12)
at Main.main(Main.java:5)
printStackTrace() 对于调试错误非常有用,上述信息表示: NumberFormatException 是在 java.lang.Integer.parseInt 方法中被抛出的,从下往上看,调用层次依次是:
1. main() 调用 process1() ;
2. process1() 调用 process2() ;
3. process2() 调用 Integer.parseInt(String) ;
4. Integer.parseInt(String) 调用 Integer.parseInt(String, int) 。
抛出异常:
当发生错误时,例如,用户输入了非法的字符,我们就可以抛出异常。
如何抛出异常?参考 Integer.parseInt() 方法,抛出异常分两步:
1. 创建某个 Exception 的实例;
2. 用 throw 语句抛出。
void process2(String s) {
if (s==null) {
NullPointerException e = new NullPointerException();
throw e;
}
}