Java中的异常(SE完结篇)

在这里插入图片描述

🎉🎉🎉写在前面:
博主主页:🌹🌹🌹戳一戳,欢迎大佬指点!
博主秋秋:QQ:1477649017 欢迎志同道合的朋友一起加油喔💪
目标梦想:进大厂,立志成为一个牛掰的Java程序猿,虽然现在还是一个小菜鸟嘿嘿
-----------------------------谢谢你这么帅气美丽还给我点赞!比个心-----------------------------

在这里插入图片描述



1,异常的定义

首先,从名字上就可以得出,异常,指的就是程序执行过程中发生生的不正常行为。

比如,我们之前经常遇到的算数异常,空指针异常等等。

public class TestDemo220606 {
    public static void main(String[] args) {
        //System.out.println(10/0);
        //Exception in thread "main" java.lang.ArithmeticException: / by zero 除数为0,算数异常
        String s1 = null;
        s1.length();
        //Exception in thread "main" java.lang.NullPointerException //空指针异常
    }
}

2,异常的体系

异常的体系结构比较复杂,种类繁多,这里简单的从宏观上给大家总结下:

在这里插入图片描述

从图片中,我们可以看出:

1. Throwable:是异常体系的顶层类,其派生出两个重要的子类, Error 和 Exception

2. Error:指的是Java虚拟机无法解决的严重问题,它是错误,比如:JVM的内部错误、资源耗尽等,典型代表:StackOverflowError和OutOfMemoryError。形象的说法就是你的程序得了’‘癌症’‘。

3. Exception:异常产生后程序员可以通过代码进行处理,使程序继续执行。形象的说法就是你的程序得了”小感冒“。


3,异常的分类

从上面的图片也可以看出,异常分为两大类,编译时异常与运行时异常。

1,编译时异常,也称受查异常,在编译的时候发生。

2,运行时异常,也称非受查异常,在程序运行的时候发生。

注意:在编写程序的过程中,出现的语法错误不是异常,这个点不要混淆了。


二,异常的处理

1,防御式编程

异常在代码中是经常存在的,所以我们就需要当异常发生时及时的通知程序员,主要方式如下:

1.1,事前防御型(LBLY)

在操作之前就做好充分的检查,如果没有问题才会继续。

boolean ret = false;
ret = 登陆游戏();
if (!ret) {
    处理登陆游戏异常;
return;
}
ret = 开始匹配();
if (!ret) {
    处理匹配异常;
return;
}
.....

但是这种方法的缺陷在于,我们的正常流程与异常的处理混在了一起,这样就导致代码的结构非常的不清晰。


1.2,事后认错型(EAFP)

先进行操作,遇到问题后再进行处理。

try {
    登陆游戏();
    开始匹配();
}catch (登陆游戏异常) {
    处理登陆游戏异常;
} catch (开始匹配异常) {
    处理开始匹配异常;
} 

通过我们的EAFP的思想,这样就把整个程序的正常执行流程与异常的处理分开了,使得代码的结构会更加的清晰。其实,我们异常处理的核心思想就是EAFP思想


2,异常的抛出

当我们在编写程序的时候,如果说出现了异常情况,此时就需要将异常的信息反馈给我们的程序员。在Java里面,我们借助关键字throw,抛出一个指定的异常对象,将错误信息反馈给调用者。

public class TestDemo220608 {
    public static void func(int a){
        if(a == 0){
            throw new RuntimeException("a==0");//new 一个异常对象并抛出,一般情况下我们new的都是一个自定义类型的异常
        }
    }
    public static void main(String[] args) {
        int a = 0;
        func(a);
    }
}

在这里插入图片描述

可以看到,如果当a=0传入函数func后,就会抛出一个a ==0的异常,这个时候是我们的程序员手动的抛出的异常。如果出现异常但是我们没有手动抛出的时候,其实是JVM检测到后帮我们抛出的。一般情况下,我们手动抛出的都是我们自己定义的异常类型。


注意:

1,throw必须写在方法体的内部。

2,抛出的必须是Expection或者Expection的子类对象。

3,如果抛出的是运行时异常或者是其子类,那么就可以不用管,交给JVM处理就好。

4,如果你抛出的是一个受查异常,也就是编译时异常,那你就必须要手动的进行异常的声明,不然程序连编译都过不去。

public class TestDemo220608 {
    public static void func(int a) throws CloneNotSupportedException { //throws进行异常的声明
        if(a == 0){
            throw new CloneNotSupportedException();
        }
    }
    public static void main(String[] args) throws CloneNotSupportedException{
        int a = 0;
        func(a);
    }
}

