第43章

Java异常基础

深入学习Java异常的概念、分类体系、异常处理机制和最佳实践

学习目标

什么是异常?

异常(Exception)是程序运行过程中出现的非正常情况,它会中断程序的正常执行流程。Java提供了完善的异常处理机制来处理这些异常情况。

异常的作用:
  • 错误报告:提供详细的错误信息
  • 程序健壮性:防止程序因错误而崩溃
  • 错误恢复:允许程序从错误中恢复
  • 调试支持:提供堆栈跟踪信息

异常示例

// 空指针异常示例
String str = null;
int length = str.length(); // 抛出 NullPointerException

// 数组越界异常示例
int[] array = {1, 2, 3};
int value = array[5]; // 抛出 ArrayIndexOutOfBoundsException

// 算术异常示例
int result = 10 / 0; // 抛出 ArithmeticException

Java异常分类

Java异常采用层次结构设计,所有异常都继承自Throwable类。

Throwable
├── Error
│   ├── OutOfMemoryError
│   ├── StackOverflowError
│   └── VirtualMachineError
└── Exception
    ├── RuntimeException (运行时异常)
    │   ├── NullPointerException
    │   ├── ArrayIndexOutOfBoundsException
    │   ├── ArithmeticException
    │   └── NumberFormatException
    └── 检查异常
        ├── IOException
        ├── ClassNotFoundException
        └── SQLException

1. 检查异常(Checked Exception)

检查异常是编译时必须处理的异常,继承自Exception但不是RuntimeException

特点:
  • 编译时必须处理
  • 通常表示可恢复的错误
  • 必须用try-catch捕获或throws声明
异常类型 描述 常见场景
IOException 输入输出异常 文件读写、网络通信
ClassNotFoundException 类未找到异常 动态加载类
SQLException 数据库异常 数据库操作
InterruptedException 线程中断异常 多线程编程
// 检查异常示例
try {
    FileReader file = new FileReader("test.txt");
    // 文件操作
} catch (FileNotFoundException e) {
    System.out.println("文件未找到: " + e.getMessage());
}

2. 运行时异常(Runtime Exception)

运行时异常是编译时不强制处理的异常,继承自RuntimeException

特点:
  • 编译时不强制处理
  • 通常表示编程错误
  • 可以选择捕获或让其传播
异常类型 描述 常见原因
NullPointerException 空指针异常 对null引用调用方法
ArrayIndexOutOfBoundsException 数组越界异常 访问超出数组范围的索引
ArithmeticException 算术异常 除零操作
NumberFormatException 数字格式异常 字符串转数字格式错误
IllegalArgumentException 非法参数异常 方法参数不合法
// 运行时异常示例
try {
    String str = "abc";
    int number = Integer.parseInt(str); // NumberFormatException
} catch (NumberFormatException e) {
    System.out.println("数字格式错误: " + e.getMessage());
}

3. 错误(Error)

错误表示严重的系统问题,通常不应该被捕获。

特点:
  • 表示严重的系统问题
  • 通常不应该被捕获
  • 程序无法恢复
错误类型 描述 常见原因
OutOfMemoryError 内存不足错误 堆内存耗尽
StackOverflowError 栈溢出错误 递归调用过深
NoClassDefFoundError 类定义未找到错误 运行时找不到类文件

异常处理语法

基本语法

try {
    // 可能抛出异常的代码
} catch (SpecificException e) {
    // 处理特定异常
} catch (Exception e) {
    // 处理其他异常
} finally {
    // 总是执行的代码
}

多重异常处理

public void processData(String data) {
    try {
        // 可能抛出多种异常的代码
        int length = data.length(); // NullPointerException
        int number = Integer.parseInt(data); // NumberFormatException
        int result = 100 / number; // ArithmeticException
        
        System.out.println("处理结果: " + result);
        
    } catch (NullPointerException e) {
        System.out.println("数据为null");
    } catch (NumberFormatException e) {
        System.out.println("数据格式不正确");
    } catch (ArithmeticException e) {
        System.out.println("除零操作");
    } catch (Exception e) {
        System.out.println("未知错误: " + e.getMessage());
    } finally {
        System.out.println("处理完成");
    }
}
重要提示:
  • catch块顺序:从子类异常到父类异常
  • finally块:无论是否有异常都会执行
  • 异常传播:未捕获的异常会向上传播

异常处理最佳实践

好的做法

1. 处理具体的异常类型
// 好的做法
try {
    int number = Integer.parseInt(input);
} catch (NumberFormatException e) {
    // 处理具体异常
    System.out.println("输入格式错误: " + input);
}
2. 提供有意义的异常信息
public void validateAge(int age) {
    if (age < 0) {
        throw new IllegalArgumentException(
            String.format("年龄不能为负数,当前值: %d", age)
        );
    }
}
3. 正确清理资源
InputStream input = null;
try {
    input = new FileInputStream("file.txt");
    // 使用资源
} catch (IOException e) {
    System.out.println("文件操作异常: " + e.getMessage());
} finally {
    if (input != null) {
        try {
            input.close();
        } catch (IOException e) {
            System.out.println("关闭文件异常: " + e.getMessage());
        }
    }
}

应该避免的做法

1. 捕获但忽略异常
// 错误做法
try {
    riskyOperation();
} catch (Exception e) {
    // 什么都不做 - 这是错误的!
}
2. 捕获过于宽泛的异常
// 错误做法
try {
    specificOperation();
} catch (Exception e) {
    // 太宽泛,应该捕获具体异常
}
3. 用异常控制程序流程
// 错误做法
try {
    return array[index];
} catch (ArrayIndexOutOfBoundsException e) {
    return null;
}

// 正确做法
if (index >= 0 && index < array.length) {
    return array[index];
} else {
    return null;
}

性能考虑

性能要点:
  • 异常处理有开销:异常对象创建需要生成堆栈跟踪
  • 不要用异常控制流程:异常应该用于异常情况
  • 避免频繁抛出异常:在循环中进行预检查
// 性能对比示例
// 坏做法:用异常控制流程(性能差)
for (int i = 0; i < 1000; i++) {
    try {
        if (i % 2 == 0) {
            throw new Exception("偶数");
        }
    } catch (Exception e) {
        // 处理偶数
    }
}

// 好做法:用条件判断(性能好)
for (int i = 0; i < 1000; i++) {
    if (i % 2 == 0) {
        // 处理偶数
    }
}
💻 查看完整代码 - 在线IDE体验

实战练习

练习建议:
  1. 练习1:编写一个计算器程序,处理各种可能的异常
  2. 练习2:实现一个文件读取工具,正确处理IO异常
  3. 练习3:创建自定义异常类,实现业务异常处理
  4. 练习4:分析开源项目中的异常处理模式

常见问题

Q: 什么时候应该捕获异常?

A: 当你能够合理地处理异常或需要进行资源清理时。

Q: 检查异常和运行时异常的区别?

A: 检查异常必须在编译时处理,运行时异常可以选择处理。

Q: 是否应该捕获Error?

A: 通常不应该,Error表示严重的系统问题,程序无法恢复。

Q: 如何选择抛出异常还是返回错误码?

A: 异常用于异常情况,错误码用于预期的错误状态。