异常机制

面向对象程序设计(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 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 执行时机

无论是否发生异常,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();
        }
    }
}

完整的异常处理包括:分类处理、日志记录、用户提示、资源清理。

异常处理流程:动态演示

public class ExceptionDemo {
public static void main(String[] args) {
try {
String result = divide(10, 0);
System.out.println(result);
} catch (IllegalArgumentException e) {
System.out.println("捕获异常:" + e.getMessage());
throw new RuntimeException("处理失败", e);
} finally {
System.out.println("资源清理完成");
}
System.out.println("程序继续...");
}
}

异常处理流程说明

点击"单步执行"按钮,逐步观察异常的产生、捕获、处理和再抛出流程。

Step 1: 进入 try 块
程序开始执行可能抛出异常的代码块。JVM 会为这段代码建立异常处理框架。
Step 2: 异常产生
divide(10, 0) 方法检测到除数为0,抛出 IllegalArgumentException 异常对象。

异常对象包含:
  • 异常类型:IllegalArgumentException
  • 错误信息:"除数不能为0"
  • 堆栈跟踪:异常发生的调用链
Step 3: 异常捕获
异常被 catch 块捕获,程序进入异常处理逻辑。此时程序流程从异常发生点跳转到 catch 块。

catch 块中的 e 参数包含:
  • e.getMessage():异常详细信息
  • e.printStackTrace():完整堆栈信息
  • e.getCause():异常原因(如果有)
Step 4: 异常再抛出
当前层处理后,将异常包装成新的 RuntimeException 再抛出,让上层调用者处理。

异常链(Exception Chaining):
  • 新异常:RuntimeException("处理失败")
  • 原始异常:IllegalArgumentException 作为 cause
  • 保持完整的异常追踪信息
Step 5: finally 块执行
无论是否发生异常,finally 块都会执行。用于资源清理和收尾工作。
Step 6: 资源清理
执行 finally 块中的清理代码,确保文件流、数据库连接等资源被正确释放。
Step 7: 异常传播
由于新异常未被捕获,程序继续向上层抛出。如果 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

实践技能

  • 选择合适的异常类型
  • 编写清晰的错误消息
  • 正确的资源管理
  • 异常处理的最佳实践

练习项目

  1. 文件处理工具:实现文件复制、移动功能,处理各种IO异常
  2. 计算器程序:支持各种运算,处理除零、格式等异常
  3. 用户管理系统:注册、登录功能,完整的异常处理机制

异常处理是编写健壮程序的关键技能。记住:异常用于异常情况,不要用作正常流程控制。

1 / X