5,异常一旦抛出,其后面的代码就不会执行了。

public class TestDemo220608 {
    public static void func(int a) throws CloneNotSupportedException {
        if(a == 0){
            throw new CloneNotSupportedException();
        }
    }
    public static void main(String[] args) throws CloneNotSupportedException{
        int a = 0;
        func(a);
        System.out.println("这是异常后的逻辑部分");
    }
}

在这里插入图片描述

我们可以看到当调用func抛出异常之后,主函数后面的内容就没有再执行了。


总结,其实抛出异常我们一般都是自定义类型的异常,按道理来说其实它的本质不能算是一个异常,只是在逻辑业务要求上它在这个情况下就是一个异常,所以我们就需要把它手动抛出,因为这个时候JVM层面上是不认为它是一个异常的,所以捕获不到,只能是进行手动的抛出。抛出的如果是一个编译时异常,那么我们就需要进行事先的声明,你得先让它通过编译,才能在运行的时候的特定情况下把异常抛出去。


3,异常的捕获

异常的捕获,也就是异常的具体处理方式。主要有两种:异常的声明throws,以及try - catch捕获处理。

1,异常声明throws

处在方法声明的参数列表之后,当方法中抛出编译时异常,此时用户不想处理该异常,此时就可以借助throws声明一下这个异常,然后让方法的调用者去处理。即当前方法不处理异常,提醒方法的调用者处理。

语法格式:
修饰符 返回值类型 方法名(参数列表) throws 异常类型1,异常类型2...{}

注意:

1,声明的异常必须是Expection或者Expection的子类

2,方法内部如果抛出了多个编译时异常,那么参数列表后面throws需要声明多个异常,之间用逗号隔开。如果他们之间有父子类关系的话,那么就可以只声明父类就好。

public class TestDemo220608 {
    public static void func(int a) throws Exception {
        if(a == 0){
            throw new CloneNotSupportedException();
        }
        if(a == 1){
            throw new FileNotFoundException();
        }
    }
    public static void main(String[] args) throws Exception {
        int a = 0;
        func(a);
    }
}

这里要抛出的是两个编译时异常,我们可以在方法的后面一个个进行声明,但是也可以直接声明它们的父类Exception就好。但是这只是一种方法,为了让程序更加的清晰明了,这种方法是不推荐的。


3,方法将编译时异常用throws进行声明之后,那么就需要我们的调用者去进行处理了。如果调用者也不想处理,那就继续用throws进行声明。


2,try - catch

对于throws而言,它其实对异常没有进行真正的处理,而是将其甩给了调用者,然后如果就这样一层层的甩出去的话,最终还是会交给到JVM的手上进行处理。如果想实现自己真正的处理,就需要使用try - catch。

public class TestDemo220608 {
    public static void main(String[] args) {
        String s1 = null;
        try{
//            这是可能会发生异常的程序主体
            s1.length();
        }catch (NullPointerException e) {
//            这是对异常进行捕获并实际处理的部分
            System.out.println("捕捉到一个空指针异常进行处理!");
        }

        System.out.println("这是其余的代码逻辑!");
    }
}

在这里插入图片描述


这个时候,因为异常捕获到后是我们程序员自己进行处理的,所以整个程序在遇到异常后是不会结束运行的,所以其余的代码逻辑能够进行执行。但像之前,如果最后还是抛给了JVM处理的话,那程序就会直接结束掉。

当然,如果你try - catch捕获到的异常与你catch中定义的不是同一个类型,那照样还是处理不了,会交给JVM。

public class TestDemo220608 {
    public static void main(String[] args) {
        String s1 = null;
        try{
//            这是可能会发生异常的程序主体
            s1.length();
        }catch (ArithmeticException e) {
//            这是对异常进行捕获并实际处理的部分
            System.out.println("捕捉到一个算数异常进行处理!");
        }
        System.out.println("这是其余的代码逻辑!");
    }
}

在这里插入图片描述


可以看到,JVM在处理异常后会把程序终止并且输出我们的异常的具体信息,那我们的try - catch同样也是可以把异常信息输出的。

public class TestDemo220608 {
    public static void main(String[] args) {
        String s1 = null;
        try{
//            这是可能会发生异常的程序主体
            s1.length();
        }catch (NullPointerException e) {
//            这是对异常进行捕获并实际处理的部分
            e.printStackTrace();//将异常信息输出
            System.out.println("捕捉到一个空指针异常进行处理!");
        }
        System.out.println("这是其余的代码逻辑!");
    }
}

在这里插入图片描述


try - catch可以捕获多个异常,但不是同时捕获,因为不会同时触发多个异常。

