12.Java的异常


一、异常的概念

1.概念:程序过程中发生的不正常的行为

程序异常就是相当于报病,就要去治疗它。

之前曾遇到过的常见的异常

1.算术异常

10÷0错误,0不能作为除数

2.数组下标越界异常

数组只有4个元素,你要打印最后一个元素arr[3],意外写成arr[4],就越界了

3.空指针异常

数组为空,却要求数组的长度

2.体系结构

Throwable是异常体系的祖宗,代表能 抛出的东西。他的子类就是Error和Exception。

分别是虚拟机的错误和异常。(GitMind思乎这个做思维导图还挺好用的,可以免费让AI生成,但是只有几次机会后面就要钱了

3、异常的分类

按处理方式分:

①(编译时异常)检查异常Checked Exception:

是程序运行中可能遇到的外部问题,即是RuntimeException以外的异常,必须显式处理,若不处理程序就会不通过,编译失败,如图上IOException、ClassNotFoundException等等

②(运行时异常)非受检查异常Unchecked Exception:

本质是代码编写错误导致运行时问题(如算术、数组下标越界、空指针异常),继承自RuntimeException,不强制处理

之前的第一篇文章可以看见一个Java文件是先编译在运行,所以也可以知道他的异常两个板块也就是编译时异常和运行时异常。(是我的图就是改了个名字)

编译器:只判断编译时异常

运行时:同时判断运行时异常和虚拟机错误

运行时异常上面已经举了三个例子了

接下来是编译时异常 ,我们以IOException输入输出异常为例

java 复制代码
public class IOExceptionDemo {
    //演示IOException(受检异常/编译时异常)
    public static void main(String[] args) {
        //以其中FileNotFoundException文件未找到为例
        FileInputStream fis = new FileInputStream("不存在的文件.txt");//读文件用的
    }
}

编译报错的原因:

→受检异常(编译时异常)

→不捕获/不抛出,直接编译不通过

(必须try-catch或throws,下一个板块讲,否则编译失败)

再拿CloneNotSupportedException举例

java 复制代码
//Student实现接口Cloneable
class Student implements Cloneable{
    private String name;
    public Student(String name) {
        this.name = name;
    }
    //这里会编译报错:CloneNotSupportedException
    //不支持克隆异常
    @Override
    protected Object clone(){
        return super.clone();
    }
}
public class CloneDemo {
    public static void main(String[] args) {
        Student student1 = new Student("小马");
    }

}

编译报错的原因:

→受检异常(编译时异常)

→不捕获/不抛出,直接编译不通过

(必须try-catch或throws,下一个板块讲,否则编译失败)


三、如何处理异常

1.防御式编程

①.事前防御型(LBYL)

**事前防御型:**Look Before You Leap在操作前充分检查

频繁用到 return ,直接结束后面的

缺陷:正常流程和错误流程代码混在一起,代码整体比较混乱

我们以前面的 算术异常 为例:

事前就要考虑除数的情况,遇到0 直接就结束return

java 复制代码
public class Demo4 {

    public static void main(String[] args) {
        //事前防御
        //1.算数异常
        int a = 0, b = 0;
        Scanner scanner = new Scanner(System.in);
        a = scanner.nextInt();//输入10
        b = scanner.nextInt();//输出0
        //事前考虑0为除数情况
        if (b == 0) {
            return;
        } else {
            System.out.println(a / b);
        }
    }
}

②事后认错型(EAFP)

异常处理的核心=事后认错型

事后认错型:"事后获取原谅比事前获取许可更容易",

也就是先操作,遇到问题再处理

用到try-catch

try:我先大胆的干这件事

catch:万一出事,我在这里认错、处理
try {

//可能会出现的错误代码

} catch (要抓的异常 变量) {

//出错了就跑这里

//处理错误,程序不会崩

}

我们依然以算术异常为例

java 复制代码
public class Demo5 {
    //事后防御
    //先大胆的干,错了在处理
    public static void main(String[] args) {
        int a, b = 0;
        Scanner scanner = new Scanner(System.in);
        a = scanner.nextInt();//10
        b = scanner.nextInt();//0
        try {
            System.out.println(a / b);
        }catch (ArithmeticException arithmeticException) {
            System.out.println("除数不能为0");//除数不能为0
        }
    }
}

执行结果:

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

2.异常的抛出

什么叫抛出异常:

代码发现错了→自己不处理→给别人处理

1.throw(手动抛)

throw + 一个异常对象

throw new XXXException("异常产生的原因");

注意事项:

①throw必须写在方法体内部

②抛出对象必须是Exception或者Exception的子类对象

③如果抛出运行时异常RunTimeException或其子类,则可以不用处理,直接交给JVM来处理

④如果抛出是编译时异常,用户必须处理,否则编译无法通过

⑤异常一旦抛出,其后代码就不会执行

①运行时异常

用数组是否越界举例

java 复制代码
public class Demo6 {
    public static void main(String[] args) {
        //运行时异常
        int[] arr = {1,2,3,4};
        Scanner scanner = new Scanner(System.in);
        System.out.println("选择你要打印的数组下标:");
        int index = scanner.nextInt();
        //throw
        System.out.println(isIndexOutBounds(arr,index));
    }

    //定义一个数组是否越界的方法
    public static int isIndexOutBounds(int[] arr,int index) {
        //数组是否未传递,空指针异常
        if(arr == null) {
            throw new NullPointerException("传递数组为null");
        }
        //下标是否越界
        if (index < 0 || index >= arr.length) {
            throw new ArrayIndexOutOfBoundsException("数组下标越界" + index);
        }else {
            return index;
        }
    }
}

当我输入大于数组下标的元素时,会抛出 我给定的异常(自己指定的异常),后面内容不再执行直接结束。

当查找数组索引合法时,执行不抛出异常

②编译时异常

我们前面举例编译时异常 的两个IOException和CloneNotSupportedException不能直接通过throw解决,因为他是编译时异常,用户必须处理,需要配合 throws,否则无法通过编译

①编译时异常throws配合处理

处理之后就可以通过编译,抛出异常了

java 复制代码
public class IOExceptionDemo {
    //演示IOException(受检异常/编译时异常)
    public static void main(String[] args) throws FileNotFoundException{
        //以其中FileNotFoundException文件未找到为例
        //FileInputStream读文件用的
        FileInputStream fis = new FileInputStream("不存在的文件.txt") ;
        throw new FileNotFoundException("文件未找到");
    }
}
②编译时异常try-catch配合处理依旧必须处理方法throws
java 复制代码
public class IOExceptionDemo {
    //演示IOException(受检异常/编译时异常)
    public static void main(String[] args){
        //编译时throw抛出异常,用try-catch处理
        try {
            checkFileExists();
        }catch (FileNotFoundException fileNotFoundException) {
            System.out.println("文件没找到");//文件没找到
        }
    }

    //方法内部手动 throw 抛出编译时异常,需要方法上声明throws
    public static void checkFileExists() throws FileNotFoundException {
        //模拟文件不存在场景
        FileInputStream fis = new FileInputStream("不存在文件.txt");
        throw new FileNotFoundException("文件未找到:" + fis);
    }
}

2.throws(方法声明抛)

throws写在方法名的后面

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

}

