Java 异常处理
真实程序一定会出错:文件读不到、网络断了、用户输入不是数字、内存不足。Java 用 try / catch / finally 优雅地捕获异常,让程序不会因一处错误而崩溃。本章带你彻底掌握 Java 异常处理。
1. try / catch / finally
这是最基础的异常处理结构:try 包裹可能出错的代码,catch 捕获特定异常,finally 无论是否异常都执行(用于清理资源)。
public class TryCatchDemo {
public static void main(String[] args) {
try {
int n = Integer.parseInt("abc"); // 抛 NumberFormatException
int result = 10 / n;
System.out.println(result);
} catch (NumberFormatException e) {
// 捕获特定异常
System.out.println("数字格式错误: " + e.getMessage());
} catch (ArithmeticException e) {
// 多重 catch:匹配第一个合适的
System.out.println("算术错误: " + e.getMessage());
} finally {
// finally 块:无论是否异常都执行(常用于关资源)
System.out.println("清理工作完成");
}
// Java 7+ 多异常合并捕获(用 | 分隔)
try {
int[] arr = null;
System.out.println(arr.length); // 抛 NullPointerException
} catch (NullPointerException | ArrayIndexOutOfBoundsException e) {
System.out.println("空指针或越界: " + e.getClass().getSimpleName());
}
}
}几个要点:
- 多重 catch:从上到下匹配,命中一个就跳出。注意子类异常要在父类之前(否则永远匹配不到父类后面的子类,编译报错)。
- Java 7+ 多异常合并:
catch (A | B e),用|分隔多个异常类型。 - finally 必执行:即使 try 里 return 了,finally 也会执行(只有
System.exit()或 JVM 崩溃才跳过)。常用于关闭资源。 - 不要 catch 一切(
catch (Exception e)):会吞掉你没想到的 bug,只捕获你预期会发生的。
2. throw 与 throws
两个关键字长得很像,含义完全不同:
throw new XxxException():主动抛出一个异常对象(在方法体内)。throws XxxException:声明方法可能抛出哪些异常(在方法签名上),告诉调用方要处理。
public class ThrowDemo {
// throws:声明方法可能抛出哪些受检异常(让调用方处理)
public static int divide(int a, int b) throws ArithmeticException {
if (b == 0) {
// throw:主动抛出异常对象
throw new ArithmeticException("除数不能为 0");
}
return a / b;
}
public static void main(String[] args) {
try {
divide(10, 0);
} catch (ArithmeticException e) {
System.out.println("捕获: " + e.getMessage()); // 捕获: 除数不能为 0
}
// 抛异常后,后面的代码不会执行(直接跳到 catch)
// 这就是"非正常流程控制"
}
}3. Java 异常体系
// Java 异常体系(简化版):
//
// Throwable
// / \
// Error Exception
// / \
// (其他) RuntimeException
//
// Error:JVM 严重错误,程序不应捕获(OutOfMemoryError、StackOverflowError)
// Exception:程序应捕获处理的异常
// - 受检异常(checked):非 RuntimeException,编译器强制 try/catch 或 throws
// 常见:IOException、SQLException、ClassNotFoundException
// - 非受检异常(unchecked,RuntimeException 子类):编译器不强制
// 常见:NullPointerException、IllegalArgumentException、
// ArrayIndexOutOfBoundsException、ArithmeticException
public class HierarchyDemo {
// 受检异常必须 throws 声明
public void readConfig() throws java.io.IOException {
// ...
}
// 非受检异常无需声明
public void checkNotNull(Object o) {
if (o == null) {
throw new NullPointerException("参数不能为 null");
}
}
}受检 vs 非受检异常是 Java 独有的设计(也是争议点):
- 受检异常(Checked Exception):继承 Exception 但非 RuntimeException。编译器强制你 try/catch 或 throws。
IOException、SQLException。代表可恢复的外部错误。 - 非受检异常(Unchecked Exception,RuntimeException 子类):编译器不强制。
NullPointerException、IllegalArgumentException。代表程序逻辑错误,应该改代码而不是捕获。
业界争议:很多团队(包括 Spring)把受检异常包装成非受检(RuntimeException),避免方法签名上写一堆 throws。
4. 自定义异常
实际业务中,你需要定义自己的异常类(如 BusinessException、UserNotFoundException),携带错误码、错误信息:
// 1. 自定义异常:继承 Exception(受检)或 RuntimeException(非受检)
class BusinessException extends Exception {
private int code; // 业务错误码
public BusinessException(int code, String message) {
super(message);
this.code = code;
}
public int getCode() { return code; }
}
class InvalidAgeException extends RuntimeException {
public InvalidAgeException(String msg) {
super(msg);
}
}
public class CustomExceptionDemo {
// 抛出受检异常必须 throws 声明
public static void checkBalance(double balance) throws BusinessException {
if (balance < 0) {
throw new BusinessException(4001, "余额不能为负: " + balance);
}
System.out.println("余额正常: " + balance);
}
// RuntimeException 不必 throws
public static void checkAge(int age) {
if (age < 0 || age > 150) {
throw new InvalidAgeException("年龄不合理: " + age);
}
}
public static void main(String[] args) {
try {
checkBalance(-100); // 受检异常必须捕获或继续 throws
} catch (BusinessException e) {
System.out.println("[" + e.getCode() + "] " + e.getMessage());
}
try {
checkAge(200); // 抛 RuntimeException
} catch (InvalidAgeException e) {
System.out.println(e.getMessage());
}
}
}自定义异常的最佳实践:
- 提供有意义的构造方法:至少
(String message),常用(int code, String message)。 - 继承 Exception 还是 RuntimeException? 强制调用方处理用 Exception,灵活传递用 RuntimeException。Spring 体系全用 RuntimeException。
- 异常名以 Exception 结尾:符合命名约定。
- 异常携带上下文:错误码、出错字段、原始值,方便排查。
5. try-with-resources(Java 7+)
资源(文件流、数据库连接、网络 socket)用完必须关,否则资源泄漏。Java 7 引入 try-with-resources,自动关闭实现了 AutoCloseable 的资源:
import java.io.*;
public class TryWithResources {
// 老式写法(Java 7 之前):资源必须手动关闭,容易漏
public static void oldStyle() throws IOException {
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader("test.txt"));
System.out.println(reader.readLine());
} finally {
if (reader != null) {
try {
reader.close(); // 嵌套 try,丑陋
} catch (IOException e) {
// ignore
}
}
}
}
// Java 7+ try-with-resources:资源自动关闭(必须实现 AutoCloseable)
public static void newStyle() throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader("test.txt"))) {
System.out.println(reader.readLine());
} // 这里自动调 reader.close(),即使发生异常也会关
// 可以同时声明多个资源,用分号隔开
}
public static void main(String[] args) {
// 简单示例:读取标准输入
try (var reader = new java.io.StringReader("hello")) {
System.out.println((char) reader.read()); // h
} catch (IOException e) {
e.printStackTrace();
}
}
}强烈推荐所有 I/O 资源都用这种写法。它解决了三个老大难问题:
- 忘了关资源(代码丑陋但至少关闭了)。
- 异常情况下没关(try-with-resources 保证一定关)。
- 嵌套 try 的丑陋代码。
6. 异常处理的最佳实践
- 不要捕获后忽略(
catch (Exception e) {}):吞掉异常会让 bug 难以排查。至少e.printStackTrace()或记日志。 - 不要用异常做流程控制:异常处理性能差于正常 if-else 判断。
- 捕获具体异常,不要
catch (Exception)或catch (Throwable)一网打尽。 - 原始异常要保留:抛新异常时用
new XxxException("msg", e)传 cause,保留堆栈。 - finally 里别 return:会覆盖 try 的返回值,出诡异 bug。
- 记日志而非打印堆栈:生产环境用 SLF4J + Logback,
e.printStackTrace()只适合调试。
小结
这一章你掌握了 Java 异常处理的全部要点:try/catch/finally、throw/throws、异常体系、受检 vs 非受检、自定义异常、try-with-resources。下一篇我们看 Java IO——读文件、写文件、网络数据流。
← 上一篇 Java 集合框架
下一篇 Java 输入输出 →