public class TestDemo220608 {
    public static void main(String[] args) {
        String s1 = null;
        try{
//            这是可能会发生异常的程序主体
            s1.length();
        }catch (NullPointerException e) {
//            这是对异常进行捕获并实际处理的部分
            e.printStackTrace();//将异常信息输出
            System.out.println("捕捉到一个空指针异常进行处理!");
        }catch (ArithmeticException e1) {
            e1.printStackTrace();
            System.out.println("捕获到一个算数异常进行处理!");
        }
        System.out.println("这是其余的代码逻辑!");
    }
}

如果我们要捕获的多个异常之间存在父子类关系,那一定是子类在前,父类在后。因为父类异常类包含所有的子类异常类,所以你把父类写在前面,会把所有有关的异常都会进行捕获,那么后面写的子类的捕获也就没有任何的意义了。

在这里插入图片描述

如图,Exception类就包含空指针异常以及算数异常,所以后面的catch没有任何的作用,会报错。

正确的写法只能是:

public class TestDemo220608 {
    public static void main(String[] args) {
        String s1 = null;
        try{
//            这是可能会发生异常的程序主体
            s1.length();
        } catch (NullPointerException e) {
//            这是对异常进行捕获并实际处理的部分
            e.printStackTrace();//将异常信息输出
            System.out.println("捕捉到一个空指针异常进行处理!");
        }catch (ArithmeticException e1) {
            e1.printStackTrace();
            System.out.println("捕获到一个算数异常进行处理!");
        }catch (Exception e2) {
            e2.printStackTrace();
            System.out.println("捕捉到了除算数异常,空指针异常外的其他异常!");
        }
        System.out.println("这是其余的代码逻辑!");
    }
}

把Exception放到最后,能够捕获除开算数异常,空指针异常外的其他异常。当然有的同学可能会说直接用一个catch,里面的异常类型放Exception,这样就可以直接捕获所有的异常。但是这种方法是不好的,因为范围太广了,这样你必须得把异常信息打印一遍你才能知道这是一个什么异常。


同样的,对于一些编译时异常,我们也可以通过try - catch进行捕获处理。

class Person implements Cloneable{
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
public class TestDemo220608 {
    public static void main(String[] args) {
        try{
            Person p1 = new Person();
            Person p2 = (Person)p1.clone();
        }catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }
}

如果说在之前,那么main函数的参数列表后面是必须要用throws进行这个编译时异常的声明的,因为方法要声明一次,调用者也要声明一次,并且最终会由JVM进行处理,但是现在有了try - catch后,我们可以直接捕获到这个异常并且由我们自己进行处理。


【注意】

try块和之前的throw手动抛异常一样,try块中抛出异常的位置之后的代码是不会执行的

public class TestDemo220608 {
    public static void main(String[] args) {
        String s1 = null;
        try{
//            这是可能会发生异常的程序主体
            s1.length();
            System.out.println("hahahaha");//这一句是不会执行的
        }
        catch (NullPointerException e) {
//            这是对异常进行捕获并实际处理的部分
            e.printStackTrace();//将异常信息输出
            System.out.println("捕捉到一个空指针异常进行处理!");
        }
        System.out.println("这是其余的代码逻辑!");//这一句会执行
    }
}

在这里插入图片描述

其实这个过程就相当于try中抛出异常被catch捕获然后并处理后,就直接顺着catch往后执行了,所以之前try块中的抛出异常后的部分根本就执行不到。


3,finally

当我们在写代码的时候,有些特定的代码是必须要执行的,不管程序是否发生异常,比如我们打开的资源需要进行回收。但是异常的处理会导致程序执行发生跳转,那么可能有的代码就执行不到,所以就需要使用finally。

public class TestDemo220608 {
    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        try{
//            这是可能会发生异常的程序主体
            String s1 = null;
            s1.length();
        }
        catch (NullPointerException e) {
//            这是对异常进行捕获并实际处理的部分
            e.printStackTrace();//将异常信息输出
            System.out.println("捕捉到一个空指针异常进行处理!");
        }finally{
            scan.close();
            System.out.println("在finally进行资源的关闭!");
        }
        System.out.println("这是其余的代码逻辑!");
    }
}

在这里插入图片描述

可以看到,当我们发生异常之后,finally中的内容依然是被执行的,完全点来说,finally中的内容不管你是否发生异常,都会被执行,所以可以很好的用来关闭一些资源。


那看到这,大家可能会有疑问,那既然其余的代码逻辑也能执行,要finally干啥,直接在后面进行操作不是一样的吗?那你看看这段代码:

