新Java基础(二十五):异常类

在 Java 开发中,程序运行时的各种意外情况(如空指针、数组越界、文件不存在)都会以异常 的形式呈现,掌握异常体系和处理方式,是写出健壮、可维护代码的必备技能。本文将从 Java 异常体系的底层结构出发,详解 Error 与 Exception 的区别、运行时异常与非运行时异常的特性,再结合try-catch-finallythrows等核心处理方式,带你彻底吃透 Java 异常处理。

一、吃透 Java 异常体系结构

Java 中所有的异常和错误都继承自 **Throwable类,它是整个异常体系的根类,拥有两个直接子类: Error(错误)和 Exception(异常)。而Exception又分为 运行时异常(RuntimeException) 非运行时异常 **,也对应着非受检异常(Unchecked Exception)受检异常(Checked Exception),整体体系结构如下:

1. Error:程序无法处理的系统错误

Error是由 JVM 产生并抛出的系统级错误 ,属于程序自身无法处理的严重问题,当Error发生时,JVM 通常会直接终止当前线程。

  • 常见示例OutOfMemoryError(内存溢出错误)、StackOverflowError(栈溢出错误)、ThreadDeath(线程死亡错误);
  • 处理原则:程序中无需捕获和处理,也无法通过代码修复,只能通过优化程序、调整 JVM 参数等外部方式解决。

2. Exception:程序可以处理的异常

Exception是程序运行过程中产生的业务级异常 ,是程序本身可以通过代码捕获并处理的问题,也是我们开发中重点关注的对象。根据处理方式的不同,又分为运行时异常非运行时异常

3. 运行时异常(非受检异常)

运行时异常是RuntimeException及其所有子类的统称,也被称为非受检异常 ,编译器不会强制要求程序捕获或处理这类异常。

  • 常见示例NullPointerException(空指针)、IndexOutOfBoundsException(数组越界)、ArithmeticException(算术异常,如除 0);
  • 产生原因 :通常是程序逻辑错误导致的,比如调用了空对象的方法、访问了数组的无效索引;
  • 处理原则 :无需强制捕获,建议通过优化程序逻辑从根源上避免,而非单纯捕获处理。

4. 非运行时异常(受检异常)

非运行时异常是RuntimeException以外的Exception子类,也被称为受检异常 ,编译器强制要求程序必须捕获或抛出这类异常,否则代码无法编译通过。

  • 常见示例IOException(文件读写异常)、SQLException(数据库操作异常)、ClassNotFoundException(类找不到异常);
  • 产生原因 :通常是外部环境问题导致的,比如文件不存在、数据库连接失败,并非程序逻辑错误;
  • 处理原则 :必须通过try-catch捕获处理,或通过throws向上抛出,由上层方法处理。

二、Java 异常的核心处理方式

Java 提供了多种异常处理的语法,核心包括 **try-catch-finally(捕获并处理异常)、 throws(向上抛出异常)、 throw**(手动抛出异常),其中try-catch-finally是最基础、最常用的方式。

方式 1:try-catch------ 捕获并处理异常

try-catch的核心作用是捕获程序运行中的异常,并对异常进行处理,让异常不会导致程序终止,后续代码可以正常执行。

基本语法

复制代码
try {
    // 可能发生异常的代码块
} catch (异常类型 异常变量) {
    // 异常的处理逻辑(如打印日志、给出提示)
}

实战示例:处理数组越界异常

复制代码
public static void main(String[] args) {
    int[] a = new int[5]; // 数组长度为5,索引0-4
    try {
        // 循环索引到9,必然触发数组越界异常
        for (int i = 0; i < 10; i++) {
            a[i] = i;
        }
    } catch (Exception e) {
        // 捕获异常并打印异常信息
        e.printStackTrace();
    }
    // 异常被处理后,后续代码正常执行
    System.out.println("----------------");
}

关键特性

  • try代码块中发生异常时,JVM 会立即跳转到catch代码块执行处理逻辑,try中异常后续的代码不会再执行;
  • try代码块中无异常,catch代码块会被直接跳过。

方式 2:try-catch-finally------ 捕获处理 + 必执行代码

