说明

这是一个完善了但又不完善的笔记,或许以后会更新

可以参考但请务必超越

源文件


Tools


Typora
PicGo

JAVA异常

异常,字面理解那就是出问题了呗

异常其实一直都有在接触,只不过没有去分类,去系统的讲述

1.异常的背景

曾经见过的异常:

除以0,算数异常

数组下标越界,数组越界异常

访问null对象,空指针异常

所以的异常,指的就是程序在运行时出现错误通知调用者的一种机制

注意关键字:运行时

关键字“运行时”

有些错误是这样的,例如将System.out.println拼写错了,写成了system.out.println,此时编译过程中就会出错,这是“编译期”出错。(当然除了大小写,还有中英文符号,忘写符号等)

而运行时指的是程序已经编译通过得到class文件了,再由JVM 执行过程中出现的错误。

异常的种类有很多,不同种类的异常具有不同的含义,也有不同的处理方式。

防御式编程

错误在代码中是客观存在的,所以我们要在程序出现问题的时候及时通知程序猿。有两种方式。

LBYL:Look Before You Leap. 在操作之前就做充分的检查。

EAFP:It's Easier to Ask Forgiveness than Permission. “事后获取原谅比事前获取许可更容易”。也就是先操作,遇到问题再处理。

这两个概念就没必要背了,很容易理解。

异常的好处

首先看一下LBYL风格的代码

比如游戏登陆,LBYL风格那就不使用异常呗

boolean flg = false;
flg = 登陆();
if (!flg) {
    处理登陆错误;
    return;
}
//...

EAFP风格的代码,就会使用异常

try {
    登陆();
    //...
} catch (登陆异常) {
    处理登陆异常;
} //catch...

对比两种风格的代码,很明显第一种方式正常流程和错误处理混杂在一起,每一次都需要使用if判断一下先。第二种方式正常流程和错误处理是分开的,前面正常流程,把错误处理都放在后面,更容易理解查看。

这就体现出了异常的好处,哪怕只是观感和理解上,也相比不使用好的多。正常的项目工作,可要复杂的多

2.异常的基本用法

try { 
 有可能出现异常的语句;
}[catch (异常类型 异常对象) {
    处理异常;
} ... ]
[finally {
 异常的出口
}]
  • try代码块中存放可能出现异常的代码。
  • catch代码块存放出现异常后处理的行为。
  • finally代码块存放处理善后工作的代码,在最后执行。
  • catchfinally视情况选择加或不加。事实上一般情况都是会加catch而不加finally,不加catch那么idea就会报错。

抛出异常

用栗子来证明理解是最好的方式

数组越界

public static void main(String[] args) {
    int[] array = {1, 2, 3};
    System.out.println(array[5]);
    System.out.println("测试");
}

可以看到,程序抛出异常就立即终止了,后面的代码没有执行

那我们用try catch包裹一下

public static void main(String[] args) {
    int[] array = {1, 2, 3};
    try {
        System.out.println(array[5]);
        System.out.println("test");
    } catch (ArrayIndexOutOfBoundsException e) {
        System.out.println("捕捉到一个数组越界异常");
    }
    System.out.println("测试");
}

可以看到,try代码块中的打印内容都没有打印,catch捕获到异常后就做了处理打印了内容,最后的打印内容也正常打印了。

这样做就可以在异常抛出的时候,程序也还是能够运行下去

printStackTrace()

上面栗子的处理方式,可以使用这个方法

e.printStackTrace();

这个方法可以抛出异常,就是我们看到的红色字体

未捕捉到异常

上面的栗子catch是成功捕捉到异常的

那要是有异常却没有捕捉到呢?

不处理异常

如果不处理异常的话,程序就会教给JVM处理

一旦交给JVM处理,程序就会立刻终止

注意,被catch捕捉到也是对异常的处理。

catch (ArrayIndexOutOfBoundsException e) {

}

