💡 摘要:你是否曾在代码中看到密密麻麻的try-catch块却不知其意?是否对throws和throw的区别感到困惑?是否遇到过系统异常无法准确描述业务错误的尴尬?
别担心,异常处理是Java编程中不可或缺的错误处理机制,掌握它能让你的代码更健壮、更清晰。
本文将带你从异常的分类体系讲起,通过生动的比喻理解Checked Exception和Unchecked Exception的根本区别。
接着深入try-catch-finally的每个细节,通过真实案例学会如何正确处理异常。然后探索方法签名中的throws声明和手动throw异常的使用场景。
最后教你如何创建自定义异常来精准表达业务错误。从异常链到try-with-resources,从性能考虑到最佳实践,让你全面掌握Java异常处理的艺术。文末附面试高频问题解析,助你写出更可靠的代码。
一、异常体系:Java的错误处理哲学
1. 异常类层次结构
Java的所有异常都继承自Throwable类,主要分为两大体系:
java
Throwable (可抛出对象)
├── Error (错误):JVM系统错误,程序无法处理
│ ├── OutOfMemoryError:内存耗尽
│ ├── StackOverflowError:栈溢出
│ └── VirtualMachineError:虚拟机错误
│
└── Exception (异常):程序可以处理的异常
├── RuntimeException (运行时异常/非受检异常)
│ ├── NullPointerException:空指针异常
│ ├── IndexOutOfBoundsException:索引越界
│ ├── IllegalArgumentException:非法参数
│ └── ArithmeticException:算术异常
│
└── 其他Exception (受检异常)
├── IOException:输入输出异常
├── SQLException:数据库操作异常
├── FileNotFoundException:文件未找到
└── ClassNotFoundException:类未找到
2. 受检异常 vs 非受检异常
关键区别:
| 特性 | 受检异常 (Checked Exception) | 非受检异常 (Unchecked Exception) |
| 继承自 | Exception(不包括RuntimeException) | RuntimeException或Error |
| 处理要求 | 必须捕获或声明抛出 | 可选择性处理 |
| 发生时机 | 编译时检查 | 运行时发生 |
| 典型例子 | IOException, SQLException | NullPointerException, ArrayIndexOutOfBoundsException |
| 设计目的 | 可预期的异常情况 | 程序逻辑错误 |
🌰 代码示例:
java
// 受检异常:必须处理
public void readFile() {
try {
FileReader file = new FileReader("test.txt"); // 可能抛出FileNotFoundException
} catch (FileNotFoundException e) {
System.out.println("文件不存在: " + e.getMessage());
}
}
// 非受检异常:可选择处理
public void calculate() {
int result = 10 / 0; // 可能抛出ArithmeticException,但不强制处理
}
二、异常处理的三板斧:try-catch-finally
1. 基本语法结构
java
try {
// 可能抛出异常的代码
riskyOperation();
} catch (SpecificException e) {
// 处理特定异常
System.out.println("处理特定异常: " + e.getMessage());
} catch (GeneralException e) {
// 处理更一般的异常
System.out.println("处理一般异常: " + e.getMessage());
} finally {
// 无论是否发生异常都会执行
cleanupResources();
}
2. 多catch块的处理顺序
java
try {
int[] numbers = {1, 2, 3};
System.out.println(numbers[5]); // 可能抛出ArrayIndexOutOfBoundsException
String str = null;
System.out.println(str.length()); // 可能抛出NullPointerException
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("数组越界: " + e.getMessage());
} catch (NullPointerException e) {
System.out.println("空指针: " + e.getMessage());
} catch (Exception e) {
System.out.println("其他异常: " + e.getMessage()); // 兜底处理
}
⚠️ 重要规则:catch块必须从具体到一般,否则编译错误:
java
// 错误示例:父类catch块不能放在子类前面
try {
// some code
} catch (Exception e) { // 太一般了
// 这里会捕获所有异常,后面的catch块永远不会执行
} catch (IOException e) { // 编译错误:Unreachable catch block
}
3. finally块的特殊性
java
public class FinallyDemo {
public static void main(String[] args) {
try {
System.out.println("try块执行");
int result = 10 / 0; // 抛出异常
} catch (ArithmeticException e) {
System.out.println("catch块执行: " + e.getMessage());
return; // 即使这里return,finally也会执行
} finally {
System.out.println("finally块执行"); // 总会执行
}
}
}
输出:
text
try块执行
catch块执行: / by zero
finally块执行
三、异常抛出:throw vs throws
1. throw:手动抛出异常
java
public class Validation {
public void setAge(int age) {
if (age < 0 || age > 150) {
// 手动抛出异常
throw new IllegalArgumentException("年龄必须在0-150之间: " + age);
}
this.age = age;
}
public void processUser(String username) {
if (username == null || username.trim().isEmpty()) {
// 可以抛出自定义异常
throw new InvalidUserException("用户名不能为空");
}
}
}
2. throws:声明可能抛出的异常
java
public class FileProcessor {
// 声明可能抛出IOException
public String readFile(String filename) throws IOException {
FileReader reader = new FileReader(filename);
// 读取文件内容...
return content;
}
// 声明可能抛出多个异常
public void processData(String filepath)
throws IOException, SQLException {
String data = readFile(filepath); // 可能抛出IOException
saveToDatabase(data); // 可能抛出SQLException
}
}
3. 两者的区别
| 特性 | throw | throws |
| 作用 | 在方法体内手动抛出异常对象 | 在方法声明中指定可能抛出的异常类型 |
| 数量 | 一次只能抛出一个异常对象 | 可以声明多个异常类型 |
| 位置 | 方法内部 | 方法签名中 |
| 语法 | throw new Exception(); |
void method() throws Exception {} |
四、自定义异常:表达业务逻辑错误
1. 创建自定义受检异常
java
/**
* 自定义业务异常:受检异常
*/
public class InsufficientBalanceException extends Exception {
private double currentBalance;
private double amountRequired;
public InsufficientBalanceException(double current, double required) {
super("余额不足。当前余额: " + current + ", 需要: " + required);
this.currentBalance = current;
this.amountRequired = required;
}
public double getCurrentBalance() {
return currentBalance;
}
public double getAmountRequired() {
return amountRequired;
}
}
2. 创建自定义非受检异常
java
/**
* 自定义参数异常:非受检异常
*/
public class InvalidParameterException extends RuntimeException {
private String parameterName;
private Object parameterValue;
public InvalidParameterException(String name, Object value) {
super("参数'" + name + "'无效: " + value);
this.parameterName = name;
this.parameterValue = value;
}
public String getParameterName() {
return parameterName;
}
public Object getParameterValue() {
return parameterValue;
}
}
3. 使用自定义异常
java
public class BankAccount {
private double balance;
public void withdraw(double amount) throws InsufficientBalanceException {
if (amount <= 0) {
throw new InvalidParameterException("amount", amount); // 非受检异常
}
if (amount > balance) {
throw new InsufficientBalanceException(balance, amount); // 受检异常
}
balance -= amount;
}
}
// 调用处
public class BankService {
public void processWithdrawal(BankAccount account, double amount) {
try {
account.withdraw(amount);
System.out.println("取款成功");
} catch (InsufficientBalanceException e) {
System.out.println("取款失败: " + e.getMessage());
System.out.println("建议金额: " + e.getCurrentBalance());
}
// InvalidParameterException 不需要捕获(非受检异常)
}
}
五、高级特性与最佳实践
1. 异常链:保留原始异常信息
java
public class DataProcessor {
public void process(String filename) throws DataProcessingException {
try {
// 可能抛出IOException
String content = readFile(filename);
// 处理数据...
} catch (IOException e) {
// 包装原始异常,保留堆栈信息
throw new DataProcessingException("处理文件失败: " + filename, e);
}
}
private String readFile(String filename) throws IOException {
// 文件读取逻辑
throw new IOException("文件损坏");
}
}
// 自定义异常支持异常链
public class DataProcessingException extends Exception {
public DataProcessingException(String message) {
super(message);
}
public DataProcessingException(String message, Throwable cause) {
super(message, cause); // 保留原始异常
}
}
2. try-with-resources:自动资源管理
java
// JDK 7之前:繁琐的资源关闭
public void readFileOldWay(String filename) {
FileReader reader = null;
try {
reader = new FileReader(filename);
// 读取文件...
} catch (IOException e) {
e.printStackTrace();
} finally {
if (reader != null) {
try {
reader.close(); // 必须手动关闭
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
// JDK 7之后:try-with-resources
public void readFileNewWay(String filename) {
try (FileReader reader = new FileReader(filename);
BufferedReader br = new BufferedReader(reader)) {
// 自动资源管理,无需finally块
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
System.out.println("读取文件失败: " + e.getMessage());
}
// 资源会自动关闭,即使发生异常
}
3. 异常处理的最佳实践
- 具体异常:捕获最具体的异常类型
- 不要吞掉异常:至少记录异常信息
- 早抛出晚捕获:在合适的地方处理异常
- 使用描述性消息:提供有用的错误信息
- 避免空的catch块:这是最糟糕的做法
java
// 不好的做法:吞掉异常
try {
riskyOperation();
} catch (Exception e) {
// 什么都不做!异常被完全隐藏
}
// 好的做法:至少记录日志
try {
riskyOperation();
} catch (SpecificException e) {
log.error("操作失败,参数: {}", parameter, e);
throw new BusinessException("业务操作失败", e);
}
六、总结:异常处理的艺术
- 了解异常体系:分清Error、受检异常、非受检异常
- 合理使用try-catch-finally:确保资源正确释放
- 正确throw和throws:明确异常抛出责任
- 创建自定义异常:准确表达业务错误
- 遵循最佳实践:写出健壮可靠的代码
🚀 良好的异常处理不仅能提高程序稳定性,还能大大提升代码的可读性和可维护性。
七、面试高频问题
❓1. Error和Exception有什么区别?
答:
Error:JVM系统错误,程序无法处理(如OutOfMemoryError)Exception:程序可以处理的异常,分为受检异常和非受检异常
❓2. throw和throws的区别?
答:
throw:在方法体内手动抛出异常对象throws:在方法声明中指定可能抛出的异常类型
❓3. final、finally、finalize的区别?
答:
final:修饰符,表示不可改变(类、方法、变量)finally:异常处理块,总是会执行finalize:Object类的方法,垃圾回收前调用(已废弃)
❓4. 什么时候创建自定义异常?
答:当标准异常无法准确描述业务错误时,特别是:
- 需要携带特定的业务信息
- 需要区分类似的错误情况
- 需要定义特定的异常处理逻辑
❓5. try-with-resources有什么优势?
答:
- 自动资源管理,避免资源泄漏
- 代码更简洁,减少finally块
- 支持多个资源自动关闭
- 更好的异常处理(支持抑制异常)