杰瑞科技汇

Java中的Exception,如何正确处理与避免?

这篇指南将从以下几个方面展开,帮助你彻底理解 Java 异常:

Java中的Exception,如何正确处理与避免?-图1
(图片来源网络,侵删)
  1. 什么是异常?
  2. 异常的体系结构
  3. 异常的分类(Checked vs Unchecked)
  4. 异常处理的关键字:try, catch, finally, throw, throws
  5. 如何正确使用异常(最佳实践)
  6. 自定义异常

什么是异常?

在 Java 中,异常指的是程序在运行过程中出现的非正常的、可被识别和处理的错误事件

异常就是程序运行时发生的问题,这些问题中断了正常的指令流,如果没有异常处理机制,程序一旦遇到错误就会立即终止,这对于构建健壮、可靠的应用程序是不可接受的。

Java 的异常处理机制提供了一种将“正常流程代码”和“错误处理代码”分离的方式,使得代码更清晰、更易于维护。


异常的体系结构

所有 Java 异常的顶级父类都是 java.lang.Throwable 类,它有两个主要的子类:

Java中的Exception,如何正确处理与避免?-图2
(图片来源网络,侵删)
  1. Error (错误)
  2. Exception (异常)

下面是这个体系的简化结构图:

java.lang.Object
    java.lang.Throwable
        java.lang.Error
        java.lang.Exception
            java.lang.RuntimeException
            ... (其他非RuntimeException)

Error (错误)

  • 性质Error 类及其子类通常表示严重的、系统级的问题,这些问题通常是不可恢复的。
  • 原因:它们不是由程序逻辑引起的,而是由 JVM(Java虚拟机)运行环境引起的。
    • OutOfMemoryError:内存溢出,JVM 没有足够内存分配。
    • StackOverflowError:栈溢出,通常由无限递归引起。
    • VirtualMachineError:虚拟机错误。
  • 处理:对于 Error,我们通常无能为力,也无法通过 try-catch 来捕获和处理,程序一般会选择终止。

Exception (异常)

Exception 是我们日常编码中主要关注的部分,它表示程序本身可以处理的异常,根据是否在编译时被检查,Exception 又分为两大类:


异常的分类:Checked vs Unchecked

这是理解 Java 异常最核心的概念。

A. Checked Exception (受检异常 / 编译时异常)

  • 定义:在编译时,Java 编译器会检查代码中是否处理了这类异常,如果没有,编译会失败。
  • 目的:提醒开发者,这段代码可能会出问题,你必须提前考虑好应对策略。
  • 特点:通常是外部环境引起的,例如文件不存在、网络连接中断、数据库访问失败等,这些错误是程序无法完全避免的。
  • 示例
    • IOException:输入/输出操作异常,如文件读写失败。
    • SQLException:数据库访问错误。
    • FileNotFoundException:文件未找到异常。
    • ClassNotFoundException:类未找到异常。

如何处理 Checked Exception? 你必须显式地处理它,有两种方式:

Java中的Exception,如何正确处理与避免?-图3
(图片来源网络,侵删)
  1. try-catch:在可能抛出异常的代码块外包裹 try-catch
    try {
        FileReader file = new FileReader("a.txt");
    } catch (FileNotFoundException e) {
        System.out.println("文件未找到,请检查路径!");
        e.printStackTrace(); // 打印异常堆栈信息
    }
  2. throws:在方法签名上声明,告诉调用者:“这个方法可能会抛出这个异常,处理的责任交给你了”。
    public void readFile() throws FileNotFoundException {
        FileReader file = new FileReader("a.txt");
        // ... 读取文件
    }

B. Unchecked Exception (非受检异常 / 运行时异常)

  • 定义:在编译时,Java 编译器不会检查这类异常,它们在运行时才可能被发现。
  • 目的:通常表示程序逻辑错误,这些错误本可以通过更好的代码设计来避免。
  • 特点:它们是 RuntimeException 类及其子类的实例,因为它们通常是编程错误,所以强制开发者去捕获每一个可能的逻辑错误是不现实的。
  • 示例
    • NullPointerException:空指针异常,试图访问一个 null 对象的成员。
    • ArrayIndexOutOfBoundsException:数组下标越界。
    • ClassCastException:类型转换异常。
    • ArithmeticException:算术异常,如除以零。
    • IllegalArgumentException:非法参数异常。

如何处理 Unchecked Exception? 你可以选择处理,但通常不推荐去捕获它们,而应该修复代码逻辑

// 错误的示范:捕获了本应修复的逻辑错误
try {
    String str = null;
    System.out.println(str.length());
} catch (NullPointerException e) {
    System.out.println("空指针了,没事,我catch一下");
} // 这是在掩盖bug,而不是修复它
// 正确的做法:修复代码
String str = "hello";
if (str != null) {
    System.out.println(str.length());
}

在某些特定场景下,比如框架或工具类中,你可能需要捕获 RuntimeException 来进行统一的日志记录或资源清理。


异常处理的关键字

关键字 作用
try 包裹可能抛出异常的代码块。
catch 捕获并处理 try 块中抛出的特定异常,可以有多个 catch 块。
finally 无论是否发生异常,finally 块中的代码一定会被执行,通常用于资源释放(如关闭文件、数据库连接)。
throw 手动抛出一个异常对象,通常用在方法体内部。
throws 在方法签名上声明该方法可能抛出的异常,将处理责任交给调用者。