用前面报错的CloneNotSupportedException来看

克隆方法后面throws抛出异常,即可编译成功,不会报错

注意事项:

①throws必须跟在方法参数列表之后

②声明异常必须是Exception或其子类

③方法中若是抛出多个异常,throws之后必须跟多个异常类型,之间用逗号隔开,如果抛出多个异常类型具有父子关系,直接声明父类即可。

java 复制代码
import java.io.IOException;

public class Demo7 {
    //抛出多个无继承异常,同时抛出FileNotFoundException, IOException
    //两者没有继承关系
    public static void doSomething() throws ArithmeticException, IOException {
        //模拟实现两种不同类型的异常
        if (Math.random() > 0.5) {
            throw new IOException("网络连接中断,IO操作失败");
        } else {
            //算数异常,0不能是被除数
            throw new ArithmeticException("除数不能为0");
        }
    }

    //异常是父子关系,直接声明父类
    public static void test() throws RuntimeException{
        //算术异常ArithmeticException继承于运行时异常RunTimeException
        throw new ArithmeticException("除数不能为0");
    }

    public static void main(String[] args) throws IOException{
        doSomething();
        test();
    }
}

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

将光标放在抛出异常方法上,alt+enter 快速处理。

关于这个抛出异常,可以直接通过报错,直接一键添加。