catch中不写任何代码,意思是说捕捉到异常后,什么都不做

如果有异常缺没有捕获到,那就是真的不处理异常了

public static void main(String[] args) {
    int[] array = {1, 2, 3};
    try {
        array = null;
        System.out.println(array[5]);
        System.out.println("test");
    } catch (ArrayIndexOutOfBoundsException e) {
        System.out.println("捕捉到一个数组越界异常");
    }
    System.out.println("测试");
}

现在在try中将数组引用array置空

但是catch捕捉的是数组越界异常

可以看到此时就是不处理异常

交给JVM后抛出空指针异常,然后终止程序

解决方式可以再添加一个catch捕获空指针异常

public static void main(String[] args) {
    int[] array = {1, 2, 3};
    try {
        array = null;
        System.out.println(array[5]);
        System.out.println("test");
    } catch (ArrayIndexOutOfBoundsException e) {
        e.printStackTrace();
        System.out.println("捕捉到一个数组越界异常");
    } catch (NullPointerException e) {
        e.printStackTrace();
        System.out.println("捕捉到一个空指针异常");
    }
    System.out.println("测试");
}

exit code

退出码,我们在执行完程序后,会显示一行信息

exit code 0意味着程序正常执行完毕并退出

当我们有异常并处理掉了,后面正常运行完毕,也是会显示exit code 0

如果不处理异常交给JVM,那就会直接终止程序,并显示退出码exit code 1。表示程序执行过程中遇到了某些问题或错误,非正常退出

exit code在大多数编程语言都适用

捕捉多个异常

上面的例子有数组越界和空指针两个异常

再添加一个catch是可以的,还可以使用|符号捕捉多个异常

public static void main(String[] args) {
    int[] array = {1, 2, 3};
    try {
        array = null;
        System.out.println(array[5]);
        System.out.println("test");
    } catch (ArrayIndexOutOfBoundsException | NullPointerException e) {
        e.printStackTrace();
        System.out.println("捕捉到一个数组越界/空指针异常");
    }
    System.out.println("测试");
}

异常信息栈

再举个栗子,用来学习下抛出的异常如何看

现在要输入一个数字,但是却输入了其他字符

public static void main(String[] args) {
    Scanner scanner = new Scanner(System.in);
    int n = scanner.nextInt();
    System.out.println(n);
}

我的背景可能导致看不太清楚,那就直接亮度拉满,或者把他拿出来看一下

Exception in thread "main" java.util.InputMismatchException
    at java.util.Scanner.throwFor(Scanner.java:864)
    at java.util.Scanner.next(Scanner.java:1485)
    at java.util.Scanner.nextInt(Scanner.java:2117)
    at java.util.Scanner.nextInt(Scanner.java:2076)
    at Test.main(Test.java:13)

InputMismatchException:输入参数不匹配异常

遇到大段的异常不要慌,慢慢来看

  1. 首先确定异常的类型,在这里是InputMismatchException这部分
  2. 再看异常信息栈。这些以at开头的信息,就是异常信息栈

上面的图片看不太清没关系,只要知道以下几点即可

  • idea中灰色的部分是Java的源代码出错。我们能怀疑和修改源码吗?肯定不行,所以找到最后一行蓝色的信息
  • 蓝色的部分是程序最后一次出现错误的信息。可以看到at Test.main(Test.java:13)意思就是说在main函数的13行
  • 因为蓝色部分的错误,导致了源码也错误

关于异常的处理方式

异常的种类有很多,我们要根据不同的业务场景来决定。

对于比较严重的问题(例如和算钱相关的场景,银行系统等)应该让程序直接崩溃,防止造成更严重的后果。

对于不太严重的问题(大多数场景),可以记录错误日志,并通过监控报警程序及时通知程序猿。

对于可能会恢复的问题(和网络相关的场景),可以尝试进行重试。

