异常处理
在程序运行过程中,总会遇到各种意外情况:文件不存在、网络中断、除数为零、数组越界……Java 提供了一套完善的异常处理机制来应对这些运行时错误,使程序更加健壮和可靠。
合理使用异常处理,可以将业务逻辑与错误处理代码分离,提高代码的可读性和可维护性。
1. 异常体系结构
Java 的异常体系以 Throwable 为根类,向下分为 Error 和 Exception 两大分支。
┌─────────────────────────────┐
│ Throwable │
└─────────────┬───────────────┘
│
┌─────────────────┴─────────────────┐
│ │
┌───────┴───────┐ ┌─────────┴─────────┐
│ Error │ │ Exception │
│ (错误) │ │ (异常) │
└───────┬───────┘ └─────────┬─────────┘
│ │
┌───────────┼───────────┐ ┌─────────┴─────────┐
│ │ │ │ │
OutOf StackOver ... │ ┌───────┴───────┐ ┌───────┴───────┐
Memory flow │ │ Checked │ │ Runtime │
Error Error │ │ Exception │ │ Exception │
│ │ (受检异常) │ │ (非受检异常) │
│ └───────────────┘ └───────────────┘// 继承关系示意代码
Throwable
├── Error // 严重问题,程序无法处理
│ ├── OutOfMemoryError
│ ├── StackOverflowError
│ └── ...
└── Exception // 程序可以处理的异常情况
├── IOException // 受检异常
├── SQLException // 受检异常
├── ClassNotFoundException // 受检异常
└── RuntimeException // 非受检异常(运行时异常)
├── NullPointerException
├── ArrayIndexOutOfBoundsException
├── ArithmeticException
└── ...2. Error vs Exception
| 特性 | Error(错误) | Exception(异常) |
|---|---|---|
| 严重程度 | 严重,通常无法恢复 | 较轻,可以捕获处理 |
| 是否需处理 | 不强制处理 | 受检异常必须处理 |
| 常见场景 | JVM 内部故障、资源耗尽 | 输入错误、逻辑错误、I/O 失败 |
| 典型例子 | OutOfMemoryError、StackOverflowError | NullPointerException、IOException |
public class ErrorVsExceptionDemo {
public static void main(String[] args) {
// ========== Error:通常不捕获,程序也无法恢复 ==========
// 递归导致栈溢出(StackOverflowError)
try {
causeStackOverflow();
} catch (StackOverflowError e) {
System.out.println("捕获到 Error:" + e);
// 即使捕获了,程序状态可能已经受损
}
System.out.println("程序继续执行...");
// ========== Exception:可以被合理处理 ==========
try {
int result = 10 / 0; // ArithmeticException
} catch (ArithmeticException e) {
System.out.println("捕获到 Exception:" + e.getMessage());
}
System.out.println("程序正常结束");
}
static void causeStackOverflow() {
causeStackOverflow(); // 无限递归
}
}关键区别总结:
- Error 代表 JVM 层面的严重问题,如内存耗尽、栈溢出。程序通常不应当捕获 Error,因为捕获后也无法恢复正常运行。
- Exception 代表程序运行中可预见的异常情况,可以通过合理的处理让程序继续执行或优雅退出。
3. 受检异常 vs 非受检异常
3.1 受检异常(Checked Exception)
受检异常是编译时就要处理的异常。如果代码中可能抛出受检异常,必须使用 try-catch 捕获或在方法签名中用 throws 声明,否则编译不通过。
特点:
- 继承自
Exception但不继承自RuntimeException - 编译器强制要求处理
- 代表可预见的、外部因素导致的异常
常见受检异常:
IOException— I/O 操作失败SQLException— 数据库访问错误ClassNotFoundException— 类未找到FileNotFoundException— 文件未找到InterruptedException— 线程中断
import java.io.*;
public class CheckedExceptionDemo {
public static void main(String[] args) {
// 编译错误:Unhandled exception: java.io.FileNotFoundException
// FileReader reader = new FileReader("test.txt");
// 方式一:try-catch 处理
try {
FileReader reader = new FileReader("test.txt");
System.out.println("文件打开成功");
} catch (FileNotFoundException e) {
System.out.println("文件未找到:" + e.getMessage());
}
// 方式二:方法签名 throws 声明(交给调用者处理)
try {
readFile("test.txt");
} catch (IOException e) {
System.out.println("读取文件异常:" + e.getMessage());
}
}
// 用 throws 声明受检异常,由调用者处理
public static void readFile(String path) throws IOException {
FileReader reader = new FileReader(path);
// 其他操作...
}
}3.2 非受检异常(Unchecked / RuntimeException)
非受检异常在编译时不强制处理,可以在运行时自行决定是否捕获。
特点:
- 继承自
RuntimeException - 编译器不强制处理
- 通常代表程序中的逻辑缺陷(bug)
常见非受检异常:
NullPointerException— 空指针引用ArrayIndexOutOfBoundsException— 数组越界ArithmeticException— 算术异常(如除零)ClassCastException— 类型转换失败IllegalArgumentException— 非法参数NumberFormatException— 数字格式错误
public class UncheckedExceptionDemo {
public static void main(String[] args) {
// 非受检异常:编译通过,运行时报错
// 可以选择捕获,也可以不捕获
String str = null;
// 下面这行会抛出 NullPointerException,编译完全通过
// str.length(); // NullPointerException
// 主动捕获非受检异常
try {
int result = 10 / 0; // ArithmeticException
System.out.println("结果:" + result);
} catch (ArithmeticException e) {
System.out.println("除零异常:" + e.getMessage());
}
// 也可以完全不处理,JVM 会终止程序并打印堆栈信息
int[] arr = new int[3];
// arr[5] = 100; // ArrayIndexOutOfBoundsException,不捕获则程序崩溃
}
}3.3 对比总结
| 对比维度 | 受检异常(Checked) | 非受检异常(Unchecked) |
|---|---|---|
| 继承关系 | Exception 的子类(非 RuntimeException) | RuntimeException 的子类 |
| 编译检查 | 强制处理(try-catch 或 throws) | 不强制处理 |
| 触发原因 | 外部环境问题(I/O、网络、数据库等) | 程序逻辑缺陷(bug) |
| 处理方式 | 必须捕获或声明抛出 | 可捕获也可忽略 |
| 典型例子 | IOException、SQLException | NullPointerException、ArithmeticException |
4. try-catch-finally
4.1 基本语法
try {
// 可能抛出异常的代码
} catch (异常类型1 变量名) {
// 处理异常类型1
} catch (异常类型2 变量名) {
// 处理异常类型2
} finally {
// 无论是否发生异常,都会执行的代码(可选)
}4.2 多种 catch 组合
import java.io.*;
public class MultiCatchDemo {
public static void main(String[] args) {
try {
// 模拟多种异常场景
int[] numbers = {1, 2, 3};
int index = 5;
int divisor = 0;
// 可能抛出 ArrayIndexOutOfBoundsException
int value = numbers[index];
// 可能抛出 ArithmeticException
int result = value / divisor;
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("数组越界:" + e.getMessage());
} catch (ArithmeticException e) {
System.out.println("算术异常:" + e.getMessage());
} catch (Exception e) {
// 兜底捕获,必须放在最后
System.out.println("其他异常:" + e.getClass().getName());
}
}
}Java 7+ 多异常合并捕获:
public class MultiCatchMergeDemo {
public static void main(String[] args) {
try {
int[] arr = new int[3];
// arr[5] = 10; // ArrayIndexOutOfBoundsException
int x = 10 / 0; // ArithmeticException
} catch (ArrayIndexOutOfBoundsException | ArithmeticException e) {
// 使用 | 合并多个异常类型
System.out.println("发生异常:" + e.getClass().getSimpleName());
System.out.println("异常信息:" + e.getMessage());
}
}
}4.3 执行顺序与 finally 关键细节
finally 块中的代码无论是否抛出异常都会执行。一个非常重要的细节是:finally 块在 return 之前执行。
public class FinallyOrderDemo {
public static void main(String[] args) {
System.out.println("测试1:" + test1());
System.out.println("---");
System.out.println("测试2:" + test2());
System.out.println("---");
System.out.println("测试3:" + test3());
}
// finally 在 return 之前执行
static String test1() {
try {
System.out.println("try 块执行");
return "try 的返回值";
} finally {
System.out.println("finally 块执行 —— 在 return 之前!");
}
}
// finally 在 catch 的 return 之前执行
static String test2() {
try {
System.out.println("try 块执行");
int x = 10 / 0; // 抛出异常
return "try 的返回值"; // 不会执行
} catch (ArithmeticException e) {
System.out.println("catch 块执行");
return "catch 的返回值";
} finally {
System.out.println("finally 块执行 —— 在 catch 的 return 之前!");
}
}
// finally 中可以修改返回值(基本类型无效,引用类型有效)
@SuppressWarnings("ResultOfMethodCallIgnored")
static StringBuilder test3() {
StringBuilder sb = new StringBuilder("Hello");
try {
return sb;
} finally {
// 引用类型:finally 中修改对象内容会生效
sb.append(" World");
System.out.println("finally 中修改了 sb");
}
}
}输出结果:
try 块执行
finally 块执行 —— 在 return 之前!
测试1:try 的返回值
---
try 块执行
catch 块执行
finally 块执行 —— 在 catch 的 return 之前!
测试2:catch 的返回值
---
try 块执行
finally 中修改了 sb
测试3:Hello World重要注意事项:
不要在
finally中使用return,因为它会覆盖try或catch中的返回值,并且会抑制异常。
public class FinallyReturnAntiPattern {
public static void main(String[] args) {
System.out.println(badPractice()); // 输出 100,而不是 10
}
@SuppressWarnings("all")
static int badPractice() {
try {
int x = 10 / 0; // 抛出 ArithmeticException
return 10;
} catch (ArithmeticException e) {
return 10; // 这里应该返回 10
} finally {
return 100; // 覆盖了 catch 中的 return!异常也被抑制
}
}
}5. try-with-resources
传统方式中,我们需要在 finally 中手动关闭资源(如文件流、数据库连接),代码冗长且容易遗漏。
5.1 传统方式 vs try-with-resources
传统方式(Java 7 之前):
import java.io.*;
public class TraditionalResourceDemo {
public static void main(String[] args) {
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader("test.txt"));
String line = reader.readLine();
System.out.println(line);
} catch (IOException e) {
System.out.println("读取异常:" + e.getMessage());
} finally {
// 手动关闭资源,还要处理空指针
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
System.out.println("关闭资源异常:" + e.getMessage());
}
}
}
}
}try-with-resources 方式(Java 7+):
import java.io.*;
public class TryWithResourcesDemo {
public static void main(String[] args) {
// 在 try 的 () 中声明资源,自动实现 close()
try (BufferedReader reader = new BufferedReader(new FileReader("test.txt"));
BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))) {
String line = reader.readLine();
writer.write(line);
System.out.println("读写完成:" + line);
} catch (IOException e) {
System.out.println("I/O 异常:" + e.getMessage());
}
// 无需 finally 手动关闭,资源会自动关闭
}
}5.2 AutoCloseable 接口
try-with-resources 要求资源类必须实现 AutoCloseable 或 Closeable 接口。这两个接口都定义了 close() 方法。
// 自定义 AutoCloseable 资源
public class MyResource implements AutoCloseable {
private final String name;
public MyResource(String name) {
this.name = name;
System.out.println(name + " 资源打开");
}
public void doSomething() {
System.out.println(name + " 执行操作");
}
@Override
public void close() {
System.out.println(name + " 资源关闭");
}
}
// 测试 try-with-resources
class MyResourceTest {
public static void main(String[] args) {
// 资源关闭顺序与声明顺序相反(后声明的先关闭)
try (MyResource r1 = new MyResource("资源1");
MyResource r2 = new MyResource("资源2")) {
r1.doSomething();
r2.doSomething();
System.out.println("--- 操作完成,即将自动关闭 ---");
} // 自动调用 close(),顺序:r2.close() → r1.close()
}
}输出结果:
资源1 资源打开
资源2 资源打开
资源1 执行操作
资源2 执行操作
--- 操作完成,即将自动关闭 ---
资源2 资源关闭
资源1 资源关闭6. throw vs throws
throw 和 throws 虽然拼写相似,但用法和含义完全不同。
| 关键字 | 位置 | 作用 |
|---|---|---|
throw | 方法体内部 | 抛出一个异常对象 |
throws | 方法签名末尾 | 声明该方法可能抛出的异常类型 |
6.1 throw:抛出异常
throw 用于在代码中主动抛出一个异常实例,通常用于条件不满足时主动中断程序执行。
public class ThrowDemo {
public static void main(String[] args) {
try {
checkAge(-5);
} catch (IllegalArgumentException e) {
System.out.println("捕获到异常:" + e.getMessage());
}
try {
registerUser(null);
} catch (NullPointerException e) {
System.out.println("捕获到异常:" + e.getMessage());
}
}
// 使用 throw 主动抛出非受检异常
public static void checkAge(int age) {
if (age < 0) {
throw new IllegalArgumentException("年龄不能为负数:" + age);
}
System.out.println("年龄合法:" + age);
}
// 使用 throw 主动抛出受检异常(需在方法签名中声明)
public static void registerUser(String username) {
if (username == null) {
throw new NullPointerException("用户名不能为空");
}
System.out.println("注册用户:" + username);
}
}6.2 throws:声明异常
throws 出现在方法签名中,告诉调用者:这个方法可能抛出这些异常,请自行处理。
import java.io.*;
public class ThrowsDemo {
public static void main(String[] args) {
// 调用声明了 throws 的方法,必须处理异常
try {
readConfig("config.properties");
} catch (IOException e) {
System.out.println("读取配置失败:" + e.getMessage());
}
// 也可以继续向上 throws
try {
loadData();
} catch (Exception e) {
System.out.println("加载数据失败:" + e.getMessage());
}
}
// 声明可能抛出受检异常
public static String readConfig(String filename) throws IOException {
BufferedReader reader = new BufferedReader(new FileReader(filename));
String content = reader.readLine();
reader.close();
return content;
}
// 可以声明多个异常类型
public static void loadData() throws FileNotFoundException, IOException {
// 可能抛出 FileNotFoundException
FileInputStream fis = new FileInputStream("data.dat");
// 可能抛出 IOException
int data = fis.read();
fis.close();
}
}6.3 throw 和 throws 对比
public class ThrowVsThrowsDemo {
// throws:声明方法可能抛出异常
public static void validate(int score) throws IllegalArgumentException {
if (score < 0 || score > 100) {
// throw:实际抛出异常
throw new IllegalArgumentException("分数必须在 0-100 之间:" + score);
}
System.out.println("分数合法:" + score);
}
public static void main(String[] args) {
try {
validate(101);
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage());
}
}
}7. 自定义异常
当 Java 标准库中的异常类型无法准确描述业务场景时,可以自定义异常。
7.1 自定义受检异常
// 自定义受检异常:继承 Exception
public class InsufficientBalanceException extends Exception {
// 无参构造
public InsufficientBalanceException() {
super("余额不足");
}
// 带消息的构造
public InsufficientBalanceException(String message) {
super(message);
}
// 带消息和原因的构造
public InsufficientBalanceException(String message, Throwable cause) {
super(message, cause);
}
}7.2 自定义非受检异常
// 自定义非受检异常:继承 RuntimeException
public class InvalidOrderException extends RuntimeException {
private final String orderId;
public InvalidOrderException(String orderId, String message) {
super(message);
this.orderId = orderId;
}
public InvalidOrderException(String orderId, String message, Throwable cause) {
super(message, cause);
this.orderId = orderId;
}
public String getOrderId() {
return orderId;
}
}7.3 使用自定义异常
// 银行账户类,演示自定义异常的使用
class BankAccount {
private String accountId;
private double balance;
public BankAccount(String accountId, double balance) {
this.accountId = accountId;
this.balance = balance;
}
// 使用自定义受检异常
public void withdraw(double amount) throws InsufficientBalanceException {
if (amount <= 0) {
throw new IllegalArgumentException("提款金额必须大于 0");
}
if (amount > balance) {
throw new InsufficientBalanceException(
"账户 " + accountId + " 余额不足:需要 " + amount + ",可用 " + balance
);
}
balance -= amount;
System.out.println("提款成功:" + amount + ",剩余余额:" + balance);
}
// 使用自定义非受检异常
public void processOrder(String orderId) {
if (orderId == null || orderId.isEmpty()) {
throw new InvalidOrderException(orderId, "订单号不能为空");
}
System.out.println("处理订单:" + orderId);
}
public double getBalance() {
return balance;
}
}
// 测试类
class CustomExceptionTest {
public static void main(String[] args) {
BankAccount account = new BankAccount("AC10086", 500.0);
// 测试受检异常
try {
account.withdraw(600); // 余额不足
} catch (InsufficientBalanceException e) {
System.out.println("业务异常:" + e.getMessage());
}
// 测试非受检异常
try {
account.processOrder(""); // 非法订单
} catch (InvalidOrderException e) {
System.out.println("订单异常:" + e.getMessage() + ",订单号:" + e.getOrderId());
}
}
}输出结果:
业务异常:账户 AC10086 余额不足:需要 600.0,可用 500.0
订单异常:订单号不能为空,订单号:8. 常见异常及原因
8.1 NullPointerException(空指针异常)
最常见的异常之一,当调用 null 对象的方法或访问其属性时抛出。
public class NullPointerExceptionDemo {
public static void main(String[] args) {
String str = null;
try {
// 调用 null 对象的方法
str.length();
} catch (NullPointerException e) {
System.out.println("NullPointerException:" + e.getMessage());
}
// 常见场景:数组元素未初始化
String[] arr = new String[3];
try {
arr[0].equals("test"); // arr[0] 为 null
} catch (NullPointerException e) {
System.out.println("数组元素为 null:" + e.getMessage());
}
}
}预防方式: 使用前进行 null 检查,使用 Optional 类,或遵循"不要返回 null"的原则。
8.2 ArrayIndexOutOfBoundsException(数组越界)
访问数组或集合中不存在的索引时抛出。
public class ArrayIndexOutOfBoundsDemo {
public static void main(String[] args) {
int[] numbers = {10, 20, 30}; // 有效索引:0, 1, 2
try {
System.out.println(numbers[5]); // 索引 5 不存在
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("ArrayIndexOutOfBoundsException:索引 5 超出范围,有效范围 0-" + (numbers.length - 1));
}
// 预防方式:检查索引范围
int index = 5;
if (index >= 0 && index < numbers.length) {
System.out.println(numbers[index]);
} else {
System.out.println("索引 " + index + " 无效");
}
}
}8.3 ClassCastException(类型转换异常)
试图将对象强制转换为其实例不兼容的类型时抛出。
public class ClassCastExceptionDemo {
public static void main(String[] args) {
Object obj = "Hello, Java!";
try {
// String 不能转换为 Integer
Integer num = (Integer) obj;
} catch (ClassCastException e) {
System.out.println("ClassCastException:无法将 String 转换为 Integer");
}
// 预防方式:使用 instanceof 检查
if (obj instanceof Integer) {
Integer num = (Integer) obj;
System.out.println("转换成功:" + num);
} else {
System.out.println("obj 不是 Integer 类型,实际类型:" + obj.getClass().getName());
}
}
}8.4 IllegalArgumentException(非法参数异常)
方法接收到不合法或不适当的参数时抛出。
public class IllegalArgumentExceptionDemo {
public static void setAge(int age) {
if (age < 0 || age > 150) {
throw new IllegalArgumentException("年龄必须在 0-150 之间:" + age);
}
System.out.println("设置年龄:" + age);
}
public static void main(String[] args) {
try {
setAge(-1);
} catch (IllegalArgumentException e) {
System.out.println("IllegalArgumentException:" + e.getMessage());
}
try {
setAge(200);
} catch (IllegalArgumentException e) {
System.out.println("IllegalArgumentException:" + e.getMessage());
}
setAge(25); // 合法参数
}
}8.5 ArithmeticException(算术异常)
当出现不合法的算术运算时抛出,最常见的是整数除零。
public class ArithmeticExceptionDemo {
public static void main(String[] args) {
try {
int result = 10 / 0; // 整数除零
} catch (ArithmeticException e) {
System.out.println("ArithmeticException:" + e.getMessage());
}
// 注意:浮点数除零不会抛出异常,会返回 Infinity 或 NaN
double d1 = 10.0 / 0;
double d2 = 0.0 / 0;
System.out.println("浮点数除零结果:" + d1 + ", " + d2);
// 预防方式:除之前检查除数
int divisor = 0;
if (divisor != 0) {
int result = 10 / divisor;
} else {
System.out.println("除数不能为零");
}
}
}8.6 NumberFormatException(数字格式异常)
当试图将字符串转换为数值类型,但字符串格式不正确时抛出。
public class NumberFormatExceptionDemo {
public static void main(String[] args) {
String validNumber = "123";
String invalidNumber = "abc";
String mixedNumber = "12.34"; // 整数解析会失败
// 正确转换
int num1 = Integer.parseInt(validNumber);
System.out.println("正确转换:" + num1);
// 非法格式
try {
int num2 = Integer.parseInt(invalidNumber);
} catch (NumberFormatException e) {
System.out.println("NumberFormatException:\"" + invalidNumber + "\" 不是有效的整数格式");
}
// 混合格式
try {
int num3 = Integer.parseInt(mixedNumber);
} catch (NumberFormatException e) {
System.out.println("NumberFormatException:\"" + mixedNumber + "\" 包含小数点,不是整数");
}
// 安全转换方式
String input = "abc";
try {
int value = Integer.parseInt(input);
System.out.println("值:" + value);
} catch (NumberFormatException e) {
System.out.println("输入 \"" + input + "\" 无法转换为整数,使用默认值 0");
int value = 0; // 默认值
}
}
}8.7 OutOfMemoryError(内存溢出错误)
当 JVM 堆内存耗尽且垃圾回收器无法回收足够空间时抛出。这是一种 Error,通常不捕获。
import java.util.ArrayList;
import java.util.List;
public class OutOfMemoryErrorDemo {
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
try {
// 不断分配大对象,耗尽堆内存
while (true) {
list.add(new byte[1024 * 1024]); // 每次分配 1MB
}
} catch (OutOfMemoryError e) {
System.out.println("OutOfMemoryError:堆内存不足");
System.out.println("已分配 " + list.size() + " MB 后溢出");
}
System.out.println("捕获后程序继续执行(但不建议这样做)");
}
}注意: 实际项目中不建议捕获
OutOfMemoryError,因为 JVM 可能已经处于不稳定状态。正确的做法是通过 JVM 参数(-Xmx、-Xms)调整堆大小,或优化代码避免内存泄漏。
8.8 StackOverflowError(栈溢出错误)
当线程栈空间耗尽时抛出,通常由无限递归导致。
public class StackOverflowErrorDemo {
private static int depth = 0;
// 无限递归导致栈溢出
public static void infiniteRecursion() {
depth++;
infiniteRecursion();
}
public static void main(String[] args) {
try {
infiniteRecursion();
} catch (StackOverflowError e) {
System.out.println("StackOverflowError:递归深度达到 " + depth + " 层后溢出");
}
System.out.println("程序继续执行...");
}
}| 异常/错误 | 类型 | 常见原因 | 预防措施 |
|---|---|---|---|
NullPointerException | RuntimeException | 调用 null 对象的成员 | 判空检查、使用 Optional |
ArrayIndexOutOfBoundsException | RuntimeException | 访问数组越界索引 | 检查索引范围,使用增强 for 循环 |
ClassCastException | RuntimeException | 强制类型转换不兼容 | 使用 instanceof 检查类型 |
IllegalArgumentException | RuntimeException | 传入非法参数 | 在方法入口校验参数 |
ArithmeticException | RuntimeException | 整数除零 | 除法前检查除数 |
NumberFormatException | RuntimeException | 字符串转数值格式错误 | 使用正则或 try-catch 校验格式 |
OutOfMemoryError | Error | JVM 堆内存耗尽 | 调整 JVM 参数,修复内存泄漏 |
StackOverflowError | Error | 栈空间耗尽(无限递归) | 检查递归终止条件,改用迭代 |
9. 最佳实践总结
9.1 异常处理原则
- 精确捕获,不要吞异常 — 捕获特定的异常类型,而不是笼统地捕获
Exception。不要写空的catch块。 - 早抛出,晚捕获 — 在发现异常的地方立即抛出,在合适的层次统一处理。
- 使用自定义异常表达业务语义 — 让异常更有意义,便于理解和处理。
- 优先使用 try-with-resources — 避免资源泄漏,代码更简洁。
- 不要使用异常控制正常流程 — 异常处理的性能开销较大,不应替代条件判断。
9.2 反例 vs 正例
// ❌ 反例:捕获了异常却什么也不做
try {
int x = 10 / 0;
} catch (Exception e) {
// 空的 catch —— 异常被吞掉,难以排查问题
}
// ❌ 反例:捕获过于宽泛
try {
// 一些操作
} catch (Throwable t) {
// 连 Error 都捕获了,不恰当
}
// ❌ 反例:用异常控制流程
try {
int i = Integer.parseInt(userInput);
// 处理数字
} catch (NumberFormatException e) {
// 处理非数字
}
// 正解:先用 isDigit 等检查格式
// ✅ 正例:精确捕获,记录日志或给出友好提示
try {
FileInputStream fis = new FileInputStream("config.properties");
} catch (FileNotFoundException e) {
System.err.println("配置文件未找到,使用默认配置:" + e.getMessage());
}
// ✅ 正例:使用 try-with-resources 自动关闭资源
try (Connection conn = getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
// 处理结果
}
} catch (SQLException e) {
System.err.println("数据库操作失败:" + e.getMessage());
}小结
Java 的异常处理机制是编写健壮程序的重要保障。本文从异常体系结构出发,依次介绍了:
- 异常体系结构:
Throwable → Error / Exception → RuntimeException的层次关系 - Error vs Exception:Error 代表 JVM 层面的严重问题,Exception 代表程序可处理的异常
- 受检异常 vs 非受检异常:受检异常编译时必须处理,非受检异常可选择性处理
- try-catch-finally:捕获和处理异常的基本语法,注意 finally 在 return 之前执行
- try-with-resources:Java 7+ 的自动资源管理机制,需实现 AutoCloseable 接口
- throw vs throws:throw 抛出异常实例,throws 声明异常类型
- 自定义异常:通过继承 Exception 或 RuntimeException 定义业务相关异常
- 常见异常:8 种常见异常/错误的触发原因和预防方式
掌握异常处理不仅仅是学会语法,更重要的是理解何时使用哪种机制,以及如何写出清晰、健壮的异常处理代码。正确使用异常处理,能让程序在面对意外情况时从容应对,而不是直接崩溃。