前言

在 Java 开发中,异常处理是保证程序健壮性的核心机制之一。很多新手写代码时只会无脑 try-catch,却分不清 ErrorException,也不知道受检 / 非受检异常的区别,甚至写出 “吞异常” 的反模式代码。

这篇博客会从底层原理、分类、语法到最佳实践,一次性讲透 Java 异常体系,帮你彻底告别 “异常处理全靠瞎写” 的困境。


一、Throwable:所有 “可抛出问题” 的顶层父类

Java 中所有错误和异常的祖宗类都是 java.lang.Throwable,只有继承了它的类,才能被 throw 抛出、被 catch 捕获。

它的核心子类只有两个:

  • Error:代表严重错误
  • Exception:代表可处理的异常

java.lang.Throwable
├── java.lang.Error
└── java.lang.Exception
    ├── java.lang.RuntimeException (非受检异常)
    └── 其他Exception (受检异常)

二、Error:程序无法挽回的致命问题

1. 什么是 Error?

Error 是 JVM 运行时出现的严重错误,这类问题和你的代码逻辑无关,是系统级别的故障,程序无法通过任何逻辑恢复,只能终止运行。

2. 核心特点

  • 由 JVM 抛出,程序无需也无法捕获处理
  • 一旦出现,通常意味着程序已经无法继续执行
  • 不强制要求代码声明或捕获

3. 常见 Error 示例

表格

错误类型 场景说明
OutOfMemoryError 堆内存溢出,对象太多导致 JVM 内存耗尽
StackOverflowError 栈溢出,比如递归调用死循环、方法嵌套过深
VirtualMachineError JVM 本身故障,比如 JVM 崩溃
NoClassDefFoundError 类定义找不到,通常是编译后类文件丢失

举个例子:写一个死递归,直接触发 StackOverflowError

public class ErrorTest {
    public static void main(String[] args) {
        recursion();
    }
    static void recursion() {
        recursion(); // 无限递归,栈帧不断压入,最终栈溢出
    }
}

三、Exception:程序可处理的异常

Exception 是程序运行时出现的、可以通过代码逻辑处理的问题,也是我们开发中重点关注的对象。它分为两大类:受检异常(Checked Exception)非受检异常(Unchecked Exception)

1. 受检异常(Checked Exception)

定义

除了 RuntimeException 及其子类之外的所有 Exception,编译器会强制要求你处理,不处理直接编译报错。

特点
  • 由外部环境导致,属于 “合理预期内的异常”
  • 必须显式处理:要么用 try-catch 捕获,要么用 throws 声明抛出
  • 代表代码无法控制的错误场景(比如文件不存在、网络断开)
常见示例

表格

异常类型 场景说明
IOException IO 操作异常(文件读写、网络传输)
SQLException 数据库操作异常
ClassNotFoundException 类找不到,反射加载类失败
FileNotFoundException 文件不存在,属于 IOException 的子类

比如你代码里的 FileInputStream 就会抛出 FileNotFoundException,属于受检异常,编译器强制要求你处理,否则编译不通过。


2. 非受检异常(Unchecked Exception)

定义

RuntimeException 及其子类,编译器不强制要求处理,不写 try-catch 也能编译通过,但运行时会直接抛出并终止程序。

特点
  • 由代码逻辑错误导致,属于 “可以避免的错误”
  • 不需要显式声明或捕获
  • 代表程序员的疏忽或逻辑漏洞(比如空指针、下标越界)
常见示例

表格

异常类型 场景说明
NullPointerException 空指针异常,调用了 null 对象的方法 / 属性
ArrayIndexOutOfBoundsException 数组下标越界
ArithmeticException 算术异常,比如除以 0
ClassCastException 类型转换异常,比如把 String 强转为 Integer
IllegalArgumentException 参数非法,传入不符合要求的参数

举个例子:

public class RuntimeExceptionTest {
    public static void main(String[] args) {
        int[] arr = new int[3];
        System.out.println(arr[5]); // 下标越界,运行时抛出 ArrayIndexOutOfBoundsException
    }
}

四、Java 异常处理的核心语法

1. try-catch-finally:基础捕获结构

这是最基础的异常捕获方式,也是你代码里用到的结构。

语法格式
try {
    // 可能抛出异常的代码块
} catch (异常类型1 e) {
    // 捕获并处理异常类型1
} catch (异常类型2 e) {
    // 捕获并处理异常类型2
} finally {
    // 无论是否发生异常,都会执行的代码块(除了 System.exit(0))
}
关键要点
  • try:包裹可能出问题的代码,一旦出现异常,后续代码不会继续执行,直接跳转到 catch
  • catch:捕获 try 中抛出的指定类型异常,多个 catch 要遵循 “子类在前,父类在后” 的顺序
  • finally:用于资源释放,比如关闭 IO 流、数据库连接,是兜底操作

2. throws:声明抛出异常

当你不想在当前方法处理异常时,可以用 throws 声明,把异常抛给调用者处理。