finally代码块是异常处理的兜底模块 ,无论try中是否发生异常、catch是否捕获到异常,finally中的代码一定会执行 ,常用于资源释放(如关闭文件、关闭数据库连接、释放流)。

基本语法

复制代码
try {
    // 可能发生异常的代码块
} catch (异常类型 异常变量) {
    // 异常处理逻辑
} finally {
    // 无论是否发生异常,都会执行的代码(资源释放为主)
}

核心规则

  1. finally不能单独使用,必须配合try,可组成try-catch-finallytry-finally两种结构;
  2. 即使trycatch中使用了returnbreak等跳转语句,finally仍会执行(跳转前执行);
  3. 唯一例外:若程序通过System.exit(0)强制终止 JVM,finally不会执行。

实战场景:资源释放

复制代码
// 伪代码:读取文件后释放流资源
FileInputStream fis = null;
try {
    fis = new FileInputStream("test.txt");
    // 读取文件操作
} catch (IOException e) {
    e.printStackTrace();
} finally {
    // 无论是否发生IO异常,都关闭流资源
    if (fis != null) {
        try {
            fis.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

方式 3:throws------ 向上抛出异常

throws用于方法声明处 ,表示该方法不处理异常 ,而是将异常向上抛出,由调用该方法的上层方法负责处理,是一种 "异常转移" 的处理方式。

基本语法

复制代码
修饰符 返回值类型 方法名(参数列表) throws 异常类型1, 异常类型2 {
    // 可能发生异常的代码
}

实战示例:多层方法的异常抛出

复制代码
public class Test {
    public static void main(String[] args) {
        Test test = new Test();
        // 上层方法调用抛出异常的方法,必须捕获或继续抛出
        try {
            test.run();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        System.out.println("--------------");
    }

    // run方法不处理异常,向上抛出给main方法
    public void run() throws CloneNotSupportedException {
        this.sun();
    }

    // sun方法产生异常,向上抛出给run方法
    public void sun() throws CloneNotSupportedException {
        User user01 = new User();
        User user02 = user01.clone(); // clone方法会抛出CloneNotSupportedException
    }
}

关键特性

  1. 可以同时抛出多个异常,用逗号分隔;
  2. 若方法抛出受检异常 ,上层调用方法必须捕获或继续抛出;若抛出运行时异常,上层方法无强制要求;
  3. 子类重写父类方法时,抛出的异常不能比父类方法更宽泛(可少抛、可抛子类异常)。

方式 4:throw------ 手动抛出异常

throw用于方法内部 ,表示手动主动抛出 一个具体的异常对象,通常用于根据业务逻辑自定义异常场景(比如判断参数是否合法,不合法则抛出异常)。

基本语法

复制代码
throw new 异常类型("异常提示信息");

实战示例:手动抛出参数异常

复制代码
public static void run(int a) throws Exception {
    // 业务逻辑:若参数a大于10,手动抛出异常
    if(a > 10){
        throw new Exception("你给我的值大于10");
    }
}

与 throws 的区别

  • throw方法内部 ,手动抛出具体异常对象 ,是执行动作
  • throws方法声明处 ,声明方法可能抛出的异常类型 ,是异常声明

三、try-catch-finally 的使用注意事项

try-catch-finally是异常处理的核心组合,使用时有多个易踩坑的细节,必须严格遵守相关规则,避免程序出现意料之外的问题。

1. 结构使用规则

  • trycatchfinally三者不能单独使用;
  • 支持三种合法结构:try-catchtry-finallytry-catch-finally
  • catch可以有一个或多个 (处理不同类型异常),finally最多一个

2. 变量作用域规则

trycatchfinally三个代码块中的变量作用域仅在自身块内 ,相互之间无法访问。若需要在多个块中访问变量,需将变量定义在块外

复制代码
// 正确:变量定义在块外
int num = 0;
try {
    num = 10;
} catch (Exception e) {
    num = 20;
} finally {
    System.out.println(num); // 可访问num
}

// 错误:try内的变量无法在catch中访问
try {
    int a = 10;
} catch (Exception e) {
    System.out.println(a); // 编译报错
}

3. 多 catch 块的匹配规则

当存在多个catch块处理不同异常时,需遵守 **"先子类,后父类"的顺序,且最多只会匹配执行一个 catch 块 **。

正确示例:先捕获子类异常,再捕获父类异常

复制代码
try {
    int[] a = new int[5];
    a[10] = 0;
} catch (ArrayIndexOutOfBoundsException e) {
    // 先捕获具体的子类异常
    System.out.println("数组越界异常");
} catch (Exception e) {
    // 再捕获通用的父类异常
    System.out.println("通用异常");
}

错误示例:父类异常在前,子类异常永远不会被匹配

复制代码
try {
    int[] a = new int[5];
    a[10] = 0;
} catch (Exception e) {
    // 父类异常先匹配,后续子类异常永远不会执行
    System.out.println("通用异常");
} catch (ArrayIndexOutOfBoundsException e) {
    System.out.println("数组越界异常"); // 永远不会执行
}

4. 代码执行顺序规则

  1. 无异常时:try代码块全部执行 → 跳过所有catch → 执行finally → 执行后续代码;
  2. 有异常且捕获到:try中异常行前的代码执行 → 跳转到匹配的catch执行 → 执行finally → 执行后续代码;
  3. 有异常但未捕获到:try中异常行前的代码执行 → 跳过所有catch → 执行finally → 异常向上抛出,后续代码不执行。

四、易混关键字:finally /final/finalize

很多初学者会混淆finallyfinalfinalize三个关键字,三者的功能和使用场景完全不同,核心区别如下:

关键字 作用场景 核心功能
finally 异常处理 配合try-catch使用,定义必执行的代码块,主要用于资源释放
final 变量 / 方法 / 类 修饰变量:变为常量,值不可修改;修饰方法:不可被重写;修饰类:不可被继承
finalize 对象回收 Object 类的方法,垃圾回收器回收对象前会调用,用于对象的最终资源释放(已被废弃)

核心记忆finally是异常处理的兜底,final是 "不可变" 的修饰符,二者毫无关联,切勿混淆。

五、Java 异常处理的核心原则

  1. 捕获具体异常,而非通用异常 :尽量避免直接捕获Exception,应捕获具体的子类异常(如ArrayIndexOutOfBoundsException),让异常处理更精准;
  2. 避免空 catch 块 :不要只捕获异常而不做任何处理(如catch (Exception e) {}),至少打印异常日志,便于问题排查;
  3. 资源释放优先用 finally :文件、流、数据库连接等资源,必须在finally中释放,确保资源不泄漏;
  4. 运行时异常优先优化逻辑:运行时异常(如空指针)是逻辑错误导致,应通过判空、索引校验等方式从根源避免,而非单纯捕获;
  5. 受检异常按需处理或抛出 :受检异常必须处理,若当前方法无法处理,可通过throws向上抛出,由上层业务逻辑统一处理;
  6. 自定义异常贴合业务 :开发中可根据业务需求自定义异常(如UserNotExistExceptionParamIllegalException),让异常更具语义化。
相关推荐
礼拜天没时间.2 小时前
力扣热题100实战 | 第31期:下一个排列——数组规律的极致探索
java·算法·leetcode·字典序·原地算法·力扣热题100
xiaoye37082 小时前
java后端面试一般问什么?
java·面试
Z9fish2 小时前
sse哈工大C语言编程练习42
c语言·开发语言·算法
YYYing.2 小时前
【Linux/C++多线程篇(一) 】多线程编程入门:从核心概念到常用函数详解
linux·开发语言·c++·笔记·ubuntu
badhope2 小时前
OpenClaw卸载命令全解析
java·linux·人工智能·python·sql·数据挖掘·策略模式
Hello.Reader2 小时前
Flink Task Lifecycle 一篇讲透 StreamTask 与 Operator 生命周期
java·大数据·flink
炸膛坦客2 小时前
单片机/C语言八股:(十三)C 语言实现矩阵乘法
c语言·开发语言·矩阵
小小小米粒2 小时前
Redisson 大量用了 Lua
java