什么是异常?

本文详细解析了Java中的异常概念,包括其产生的原因、分类(运行时异常、编译时异常、自定义异常),以及异常处理的关键字(throw、try-catch、finally)和防御式编程策略。讲解了异常的处理流程,强调了编译时异常必须处理的重要性。
摘要由CSDN通过智能技术生成

1.异常的概念

什么是异常(Exception)呢?

异常就是在程序执行过程中发生的不正常的行为.异常中断了正在执行程序的正常指令流

异常其实就是类.异常是可以继承的

异常产生后程序员可以通过代码进行处理,使程序继续执行,比如:我们感冒发烧,进行一些处理是会好起来的.

2.异常产生的原因

主要有3种原因:

😊编写程序的代码产生的错误.比如数组越界,空指针异常等.这些异常叫做未受查异常.

😊Java内部错误产生的异常,Java虚拟机产生的异常

😊通过throw(抛出异常)语句手动生成的异常,这种异常叫做受查异常,可以用来给方法的调用者一些信息.

3.异常的分类

🤨Throwable是异常体系的顶层类,它派生出两个子类,分别是Error(错误)和Exception(异常),前者是不受查异常(Unchecked Exception),后者是受查异常(Checked Exception).

异常的继承结构:

基类为Throwable,Error和Exception继承自Throwable,RunTimeException和IOException等继承自Exception.
非RunTimeException一般是外部错误(除了Error),其必须被try-catch语句捕获

🤨Error定义了在通常情况下不希望被程序捕获的异常.Error类型的异常是在运行时出现的错误.比如堆和栈的溢出.(StackOverflowError,OutOfMemoryError)这类异常是Java虚拟机无法解决的严重问题.

Error类体系描述了Java运行系统的内部错误以及资源耗尽的情景,Error不需要捕获.

因为异常可以在运行时发生,也可以在编译时发生,所以我们将异常分为运行时异常和编译时异常.

3.1 运行时异常

**运行时异常(不受查异常)**包括RuntimeException及其子类异常,比如

NullPointerException,IndexOutOfBoundsException,ArithmeticException.

也就是说这一类型的异常程序可以选择处理也可以选择不处理.这一类型的异常一般由于程序逻辑错误引起.

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

我们来举个例子:

当我们执行这段代码:

public class Test2 {
    public static void main(String[] args) {
        System.out.println(10/0);
    }
}

运行后的结果是:

在这里插入图片描述
ArthmeticException这个异常叫做算术异常

再来看另一个代码:

public class Test2 {
    public static void main(String[] args) {
        int[] array=null;
        System.out.println(array.length);
    }
}

运行后的结果是:

在这里插入图片描述

NullPointerException这个异常叫做空指针异常.

再来看另一段代码:

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

运行后的结果:

在这里插入图片描述
IndexOutOfBoundsException叫做下标越界异常

3.2 编译时异常

编译时异常就是RuntimeException以外的其他异常,包括Exception类及其子类异常.编译时异常是必须要处理的异常,如果不进行处理,那么程序的编译就不能通过.例如IOException,ClassNotFoundException,以及其他一些用户自定义的异常.一般用户自定义的异常都是编译时异常.

🐱例子:

public class Person implements Cloneable{
    public Object clone() throws CloneNotSupportedException{
        return super.clone();
    }
}

public class Test2 {
    public static void main(String[] args) {
        Person person1=new Person();
        Person person2=(Person) person1.clone();
    }
}

结果:

在这里插入图片描述

正确的处理是这样:

在这里插入图片描述

🐶注意:**编译时出现的语法性错误,不能称之为异常.比如:System.out.println这一句代码拼写错误**,这属于"编译期出错"

3.3 自定义异常

😆虽然Java中有很多异常类,但是在实际开发中所遇到的一些异常,不能完全表示,所以就需要自定义异常类.

自定义异常默认会继承Exception或者RunTimeException.继承自Exception的异常默认是受查异常,继承自RunTimeException的异常默认是非受查异常.

比如,我们来自定义一个运行时异常:

