返回首页
📝416 ⏱️3 分钟📅2025-12-16📄Java Basic#Java / Exception Handling / Best Practices / Interview / Backend

Java 异常体系详解与最佳实践

Java 异常体系详解与最佳实践

异常体系架构

Java 中的所有异常和错误都继承自 Throwable 类。理解这个层级结构是掌握异常处理的基础。

Throwable (所有错误和异常的根父类)
|
├── Error (严重错误)
│    ├── OutOfMemoryError (OOM)
│    ├── StackOverflowError (栈溢出)
│    └── ... (通常是 JVM 或系统级问题,程序无法恢复,不应捕获)
│
└── Exception (程序可处理的异常)
     │
     ├── Checked Exception (检查型/编译时异常)
     │    ├── IOException (文件/网络操作)
     │    ├── SQLException (数据库操作)
     │    ├── ClassNotFoundException
     │    └── ... (编译器强制要求处理,要么 try-catch,要么 throws)
     │
     └── Unchecked Exception / RuntimeException (非检查型/运行时异常)
          ├── NullPointerException (NPE)
          ├── ArrayIndexOutOfBoundsException (数组越界)
          ├── ClassCastException (类型转换错误)
          ├── ArithmeticException (算术异常, 如 /0)
          └── ... (通常由代码逻辑错误导致,编译器不强制处理)

核心概念与分类

1. Error vs Exception

  • Error: 代表了 JVM 本身无法处理的严重错误(如内存耗尽、虚拟机崩溃)。程序不应该尝试捕获这些错误,因为一旦发生,程序通常无法恢复正常运行。
  • Exception: 代表程序运行过程中可以预见、可以捕获并处理的异常情况。

2. Checked vs Unchecked (RuntimeException)

这是面试中最常问到的区别,可以用口诀记忆:“检查要动手,运行靠逻辑”。

特性Checked Exception (检查型)Unchecked Exception (运行时)
定义Exception 下除 RuntimeException 之外的异常RuntimeException 及其子类
强制性编译器强制要求处理编译器不强制要求处理
处理方式必须 try-catch 捕获 或 throws 声明抛出不需要显式捕获,应通过代码逻辑避免
典型场景文件 IO、数据库连接、网络请求空指针、数组越界、参数错误

关键字与语法解析

throw vs throws

  • throws: 用在方法签名上。
    • 作用: 声明该方法可能抛出的风险。告诉调用者:“我可能会出问题,你自己看着办”。
    • 示例: public void readFile() throws IOException { ... }
  • throw: 用在方法体内部
    • 作用: 执行抛出异常对象的动作。
    • 示例: if (obj == null) throw new NullPointerException("参数为空");

try-catch-finally 执行机制

  • try: 标记危险区域,包裹可能抛出异常的代码。
  • catch: 捕获处理异常。
    • 注意: 必须先捕获子类异常,再捕获父类异常。如果 ExceptionIOException 前面,具体的 IO 异常将永远无法进入其专属的 catch 块。
  • finally: 保底执行。无论是否发生异常,该块代码几乎总会执行(除非 JVM 退出)。主要用于资源释放。

进阶与最佳实践

1. 资源管理的进化:try-with-resources

在 JDK 7 之前,我们需要在 finally 中繁琐地关闭流。JDK 7 引入了 try-with-resources,适用于实现了 AutoCloseable 接口的资源。

传统写法 (不推荐):

FileInputStream fis = null;
try {
    fis = new FileInputStream("file.txt");
    // ...
} catch (IOException e) {
    e.printStackTrace();
} finally {
    if (fis != null) {
        try {
            fis.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

现代写法 (推荐):

// 编译器自动生成关闭逻辑,安全且简洁
try (FileInputStream fis = new FileInputStream("file.txt")) {
    // 业务逻辑
} catch (IOException e) {
    e.printStackTrace();
}

2. 异常处理的性能与规范

  • 不要用异常控制流程: 创建异常对象(需要捕获栈轨迹)开销较大。例如,不要用 try-catch 来处理数组遍历结束,而应该用循环条件。
  • 不要“吞”异常: catch 块中至少要记录日志。catch (Exception e) {} 是大忌,这会让排查问题变得不可能。
  • 异常链保持: 捕获低级异常抛出高级异常时,务必保留原始异常:throw new BusinessException("业务错误", causeException);

3. 自定义异常

  • 目的: 提供更具业务含义的错误信息。
  • 实现:
    • 业务逻辑错误继承 Exception (Checked)。
    • 通用技术错误继承 RuntimeException (Unchecked)。
    • 必须提供包含 String messageThrowable cause 的构造函数。

高频面试题剖析

Q1: finally 块中的代码一定会执行吗?

: 几乎一定。只有在 try 块中调用了 System.exit(0) 强制终止 JVM 时,finally 才不会执行。

Q2: 如果 try 中有 returnfinally 还会执行吗?

: 会执行
执行顺序是:

  1. 执行 try 中的代码。
  2. 遇到 return,先计算返回值并暂存。
  3. 执行 finally 块。
  4. 如果 finally 中没有 return:方法返回第 2 步暂存的值。
  5. 如果 finally 中也有 return (危险)finally 的返回值会覆盖 try 中的返回值。应避免在 finally 中写 return

Q3: 常见的运行时异常有哪些?

  • NullPointerException (NPE)
  • ArrayIndexOutOfBoundsException
  • ClassCastException
  • IllegalArgumentException
  • ArithmeticException