一、Java 异常的继承体系
Java 中的异常体系是一个树形结构,所有异常类最终都继承自java.lang.Throwable
。这个体系主要分为三大类:
异常继承关系示意:
Object
└── Throwable(所有异常可以抛出)
├── Error(系统错误,不可处理)
└── Exception(程序可处理的异常)
├── RuntimeException(运行时异常,非受检异常。子类无需处理异常,异常概率低)
└── 其他Exception子类(编译时异常,受检异常。子类需处理异常)
1. Throwable
所有异常和错误的基类,任何可以被抛出的类都必须继承自Throwable
。
2. Error
表示系统级错误,通常是 JVM 内部问题(如内存溢出、栈溢出),程序无法处理,只能退出 JVM。例如:
OutOfMemoryError
:内存不足StackOverflowError
:栈溢出(递归过深)VirtualMachineError
:JVM 错误
3. Exception
程序可处理的异常,又分为两类:
编译时异常(Checked Exception)
- 必须在编译时处理(用
try-catch
或throws
声明); - 继承自
Exception
但不继承自RuntimeException
; - 常见的如
FileNotFoundException
、IOException
、SQLException
等。
运行时异常(Unchecked Exception)
- 编译时无需处理,程序运行时可能抛出;
- 继承自
RuntimeException
; - 常见的如
NullPointerException
、ArrayIndexOutOfBoundsException
、ArithmeticException
等。
二、异常处理的核心原则
1. 异常一定在运行时产生
虽然编译时异常需要在编译阶段处理,但异常对象本身是在程序运行时创建的。例如:
FileInputStream file = new FileInputStream("C:\\abc.txt");
// 当文件不存在时,运行到这行才会new一个FileNotFoundException对象
2. 编译时异常的两种处理方式
方式一:try-catch 捕获异常
public static void f3() {
try {
FileInputStream file = new FileInputStream("C:\\abc.txt");
file.read();
} catch (FileNotFoundException e) {
System.out.println("文件不存在:" + e.getMessage());
e.printStackTrace(); // 打印详细堆栈信息
} catch (IOException e) {
System.out.println("读取文件失败:" + e.getMessage());
}
}
方式二:throws 声明异常
public static void f1() throws FileNotFoundException {
f2(); // 调用f2,将异常抛给上层调用者
}
public static void f2() throws FileNotFoundException {
FileInputStream file = new FileInputStream("C:\\abc.txt");
}
3. 运行时异常的处理
运行时异常编译时不强制要求处理,但在代码中应该通过逻辑判断避免。例如:
public void divide(int a, int b) {
if (b == 0) {
throw new ArithmeticException("除数不能为0"); // 主动抛出运行时异常
}
System.out.println(a / b);
}
三、try-catch 的实战技巧
1. 异常捕获顺序:从小到大写 catch 块
捕获异常时,应该先捕获子类异常,再捕获父类异常。否则,父类异常会优先捕获,导致子类异常无法被单独处理。
try {
// 可能抛出多种异常的代码
} catch (FileNotFoundException e) { // 先捕获子类异常
System.out.println("文件不存在");
} catch (IOException e) { // 再捕获父类异常
System.out.println("IO操作失败");
} catch (Exception e) { // 最后捕获通用异常
System.out.println("未知异常");
}
2. JDK 8 的多异常捕获
JDK 8 允许在一个 catch 块中捕获多个异常,用|
分隔:
try {
// 可能抛出异常的代码
} catch (FileNotFoundException | IOException e) {
System.out.println("文件操作异常:" + e.getMessage());
}
3. try 中异常发生后的执行流程
一旦 try 块中的某行代码抛出异常,该行后面的代码不会执行,直接跳转到匹配的 catch 块:
try {
FileInputStream file = new FileInputStream("C:\\abc.txt"); // 若此处抛出异常
file.read(); // 这行不会执行
System.out.println("读取成功"); // 这行也不会执行
} catch (FileNotFoundException e) {
System.out.println("文件不存在");
}
四、finally 块的作用与注意事项
1. finally 块一定会执行
无论 try 块中是否发生异常,finally 块中的代码都会执行。通常用于释放资源(如关闭文件、数据库连接等):
public static void f4() {
FileInputStream file = null;
try {
file = new FileInputStream("C:\\abc.txt");
// 执行IO操作
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (file != null) {
try {
file.close(); // 释放资源
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
2. finally 块的特殊情况
- 当 try 或 catch 块中执行
System.exit(0)
时,finally 块不会执行; - 当线程被中断或终止时,finally 块可能不会执行。
3. JDK 7 的 try-with-resources 语法
对于实现了AutoCloseable
接口的资源(如FileInputStream
、Connection
),可以使用更简洁的语法自动关闭资源:
public static void f5() {
try (FileInputStream file = new FileInputStream("C:\\abc.txt")) {
// 执行IO操作,资源会自动关闭
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
五、自定义异常的设计与使用
在实际开发中,除了使用 Java 内置的异常,还可以自定义异常来满足业务需求。
1. 自定义编译时异常
// 继承Exception,成为编译时异常。1 继承Exception;2 写一个构造方法
public class BusinessException extends Exception {
public BusinessException(String message) {
super(message);
}
}
// 使用自定义异常
public void transferMoney(double amount) throws BusinessException {
if (amount <= 0) {
throw new BusinessException("转账金额必须大于0");
}
// 转账逻辑
}
2. 自定义运行时异常
// 继承RuntimeException,成为运行时异常。1 继承RuntimeException ;2 写一个构造方法
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
}
// 使用自定义异常
public User getUserById(Long id) {
User user = userDao.findById(id);
if (user == null) {
throw new UserNotFoundException("用户ID不存在:" + id);
}
return user;
}
六、异常处理的最佳实践
1. 避免捕获通用异常
尽量避免捕获Exception
或Throwable
,这会掩盖代码中的潜在问题:
try {
// 业务逻辑
} catch (Exception e) { // 不推荐
e.printStackTrace();
}
2. 异常信息要明确
抛出异常时,提供足够的上下文信息,方便调试:
if (file == null) {
throw new NullPointerException("文件对象为空,无法执行操作");
}
3. 不要 “吞掉” 异常
catch 块中至少要记录日志,避免悄无声息地忽略异常:
try {
// 可能抛出异常的代码
} catch (IOException e) {
// 不要空实现
// 至少记录日志:
logger.error("IO操作失败", e);
}
4. 优先使用 try-with-resources
处理需要关闭的资源时,优先使用 try-with-resources 语法,减少代码冗余:
// 旧写法(需手动关闭)
FileInputStream file = null;
try {
file = new FileInputStream("path");
} catch (IOException e) {
// 处理异常
} finally {
if (file != null) {
try {
file.close();
} catch (IOException e) {
// 处理关闭异常
}
}
}
// 新写法(自动关闭)
try (FileInputStream file = new FileInputStream("path")) {
// 使用资源
} catch (IOException e) {
// 处理异常
}
七、总结
在实际开发中,要根据场景选择合适的异常处理方式:对于编译时异常,必须显式处理;对于运行时异常,要通过逻辑判断提前预防。