3.异常的捕获

异常的捕获 = 抓住异常,

1.try - catch 捕获并处理

throws对异常并未真正处理,而是将异常报告给抛出异常方法的调用者,由调用者处理。

真正对异常处理,就需要try-catch

throw / throws 对比 try- catch

try- catch:自己抓住,自己处理→ 事后认错

throw / throws:自己不处理,扔给别人→ 甩锅

语法格式:

try {

//将可能出现的异常的代码放在这里

} catch (要捕获的异常类型 e) {

//如果try中代码抛出异常,此处catch捕获异常类型与try中异常类型一致时,或者try中抛出了异常的基类,就会被捕获

//对异常就可以正常处理,处理完成之后,就跳出try-catch 结构,继续处理后续代码

}【catch (异常类型 e) {

//对异常进行处理

}finally {

//此处代码一定会被执行到

}

//后续代码

//当一场被捕获,异常就被处理,后续代码一定会执行

//如果捕获了,由于捕获时类型不对,那就没有捕获到,这里的代码不会被执行

(①【】中表示可选项,可以添加,也可以不添加

② try 中代码可能抛出代码,也可能不会,

看 try代码中产生的异常类型 是否有 与之匹配的catch代码 )

try中抛出单个异常(异常的处理方式)
java 复制代码
public class Demo9 {
    public static void main(String[] args) {
        //try-catch的方法注意事项    
        try {
            String[] strings ={"abc","lll","g","o"};
            //数组越界异常
            System.out.println(strings[strings.length]);
        }catch (RuntimeException r) {
            //ArrayIndexOutOfBoundsException数组异常
            // 是 RunTimeException运行时异常的子类
            //异常的处理方式
//            System.out.println(r.getMessage());//只打印异常信息
            //输出:Index 4 out of bounds for length 4
            System.out.println(r);//只打印异常类型:异常信息
            //输出:java.lang.ArrayIndexOutOfBoundsException: Index 4 out of bounds for length 4
            r.printStackTrace();//打印具体的异常
            //输出:抛出异常
        }
    }
}
try中抛出多个不同异常对象,必须用多个catch来捕获,→ 多种异常,多次捕获
java 复制代码
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

/**
 * @ClassName Demo8
 * @Description TODO:异常的捕获
 * @Author yangyutong
 * Version 1.0
 */
public class Demo8 {
    //方法内部手动 throw 抛出编译时异常,需要方法上声明throws
    public static void checkFileExists() throws IOException {
        //模拟文件不存在场景
        FileInputStream fis = new FileInputStream("不存在文件.txt");
        throw new FileNotFoundException("文件未找到:" + fis);
    }

    //定义一个数组是否越界的方法
    public static int isIndexOutBounds(int[] arr,int index) {
        //数组是否未传递,空指针异常
        if(arr == null) {
            throw new NullPointerException("传递数组为null");
        }
        //下标是否越界
        if (index < 0 || index >= arr.length) {
            throw new ArrayIndexOutOfBoundsException("数组下标越界" + index);
        }else {
            //下标合法时返回下标值
            return index;
        }
    }

    /**
     * try-catch 异常捕获并处理
     * @param args 命令行参数
     */
    public static void main(String[] args) {
        //捕获异常并处理
        try {
            String s = null;
            //空指针异常,运行到这一行,直接执行第二个catch,后面代码不执行
            s.length();
            //调用可能抛出编译时异常的方法
            checkFileExists();
            System.out.println("执行到这个地方,完成checkFileExists方法的调用");
        } catch (IOException e) {
            System.out.println("FileNotFoundException文件未找到异常");
            //打印异常的详细调用栈信息,便于定位问题
            e.printStackTrace();
        } catch (RuntimeException r) {
            System.out.println("空指针异常");//输出:空指针异常
            //打印异常的详细调用栈信息,便于定位问题
            r.printStackTrace();
        }
    }
}

