Skip to content

第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");
IOExceptionIO错误文件读写错误
SQLExceptionSQL错误数据库操作错误

⚠️ 常见陷阱

陷阱1:吞掉异常

捕获异常后什么都不做,会让问题消失在日志之外,后续排查非常困难。

陷阱2:用异常控制正常流程

异常适合表达异常情况,不适合替代 if 判断。高频流程里抛异常还会带来性能和可读性问题。

陷阱3:丢失异常链

重新抛异常时不传入 cause,会丢失根因。

java
throw new BusinessException("处理失败", e);

陷阱4:finally 中 return

finally 中 return 会覆盖 try/catch 的返回值或异常,生产代码应避免。


🆚 Java vs C 对比

特性CJava
错误表达返回码、errno异常对象
传播机制手动层层返回调用栈自动传播
资源清理goto cleanup / 手动 closetry-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

  1. try-catch-finally 基本使用
  2. 多异常捕获
  3. 自定义异常
  4. 异常链
  5. try-with-resources
  6. 综合:银行转账系统(含异常处理)

🎓 下一步

  • 第14课:集合框架 - List - ArrayList、LinkedList、Vector