WHCSRL 技术网

[java篇] 一次性处理异常

前言:博主在以前的博客中曾经也提起了异常,比如[java篇]多态,接口中在实现clone接口时,就用到了抛出异常,今天就给大家详细的介绍一下关于异常的一些知识。
前期文章:
1.[java篇] 包,继承,组合
2.[java篇] 多态,抽象类,接口
3.[java篇] 图书管理系统,是你的期末大作业吗?

4.[java篇] 帮你搞懂String,StringBuffer和StringBuilder的关系

异常背景

初始异常

初学异常的友友们,会想到底什么是异常呀?
那就仔细看看下面的代码吧!!!

     public static void main(String[] args) {
        System.out.println(10/0);
    }
  • 1
  • 2
  • 3

看一看代码,10能除以0吗,显然不能,0不能做被除数,这是小学问题,不要给我说你不会哈。
那么在运行的栏目中就会这样显示:
在这里插入图片描述
看一看红框里面的,里面说的是算术异常,第二个红框框的是造成异常的原因。
在我们平时写代码的时候,哦,不不不,在写bug的时候,肯定会遇到许多异常,博主就在这简单的介绍几个异常。
空指针异常:

   public static void main(String[] args) {
        int []array = {1,2,3};
        array = null;
        System.out.println(array[100]);
    }
  • 1
  • 2
  • 3
  • 4
  • 5

在这里插入图片描述
数组下标越界异常:

     public static void main(String[] args) {
        int []array = {1,2,3};
        System.out.println(array[100]);
    }
  • 1
  • 2
  • 3
  • 4