输出结果:

多个异常处理方式完全相同,可以这样写:

catch (ArrayIndexOutOfBoundsException | NullPointerException e) {

...

}

或者直接继承他们的父类:

catch (RunTimeException r) {

...

}
注意事项:

①try块内抛出异常位置,之后的代码不会被执行

②如果抛出异常类型与catch异常类型不匹配,即异常不会被成功捕获,也不会被处理,继续往外抛,直到JVM虚拟机收到后,中断程序 (异常都是按照类型捕获的)

java 复制代码
public class Demo9 {
    // try中异常 与 catch 不匹配
    public static void main(String[] args) {
        try {
            String[] strings ={"abc","lll","g","o"};
            //数组越界异常
            System.out.println(strings[strings.length]);
        }catch (IOException i) {
            //try里的异常与catch 没有相符的
            System.out.println("输入输出异常");
        }
    }
}

try和catch异常没有匹配,直接报错

2.finally

在特定的代码中,不论程序是否发生异常,都需要执行,用来解决程序某些语句执行不到的情况

比如程序中打开的资源:网络连接、数据库连接、IO流等

在程序正常或者异常退出时,必须要对资源进行回收。另外,因为异常会引发程序的跳转,可能导致有些语句执行不到,finally就是用来解决这个问题的。

比如:

java 复制代码
public class Demo10 {
    public static void main(String[] args) {
        try {
            //此处代码执行过程中,可能会出现异常、return等情况,
            // 导致后续的资源释放无法直接执行
            //这也是为啥需要finally块来兜底执行资源释放逻辑原因

            //定义两个测试用的整型变量
            int a = 10;
            int b = 20;

            //当 a<b ,空指针异常
            //这里的异常会导致try块后续代码直接终止执行
            if (a < b) {
                throw new NullPointerException("a 小于 b");
            }
            //若前面异常,后面的代码将无法执行
            release();
            return;

        } catch (IndexOutOfBoundsException e) {
            e.printStackTrace();
        } finally {
            //必须执行
            //无论是否抛出异常、是否执行return
            // 也无论catch块是否捕获到异常
            //finally必执行,适合放置资源释放逻辑
            release();
        }
    }

    // 释放资源....
    private static void release() {
        System.out.println("release 释放资源");
    }
}

语法格式(try - catch - finally)或 try- finally:

try{

//可能会发生异常的代码

}catch (异常类型 e) {

//对捕获异常进行处理

}finally {

//此处语句不论是否发生异常,都会被执行到

}

//没有抛出异常,或者异常已经被捕获处理,这里的代码也会执行

不管程序是否抛出异常,finally都会执行

无异常情况

java 复制代码
public class Demo11 {
    //初始化资源
    private static String resource = "初始化资源";
    public static void main(String[] args) {
        //场景一:try块中无异常,正常执行
        try {
            System.out.println("try块执行:无异常,处理业务逻辑");
            resource = "初始化后的资源";
        } finally {
           //无论try块是否正常,都需要执行finally:释放资源
           releaseResource ();
        }
    }

    //释放资源
    private static void releaseResource() {
        System.out.println("执行资源释放:" + resource + "已释放");
        resource = null;//释放资源
    }
}
即使有return,finally 也会执行
java 复制代码
public class Demo12 {
    //不论是否有return,finally必执行
    public static int func() {
        int a = 10;
        Scanner scanner = new Scanner(System.in);
        int b = scanner.nextInt();
        // a÷b 是否算数异常
        try {
            //除数不能为0
            if (b == 0) {
                throw new ArithmeticException("算数异常!");
            }
            return b;
        } catch (ArithmeticException e) {
            System.out.println("算数异常");
        } finally {
            //关闭输入
            scanner.close();
            return 20;
        }
    }
    public static void main(String[] args) {
        System.out.println(func());//20
    }
}

执行结果:

总结(trhow、throws、try-catch、finally的区别):