语法示例
// 声明抛出 IOException,交给调用者处理
public static void readFile() throws IOException {
    FileInputStream fis = new FileInputStream("demo.txt");
}

// 调用者可以选择 try-catch 处理,或者继续 throws 抛出
public static void main(String[] args) throws IOException {
    readFile();
}
注意事项
  • 子类重写父类方法时,不能抛出比父类更宽泛的异常(比如父类没抛异常,子类不能抛)
  • 受检异常必须声明或捕获,非受检异常可以不写

3. throw:手动抛出异常

throw 用于手动抛出一个异常对象,通常用在业务逻辑校验中,主动抛出不符合业务规则的异常。

语法示例
public static void checkAge(int age) {
    if (age < 0 || age > 150) {
        // 手动抛出参数非法异常
        throw new IllegalArgumentException("年龄必须在 0-150 之间,传入值:" + age);
    }
}

public static void main(String[] args) {
    checkAge(-5); // 运行时抛出 IllegalArgumentException
}

4. try-with-resources:自动关闭资源(Java 7+)

这是 Java 7 新增的语法,专门用来简化资源关闭操作,比如 IO 流、数据库连接,自动实现 finally 中的资源释放,比手动写 finally 更安全简洁。

适用场景

实现了 AutoCloseable 接口的资源(所有 IO 流、数据库连接池等都实现了这个接口)。

语法示例
// 传统写法:手动关闭流,容易忘写 finally 或出现空指针
FileInputStream fis = null;
try {
    fis = new FileInputStream("demo.txt");
    // 业务代码
} catch (IOException e) {
    e.printStackTrace();
} finally {
    if (fis != null) {
        try {
            fis.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

// try-with-resources 写法:自动关闭,无需写 finally
try (FileInputStream fis = new FileInputStream("demo.txt");
     InputStreamReader isr = new InputStreamReader(fis, "UTF-8")) {
    // 业务代码
} catch (IOException e) {
    e.printStackTrace();
}

原理:离开 try 块时,会自动按逆序调用资源的 close() 方法,无需手动处理。


五、异常处理的最佳实践(避坑指南)

1. 不要 “一刀切” 捕获所有异常

❌ 反模式:

try {
    // 业务代码
} catch (Exception e) {
    e.printStackTrace();
}

这种写法会掩盖所有异常,包括你没预料到的错误,后期排查问题极难。

✅ 正确做法:捕获具体的异常类型,按优先级从子类到父类排列。

try {
    FileInputStream fis = new FileInputStream("demo.txt");
} catch (FileNotFoundException e) {
    System.out.println("文件不存在,请检查路径");
} catch (IOException e) {
    System.out.println("文件读取失败");
}

2. 不要 “吞异常”

❌ 反模式:

try {
    // 业务代码
} catch (IOException e) {
    // 空的 catch 块,什么都不做
}

捕获了异常却不处理、不打印日志,会导致问题被隐藏,程序出了问题完全无法定位。

✅ 正确做法:至少打印异常栈轨迹,或记录日志,带上上下文信息。

catch (IOException e) {
    log.error("读取文件失败,路径:demo.txt", e); // 打印日志,带上上下文
    e.printStackTrace(); // 开发环境打印栈轨迹
}

3. finally 里不要写 return

❌ 反模式:

public static int test() {
    try {
        return 1;
    } finally {
        return 2; // 会覆盖 try 里的 return,而且会吞掉异常
    }
}

finally 里的 return 会覆盖 try/catch 里的返回值,同时会吞掉 try 中抛出的异常,导致程序行为不可预期。

✅ 正确做法:finally 只做资源释放,不写 returnbreak 等改变流程的语句。


4. 合理使用自定义异常

当业务场景有特殊的错误类型时,可以自定义异常,比如 BusinessExceptionOrderException,但不要滥用。

示例:

// 自定义业务异常
public class BusinessException extends RuntimeException {
    public BusinessException(String message) {
        super(message);
    }
}

// 使用场景
public static void payOrder(int orderId) {
    if (orderId <= 0) {
        throw new BusinessException("订单号非法:" + orderId);
    }
    // 支付逻辑
}

5. 优先修复非受检异常,而不是捕获

非受检异常(比如 NullPointerExceptionArrayIndexOutOfBoundsException)都是代码逻辑错误导致的,优先通过代码优化避免,而不是用 try-catch 捕获。

❌ 反模式:

try {
    String str = null;
    System.out.println(str.length());
} catch (NullPointerException e) {
    // 捕获空指针,却不修复问题
}

✅ 正确做法:提前判空,避免空指针:

String str = null;
if (str != null) {
    System.out.println(str.length());
}

六、总结

  • Throwable 是所有错误和异常的顶层父类,分为 ErrorException
  • Error 是 JVM 级别的严重错误,程序无法处理,只能终止
  • Exception 分为受检异常(编译强制处理)和非受检异常(代码逻辑错误)
  • 核心处理语法:try-catch-finallythrowsthrowtry-with-resources
  • 最佳实践:捕获具体异常、不吞异常、不滥用捕获、优先修复非受检异常
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