异常机制
面向对象程序设计(Java)
掌握异常类型、抛出与处理、断言;编写更健壮的程序为什么需要异常?
理想情况下,程序会按预期运行;现实中会遇到不可预料的输入与状态。异常用于在运行期报告问题,并提供优雅的处理路径。
public static void main(String[] args) {
test(1, 0); // 当 b 为 0 时会怎样?
}
private static int test(int a, int b){
return a / b; // 未进行判断,运行期可能抛出异常
}
异常是"程序遇到非正常情况时抛出的对象",由JVM或我们手动抛出。
学会识别与处理它,程序就能在出错后继续有序运行。
异常体系概览
Error(错误)
严重系统问题,无法恢复
如:OutOfMemoryError
Checked Exception
编译器强制处理
如:IOException
RuntimeException
编程逻辑错误
如:NullPointerException
所有异常都继承自 Throwable。了解类型有助于选择合适的处理策略。
Checked vs Unchecked 异常
Checked Exceptions
- 编译器强制处理
- 可预期的外部问题
- 必须捕获或声明throws
- 示例:IOException, SQLException
Unchecked Exceptions
- 编译器不强制处理
- 程序逻辑错误
- 可通过编码避免
- 示例:NullPointerException, IndexOutOfBoundsException
"check"指的是编译器检查。Checked异常是契约,Unchecked是提醒。
抛出异常:及时失败
当检测到非法参数或状态时,立即抛出异常并提供清晰的错误信息。
public static int divide(int a, int b) {
if (b == 0) {
throw new IllegalArgumentException("被除数不能为 0");
}
return a / b;
}
public void processAge(int age) {
if (age < 0 || age > 150) {
throw new IllegalArgumentException("年龄必须在 0-150 之间,当前:" + age);
}
// 处理逻辑
}
最佳实践:
- 抛出最具体的异常类型
- 提供清晰、有用的错误消息
- 在问题源头及时抛出
方法声明:throws 契约
当方法可能抛出Checked异常时,必须在方法签名中声明。
// 文件读取方法
public String readFile(String filename) throws IOException {
return Files.readString(Paths.get(filename));
}
// 网络请求方法
public void sendRequest(String url) throws IOException, TimeoutException {
// 网络操作代码
}
// 多个异常声明
public void process() throws IOException, SQLException, ParseException {
// 可能抛出多种异常的操作
}
throws 为调用者提供明确契约:该方法可能失败,调用者必须处理。
捕获异常:try-catch
public static void main(String[] args) {
try {
// 可能出现异常的代码
String content = readFile("data.txt");
System.out.println(content);
} catch (IOException e) {
// 处理异常
System.err.println("文件读取失败:" + e.getMessage());
logger.error("文件操作异常", e);
}
System.out.println("程序继续执行...");
}
⚠️ 避免空的 catch 块 - 这会"吞掉"异常,让调试变得困难
多重捕获与父子类顺序
异常捕获必须从具体到一般排列,否则编译错误!
⚠️ 编译错误示例
try {
processData();
} catch (IOException e) { // ❌ 父类在前
System.out.println("IO错误:" + e.getMessage());
} catch (FileNotFoundException e) { // ❌ 子类在后 - 编译错误!
System.out.println("文件不存在:" + e.getMessage());
}
错误原因:FileNotFoundException 继承自 IOException,父类捕获器会"吞掉"所有子类异常,导致子类捕获器永远无法执行。
✅ 正确顺序示例
try {
processData();
} catch (FileNotFoundException e) { // ✅ 最具体的子类在前
System.out.println("文件不存在:" + e.getMessage());
} catch (IOException e) { // ✅ 父类在后
System.out.println("其他IO错误:" + e.getMessage());
} catch (Exception e) { // ✅ 最通用的父类在最后
System.out.println("其他错误:" + e.getMessage());
}
异常继承关系示例
Exception ← IOException ← FileNotFoundException
多异常合并(同级别异常)
// ✅ 可以合并不相关的同级别异常
try {
processData();
} catch (FileNotFoundException | SocketTimeoutException e) {
System.out.println("文件或网络错误:" + e.getMessage());
} catch (IOException e) {
System.out.println("其他IO错误:" + e.getMessage());
}
📌 记忆要点:
- 异常捕获顺序:子类 → 父类 → 祖父类
- 编译器会检查异常的可达性
- 同级别异常可以用 | 符号合并
- 永远把最通用的 Exception 放在最后
finally:资源清理必执行
无论是否发生异常,finally 块总会执行,确保资源正确释放。
FileInputStream fis = null;
try {
fis = new FileInputStream("data.txt");
// 文件操作
int data = fis.read();
} catch (IOException e) {
System.out.println("IO错误:" + e.getMessage());
} finally {
// 确保文件流被关闭
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
System.out.println("关闭文件时出错");
}
}
System.out.println("资源清理完成");
}
使用 try-with-resources 可以自动处理资源关闭(Java 7+)。
异常传播:rethrow
在当前层无法或不想处理异常时,可以将其传播给上层调用者。
// 方法1:直接传播
public void readConfig() throws IOException {
String content = Files.readString(Paths.get("config.json"));
processConfig(content);
}
// 方法2:处理后传播
public void loadData() {
try {
readConfig();
} catch (IOException e) {
// 记录日志
logger.error("配置加载失败", e);
// 继续向上传播
throw new RuntimeException("系统配置异常", e);
}
}
合理的异常传播能让"最了解上下文"的层来处理问题。
完整异常处理流程
public class FileProcessor {
public void processFile(String filename) {
try {
String content = readFile(filename);
String result = transform(content);
saveResult(result);
System.out.println("处理完成");
} catch (FileNotFoundException e) {
System.out.println("文件不存在,请检查路径:" + filename);
} catch (IOException e) {
System.out.println("文件操作失败:" + e.getMessage());
logger.error("IO异常", e);
} catch (DataFormatException e) {
System.out.println("数据格式错误:" + e.getMessage());
} catch (Exception e) {
System.out.println("未知错误:" + e.getMessage());
logger.error("处理异常", e);
} finally {
cleanup();
}
}
}
完整的异常处理包括:分类处理、日志记录、用户提示、资源清理。
异常处理流程:动态演示
异常处理流程说明
点击"单步执行"按钮,逐步观察异常的产生、捕获、处理和再抛出流程。
程序开始执行可能抛出异常的代码块。JVM 会为这段代码建立异常处理框架。
divide(10, 0) 方法检测到除数为0,抛出 IllegalArgumentException 异常对象。
异常对象包含:
- 异常类型:IllegalArgumentException
- 错误信息:"除数不能为0"
- 堆栈跟踪:异常发生的调用链
异常被 catch 块捕获,程序进入异常处理逻辑。此时程序流程从异常发生点跳转到 catch 块。
catch 块中的 e 参数包含:
- e.getMessage():异常详细信息
- e.printStackTrace():完整堆栈信息
- e.getCause():异常原因(如果有)
当前层处理后,将异常包装成新的
RuntimeException 再抛出,让上层调用者处理。
异常链(Exception Chaining):
- 新异常:RuntimeException("处理失败")
- 原始异常:IllegalArgumentException 作为 cause
- 保持完整的异常追踪信息
无论是否发生异常,finally 块都会执行。用于资源清理和收尾工作。
执行 finally 块中的清理代码,确保文件流、数据库连接等资源被正确释放。
由于新异常未被捕获,程序继续向上层抛出。如果 main 方法也没有处理,程序将终止并打印堆栈信息。
🔍 关键概念
- 异常产生:使用
throw抛出异常对象 - 异常捕获:
catch块匹配并处理异常 - 异常链:通过构造函数参数传递原始异常
- 资源清理:
finally块确保清理逻辑执行 - 异常传播:未处理的异常向调用栈上层传播
自定义异常
根据业务需求定义专门的异常类型,提供更精确的错误信息。
// 业务异常(Checked)
public class BusinessException extends Exception {
private final int errorCode;
public BusinessException(int errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public int getErrorCode() { return errorCode; }
}
// 业务运行时异常(Unchecked)
public class BusinessException extends RuntimeException {
public BusinessException(String message) {
super(message);
}
}
// 使用示例
public void withdraw(double amount) throws BusinessException {
if (amount > balance) {
throw new BusinessException(1001, "余额不足");
}
balance -= amount;
}
自定义异常让错误处理更精确,便于分类和统计。
异常处理最佳实践
✅ 推荐做法
- 具体异常类型优先
- 有意义的错误消息
- 记录详细日志
- 及时释放资源
- 提供用户友好提示
- 异常链传递
❌ 避免做法
- 空的catch块
- 捕获Exception过于宽泛
- 忽略异常信息
- 异常中返回正常值
- 长时间不处理异常
- 异常用于流程控制
黄金法则:异常应该用于处理异常情况,不应该用作正常的程序流程控制。
实战案例:用户注册系统
public class UserService {
private final UserDao userDao;
private final Logger logger = LoggerFactory.getLogger(UserService.class);
// 构造函数注入依赖
public UserService(UserDao userDao) {
this.userDao = userDao;
}
public void register(User user) throws UserException {
try {
// 参数验证
if (user.getEmail() == null || !isValidEmail(user.getEmail())) {
throw new ValidationException("邮箱格式不正确");
}
// 检查重复
if (userDao.existsByEmail(user.getEmail())) {
throw new DuplicateException("邮箱已被注册");
}
// 保存用户
userDao.save(user);
sendWelcomeEmail(user);
} catch (DatabaseException e) {
logger.error("数据库操作失败", e);
throw new UserException("注册失败,请稍后重试", e);
} catch (EmailException e) {
logger.warn("欢迎邮件发送失败", e);
// 注册成功,邮件失败不影响主流程
}
}
private boolean isValidEmail(String email) {
// 邮箱格式验证逻辑
return email != null && email.contains("@");
}
private void sendWelcomeEmail(User user) throws EmailException {
// 发送欢迎邮件逻辑
// 实际实现可能调用邮件服务
}
}
实际项目中,异常处理要平衡用户体验、系统稳定性和开发效率。
断言:开发阶段的检查
断言用于在开发和测试阶段验证程序的不变量,失败时抛出 AssertionError。
public class Calculator {
public double divide(int a, int b) {
// 断言:除数不能为0(开发阶段检查)
assert b != 0 : "除数不能为0";
return (double) a / b;
}
public void processAge(int age) {
// 断言:年龄必须在合理范围内
assert age >= 0 && age <= 150 : "年龄超出合理范围:" + age;
// 处理逻辑
}
}
// 启用断言编译和运行
// javac -ea Calculator.java
// java -ea Calculator
注意:断言默认关闭,仅用于开发测试。生产环境应使用参数验证和异常处理。
IDEA 中,需要右键点击edit Main.main() 然后再vm
option中填入 -ea
总结与学习建议
核心概念
- 异常分类:Error vs Exception
- Checked vs Unchecked
- 异常处理:try-catch-finally
- 异常传播:throws & rethrow
实践技能
- 选择合适的异常类型
- 编写清晰的错误消息
- 正确的资源管理
- 异常处理的最佳实践
练习项目
- 文件处理工具:实现文件复制、移动功能,处理各种IO异常
- 计算器程序:支持各种运算,处理除零、格式等异常
- 用户管理系统:注册、登录功能,完整的异常处理机制
异常处理是编写健壮程序的关键技能。记住:异常用于异常情况,不要用作正常流程控制。