关键字 核心作用 位置
throw 手动抛出一个异常对象 方法体内
throws 声明方法可能抛出异常类型 方法声明上
try - catch 捕获并处理异常 方法体内
finally 任何情况必执行的代码块 在 try / catch后

4.异常的处理流程

程序先执行 try 中的代码

如果 try 中的代码出现异常, 就会结束 try 中的代码, 看和 catch 中的异常类型是否匹配.如果找到匹配的异常类型, 就会执行 catch 中的代码如果没有找到匹配的异常类型, 就会将异常向上传递到上层调用者.

无论是否找到匹配的异常类型, finally 中的代码都会被执行到(在该方法结束之前执行).如果上层调用者也没有处理的了异常, 就继续向上传递.

一直到 main 方法也没有合适的代码处理异常, 就会交给 JVM 来进行处理, 此时程序就会异常终止


四、自定义异常

java异常分类很多,但是实际中我们的代码题和企业开发所遇到的是特有情况,不能完全表示,所以就需要自定义异常类,也比较的方便

比如用户登录功能

就可以用自定义异常来实现

我们需要先定义一个登录界面

用户名的异常类和密码的异常类

在执行登录界面

登录界面:

java 复制代码
//用户信息
public class Login {
    private String username = "admin";
    private String password = "123123.";

    //判断是否有误
    public void login (String username, String password) throws UsernameNotFoundException, PasswordErrorException{
        //用户名是否存在
        if (!username.equals(this.username)) {
//            System.out.println("用户名不存在");
//            return;
            throw new UsernameNotFoundException("用户名不存在");
        }
        //密码是否正确
        if (!password.equals(this.password)) {
            throw new UsernameNotFoundException("密码错误");
        }

        System.out.println("登录成功");
    }
}

登录异常类:

java 复制代码
//登录异常父类
public class LoginException extends Exception{
    public LoginException(String message) {
        //异常提示信息
        //向用户和开发者说明登录失败的具体原因
        super(message);
    }
}

用户名不存在异常:

java 复制代码
//用户名不存在异常类
public class UsernameNotFoundException extends LoginException{

    public UsernameNotFoundException(String message) {
        super("用户名不存在");
    }
}

密码错误异常:

java 复制代码
//密码错误异常类
public class PasswordErrorException extends LoginException{
    public PasswordErrorException(String message) {
        super("密码错误");
    }
}

测试登录界面:

java 复制代码
public class Test {
    public static void main(String[] args) {
        //捕获并处理异常
        try {
            Login userA = new Login();
            userA.login("yyy", "123890");
            System.out.println("登陆成功");
        } catch (UsernameNotFoundException u) {
            System.out.println("用户名错误");
        } catch (PasswordErrorException e) {
            System.out.println("密码错误");
        }finally {
            System.out.println("登录操作结束");
        }
    }
}

注意事项:

自定义异常通常继承自Exception或者RunTimeException

继承Exception的默认是编译时异常

继承自RunTimeException的默认是运行时异常


相关推荐
-To be number.wan1 小时前
Python数据分析:时间序列数据分析
开发语言·python·数据分析
装不满的克莱因瓶1 小时前
Java7新特性:try-with-resources写法
java·前端·javascript·jdk·新特性·jdk7
前路不黑暗@2 小时前
Java项目:Java脚手架项目的通用组件的封装(六)
java·开发语言·spring
马士兵教育2 小时前
程序员简历如何编写才能凸显出差异化,才能拿到更多面试机会?
开发语言·后端·面试·职场和发展·架构
jz_ddk3 小时前
[指南] Python循环语句完全指南
开发语言·python·continue·循环·for·while·break
chilavert3183 小时前
技术演进中的开发沉思-368:锁机制(中)
java·开发语言·jvm
~央千澈~3 小时前
抖音弹幕游戏开发之第12集:添加冷却时间机制·优雅草云桧·卓伊凡
java·服务器·前端
大黄说说3 小时前
MySQL数据库运维管理基础知识:从安装到日常维护的完整指南
开发语言
HAPPY酷3 小时前
C++ 多线程实战三板斧
java·开发语言·c++·技术美术