【java基础】异常处理(Exception)

基本介绍

对于一个程序,总是有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之前。

总结

经过上面的说明,最后再给出几点使用异常的技巧

  1. 异常处理不能代替简单的测试(捕获异常比 if 耗时大得多)
  2. 不要过分细化异常
  3. 充分利用异常层次结构
  4. 不要压制异常
  5. 在检查错误时,苛刻要比放任更好
  6. 不要羞于传递异常

猜你喜欢

转载自blog.csdn.net/m0_51545690/article/details/129330584