基本介绍
对于一个程序,总是有bug的。如果我们的程序遇到一个错误就终止了,那么肯定是不合理,程序发生错误时,应该有一种通用的解决方式才合理。好在java给我们提供了一整套处理异常的机制,下面就来进行介绍
异常分类
在java中,所有的异常对象都派生与Throwable
由于Throwable是所有异常类的父类,我们有必要去看一下它的源码
很多方法和字段,具体用法大多是和名称一样的,这里建议大家先去看一下源码
需要注意的是,所有的异常都是由Throwable继承而来,但在下一层立即分解为两个分支:Error和Exception。
Error类层次结构描述了Java运行时系统的内部错误和资源耗尽错误。你的应用程序不应该抛出这种类型的对象。如果出现了这样的内部错误,除了通知用户,并尽力妥善地终止程序之外,你几乎无能为力。这种情况很少出现。
在设计Java程序时,要重点关注Exception层次结构。这个层次结构又分解为两个分支:一个分支派生于RuntimeException;另一个分支包含其他异常。一般规则是:由编程错误导致的异常属于RuntimeException;如果程序本身没有问题,但由于像I/O错误这类问题导致的异常属于其他异常。
下面就是属于RuntimeException和不属于RuntimeException的一些举例
派生于RuntimeException的异常包括以下问题:
- 错误的强制类型转换。
- 数组访问越界。
- 访问null指针。
不是派生于RuntimeException的异常包括:
- 试图超越文件末尾继续读取数据。
- 试图打开一个不存在的文件。
- 试图根据给定的字符串查找Class对象,而这个字符串表示的类并不存在。
“如果出现RuntimeException异常,那么就一定是你的问题”,这个规则很有道理。应该通过检测数组下标是否越界来避免ArrayIndexOutOfBoundsException异常;应该在使用变量之前通过检测它是否为null来杜绝NullPointerException异常的发生。
如何处理不存在的文件呢?难道不能先检查文件是否存在再打开它吗?嗯,这个文件有
可能在你检查它是否存在之后就立即被删除了。因此,“是否存在”取决于环境,而不只是
取决于你的代码。
Java语言规范将派生于Error类或RuntimeException类的所有异常称为非检查型(unchecked),所有其他的异常称为检查型(checked)异常。
抛出异常
如果我们遇到了一个无法处理的情况,我们就可以使用throw抛出一个异常。原因很简单,因为方法不仅仅想要告诉编译器要返回上面值,还要告诉编译器有可能发生什么错误。
非检查型异常
现在假设我们自己写了一个方法,这个方法对传入的参数进行处理,但是参数可能不合法,我们已经无法处理了,这时我们就可以抛出一个异常
public void handleParam(String param) {
if (param.length() < 5) {
throw new IllegalArgumentException("参数长度必须大于5");
}
// code logic
// ....
}
当然,我们也可以在方法上写上可能会抛出的异常。多个异常使用 **,**进行分隔,或者抛出它们的公共父类
public void handleParam(String param) throws IllegalArgumentException {
if (param.length() < 5) {
throw new IllegalArgumentException("参数长度必须大于5");
}
// code logic
// ....
}
上面的IllegalArgumentException是一个RuntimeException,是一个非检查型异常。
我们上面写的方法throw了一个非检查异常,表示可能发生异常,这样,这个方法还是可以正常调用,调用者不需要去处理这个异常
检查型异常
现在我们来说应该检查型异常,例如IOException
我们来看一个FileInputStream的构造器会抛出一个FileNotFoundException,这是IOException的子类
现在,如果我们创建一个FileInputStream,看看会出现什么情况
@Test
public void t2() {
FileInputStream fileInputStream = new FileInputStream("");
}
运行代码编译器给出以下信息,编译都不能通过
对于检查型异常,我们必须将其捕获或者将其抛出,我们可以进行如下更改,继续往外抛出
@Test
public void t2() throws FileNotFoundException {
FileInputStream fileInputStream = new FileInputStream("");
}
捕获异常
捕获单个异常
我们先来看一段程序如下
@Test
public void t1() {
int a = 1 / 0;
System.out.println("程序结束....");
}
上面代码如果运行就会报错
程序抛出了应该ArithmeticException异常,然后程序就结束,这显然不合理,就因为一个小错误就终止程序,于是我们就可以使用try-catch来捕获异常,捕获到异常后就不会终止程序。
try-catch语法如下
try{
code
more code
more code
.....
}catch(ExceptionType e){
handler exception
}
我们将可能出现异常的代码放在try代码块中,发生异常的时候,try中代码就不会再执行了,catch就会根据写在catch中的异常进行匹配,如果异常类型相同或者抛出的异常为写在catch上的异常的子类,那么可以成功捕获,捕获后就会执行catch中的内容,然后继续执行try-catch下面的代码。如果没有捕获成功,那么程序就会终止。
现在我们就可以对上面代码进行改造
@Test
public void t1() {
try {
int a = 1 / 0;
} catch (ArithmeticException e) {
System.out.println("发生了异常,原因是:" + e.getMessage());
}
System.out.println("程序结束....");
}
代码运行输出如下,发生异常后就不会直接结束程序了,而是被catch捕获
上面我使用了ArithmeticException来进行异常的捕获,至于为什么用这个异常,这是因为我刚好知道当除0时就会抛出这个异常,下面为这个类的源代码
我们再来看一下这个类的类图,可以发现这个异常就是RuntimeException下的子类。
上面我知道会发生什么异常,所以我可以捕获,如果我不知道要发生什么异常呢?上面不是说明了吗,捕获异常我们只需和抛出的异常类型相同或者为其父类就行了。我们既然不知道是上面异常,是不是就可以直接使用Exception接收呢?当然可以
@Test
public void t2() {
try {
int a = 1 / 0;
} catch (Exception e) {
System.out.println("发生了异常,原因是:" + e.getMessage());
}
System.out.println("程序结束....");
}
输出和上面没有任何区别
对于知道可能发生的异常类型的,我们尽量都使用精确的异常来进行匹配。
捕获多个异常
我们还是先来看一段代码
@Test
public void t3() {
Scanner in = new Scanner(System.in);
String next = in.next();
int num = Integer.parseInt(next);
int ans = 100 / num;
System.out.println("程序结束....");
}
上面这个代码就有可能发生2个地方的异常,一个是Integer.parseInt,还有一个是100 / num,对于上面的情况,我们该怎么进行处理呢?很简单,因为它们发出异常肯定要抛出一个异常类,那么父类肯定就是Exception,我们直接使用Exception进行捕获即可
@Test
public void t3() {
try {
Scanner in = new Scanner(System.in);
String next = in.next();
int num = Integer.parseInt(next);
int ans = 100 / num;
} catch (NumberFormatException e) {
System.out.println("发生了异常,原因是:" + e.getMessage());
}
System.out.println("程序结束....");
}
但是如果我们想要对不同的异常进行不同的处理该怎么做呢?这时就可以使用多个catch,异常捕获将按照顺序来,如果捕获到了就不再执行其他catch
try {
}catch(exceptionType1 e1){
}catch(exceptionType2 e2){
}catch(exceptionType3 e3){
}catch(exceptionType4 e4){
}
但是现在又有一个问题,我们并不知道上面的代码中会抛出什么异常,其实,我们可以进入源代码查看,下面为Integer.parseInt()的源代码
可以发现异常是NumberFormatException,对于除0的异常上面已经说过了,于是我们可以编写以下代码
@Test
public void t4() {
try {
Scanner in = new Scanner(System.in);
String next = in.next();
int num = Integer.parseInt(next);
int ans = 100 / num;
} catch (NumberFormatException e) {
System.out.println("数字格式化异常" + e.getMessage());
} catch (ArithmeticException e) {
System.out.println("发生了算术异常--" + e.getMessage());
}
System.out.println("程序结束....");
}
这样我们在发生不同异常的时候就可以进行不同的处理
如果要捕获的异常特别多,但是有些处理逻辑相同的,我们可以在一个catch里面使用 | 进行分隔,表示或
@Test
public void t5() {
try {
} catch (ClassCastException | ArrayStoreException e) {
System.out.println("XXX");
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("xxx");
}
}
创建自定义异常类
对于创建自定义异常类,想必大家看完上面应该都已经会了,我们只需要继承Exception或者RuntimeException就行了。继承RuntimeException就是非检查型异常,继承Exception就是检查型异常
定义检查型异常
public class MyCheckedException extends Exception{
}
自定义非检查型异常
public class MyNoCheckedException extends RuntimeException{
}
对于为什么继承Exception就是检查型异常,继承RuntimeException就是非检查型异常,上面已经说明过了,这里再来看一下源代码,注释上面也有说明
Exception
RuntimeException
finally字句
上面的try-catch中,只要出现异常,那么就会停止处理try后续的代码,有没有一种方法让try-catch无论是否出现异常都一定执行一些代码呢。finally就是这个作用,finally写在try或catch后面
try {
}catch (Exception e){
}finally {
// 一定会执行
}
try {
} finally {
// 一定会执行
}
我们可以将一些通用的操作放在finally中,例如文件的关闭操作。
由于finally中的代码一定会执行,下面大家来看一段代码,看看返回上面
public int getNum() {
try {
int a = 1 / 0;
return 0;
} catch (Exception e) {
return 1;
} finally {
return 2;
}
}
上面的代码在try中会发生异常,所以return 0 肯定不会执行,发生异常后会执行catch中的语句return 1,但是发现还有finally,所以会暂缓返回,先执行finally,由于finally中是return 2,就直接返回了。所以最终的返回的结果是2
@Test
public void t1() {
System.out.println(getNum());
}
try-with-Resource
前面说了,finally这条语言一定会执行,一般用于关闭文件或者释放资源。但是这样写还是有点麻烦,在java7之后还有一种更便捷的写法。try-with-Resource语句
try(Resource res = ...){
use res
}
// 可以写catch和finally,也可以不写
当try退出的时候,会自动调用res.close()。
看到这样,大家应该想到了,使用这种语法是有条件的,因为要调用close方法,所以一定要实现某个接口,该接口含有close方法,这个接口就是AutoCloseable即可
下面就使用FileInputStream举个例子,该类实现了AutoCloseable
@Test
public void t1() {
try (FileInputStream fileInputStream = new FileInputStream("")) {
int read = fileInputStream.read();
} catch (IOException e) {
e.printStackTrace();
}
}
在try种还可以指定多个资源,使用 **;**分隔
@Test
public void t1() {
try (FileInputStream fileInputStream = new FileInputStream("");
FileOutputStream fileOutputStream = new FileOutputStream("")) {
int read = fileInputStream.read();
int available = fileInputStream.available();
} catch (IOException e) {
e.printStackTrace();
}
}
再次强大,当try退出时就会调用 xxx.close()方法。优先级在catch和finally之前。
总结
经过上面的说明,最后再给出几点使用异常的技巧
- 异常处理不能代替简单的测试(捕获异常比 if 耗时大得多)
- 不要过分细化异常
- 充分利用异常层次结构
- 不要压制异常
- 在检查错误时,苛刻要比放任更好
- 不要羞于传递异常