欢迎光临
我们一直在努力

初识java(十六):异常

目录

一:异常的概念与体系结构

1.异常的概念

2.异常的体系结构

3.异常的分类

1.编译时异常

2.运⾏时异常

二:异常的处理

1.防御式编程

2.异常的抛出-throw

3.异常的声明-throws

4.异常的捕获-try-catch捕获并处理异常

5.finally

6.异常的处理流程

三:⾃定义异常类

1.实现⾃定义异常


内容大纲:

本文全面介绍了Java异常处理机制,主要内容包括:

  • 异常概念与体系结构:异常是程序执行中的特殊情况,Java通过Throwable类及其子类Error和Exception构建了异常体系,分为编译时异常(受检异常)和运行时异常(非受检异常)。
  • 异常处理方式:
    • 防御式编程的两种模式(LBYL事前防御和EAFP事后处理)
    • 5个核心关键字:throw(抛出异常)、throws(声明异常)、try-catch(捕获处理)、finally(资源清理)
  • 异常处理流程:异常会沿调用栈向上传递,直到被捕获或由JVM处理。
  • 自定义异常:通过继承Exception或RuntimeException创建业务相关异常类。
  • 文章通过代码示例详细演示了异常处理的实际应用,包括常见异常类型、异常捕获的优先级、finally块的作用等关键知识点,并比较了不同异常处理方式的优缺点。最后通过登录验证案例展示了自定义异常的实现方法。

    一:异常的概念与体系结构

    1.异常的概念

    异常是在程序执⾏过程中发⽣的⼀种特殊情况或错误状态。它打断了程序的正常流程,需要特别处 理。

    在⽇常开发中,程序员绞尽脑汁将代码写的尽善尽美,在程序运⾏过程中,难免会出现⼀些奇奇怪怪 的问题。有时通过代码很难去控制,⽐如:数据格式不对、⽹络不通畅、内存报警等。

    在Java中,将程序执⾏过程中发⽣的不正常⾏为称为异常,⽐如之前写代码时经常遇到的:

    1.算术异常

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

    输出:

    2.数组越界异常

    public static void main(String[] args) {
    int[] arry={1,2,3};
    System.out.println(arry[12]);
    }

    输出:

    3.空指针异常

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

    输出:

    从上述过程中可以看到,java中不同类型的异常,都有与其对应的类来进⾏描述

    2.异常的体系结构

    异常种类繁多,为了对不同异常或者错误进⾏很好的分类管理,Java内部维护了⼀个异常的体系结 构:

    从上图中可以看到:

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

    2. Error:指的是Java虚拟机⽆法解决的严重问题,⽐如:JVM的内部错误、资源耗尽等,典型代 表:StackOverflowError和OutOfMemoryError,⼀旦发⽣回⼒乏术

    3. Exception:异常产⽣后程序员可以通过代码进⾏处理,使程序继续执⾏。⽐如:感冒、发烧。我们平时所说的异常就是Exception。

    3.异常的分类

    异常可能在编译时发⽣,也可能在程序运⾏时发⽣,根据发⽣的时机不同,可以将异常分为:

    1.编译时异常

    在程序编译期间发⽣的异常,称为编译时异常,也称为受检查异常(CheckedException)

    class Person{
    private String name;
    private String gender;
    int age;

    @Override
    protected Object clone() {
    return (Person)super.clone();
    }
    }

    输出:

    2.运⾏时异常

    在程序执⾏期间发⽣的异常,称为运⾏时异常,也称为⾮受检查异常(UncheckedException)

    RunTimeException以及其⼦类对应的异常,都称为运⾏时异常。⽐如:NullPointerException、 ArrayIndexOutOfBoundsException、ArithmeticException。

    注意:编译时出现的语法性错误,不能称之为异常。例如将System.out.println拼写错了,写成了 system.out.println. 此时编译过程中就会出错,这是"编译期"出错。⽽运⾏时指的是程序已经编译通 过得到class⽂件了,再由JVM执⾏过程中出现的错误.

    二:异常的处理

    1.防御式编程

    错误在代码中是客观存在的.因此我们要让程序出现问题的时候及时通知程序猿.主要的⽅式

    LBYL:LookBeforeYouLeap.在操作之前就做充分的检查. 即:事前防御型

    boolean ret = false;

    // 1. 尝试登陆游戏
    ret = loginGame();
    if (!ret) {
    handleLoginError();
    return;
    }

    // 2. 开始匹配
    ret = startMatchmaking();
    if (!ret) {
    handleMatchmakingError();
    return;
    }

    // 3. 游戏确认
    ret = confirmGame();
    if (!ret) {
    handleConfirmationError();
    return;
    }

    // 4. 选择英雄
    ret = selectHero();
    if (!ret) {
    handleHeroSelectionError();
    return;
    }

    // 5. 载入游戏画面
    ret = loadGameScene();
    if (!ret) {
    handleLoadError();
    return;
    }

    // …… 后续逻辑

    缺陷:正常流程和错误处理流程代码混在⼀起,代码整体显的⽐较混乱。

    EAFP:It's Easier to Ask Forgiveness than Permission. "事后获取原谅⽐事前获取许可更容易".也 就是先操作,遇到问题再处理.即:事后认错型

    try {
    // 尝试执行一系列可能失败的操作
    loginGame(); // 登陆游戏
    startMatchmaking(); // 开始匹配
    confirmGame(); // 游戏确认
    selectHero(); // 选择英雄
    loadGameScene(); // 载入游戏画面
    // … 后续流程

    } catch (LoginException e) {
    // 捕获并处理登陆阶段的异常
    handleLoginError(e);

    } catch (MatchmakingException e) {
    // 捕获并处理匹配阶段的异常
    handleMatchmakingError(e);

    } catch (ConfirmationException e) {
    // 捕获并处理确认阶段的异常
    handleConfirmationError(e);

    } catch (HeroSelectionException e) {
    // 捕获并处理选英雄阶段的异常
    handleHeroSelectionError(e);

    } catch (LoadSceneException e) {
    // 捕获并处理加载阶段的异常
    handleLoadSceneError(e);

    }
    // ……

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

    异常处理的核⼼思想就是EAFP。

    在Java中,异常处理主要的5个关键字:throw、try、catch、finally、throws。

    2.异常的抛出-throw

    在编写程序时,如果程序中出现错误,此时就需要将错误的信息告知给调⽤者,⽐如:参数检测。

    在Java中,可以借助throw关键字,抛出⼀个指定的异常对象,将错误信息告知给调⽤者。具体语法 如下:

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

    【需求】:实现⼀个获取数组中任意位置元素的⽅法。

    class ExceptionDemo {

    /**
    * 获取数组元素的方法,包含防御性编程检查
    */
    public static int getElement(int[] array, int index) {
    // 1. 检查数组是否为空
    if (null == array) {
    throw new NullPointerException("传递的数组为null");
    }

    // 2. 检查下标是否越界
    if (index < 0 || index >= array.length) {
    throw new ArrayIndexOutOfBoundsException("传递的数组下标越界");
    }

    // 3. 正常返回元素
    return array[index];
    }

    public static void main(String[] args) {
    int[] array = {1, 2, 3};

    // 调用方法,传入下标 3
    // 注意:数组长度为3,有效下标是 0, 1, 2。
    // 传入 3 会导致 index >= array.length 成立,从而抛出异常
    getElement(array, 3);
    }
    }

    输出:

    【注意事项】

    1. throw必须写在⽅法体内部

    2. 抛出的对象必须是Exception或者Exception的⼦类对象

    3. 如果抛出的是RunTimeException或者RunTimeException的⼦类,则可以不⽤处理,直接交给 JVM来处理

    4. 如果抛出的是编译时异常,⽤⼾必须处理,否则⽆法通过编译

    5. 异常⼀旦抛出,其后的代码就不会执⾏

    3.异常的声明-throws

    throws 关键字⽤于在⽅法声明中列出该⽅法可能抛出的异常,它告诉调⽤者这个⽅法可能会抛出某 些异常,调⽤者需要处理这些异常。使⽤ throws 实际上是将异常的处理责任转移给了调⽤该⽅法 的代码

    语法格式:

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

    }

    【需求】:加载指定的配置⽂件config.ini

    import java.io.File;
    import java.io.FileNotFoundException;

    public class Config {

    File file;

    /**
    * 打开配置文件的方法
    *
    * FileNotFoundException 是编译时异常(受检异常)。
    * 此处方法内部没有能力处理文件不存在的问题,因此通过 throws 关键字
    * 将异常抛给调用者,让调用者去检查文件名是否给错了。
    */
    public void OpenConfig(String filename) throws FileNotFoundException {
    // 模拟逻辑:如果文件名是 "config.ini",则人为抛出一个文件未找到的异常
    if (filename.equals("config.ini")) {
    throw new FileNotFoundException("配置文件名字不对");
    }

    // 打开文件的其他逻辑…
    }

    public void readConfig() {
    // 读取配置的逻辑…
    }
    }

    【注意事项】

    1. throws必须跟在⽅法的参数列表之后

    2. 声明的异常必须是Exception或者Exception的⼦类

    3. ⽅法内部如果抛出了多个异常,throws之后必须跟多个异常类型,之间⽤逗号隔开,如果抛出多个 异常类型具有⽗⼦关系,直接声明⽗类即可。

    4. 调⽤声明抛出异常的⽅法时,如果该异常是编译时异常/受查异常时,调⽤者必须对该异常进⾏处 理,或者继续使⽤throws抛出

    4.异常的捕获-try-catch捕获并处理异常

    throws对异常并没有真正处理,⽽是将异常报告给抛出异常⽅法的调⽤者,由调⽤者处理。如果真正 要对异常进⾏处理,就需要try-catch。

    public class ExceptionSyntaxDemo {

    public void executeLogic() {
    try {
    // 【可能出现异常的代码】
    // 这里放置可能会抛出异常的逻辑(例如文件读取、网络请求、数组访问等)
    System.out.println("尝试执行可能出错的代码…");

    } catch (SpecificException e) {
    // 【捕获特定异常】
    // 如果 try 中抛出的异常类型与这里的 SpecificException 一致(或是其子类),则进入此块
    System.out.println("捕获到特定异常:" + e.getMessage());

    } catch (Exception e) {
    // 【捕获通用异常 – 可选】
    // 如果上面的 catch 没匹配到,但异常是 Exception 的子类,则进入此块
    // 注意:多个 catch 块时,父类异常必须放在子类异常后面
    System.out.println("捕获到其他异常:" + e.getMessage());

    } finally {
    // 【最终执行块 – 可选】
    // 无论是否发生异常,也无论异常是否被捕获,这里的代码一定会被执行
    // 通常用于释放资源(如关闭文件流、数据库连接)
    System.out.println("资源清理或收尾工作…");
    }

    // 【后序代码】
    // 1. 如果异常被成功捕获(catch 匹配到了),程序会跳过 try 中剩余的代码,
    // 执行 catch 块,然后执行 finally 块,最后继续执行这里的代码。
    // 2. 如果异常没有被捕获(没有匹配的 catch),程序在执行完 finally 后会直接终止或向上层抛出。
    System.out.println("后续业务逻辑…");
    }
    }

    关于异常的处理⽅式

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

    1. 对于⽐较严重的问题(例如和算钱相关的场景),应该让程序直接崩溃,防⽌造成更严重的后果

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

    3. 对于可能会恢复的问题(和⽹络相关的场景),可以尝试进⾏重试

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

    【注意事项】

    1. try块内抛出异常位置之后的代码将不会被执⾏

    2. 如果抛出异常类型与catch时异常类型不匹配,即异常不会被成功捕获,也就不会被处理,继续往 外抛,直到JVM收到后中断程序—-异常是按照类型来捕获的

    3. try中可能会抛出多个不同的异常对象,则必须⽤多个catch来捕获—-即多种异常,多次捕获

    public class ExceptionDemo {
    public static void main(String[] args) {
    // 1. 初始化一个长度为3的数组
    int[] array = {1, 2, 3};

    try {
    System.out.println("before");

    // 模拟可能出错的代码(当前状态)
    // 注意:array 长度为3,访问下标 100 会触发 ArrayIndexOutOfBoundsException
    System.out.println(array[100]);

    // 这行代码在发生异常时不会被执行
    System.out.println("after");

    /*
    * 备选测试场景:
    * 如果取消下面注释并注释掉上面的 array[100],
    * 则会触发 NullPointerException
    */
    // array = null;
    // System.out.println(array[0]);

    } catch (ArrayIndexOutOfBoundsException e) {
    // 2. 捕获数组下标越界异常
    System.out.println("这是个数组下标越界异常");
    e.printStackTrace(); // 打印详细的错误堆栈信息

    } catch (NullPointerException e) {
    // 3. 捕获空指针异常
    System.out.println("这是个空指针异常");
    e.printStackTrace();

    }

    // 4. 无论是否发生异常,只要程序未崩溃退出,后续代码都会执行
    System.out.println("after try catch");
    }
    }

    4. 可以通过⼀个catch捕获所有的异常,即多个异常,⼀次捕获(不推荐)

    5.finally

    在写程序时,有些特定的代码,不论程序是否发⽣异常,都需要执⾏,⽐如程序中打开的资源:⽹络 连接、数据库连接、IO流等,在程序正常或者异常退出时,必须要对资源进进⾏回收。另外,因为异 常会引发程序的跳转,可能导致有些语句执⾏不到,finally就是⽤来解决这个问题的

    public class FinallyDemo {
    public static void main(String[] args) {
    try {
    // 1. 定义一个长度为3的数组
    int[] array = {1, 2, 3};

    // 2. 尝试访问下标为100的元素,这会导致 ArrayIndexOutOfBoundsException
    // 注意:一旦这里出错,下面的 array[0] = 10; 将不会被执行
    array[100] = 10;

    // 3. 正常赋值逻辑(在上面的异常发生时会被跳过)
    array[0] = 10;

    } catch (ArrayIndexOutOfBoundsException e) {
    // 4. 捕获异常并打印异常信息
    System.out.println(e);

    } finally {
    // 5. 无论是否发生异常,这里的代码都会执行
    // 常用于关闭文件流、数据库连接等清理工作
    System.out.println("finally中的代码一定会执行");
    }

    // 6. try-catch 结构结束后的后续代码
    // 因为异常已经被 catch 捕获处理了,所以程序不会崩溃,会继续执行这里
    System.out.println("如果没有抛出异常,或者异常被处理了,try-catch后的代码也会执行");
    }
    }

    注意:finally中的代码⼀定会执⾏的,⼀般在finally中进⾏⼀些资源清理的扫尾⼯作。

    6.异常的处理流程

    关于"调⽤栈"

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

    如果本⽅法中没有合适的处理异常的⽅式,就会沿着调⽤栈向上传递

    public class ExceptionPropagationDemo {

    public static void main(String[] args) {
    try {
    // 1. 调用 func() 方法
    // 因为 func() 可能会抛出异常,所以放在 try 块中进行监控
    func();

    } catch (ArrayIndexOutOfBoundsException e) {
    // 2. 捕获从 func() 传递过来的数组下标越界异常
    System.out.println("在 main 方法中捕获到了异常!");
    e.printStackTrace(); // 打印详细的错误堆栈信息
    }

    // 3. 异常被处理后,程序继续向下执行
    System.out.println("after try catch");
    }

    /**
    * 模拟一个可能产生异常的方法
    */
    public static void func() {
    int[] array = {1, 2, 3};

    // 尝试访问下标 100,这超出了数组长度(0-2)
    // 这里会抛出 ArrayIndexOutOfBoundsException
    // 由于 func() 内部没有 try-catch,异常会自动抛给调用者(main)
    System.out.println(array[100]);
    }
    }

    输出:

    【异常处理流程总结】

    • 程序先执⾏try中的代码

    • 如果try中的代码出现异常,就会结束try中的代码,看和catch中的异常类型是否匹配.

    • 如果找到匹配的异常类型,就会执⾏catch中的代码

    • 如果没有找到匹配的异常类型,就会将异常向上传递到上层调⽤者.

    • ⽆论是否找到匹配的异常类型,finally中的代码都会被执⾏到(在该⽅法结束之前执⾏).

    • 如果上层调⽤者也没有处理的了异常,就继续向上传递.

    • ⼀直到main⽅法也没有合适的代码处理异常,就会交给JVM来进⾏处理,此时程序就会异常终⽌

    三:⾃定义异常类

    java 中虽然已经内置了丰富的异常类,但是并不能完全表⽰实际开发中所遇到的⼀些异常,此时就需 要维护符合我们实际情况的异常结构.

    例如,我们实现⼀个⽤⼾登陆功能

    public class LogIn {
    // 定义系统预设的正确用户名
    private String userName = "admin";
    // 定义系统预设的正确密码
    private String password = "123456";

    /**
    * 登录验证方法
    * @param userName 用户输入的用户名
    * @param password 用户输入的密码
    */
    public void loginInfo(String userName, String password) {
    // 1. 校验用户名 (使用 this.userName 区分成员变量和参数)
    if (!this.userName.equals(userName)) {
    System.out.println("用户名错误!");
    return; // 如果用户名不对,直接结束方法,不再校验密码
    }

    // 2. 校验密码
    if (!this.password.equals(password)) {
    System.out.println("密码错误!");
    return; // 如果密码不对,直接结束方法
    }

    // 3. 如果以上检查都通过,说明登录成功
    System.out.println("登陆成功");
    }

    public static void main(String[] args) {
    // 创建 LogIn 对象
    LogIn logIn = new LogIn();

    // 调用登录方法进行测试
    // 这里传入的是 "admin111",与预设的 "admin" 不符
    logIn.loginInfo("admin111", "123456");
    }
    }

    输出:

    此时我们在处理⽤⼾名密码错误的时候可能就需要抛出两种异常.我们可以基于已有的异常类进⾏扩展 (继承),创建和我们业务相关的异常类.

    1.实现⾃定义异常

    具体⽅式:

    1.⾃定义异常类,然后继承⾃Exception或者RunTimeException

    2.实现⼀个带有String类型参数的构造⽅法,参数含义:出现异常的原因

    /**
    * 自定义用户名异常
    * 继承自 Exception,属于受检异常(Checked Exception)
    */
    class UserNameException extends Exception {

    // 构造方法,接收一个字符串作为异常信息
    public UserNameException(String message) {
    // 调用父类 Exception 的构造方法,将错误信息传递上去
    super(message);
    }
    }

    /**
    * 自定义密码异常
    */
    class PasswordException extends Exception {

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

    // 测试类(为了演示如何使用上面定义的异常)
    public class CustomExceptionDemo {
    public static void main(String[] args) {
    try {
    // 模拟抛出异常
    throw new UserNameException("用户名不能为空!");
    } catch (UserNameException e) {
    System.out.println("捕获到异常:" + e.getMessage());
    }
    }
    }

    注意事项

    • ⾃定义异常通常会继承⾃Exception或者RuntimeException

    • 继承⾃Exception的异常默认是受查异常

    • 继承⾃RuntimeException的异常默认是⾮受查异常

    以上就是我们的全部内容了!!!!

    赞(0)
    未经允许不得转载:171主机测评 » 初识java(十六):异常
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址