在我们当前的代码中采取的是经过简化的第二种方式,我们记录的错误日志是出现异常的方法调用信息,能很快速的让我们找到出现异常的位置。以后在实际工作中我们会采取更完备的方式来记录异常信息。

finally

在逻辑控制那一篇文章中提到过

Scanner也是属于一种资源,不用了就关闭

现在我们结合异常写一段代码

public static void main(String[] args) {
    Scanner scanner = new Scanner(System.in);
    try {
        int n = scanner.nextInt();
        System.out.println(10 / n);
    } catch (InputMismatchException e) {
        e.printStackTrace();
        System.out.println("输入有误!");
    } catch (ArithmeticException e) {
        e.printStackTrace();
        System.out.println("捕捉到一个算数异常,可能0作为了除数!");
    } finally {
        scanner.close();
    }
    System.out.println("测试");
}

首先正常运行,随便输入一个数据,catch捕捉看看有没有异常

最后,不管前面经过结果,finally都会执行

当然,执行完毕,依然按照正常流程走

所以,finally一般用作资源的关闭

优先级

既然finally一定会被执行,那么有这样一段代码

public static int func() {
    int a = 10;
    try {
        return a;
    } catch (ArithmeticException e) {
        e.printStackTrace();
    }finally {
        return 20;
    }
}


public static void main(String[] args) {
    System.out.println(func());
}

方法遇到return就会结束,那这里是返回try的10还是finally的20呢?

可以看到,本来应该返回的a应该是10,结果执行了finally返回了20,这就是一定会执行finally的含义

所以,尽量避免在finally当中写return

小结

异常处理流程

  • 程序先执行try中的代码。
  • 如果try中的代码出现异常,就会结束try中的代码,看和catch中的异常类型是否匹配。
  • 如果找到匹配的异常类型,就会执行catch中的代码。
  • 如果没有找到匹配的异常类型,就会将异常向上传递到上层调用者。
  • 无论是否找到匹配的异常类型,finally中的代码都会被执行到(在该方法结束之前执行)。
  • 如果上层调用者也没有处理的了异常,就继续向上传递。
  • 一直到main方法也没有合适的代码处理异常,就会交给JVM来进行处理,此时程序就会异常终止。

注意

打印信息是红色的,不一定是错误

如果有运行过Java程序的话,就会常见有很多红色的信息,但程序是正常运行的。红色的信息有可能是其他的信息

throws

前面的例子,异常都是在main函数中发生的

那么如果现在有一个方法,在方法内部出现了异常,会是什么情况?

public static void func(int n) {
    System.out.println(10 / n);
}

public static void main(String[] args) {
    func(0);
    System.out.println("测试");
}

交给了JVM直接结束了程序,没有打印main函数中的“测试”

那么使用try catch在方法内部和main函数中分别包裹看看

事实上,异常会沿着异常的信息调用栈进行传递

方法区没处理,那就会进行到main函数处理

main函数中处理,那关键的问题来了

怎么才能知道别人的方法中会出现什么异常呢?

使用throws声明异常

throws ArithmeticException

声明异常

在面向对象章节中有使用过clone(),下面画了红线

强转类型就不用多说了。强转之后,又使用alt+enter快捷声明了异常

throws CloneNotSupportedException

使用throws声明代码可能出现的异常。

告诉使用者,可能会出现什么样的异常

使用try catch处理后就会正常进行,不处理异常则交给JVM处理

throw

还有一种抛异常的方法

使用throw直接抛出异常

public static void func(int x, int y) throws RuntimeException {
    if ((x - y) == 10) {
        throw new RuntimeException("结果为" + (x - y));
    }
}

public static void main(String[] args) {
    func(20, 10);
}

一般来说即使是在方法内使用了throw,还是应该要使用throws声明一下

一般情况使用throw用来抛一些自定义的异常

3.异常的体系结构

所有的异常都来自于父类Throwable没错,异常也是一个类

Throwable又分为错误Error和异常Exception

错误必须由程序猿自己去处理代码