public class MyException extends RuntimeException{
    public MyException(){

    }
    public MyException(String message){
        super(message);
    }
}

我们来捕获一下这个异常:

在这里插入图片描述

结果:

在这里插入图片描述

👀接下来我们来看一个用户登录的自定义异常类:

public class UserNameException extends RuntimeException{
    public UserNameException(){

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

    }
    public PassWordException(String message){
        super(message);
    }
}
public class Login {
    public static String uName="zuozuo";
    public static String pWord="666666";
    public static void loginInfo(String userName,String passWord){
        if(!uName.equals(userName)){
            throw new UserNameException("用户名错误");
        }
        if(!pWord.equals(passWord)){
            throw new PassWordException("密码错误");
        }
        System.out.println("登录成功");
    }
    public static void main(String[] args) {
        try{
            loginInfo("zuozuo","666666");
        }catch (UserNameException e){
            e.printStackTrace();
        }catch (PassWordException e){
            e.printStackTrace();
        }
    }
}

4.异常的处理

在java中,异常处理主要的5个关键字就是:throw,try,catch,final,throws

前提:错误在代码中是客观存在的,因此我们要让程序出现问题的时候及时通知程序员.所采取的方式有以下几种.

4.1 防御式编程

😺(1)LBYL:也就是Look before you leap.在操作之前就做充分的检查,也就是事先预防性

例:

public  int func()  {
        Scanner sc=new Scanner(System.in);
        int a=sc.nextInt();
        int b=sc.nextInt();
        if(b==0){
            System.out.println("除数为0");
            return 0;
        }else {
            return a/b;
        }
    }

但是这样做的缺点是:

正常流程和错误处理流程混在了一起,代码整体显得比较混乱

😺(2)EAFP:It is easier to ask forgiveness than permission.“事后获取原来比事先获取许可更容易”,也就是先操作,遇到问题再处理.事后认错型.

例:

public  int func()  {
        try(Scanner sc=new Scanner(System.in)){
            int a=sc.nextInt();
            int b=sc.nextInt();
            return a/b;
        }catch(ArithmeticException e){
            System.out.println("除数为0");
            return 0;
        }
    }

优点:正常流程和错误流程分离开,程序员更关注正常流程,代码更清晰,容易理解代码.

🙉那么,异常处理的核心思想就是EAFP

问题来了:

处理异常的前提是什么?

处理异常的前提是得有异常,那么怎么才能有异常,那你就得抛出(触发)异常.

那如何抛出异常呢?请看下面.

4.2 异常的抛出

在编写程序时,如果程序中出现了错误,那么就需要将错误的信息告诉调用者.

在java中,可以通过throw关键字.抛出一个指定的异常对象,将错误信息告诉调用者.

语法规则是这样的:

throw new XXXXException(“异常产生的原因”);

例:获取数组任意位置元素

public static int getElement(int[] array,int index){
        if(array==null){
            throw new NullPointerException("传递的数组为null");
            //抛出的是一个指定的异常,经常用的方式是,抛出自定义异常
        }
        return array[index];
    }

🧐一些注意事项:

(1).throw必须写在方法体内部.

(2).抛出的对象必须是Exception或者Exception的子类对象

(3).如果抛出的是不受查异常(RunTimeException或者RunTimeException的子类),那么可以不用处理,直接交给JVM来处理

(4).如果抛出的是编译时异常,那么用户必须处理,否则无法通过编译

(5).异常一旦抛出,后面的代码就不会执行.

第五个注意点的例子:

在这里插入图片描述

结果就是:在没有对异常进行处理的情况下,抛出了异常,并且异常后面的内容不会被打印出来.(也就是异常抛出之后的代码都不会执行)

😎补充:

当我们没有解决这个异常的时候,这个异常就会被交给JVM来处理,一旦交给JVM来处理,程序就崩溃了.

5.异常的捕获

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

5.1 异常声明throws

也就是,在方法声明时参数列表之后,当方法中抛出编译时异常,用户不想处理该异常,就可以借助throws将异常抛给方法的调用者来处理,换句话说就是:当前方法不处理异常,提醒方法的调用者处理异常.