代码示例:try-catch-finally

public class ExceptionDemo {
    public static void main(String[] args) {
        try {
            System.out.println("进入 try 块");
            int result = 10 / 0; // 抛出 ArithmeticException
            System.out.println("这行代码不会执行");
        } catch (ArithmeticException e) {
            // 捕获到 ArithmeticException
            System.out.println("捕获到异常: " + e.getMessage());
            e.printStackTrace(); // 打印详细的异常堆栈,非常有助于调试
        } finally {
            // 无论是否发生异常,这里都会执行
            System.out.println("进入 finally 块,执行资源清理工作...");
        }
        System.out.println("程序继续执行...");
    }
}

输出:

进入 try 块
捕获到异常: / by zero
java.lang.ArithmeticException: / by zero
    at ExceptionDemo.main(ExceptionDemo.java:6)
进入 finally 块,执行资源清理工作...
程序继续执行...

throw vs throws 的区别

特性 throw throws
位置 方法体内部 方法签名后面
作用 主动抛出一个异常对象 声明该方法可能抛出的异常
数量 一次只能抛出一个异常对象 可以声明多个异常,用逗号隔开
后续 抛出后,当前方法会立即终止,并将异常交给调用者处理 只是一个声明,方法本身不一定真的会抛出

示例:throw

public void checkAge(int age) {
    if (age < 18) {
        // 手动创建并抛出一个异常
        throw new IllegalArgumentException("年龄必须大于18岁");
    }
    System.out.println("年龄合法");
}

示例:throws

public void readFile() throws IOException, FileNotFoundException {
    // ... 读写文件,可能抛出这两种异常
    // 这里不需要 try-catch,而是声明出去
}
// 调用方必须处理
public void callReadFile() {
    try {
        readFile();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

如何正确使用异常(最佳实践)

  1. 只对异常情况进行处理:不要用 try-catch 来控制正常的业务流程。try 块应该尽可能小,只包裹真正可能出错的代码。

  2. 优先捕获具体的异常:永远不要只捕获 ExceptionThrowable,这会让你无法区分不同类型的错误,掩盖真正的问题。

    // 不好的做法
    try { ... } catch (Exception e) { ... }
    // 好的做法
    try { ... } catch (IOException e) { ... } catch (SQLException e) { ... }
  3. 不要吞掉异常:不要只 catch 一个异常然后什么都不做(比如只打印一个日志),这会让问题在底层静默消失,导致上层调用者无法感知,从而引发更严重的错误。

    // 不好的做法:吞掉异常
    try { ... } catch (IOException e) {
        System.out.println("出错了");
    }
  4. finally 块中释放资源:确保数据库连接、文件流、网络连接等资源能够被正确关闭,避免资源泄漏。

  5. 记录异常信息:在 catch 块中,使用 logger.error(..., e)e.printStackTrace() 记录完整的异常堆栈信息,这对于后续的调试至关重要。

  6. 优先使用标准异常:Java 标准库中已有合适的异常(如 IllegalArgumentException, IllegalStateException),直接使用它们,而不是创建新的自定义异常。

  7. 保持异常的原始信息:在捕获一个异常后重新抛出另一个异常时,最好将原始异常作为“原因”传递进去,这样可以保留完整的调用链。

    try {
        // ... 一些操作
    } catch (SQLException e) {
        // 将原始异常作为原因包装后重新抛出
        throw new BusinessException("数据库操作失败", e);
    }

自定义异常

当你需要表达特定的业务错误,并且希望这个错误有明确的业务含义时,可以创建自定义异常。

步骤:

  1. 继承 Exception(对于受检异常)或 RuntimeException(对于非受检异常)。
  2. 可以选择性地重写构造方法,以便可以传递错误消息和原始异常。

示例:自定义业务异常

// 1. 自定义一个非受检的业务异常
public class BusinessException extends RuntimeException {
    public BusinessException(String message) {
        super(message);
    }
    public BusinessException(String message, Throwable cause) {
        super(message, cause);
    }
}
// 2. 在业务代码中使用它
public class UserService {
    public void register(String username, String password) {
        if (username == null || username.trim().isEmpty()) {
            // 抛出自定义异常
            throw new BusinessException("用户名不能为空");
        }
        if (password.length() < 6) {
            throw new BusinessException("密码长度不能少于6位");
        }
        // ... 注册逻辑
    }
}
// 3. 调用方处理
public class Main {
    public static void main(String[] args) {
        UserService userService = new UserService();
        try {
            userService.register("", "123456");
        } catch (BusinessException e) {
            System.out.println("注册失败: " + e.getMessage());
            // e.printStackTrace(); // 可以打印堆栈
        }
    }
}

特性 Error Checked Exception Unchecked Exception
父类 Error Exception (非 RuntimeException) RuntimeException
处理方式 不处理,程序终止 必须处理 (try-catchthrows) 不强制处理,建议修复代码
发生时机 运行时 编译时检查 运行时
常见原因 JVM系统问题 外部环境问题(IO、DB等) 程序逻辑错误(NPE、越界等)

掌握 Java 异常是编写高质量、健壮代码的必备技能,理解其分类、处理机制并遵循最佳实践,能让你写出更专业、更易维护的程序。

分享:
扫描分享到社交APP
上一篇
下一篇