比如无限递归

public static void func() {
    func();
}

public static void main(String[] args) {
    func();
}

StackOverflowError:栈溢出错误(一般栈会溢出,堆溢出很少,因为现在内存都8个G以上,4个G都很少见)

异常分为运行时异常和编译时异常,也叫非受查异常和受查异常

异常就有太多了,一般是用到的时候查就行了

IOExceptionClassNotFoundExceptionCloneNotSupportedException属于编译时异常

运行时异常RuntimeException派生出很多常见的异常类

我们可以看看他有很多异常,其中就有我们遇过的

还有异常的体系结构图,可以自行搜索。也可以在idea中一个一个的打开直到父类Throwable

旗舰版idea支持使用yFiles生成可视化效果图,选中异常右键Diagrams即可查看

比如:

捕获父类

现在明白我们前面使用catch只是捕捉了其中的一个子类

那么是不是可以直接捕捉父类从而捕捉所有异常?

当然可以

catch (Exception e) {
    e.printStackTrace();
}

一般不推荐捕捉父类,因为太大了

且如果父类异常和子类异常同时存在的话,父类异常在后面如果捕捉到了子类异常那父类异常就没用了,父类异常在前面那子类异常就直接报错。

catch在捕获异常的时候,最好是从上往下。子类->父类,不要父类->子类

所以,最好捕捉具体的异常

4.自定义异常类

Java虽然已经提供了很多的异常类,但是实际情况可能会需要扩展一些其他的,符合我们实际情况的异常

直接举栗

举栗:登陆功能

private static final String name = "kirito";
private static final String password = "abc123";

public static void login(String name, String password) {
    if (!Login.name.equals(name)) {
        throw new NameException("无此用户名,请先注册!");
    }
    if (!Login.password.equals(password)) {
        throw new PasswordException("密码错误!");
    }
}

使用这个方法来判断输入的用户名和密码是否正确,不正确就抛出异常

这里的2个异常NameExceptionPasswordException就是我们要实现的异常类

实现条件

1.要在自定义的异常中写入信息

这个很简单,异常也是类嘛,直接重写一个调用一个参数的构造方法即可

2.要继承父类Throwable

当我们简单的写下2个自定义异常类时,肯定是会报错的

class NameException {

}

class PasswordException {

}

...
throw new NameException();
throw new PasswordException();
...

idea会提示我们应该抛出Throwable,这当然好解决,只要继承就可以了

这个时候,就会产生一个问题

受查异常和非受查异常的区别

前面提到了,但是在这里结合自定义异常类来讲比较好

class NameException extends Exception {

}

class PasswordException extends RuntimeException {

}

上面的NameException继承的是Exception,这时他是个受查异常,必须使用try catch包裹,并且使用throw就要使用throws声明

下面的PasswordException继承的是RuntimeException,这时他是个非受查异常,只会在运行时产生异常,就可以直接使用throw而不用其他,当然如果想用也是可以的

这很简单,可以自己验证,这里就不验证了。

完善代码

整合在一起,就是这样

class NameException extends RuntimeException {
    public NameException(String message) {
        super(message);
    }
}

class PasswordException extends RuntimeException {
    public PasswordException(String message) {
        super(message);
    }
}

public class Login {
    private static final String name = "kirito";
    private static final String password = "abc123";

    public static void login(String name, String password) {
        if (!Login.name.equals(name)) throws NameException, PasswordException {
            throw new NameException("无此用户名,请先注册!");
        }
        if (!Login.password.equals(password)) {
            throw new PasswordException("密码错误!");
        }
    }

效果

用户名错误,就会直接抛出异常

用户名没错,就会再看密码

小头图版权:《Ganqing》by Aviary 2021年12月7日中午12点05分 pid:94623280

广告位招租
最后修改:2021 年 12 月 09 日 08 : 09 PM
如果觉得我的文章对你有用,请喂饱我!(理直气壮)