数组总共就有三个元素,是不可能让我们访问到下标为100的元素的。
在这里插入图片描述
所谓异常就是程序在运行时出现的错误,编译器把运行程序时发生的错误通知调用者的一种机制。
有些童鞋就会为那在程序编译的时候出现的是什么呀?这个就是一个错误,就是Error,比如说我们在写一句打印的代码时,不小心把一个字母给打错了,然后编译器就会在你写错的地方标注红线,这就是在编译的时候出现的错误,当然编译器也会有打盹的时候,比如下面的一段代码,它不会显示红线但是它依旧是发生错误的。

    public static void func(){
        func();
    }
    public static void main(String[] args) {
        func();
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

很明显,代码的逻辑上出现了问题,在代码进行递归的时候没有限制条件,造成了栈溢出,让func()方法一直调用自己。
在这里插入图片描述
所以说通常我们所说的编译时出现的错误,就是程序猿在写代码时的逻辑出现了问题,必须有自己进行修改。但是在运行时出现的异常我们可以通过异常处理就可以轻松搞定。

程序在运行的时候出现的异常有很多种,在这里就不和大家一一介绍了。下来请看我们是怎样捕获异常的。

防御式编程

防御式编程分为两种:

  1. LBYL:在操作之前做好充分检查。
  2. EAFP:事后解决比事前获得许可更容易,简单的就是说,先不管三七二十一直接进行操作,在操作之后再检查哪里出现了问题。

这里利用一个通俗的例子为了大家好理解:
比如说博主现在在搞对象,有一天在街上逛街,然后博主想想牵住对象的手,说了一句,我能牵住你的手吗?然后对象勉为其难的同意了,哭笑,这也太钢铁直男了吧。(这就属于LBYL防御式编程)也只有钢铁直男这样做了,正确的直接就应该顺手就去牵呀,管她同不同意,不同意就算了呗。(这就属于EAFP编程)。
在我们写程序,捕获异常的时候一般都会选择后者。

异常的好处

那么博主异常在那些情况下会发生呢,在这里又举了一个例子。
比如,你在打王者荣耀的时候,突然接到了一个你对象打来的电话,这个时候你不得不接,这就属于异常。
代码如下:
LBYL防御式编程下(不使用异常)

 boolean ret = false;
ret = 登陆游戏();
if (!ret) {
 处理登陆游戏错误;
    return; }
ret = 开始匹配();
if (!ret) {
 处理匹配错误;
    return; }
ret = 游戏确认();
if (!ret) {
 处理游戏确认错误;
    return; }
    ret = 选择英雄();
if (!ret) {
    处理选择英雄错误;
    return; }
ret = 载入游戏画面();
if (!ret) {
 处理载入游戏错误;
     return; }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

EAFP状态下,使用异常

 try {
    登陆游戏();
    开始匹配();
    游戏确认();
    选择英雄();
    载入游戏画面();
   ...
} catch (登陆游戏异常) {
    处理登陆游戏异常;
} catch (开始匹配异常) {
 处理开始匹配异常;
} catch (游戏确认异常) {
 处理游戏确认异常;
} catch (选择英雄异常) {
 处理选择英雄异常;
} catch (载入游戏画面异常) {
 处理载入游戏画面异常;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

明人不说暗话,那种代码好看呀,当然是第二种,在EAFP状态下,我们先进行操作,再捕获异常。这样在运行之后,哪里出现了异常我们一眼就可以看出,相比第一种代码,就不怎么好辨认,所以说在我们日常编写代码,捕获异常的时候都用第二种。

异常的基本用法

java异常体系

让我们先看一看java的异常体系,大致是这样的,每个具体异常都是一个类

每个异常都属于一个类,而异常又分为运行时异常和编译时异常,同时编译时异常和运行时异常都继承了一个类,就是Exception类。而我们知道的空指针异常类和数组下标越界异常类都继承了运行时异常类。
请看下面源码:
空指针异常类继承了运行时异常类
在这里插入图片描述
运行时异常类继承了Exception类
在这里插入图片描述
Exception类又继承了Throwable类(主类)
在这里插入图片描述

捕获异常

现在就介绍一下,怎样捕捉异常吧,它的语法是这样的。

     try{
       //在这里放的是将要接受异常捕获的代码
       }catch (//在这里放的是代码在运行时可能出现的异常类型){
           //在这里是处理异常的代码
       }finally{
       //异常出口
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 在try代码块中存放的是可能出现异常的代码
  • 在catch代码块中存放的是代码在出现异常时的处理
  • 在finally代码块中存放的是异常处理完后的善后工作
  • finally代码块和catch代码块可以根据代码环境进行增添。

代码示例一:
不检查异常:

     public static void main(String[] args) {
    int []array = new int[10];
        System.out.println("before");
        System.out.println(array[100]);
        System.out.println("after");
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

我们可以很明显的看出,发生了数组下标越界异常。
在这里插入图片描述
我们注意,在代码运行的过程中如果检查到了异常,代码就会自动的交给jvm进行处理,并且结束代码运行。正如上图所示我们只打印了before,没有打印到after.

代码示例二:
我们捕获异常后的代码:

 public static void main(String[] args) {
    int []array = new int[10];
        try{
            System.out.println("before");
            System.out.println(array[100]);
            System.out.println("after");
        }catch (ArrayIndexOutOfBoundsException e){
            //打印出现异常的调用栈
            e.printStackTrace();
            System.out.println("数组下标越界异常");
        }finally {
            System.out.println("1");
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

在这里插入图片描述

我们可以很明显的看到 在try代码块中发现异常之后,没有继续执行try代码块中的语句,直接交给了catch代码块,检查出现异常的类型,同时如果我们在编写代码的时候,catch()括号中,不是try代码块中的异常类型时,就不会执行到catch代码块中去,就直接交给了jvm进行处理。

代码实例三:

     public static void main(String[] args) {

    int []array = new int[10];
        try{
            System.out.println("before");
            System.out.println(array[100]);
            System.out.println("after");
        }catch (NullPointerException e){//try代码块中所捕获的异常,在catch()中不能得到处理,我们在try代码快中捕获的是数组下标越界异常,但是在cathc()中处理的是空指针异常,类型不匹配
            //打印出现异常的调用栈
            e.printStackTrace();
            System.out.println("数组下标越界异常");
        }finally {
            System.out.println("1");
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

在这里插入图片描述
比如上面的这一串代码,很明显的看到代码块中出现的不是我们要的异常类,那么这个异常就由jvm进行处理,但是我们可以看到finally代码块中的语句还能执行,所以说finally代码块中的代码执行时独立的。不受try代码块和catch代码块的影响。

关于调用栈:在方法之间存在调用关系,这种调用关系我们可以用调用栈来描述,在jvm中一块专用的内存空间被称为“虚拟机栈”在这个栈中专门用来处理方法之间的调用 。当当前代码出现异常时,我们可以利用e.printStackTracce(),来调用当前出现异常代码的调用栈。

代码示例四:当然代码中还可能出现多种异常,我们可以同时catch多个异常
例如下面代码:

  public static void main2(String[] args) {
           int []array = {1,2,3};
        try{
            System.out.println(array[100]);
            array = null;
            System.out.println(array[1]);
        }catch (ArrayIndexOutOfBoundsException e){
            e.printStackTrace();
            System.out.println("数组下标越界异常");
        }catch (NullPointerException e){
            e.printStackTrace();
            System.out.println("空指针异常");
        }finally {
            System.out.println("1");
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

在这里插入图片描述
一段代码可能会出现多种异常,每种异常都有自己的处理方式当我们在处理异常的时候采取的方式一致的时候,我们可以这样:

 public static void main(String[] args) {
        int []array = {1,2,3};
        try{
            System.out.println(array[100]);
        }catch (ArrayIndexOutOfBoundsException | NullPointerException e){
            e.printStackTrace();
            System.out.println("出现异常");
        }finally {
            System.out.println("1");
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

代码示例五:当然我们可以用一个异常类,来检查try代码块中的所有异常

   public static void main(String[] args) {
        int []array = {1,2,3};
        try{
            System.out.println(array[100]);
        }catch (Exception e){
            e.printStackTrace();
            System.out.println("出现异常");
        }finally {
            System.out.println("1");
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

我们可以有上面的异常体系一览图可以得知,Exception类的是所有异常的父类,所以可以由Exception异常类,来查找并处理出try代码块中的所有异常。
备注:在catch代码块中进行异常匹配得时候,不仅会匹配相同的异常类,还会匹配异常类的父类

代码示例六: 不管try代码块中的代码是否出现异常,finally代码块中的代码都一定会被执行,finally代码块用来处理善后工作,比如释放资源。

     public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        try {
            int a = scanner.nextInt();
        }catch (InputMismatchException e) {
            e.printStackTrace();
            System.out.println("发生异常");
        }finally {
            scanner.close();//释放资源
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

在这里插入图片描述
代码示例七:使用try进行回收资源
刚才代码的等价写法:我们可以在try()括号中,实现Scanner对象,这样try代码块执行完后,自动调用Scanner,close()方法。

     public static void main(String[] args) {
        try (Scanner scanner = new Scanner(System.in)) {
            int a = scanner.nextInt();
        } catch (InputMismatchException e) {
            e.printStackTrace();
            System.out.println("发生异常");
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

奇淫巧技:
在这里插入图片描述
我们可以看到,try关键字背标黄了,这说明我们的代码还可以优化,点中try关键字按住Alt和回车(enter)就会出现。
在这里插入图片描述
点击被小灯泡标记的选项。
在这里插入图片描述

代码示例八:如果在catch代码块中,没有我们所匹配的异常类,我们就调用调用栈,依次向上调用,如果还是没有那只能有jvm进行处理。

 public static void func(){
        Scanner scanner = new Scanner(System.in);
        try{
            int a = scanner.nextInt();
        }catch (NullPointerException | ArrayIndexOutOfBoundsException e){
            e.printStackTrace();
            System.out.println("出现异常");
        }finally {
            System.out.println("1");
        }
    }
    public static void main(String[] args) {
        try{
            func();
        }catch (ArithmeticException e){
            e.printStackTrace();
            System.out.println("出现异常");
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

在这里插入图片描述

异常的处理方法

  1. 先执行try代码块中的代码
  2. 如果try中的某行代码出现异常,那么try代码块中的代码就不会再进行执行,直接跳到catch代码块中,匹配到与try中异常所匹配到的异常类
  3. 如果匹配到了相应的异常类,那么在调用调用栈,打印出该异常的调用栈。
  4. 无论代码是否出现了异常,finally代码块中的代码都会被执行。
  5. 如果在匹配异常类的时候,没有匹配到相应的异常类,那么我们就向上调用栈,直到调用到相应的异常类,如果还是没有调用到,那么只能进入main方法,在jvm中处理异常。此时程序异常停止。

抛出异常

我们其实还可以手动的抛出异常

     public static int Div(int x,int y){
        if(y == 0){
            throw new ArithmeticException("运算异常");
        }else{
            return x / y;
        }
    }
    public static void main(String[] args) {
        System.out.println(Div(10,0));
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

在这里插入图片描述

异常说明

通常我们在处理异常的时候,首先要知道某个方法会处理怎样的异常,所以我们会进行标识。

public static int Div(int x,int y)throws ArithmeticException{//通过throws标记我们在查看自己代码的时候,就很清楚的看到这个方法要处理怎样的异常
    if(y == 0){
        throw new ArithmeticException("运算异常");
    }else{
        return x / y;
    }
}
public static void main(String[] args) {
    System.out.println(Div(10,0));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

关于finally的一些认识

finally代码块有时还会给我们带来许多麻烦。
比如我们在try代码块和finally代码块中,都分别return一个数字,那么最后执行,并且返回的是finally中的代码块。
因为finally代码块是在方法返回之前执行的(当try或catch中有return的话,会在这个return 之前执行finally中的代码块),当finally中的代码块可有return 时,是先会执行finally中的return.
所以我们在编写代码的时候,不要再finally代码块中,进行返回。

     public static int func2(){
        try{
            return 10;
        }finally {
        return 100;
        }
    }
    public static void main(String[] args) {
        System.out.println(func2());
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

自定义异常

其实我们在日常生活中碰到的异常,肯定有许多,有些在java类库中是没有的所以我们要自己实现一个异常,比如说我们在登录某个账户的时候要输入自己的姓名,和相关密码。如果我们没有输入正确,就会产生异常,为输入姓名错误异常,和输入密码错误异常。这就必须我们自己手动实现了。

Exception类作为所有编译时异常和运行时异常的父类,所有的异常类都继承他,所以我们可以设置两个异常分别都继承Exception类,并且调用父类中的构造方法。
在这里插入图片描述

 class UserNameException extends Exception {
    public UserNameException(String message) {
        super(message);
    }
}
class PassWorldException extends Exception {
    public PassWorldException(String message) {
        super(message);
    }
}
public class TestDemo1 {
    private  static String userName = "abc";
    private  static String password = "123";
    public  static void login(String userName, String password) throws UserNameException, PassWorldException {//表明方法中要处理怎样的异常
        if(!TestDemo1.userName.equals(userName)){
            throw new UserNameException("姓名输入错误");//抛出异常
        }
        if(!TestDemo1.password.equals(password)){
            throw new PassWorldException("密码输入错误");//抛出异常
        }
        System.out.println("输入正确");
    }
    public static void main(String[] args) {
        try {
            login("abcd", "12");
        } catch (UserNameException userNameException) {
            userNameException.printStackTrace();
        } catch (PassWorldException passWorldException) {
            passWorldException.printStackTrace();
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

在这里插入图片描述

推荐阅读