public class TestDemo220608 {
    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        try{
//            这是可能会发生异常的程序主体
            String s1 = null;
            s1.length();
        }
        catch (NullPointerException e) {
//            这是对异常进行捕获并实际处理的部分
            e.printStackTrace();//将异常信息输出
            System.out.println("捕捉到一个空指针异常进行处理!");
            return;//在这里直接return掉
        }finally{
            scan.close();
            System.out.println("在finally进行资源的关闭!");
        }
        System.out.println("这是其余的代码逻辑!");
    }
}

在这里插入图片描述

可以看到,当我们将异常处理好后,直接使用return结束掉程序的执行的话,那你后买你的其余代码逻辑是不是都执行不到了,但是你看即使是这样,finally中的内容还是被执行了,所以,由此可以看出,finally中的内容,无论如何都会执行,用来进行一些必须代码的执行最好不过。

总结,finally 执行的时机是在方法返回之前(try 或者 catch 中如果有 return 会在这个 return 之前执行 finally). 但是如果 finally 中也存在 return 语句, 那么就会执行 finally 中的 return,从而不会执行到 try 中原有的 return。(但是在finally中写return不是一个好的习惯)


✅【面试题:】

throw 与 throws 的区别?

throw是直接抛出一个异常,多用于抛出自定义类型的异常,抛出的异常会直接被JVM处理,此时程序也会结束执行。throws是声明一个异常,此时只是让方法本身不用去处理这个异常,而让调用者去处理,如果调用者也不处理继续使用throws最终会被JVM处理,throws可以让程序暂时编译层面上不会报错。


4,异常的处理流程

【调用栈】

关于 “调用栈” 方法之间是存在相互调用关系的, 这种调用关系我们可以用 “调用栈” 来描述. 在 JVM 中有一块内存空间称为"虚拟机栈" 专门存储方法之间的调用关系. 当代码中出现异常的时候, 我们就可以使用 e.printStackTrace() 的方式查看出现异常代码的调用栈。


public class TestDemo220608 {
    public static void func(){
        String s1 = null;
        s1.length();
    }
    public static void main(String[] args) {
        try{
            func();
        }catch (NullPointerException e) {
            e.printStackTrace();
        }
    }
}

在这里插入图片描述

因为是20行func内部s1.length(),这里就是出现了空指针异常,然后main函数中调用了func,所以24行也会显示有错误。根据这一点,我们以后在解决异常的时候,看报的第一行错误就好,一般只要找到报的第一行的异常的位置,然后将其解决掉程序就正常了。


✅总结:异常的处理流程

1,首先是执行try块中的代码,看是否有异常。

2,如果发生异常,终止try块中的代码执行,去catch中进行异常的匹配。

3,如果catch中匹配到了异常类型,那就执行catch中的代码将异常解决。

4,如果catch中没有匹配到异常类型,那就将异常传递给上层调用者。

5,当然对于finally而言,无论是否匹配到,都会执行。

6,如果上层调用者也没有具体的解决措施,那就继续往上层调用者传递。

7,最终如果传递到main函数,而main函数也没有解决措施,那就会直接交给JVM处理,届时程序也会被终止执行。


5,自定义异常类

Java中虽然已经给我们提供了很多的异常类,但是考虑到实际开发过程中的遇到的一些特殊的异常情况,此时需要我们自己去定义这么一个异常类。



class myException extends RuntimeException{//这是一个自定义异常类
    public myException() {
    }

    public myException(String message) {
        super(message);//可能会需要输出异常的具体信息,所以要有一个有参的构造
    }
}

public class Test {
    public static void isLegal(String s){
        if(s.length() > 5){
            throw new myException("用户名长度超过限制!");
        }else{
            System.out.println("登录成功!");
        }
    }
    public static void main(String[] args) {
        //    模拟设置用户名中的异常
        Scanner scan = new Scanner(System.in);
        String s = scan.next();
        try{
            isLegal(s);
        }catch (myException e){
            e.printStackTrace();
        }finally {
            scan.close();
        }
    }
}

在这里插入图片描述


正如上面的代码所展示,我们可以自定义异常类,如果是继承于Exception类,那么你自定义的这个异常就是一个受查异常,如果是继承于RuntimeExpection,那么你自定义的就是一个非受查异常。这个自定义异常类的内部,根据你自己的需求去定义它的构造函数。


最后,今天的文章分享比较简单,就到这了,如果大家觉得还不错的话,还请帮忙点点赞咯,十分感谢!🥰🥰🥰
在这里插入图片描述

  • 45
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 43
    评论
评论 43
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

努力学习.java

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值