语法格式:

方法名(参数列表) throws 异常类型1,异常类型2…{
}

例:

在这里插入图片描述

还需要注意的一点:

调用声明抛出异常的方法时,调用者必须对异常进行处理,或者继续使用throws抛出.

在这里插入图片描述

😏如果方法内部抛出了多个异常,那么throws之后必须跟多个异常类型,用逗号分隔.

例:

在这里插入图片描述

😏如果抛出多个异常,并且有父子关系,那么直接声明父类.

例:

在这里插入图片描述

😁注意事项:

(1)throws必须跟在声明的方法的参数列表之后

(2)声明的异常必须是Exception或者Exception的子类

(3)方法内部如果抛出了多个异常,throws之后必须跟多个异常类型,之间用逗号隔开,如果抛出的多个异常类型之间有父子关系,那么直接声明父类即可.

5.2 try-catch捕获异常并处理

当程序抛出异常时,可以通过try-catch捕获并处理异常.

语法格式:

try{
//可能出现异常的代码,try中的代码可能会抛出异常,也可能不会抛出异常
}catch(要捕获的异常类型 e){
//如果try中的代码抛出异常了,此处catch捕获到的异常类型与try中抛出的异常类型一样时,或者是try中抛出的异常的父类时,就会被捕获到
//对异常就可以正常处理,处理完成后,跳出try-catch结构,继续执行之后的代码
}catch(异常的类型 e){
//对异常进行处理
}finally{
//此处的代码一定会被执行到
}
//之后的代码
//当异常被捕获到时,异常就被处理了,之后的代码一定会执行
//如果被捕获了,由于捕获的类型与抛出的异常的类型不匹配,还是会交给JVM来处理,那就没有捕获到,之后的代码也不会执行,相当于没有对抛出的异常进行正确的处理

例:

public static void main(String[] args) {
        try{
            int[] array=null;
            System.out.println(array.length);
        }catch (NullPointerException e){
            System.out.println("捕获到一个空指针异常");
        }
        System.out.println("其他程序");
    }

结果:

在这里插入图片描述

例:

public static void main(String[] args) {
        try{
            int[] array=null;
            System.out.println(array.length);
        }catch(ArithmeticException e){
            System.out.println("捕获到空指针异常");
        }
        System.out.println("其他代码");
        }

try-catch捕获到的不是抛出来的对应的异常,那就相当于没有对异常进行处理,那就会交给JVM来处理,后面的代码不会执行到.因为异常是按照类型来捕获的

结果:

在这里插入图片描述

😜如果try抛出了多个异常,就必须用多个catch来进行捕获

但是需要注意:用多个catch进行捕获,不是同时进行捕获的,因为不可能同时抛出不同的异常.

不会同时抛出两个或两个以上的异常,在同一时间,只会抛出一个异常.

例:

public static void main(String[] args) {
        try{
            int[] array=null;
            System.out.println(array.length);
        }catch(ArithmeticException e){
            System.out.println("捕获到算术异常");
        }catch(NullPointerException e){
            System.out.println("捕获到空指针异常");
        }
        System.out.println("其他代码");
    }

也可以简写成:

public static void main(String[] args) {
        try{
            int[] array=null;
            System.out.println(array.length);
        }catch(ArithmeticException | NullPointerException e){
            System.out.println("捕获到算术异常或者空指针异常");
        }
        System.out.println("其他代码");
    }

😁当try中存在多个异常时,从上往下执行,谁先抛出异常就捕获哪个异常.
换句话说也就是:try块内抛出异常位置之后的代码将不会执行.

catch的顺序不是捕获的顺序

