Appearance
第13课:异常处理
🎯 学习目标
- 理解异常的概念和分类
- 掌握 try-catch-finally 语法
- 理解 throws 和 throw 的使用
- 掌握自定义异常
- 理解异常链
📖 一、异常是什么?
C vs Java 异常处理
C 语言:
c
// C: 返回错误码
int divide(int a, int b) {
if (b == 0) {
return -1; // 错误码
}
return a / b;
}
int result = divide(10, 0);
if (result == -1) {
printf("Error: division by zero\n");
}Java:
java
// Java: 抛出异常
public int divide(int a, int b) {
if (b == 0) {
throw new ArithmeticException("除数不能为零");
}
return a / b;
}
try {
int result = divide(10, 0);
} catch (ArithmeticException e) {
System.out.println("捕获异常: " + e.getMessage());
}📖 二、异常体系结构
Throwable
├── Error(错误,程序无法处理)
│ ├── OutOfMemoryError
│ ├── StackOverflowError
│ └── VirtualMachineError
└── Exception(异常,程序可以处理)
├── 受检异常(Checked Exception)- 编译时检查
│ ├── IOException
│ ├── SQLException
│ ├── ClassNotFoundException
│ └── FileNotFoundException
└── 非受检异常(Unchecked Exception)- 运行时异常
├── RuntimeException
├── NullPointerException
├── ArrayIndexOutOfBoundsException
├── ArithmeticException
└── IllegalArgumentException受检异常 vs 非受检异常
受检异常(必须处理):
java
// ✅ 方式1:try-catch
public void readFile() {
try {
FileReader reader = new FileReader("file.txt");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
// ✅ 方式2:throws 声明
public void readFile() throws FileNotFoundException {
FileReader reader = new FileReader("file.txt");
}非受检异常(可选处理):
java
// 不强制处理,但最好处理
public int divide(int a, int b) {
return a / b; // 可能抛 ArithmeticException
}📖 三、try-catch-finally
1. 基本语法
java
try {
// 可能抛出异常的代码
int result = 10 / 0;
} catch (ArithmeticException e) {
// 捕获并处理异常
System.out.println("发生异常: " + e.getMessage());
} finally {
// 无论是否异常,都会执行
System.out.println("清理资源");
}2. 多个 catch 块
java
try {
String str = null;
System.out.println(str.length()); // NullPointerException
int[] arr = new int[5];
arr[10] = 100; // ArrayIndexOutOfBoundsException
} catch (NullPointerException e) {
System.out.println("空指针异常");
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("数组越界");
} catch (Exception e) {
// 捕获所有其他异常(必须放在最后)
System.out.println("其他异常: " + e);
}3. 多异常合并捕获(Java 7+)
java
try {
// ...
} catch (NullPointerException | ArrayIndexOutOfBoundsException e) {
System.out.println("空指针或数组越界: " + e);
}4. finally 的执行时机
java
public static int test() {
try {
return 1;
} catch (Exception e) {
return 2;
} finally {
return 3; // ⚠️ finally 中的 return 会覆盖 try/catch 的 return
}
}
System.out.println(test()); // 输出 3⚠️ 注意:
- finally 在 return 之前执行
- finally 中不要写 return(会覆盖前面的返回值)
- finally 中不要抛异常(会吞掉原异常)
5. try-with-resources(Java 7+)
java
// 传统写法:手动关闭资源
FileReader reader = null;
try {
reader = new FileReader("file.txt");
// 读取文件
} catch (IOException e) {
e.printStackTrace();
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// ✅ 新写法:自动关闭资源
try (FileReader reader = new FileReader("file.txt")) {
// 读取文件
} catch (IOException e) {
e.printStackTrace();
}
// reader 自动关闭,无需手动 close要求:资源类必须实现 AutoCloseable 接口
📖 四、throws 和 throw
1. throws - 声明异常
java
// 在方法签名中声明可能抛出的异常
public void readFile(String path) throws FileNotFoundException, IOException {
FileReader reader = new FileReader(path); // 可能抛 FileNotFoundException
reader.read(); // 可能抛 IOException
}
// 调用者必须处理
public void caller() {
try {
readFile("test.txt");
} catch (FileNotFoundException e) {
System.out.println("文件未找到");
} catch (IOException e) {
System.out.println("IO错误");
}
}2. throw - 抛出异常
java
public void setAge(int age) {
if (age < 0 || age > 150) {
throw new IllegalArgumentException("年龄不合法: " + age);
}
this.age = age;
}
// 使用
try {
setAge(-5);
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage()); // "年龄不合法: -5"
}3. throws vs throw
| 关键字 | 位置 | 作用 | 示例 |
|---|---|---|---|
| throws | 方法签名 | 声明可能抛出的异常 | void method() throws IOException |
| throw | 方法体内 | 主动抛出异常 | throw new Exception("错误") |
📖 五、自定义异常
1. 创建自定义异常
java
// 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);
}
}
// 2. 继承 RuntimeException(非受检异常)
public class UserNotFoundException extends RuntimeException {
private Long userId;
public UserNotFoundException(Long userId) {
super("用户不存在: " + userId);
this.userId = userId;
}
public Long getUserId() {
return userId;
}
}2. 使用自定义异常
java
public class BankAccount {
private double balance;
public void withdraw(double amount) throws InsufficientBalanceException {
if (amount > balance) {
throw new InsufficientBalanceException("余额不足,当前余额: " + balance);
}
balance -= amount;
}
}
// 使用
BankAccount account = new BankAccount();
try {
account.withdraw(1000);
} catch (InsufficientBalanceException e) {
System.out.println(e.getMessage());
}📖 六、异常链
java
public class ExceptionChainDemo {
public void method1() throws Exception {
try {
int result = 10 / 0;
} catch (ArithmeticException e) {
// 包装原异常,抛出新异常
throw new Exception("方法1发生错误", e);
}
}
public void method2() throws Exception {
try {
method1();
} catch (Exception e) {
// 继续包装
throw new Exception("方法2发生错误", e);
}
}
}
// 使用
try {
new ExceptionChainDemo().method2();
} catch (Exception e) {
e.printStackTrace();
// 输出:
// java.lang.Exception: 方法2发生错误
// Caused by: java.lang.Exception: 方法1发生错误
// Caused by: java.lang.ArithmeticException: / by zero
}📖 七、异常信息获取
java
try {
int result = 10 / 0;
} catch (ArithmeticException e) {
// 1. 异常消息
String message = e.getMessage(); // "/ by zero"
// 2. 异常类型
String className = e.getClass().getName(); // "java.lang.ArithmeticException"
// 3. 堆栈跟踪
e.printStackTrace(); // 打印完整堆栈
// 4. 获取堆栈元素
StackTraceElement[] stackTrace = e.getStackTrace();
for (StackTraceElement element : stackTrace) {
System.out.println(element.getClassName());
System.out.println(element.getMethodName());
System.out.println(element.getLineNumber());
}
// 5. 原因异常
Throwable cause = e.getCause();
}📖 八、常见异常
| 异常 | 原因 | 示例 |
|---|---|---|
| NullPointerException | 空指针 | String s = null; s.length(); |
| ArrayIndexOutOfBoundsException | 数组越界 | int[] arr = {1,2}; arr[5]; |
| ClassCastException | 类型转换错误 | Object obj = "str"; Integer i = (Integer)obj; |
| NumberFormatException | 数字格式错误 | Integer.parseInt("abc"); |
| ArithmeticException | 算术错误 | 10 / 0; |
| IllegalArgumentException | 非法参数 | setAge(-5); |
| FileNotFoundException | 文件未找到 | new FileReader("notexist.txt"); |
| IOException | IO错误 | 文件读写错误 |
| SQLException | SQL错误 | 数据库操作错误 |
⚠️ 常见陷阱
陷阱1:吞掉异常
捕获异常后什么都不做,会让问题消失在日志之外,后续排查非常困难。
陷阱2:用异常控制正常流程
异常适合表达异常情况,不适合替代 if 判断。高频流程里抛异常还会带来性能和可读性问题。
陷阱3:丢失异常链
重新抛异常时不传入 cause,会丢失根因。
java
throw new BusinessException("处理失败", e);陷阱4:finally 中 return
finally 中 return 会覆盖 try/catch 的返回值或异常,生产代码应避免。
🆚 Java vs C 对比
| 特性 | C | Java |
|---|---|---|
| 错误表达 | 返回码、errno | 异常对象 |
| 传播机制 | 手动层层返回 | 调用栈自动传播 |
| 资源清理 | goto cleanup / 手动 close | try-with-resources |
| 错误上下文 | 需要手工拼接 | 异常链和堆栈 |
对 C 程序员来说,Java 异常的优势是自动传播和保留堆栈;代价是不能滥用,尤其不能把业务分支写成异常驱动。
💡 最佳实践
1. 不要捕获 Exception
java
// ❌ 不推荐:捕获范围太大
try {
// ...
} catch (Exception e) {
e.printStackTrace();
}
// ✅ 推荐:捕获具体异常
try {
// ...
} catch (FileNotFoundException e) {
// 处理文件未找到
} catch (IOException e) {
// 处理IO错误
}2. 不要吞掉异常
java
// ❌ 错误:空catch块
try {
// ...
} catch (Exception e) {
// 什么都不做,异常被吞掉
}
// ✅ 正确:至少记录日志
try {
// ...
} catch (Exception e) {
logger.error("发生异常", e);
}3. 异常要有意义
java
// ❌ 不好
throw new Exception("错误");
// ✅ 好
throw new InsufficientBalanceException("余额不足,需要: " + amount + ", 当前: " + balance);4. 早检查,晚捕获
java
// ✅ 在方法开始就检查参数
public void transfer(Account from, Account to, double amount) {
if (from == null || to == null) {
throw new IllegalArgumentException("账户不能为null");
}
if (amount <= 0) {
throw new IllegalArgumentException("金额必须大于0");
}
// ...
}5. 使用 try-with-resources
java
// ✅ 推荐
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement()) {
// 使用连接
} // 自动关闭,即使发生异常📝 练习
完成 练习/Ex13_Exception.java:
- try-catch-finally 基本使用
- 多异常捕获
- 自定义异常
- 异常链
- try-with-resources
- 综合:银行转账系统(含异常处理)
🎓 下一步
- 第14课:集合框架 - List - ArrayList、LinkedList、Vector