![在这里插入图片描述](https://img-blog.csdnimg.cn/146cf73ce5e64a8b82fdba34ac777e7a.png在这里插入图片描述

😮还有需要注意的一点是:

如果抛出的异常之间具有父子关系,那在catch时,子类异常的catch必须写在前面,父类异常的catch必须写在后面.

例:

public static void main(String[] args) {
        try{
            int[] array=null;
            System.out.println(array.length);
        }catch(NullPointerException e){
            System.out.println("捕获到空指针异常");
        }catch(Exception e){//可以把父类异常放在最后
            System.out.println("捕获到一个异常");
        }
        System.out.println("其他代码");
    }

结果:

在这里插入图片描述

来看这张图片:

在这里插入图片描述
这样的写法就是把父类异常的catch写在了子类异常的catch的前面,这样写代码编译就会报错.

原因是:Exception是所有异常的父类,它能捕获所有的异常,如果把他放到catch的第一个,后面的异常永远都不会捕获到,那么后续的catch没有任何作用了.

注意:

catch进行类型匹配的时候,不光会匹配相同类型的异常对象,也会捕获目标异常类型的子类对象.比如NullPointerException和ArrayIndexOutOfBoundsException都是Exception的子类,因此都能被捕获到.

5.3 finally

在写程序时,有些代码,不论程序是否发生异常,都需要执行,比如程序中打开的资源:网络连接,数据库连接,IO流,在程序正常或者异常退出时,都必须要对资源进行回收.而且,因为异常会引发程序的跳转,可能导致有些语句执行不到,finally就是用来解决这个问题的.

语法格式:

try{
//可能发生异常的代码
}catch(异常类型 e){
//对捕获到的异常进行处理
}finally{
//此处的语句无论是否发生异常,都会被执行到
}
//如果没有抛出异常,或者异常被捕获处理了,这里的代码也会执行.

😜例:

public static void main(String[] args) {
        Scanner sc=new Scanner(System.in);
        try{
            int[] array=null;
            System.out.println(array.length);
        }catch(NullPointerException e){
            e.printStackTrace();
            System.out.println("捕获到空指针异常");
        }catch (ArithmeticException e){
            e.printStackTrace();
            System.out.println("捕获到算术异常");
        }finally {
            sc.close();
            System.out.println("资源关闭");
        }
        System.out.println("其他代码");
    }

结果:

在这里插入图片描述

以下的代码是没有抛出异常的情况:

public static void main(String[] args) {
        Scanner sc=new Scanner(System.in);
        try{
            int[] array= {1,2,3,4};
            System.out.println(array.length);
        }catch(NullPointerException e){
            e.printStackTrace();
            System.out.println("捕获到空指针异常");
        }catch (ArithmeticException e){
            e.printStackTrace();
            System.out.println("捕获到算术异常");
        }finally {
            sc.close();
            System.out.println("资源关闭");
        }
        System.out.println("其他代码");
    }

结果:

在这里插入图片描述

我们发现:不管是否抛出异常,finally中的代码都会执行.

😎补充一点:

资源写到try之后,finally中就不用手动close了

在这里插入图片描述

来看一段特殊的代码:

在这里插入图片描述
结果是20.如果finally中也存在return,那么就会执行finally中的return,从而不会执行到try中原有的return.

但是,一般不建议在finally中写return

🤓面试题:

1.throw和throws的区别?
throw用来抛出异常,throws用来声明异常

2.finally中的语句一定会执行吗?
finally中的语句一定会执行

6.异常的处理流程

😎关于调用栈:

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

如果本方法中没有合适的处理异常的方式,就会沿着调用栈(先进后出)向上传递.如果向上一直传递都没有处理异常,最终就会交给JVM来处理,程序就会崩溃.

例:

public static void func(){
        int[] array={1,2,3,4,5};
        System.out.println(array[99]);
    }

    public static void main(String[] args) {
        try{
            func();
        }catch (ArrayIndexOutOfBoundsException e){
            e.printStackTrace();
        }
}

结果:

在这里插入图片描述

😶异常处理的流程:

1.程序先执行try中的代码
2.如果try中的代码抛出异常,就会结束try中的代码,然后和catch中的异常类型进行匹配
3.如果找到匹配的异常类型,就会执行catch中的代码
4.如果没有找到匹配的异常类型,就会将异常向上传递到上层调用者
5.无论是否找到匹配的异常类型,finally中的代码都会被执行
6.一直到main方法也没有代码去处理异常,就会把异常将给JVM来处理,这样程序就异常终止了.(崩溃了)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值