跳到主要内容

异常处理与 final/finally/finalize

异常处理

final、finally、finalize 的区别

final、finally 和 finalize 是 Java 中三个完全不同的概念,它们之间没有直接关系,只是名称相似容易混淆。

final 关键字

作用:用于声明常量、防止继承或方法重写

final 可以修饰变量、方法和类:

1. final 变量:一旦赋值后不可修改,即常量

public class FinalVariableDemo {
// final 成员变量必须初始化
private final int MAX_SIZE = 100;

// final 静态常量
public static final String APP_NAME = "MyApp";

public void test() {
// final 局部变量
final int count = 10;
// count = 20; // 编译错误:无法修改 final 变量

// final 引用变量:引用不可变,但对象内容可变
final List<String> list = new ArrayList<>();
list.add("Hello"); // 可以修改对象内容
// list = new ArrayList<>(); // 编译错误:不能修改引用
}
}

2. final 方法:不能被子类重写

public class Parent {
// final 方法不能被重写
public final void show() {
System.out.println("这个方法不能被重写");
}
}

public class Child extends Parent {
// @Override
// public void show() { // 编译错误:不能重写 final 方法
// System.out.println("尝试重写");
// }
}

3. final 类:不能被继承

// final 类不能被继承
public final class FinalClass {
// String、Integer 等包装类都是 final 类
}

// public class SubClass extends FinalClass { // 编译错误:不能继承 final 类
// }

finally 关键字

作用:异常处理的一部分,用于确保代码块总是执行

finally 与 try-catch 配合使用,通常用于资源清理:

public void readFile(String path) {
FileInputStream fis = null;
try {
fis = new FileInputStream(path);
// 读取文件内容
int data = fis.read();
System.out.println(data);
} catch (IOException e) {
System.err.println("读取文件失败: " + e.getMessage());
} finally {
// 无论是否发生异常,finally 块都会执行
// 通常用于关闭资源
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

finally 的执行时机:

  • try 块正常执行完毕后执行 finally
  • catch 块捕获异常后执行 finally
  • try 或 catch 中有 return 语句,在 return 前执行 finally
  • 某些特殊情况下 finally 可能不执行(如 System.exit()、JVM 崩溃等)

finalize 方法

作用:对象被垃圾回收前的清理方法

finalize 是 Object 类的一个方法,在对象被垃圾回收器回收前调用:

public class FinalizeDemo {
@Override
protected void finalize() throws Throwable {
try {
// 在对象被回收前执行清理操作
System.out.println("对象即将被回收");
// 释放资源,如关闭文件、数据库连接等
} finally {
super.finalize();
}
}
}

为什么不推荐使用 finalize:

  1. 调用时机不确定:无法保证何时被调用,甚至可能不被调用
  2. 性能开销大:会影响垃圾回收效率
  3. 可能导致对象复活:finalize 中可以让对象重新被引用
  4. 从 Java 9 开始已被废弃:推荐使用 try-with-resources 或 Cleaner API
// 不推荐:使用 finalize
public class OldWay {
private FileInputStream fis;

@Override
protected void finalize() throws Throwable {
if (fis != null) {
fis.close();
}
}
}

// 推荐:使用 try-with-resources
public class NewWay {
public void readFile(String path) {
try (FileInputStream fis = new FileInputStream(path)) {
// 使用文件流
} catch (IOException e) {
e.printStackTrace();
}
// 自动关闭,无需 finalize
}
}

三者对比总结

特性finalfinallyfinalize
类型关键字关键字方法
用途声明常量/防止继承或重写异常处理,确保代码执行对象回收前的清理
修饰对象变量、方法、类try-catch 语句块Object 类的方法
是否推荐推荐使用推荐使用不推荐(已废弃)
执行时机编译时确定异常处理时执行垃圾回收时可能执行

异常类层次结构

所有异常的共同祖先是 Throwable,分为两大类:

1. Exception(可处理异常)

  • Checked Exception(受检查异常):编译时必须处理

    • IOException
    • SQLException
    • ClassNotFoundException
  • Unchecked Exception(非受检查异常):编译时可不处理

    • RuntimeException 及其子类
    • NullPointerException
    • ArrayIndexOutOfBoundsException
    • ArithmeticException

2. Error(系统错误)

  • JVM 运行错误,程序无法处理
  • OutOfMemoryError
  • StackOverflowError
  • NoClassDefFoundError

Exception 与 Error 的差异

// Exception:可以捕获并处理
try {
int result = 10 / 0;
} catch (ArithmeticException e) {
System.out.println("除数不能为 0");
}

// Error:不建议捕获,应该避免发生
public class StackOverflowDemo {
public static void main(String[] args) {
recursion(); // 递归无出口,最终导致 StackOverflowError
}

public static void recursion() {
recursion();
}
}

Checked Exception 与 Unchecked Exception

Checked Exception(受检查异常):编译器强制要求处理

// 必须处理,否则编译不通过
public void readFile(String path) throws IOException {
FileReader reader = new FileReader(path); // 可能抛 FileNotFoundException
// ...
}

// 处理方式 1: try-catch
public void method1() {
try {
readFile("test.txt");
} catch (IOException e) {
e.printStackTrace();
}
}

// 处理方式 2: throws 声明
public void method2() throws IOException {
readFile("test.txt");
}

Unchecked Exception(非受检查异常):编译器不强制处理

// RuntimeException 及其子类,可以不处理
public int divide(int a, int b) {
return a / b; // 可能抛 ArithmeticException,但编译通过
}

public String getName(User user) {
return user.getName(); // 可能抛 NullPointerException
}

// 常见的 RuntimeException
public class CommonExceptions {
public static void main(String[] args) {
// 1. NullPointerException
String str = null;
// str.length(); // NPE

// 2. ArrayIndexOutOfBoundsException
int[] arr = {1, 2, 3};
// int x = arr[5]; // 数组越界

// 3. ClassCastException
Object obj = "Hello";
// Integer num = (Integer) obj; // 类型转换异常

// 4. NumberFormatException
// int num = Integer.parseInt("abc"); // 数字格式异常

// 5. IllegalArgumentException
// setAge(-5); // 参数非法
}

public static void setAge(int age) {
if (age < 0) {
throw new IllegalArgumentException("年龄不能为负数");
}
}
}

异常处理最佳实践

默认使用 Unchecked Exception,只在必要时用 Checked Exception:

// 推荐:用 RuntimeException 表示代码 Bug
public class UserService {
public User findById(Long id) {
if (id == null) {
throw new IllegalArgumentException("ID 不能为 null"); // 非受检查
}
// ...
return null;
}
}

// 只在业务逻辑必须处理时用 Checked Exception
public class PaymentService {
// 余额不足是正常业务分支,强制调用者处理
public void pay(double amount) throws InsufficientBalanceException {
if (balance < amount) {
throw new InsufficientBalanceException("余额不足");
}
// 执行支付
}
}

try-catch-finally 用法

try {
// 可能抛异常的代码
int result = riskyOperation();
System.out.println("结果:" + result);
} catch (SpecificException e) {
// 处理特定异常
System.out.println("处理特定异常:" + e.getMessage());
} catch (Exception e) {
// 处理其他异常
System.out.println("处理通用异常:" + e.getMessage());
} finally {
// 无论是否发生异常,都会执行
// 通常用于资源清理
System.out.println("清理资源");
}

注意:不要在 finally 中使用 return,会屏蔽 try 中的 return

public int badExample() {
try {
return 1;
} finally {
return 2; // 不好!将覆盖 try 中的返回值
}
}

// 返回 2,而不是 1

finally 一定会执行吗?

大多数情况下会,但有例外:

// 1. System.exit() 终止 JVM
try {
System.exit(0); // JVM 退出,finally 不执行
} finally {
System.out.println("不会执行");
}

// 2. 线程被 kill
Thread.currentThread().stop(); // 强制停止线程

// 3. CPU 关闭
// 无法模拟...

try-with-resources 用法

Java 7 引入,自动关闭资源:

// 传统方式:手动关闭资源
Scanner scanner = null;
try {
scanner = new Scanner(new File("test.txt"));
while (scanner.hasNext()) {
System.out.println(scanner.nextLine());
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (scanner != null) {
scanner.close(); // 手动关闭
}
}

// try-with-resources:自动关闭
try (Scanner scanner = new Scanner(new File("test.txt"))) {
while (scanner.hasNext()) {
System.out.println(scanner.nextLine());
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
// 自动调用 scanner.close()

// 多个资源
try (BufferedReader br = new BufferedReader(new FileReader("input.txt"));
BufferedWriter bw = new BufferedWriter(new FileWriter("output.txt"))) {
String line;
while ((line = br.readLine()) != null) {
bw.write(line);
bw.newLine();
}
}

适用条件:资源必须实现 AutoCloseableCloseable 接口。

异常使用注意事项

// 1. 不要将异常定义为静态变量
public class BadExample {
private static final Exception EXCEPTION = new Exception("错误"); // 错误!

public void method() throws Exception {
throw EXCEPTION; // 所有地方抛同一个异常,堆栈信息不准
}
}

// 2. 抛出更具体的异常
public class GoodExample {
public void parseInt(String str) {
if (!str.matches("\\d+")) {
throw new NumberFormatException("无效数字:" + str); // 具体异常
}
}
}

// 3. 避免重复记录日志
public class LogExample {
public void process() {
try {
riskyOperation();
} catch (Exception e) {
log.error("操作失败", e); // 已经记录日志
throw e; // 再次抛出时不要再记录
}
}
}

/ 4. 异常信息要有意义
throw new Exception("错误"); // 不好
throw new Exception("用户名不能为空"); // 好

为什么不建议使用异常控制业务流程

在《Effective Java》一书中,作者明确指出:不应该使用异常来控制常规的业务流程。异常机制是为处理非预期的错误情况而设计的,将其用于正常的业务逻辑控制会带来诸多问题。

什么是用异常控制业务流程

简单来说,就是将异常作为程序的正常执行路径,而不是仅仅用于处理错误情况。

典型的错误示例:使用异常处理数据验证

// 不推荐:使用异常控制业务流程
public class UserService {
public User createUser(String username, String email) {
try {
// 尝试创建用户
User user = new User(username, email);
return userRepository.save(user);
} catch (IllegalArgumentException e) {
// 捕获参数校验异常来控制业务流程
log.warn("用户创建失败: {}", e.getMessage());
return null;
}
}
}

// 推荐:使用条件判断
public class UserService {
public User createUser(String username, String email) {
// 先进行业务校验
if (username == null || username.isEmpty()) {
log.warn("用户名不能为空");
return null;
}
if (email == null || !email.contains("@")) {
log.warn("邮箱格式不正确");
return null;
}

// 正常创建用户
User user = new User(username, email);
return userRepository.save(user);
}
}

使用异常控制业务流程的问题

1. 严重的性能问题

异常的创建和处理开销非常大,主要体现在:

public class PerformanceTest {
public static void main(String[] args) {
int iterations = 100000;

// 测试1:使用异常控制流程
long start = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
try {
throw new Exception("测试异常");
} catch (Exception e) {
// 捕获并继续
}
}
System.out.println("异常方式耗时: " + (System.currentTimeMillis() - start) + "ms");

// 测试2:使用条件判断
start = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
if (false) {
// 条件不满足
}
}
System.out.println("判断方式耗时: " + (System.currentTimeMillis() - start) + "ms");
}
}
// 输出结果:
// 异常方式耗时: 约 2000-3000ms
// 判断方式耗时: 约 1-2ms

性能差异的原因:

  • 创建异常对象需要填充完整的调用栈信息(StackTrace)
  • 异常的抛出和捕获涉及 JVM 的栈展开(Stack Unwinding)机制
  • 频繁的异常处理会影响 JIT 编译器的优化

2. 违背异常设计的初衷

Java 异常机制的设计目的是处理非预期的错误情况,而不是作为控制流程的工具:

// 错误示例:用异常控制循环退出
public class BadLoop {
public void processArray(String[] array) {
try {
int i = 0;
while (true) {
System.out.println(array[i]);
i++;
}
} catch (ArrayIndexOutOfBoundsException e) {
// 用异常来跳出循环
}
}
}

// 正确做法:使用循环条件
public class GoodLoop {
public void processArray(String[] array) {
for (int i = 0; i < array.length; i++) {
System.out.println(array[i]);
}
}
}

这样的代码会:

  • 降低代码的可读性和可维护性
  • 让其他开发者难以理解代码的真实意图
  • 违背"代码即文档"的最佳实践

3. 影响事务的正确回滚

在涉及数据库事务的场景中,异常被捕获可能导致事务无法正常回滚:

@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;

@Autowired
private InventoryService inventoryService;

// 错误示例:异常被捕获导致事务无法回滚
@Transactional
public void createOrder(Order order) {
// 保存订单
orderRepository.save(order);

try {
// 扣减库存
inventoryService.deductStock(order.getProductId(), order.getQuantity());
} catch (InsufficientStockException e) {
// 异常被捕获,事务不会回滚
// 导致订单已保存,但库存扣减失败
log.error("库存不足", e);
return;
}

// 其他业务逻辑...
}

// 正确做法:让异常向上抛出,或手动回滚
@Transactional(rollbackFor = Exception.class)
public void createOrderCorrect(Order order) throws BusinessException {
// 先检查库存
if (!inventoryService.checkStock(order.getProductId(), order.getQuantity())) {
throw new BusinessException("库存不足");
}

// 保存订单
orderRepository.save(order);

// 扣减库存
inventoryService.deductStock(order.getProductId(), order.getQuantity());
}
}

4. 过度依赖底层实现细节

使用异常控制业务流程往往会过度依赖特定的异常类型:

// 不推荐:依赖特定的数据库异常
public class ProductService {
public Product saveProduct(Product product) {
try {
// 尝试保存商品
return productRepository.save(product);
} catch (DataIntegrityViolationException e) {
// 依赖 Spring Data 的特定异常
if (e.getCause() instanceof ConstraintViolationException) {
// 进一步依赖具体的数据库异常
return productRepository.findByCode(product.getCode());
}
throw e;
}
}
}

// 推荐:先检查再操作
public class ProductService {
public Product saveProduct(Product product) {
// 先查询是否存在
Product existing = productRepository.findByCode(product.getCode());
if (existing != null) {
// 已存在,返回现有数据
return existing;
}

// 不存在,执行保存
return productRepository.save(product);
}
}

带来的风险:

  • 框架或数据库版本升级可能改变异常类型
  • 不同的数据库可能抛出不同的异常
  • 代码的可移植性变差
  • 单元测试难以编写和维护

5. API 设计不清晰

良好的 API 设计应该明确表达意图,而使用异常控制流程会模糊 API 的真正用途:

// 不好的 API 设计
public class CacheService {
/**
* 获取缓存数据
* 注意:如果缓存不存在会抛出 CacheNotFoundException
*/
public String get(String key) throws CacheNotFoundException {
String value = cache.get(key);
if (value == null) {
throw new CacheNotFoundException("Key not found: " + key);
}
return value;
}
}

// 使用方不得不用异常控制流程
public class UserService {
public String getUserName(Long userId) {
try {
return cacheService.get("user:" + userId);
} catch (CacheNotFoundException e) {
// 缓存未命中是正常情况,不应该用异常表示
return loadFromDatabase(userId);
}
}
}

// 好的 API 设计
public class CacheService {
/**
* 获取缓存数据
* @return 缓存值,如果不存在返回 null
*/
public String get(String key) {
return cache.get(key);
}

/**
* 获取缓存数据,如果不存在则执行加载函数
*/
public String getOrLoad(String key, Supplier<String> loader) {
String value = cache.get(key);
if (value == null) {
value = loader.get();
cache.put(key, value);
}
return value;
}
}

// 使用方代码更清晰
public class UserService {
public String getUserName(Long userId) {
return cacheService.getOrLoad(
"user:" + userId,
() -> loadFromDatabase(userId)
);
}
}

正确使用异常的原则

// 1. 异常应该表示真正的错误情况
public class FileService {
public String readFile(String path) throws IOException {
// IOException 表示文件不存在或读取失败,这是真正的错误
return Files.readString(Paths.get(path));
}
}

// 2. 正常的业务分支应该用返回值或条件判断
public class LoginService {
public LoginResult login(String username, String password) {
// 用户不存在是正常的业务情况,不应该抛异常
User user = userRepository.findByUsername(username);
if (user == null) {
return LoginResult.failure("用户不存在");
}

// 密码错误也是正常的业务情况
if (!passwordEncoder.matches(password, user.getPassword())) {
return LoginResult.failure("密码错误");
}

return LoginResult.success(user);
}
}

// 3. 使用 Optional 处理可能为空的情况
public class UserService {
public Optional<User> findById(Long id) {
return Optional.ofNullable(userRepository.findById(id));
}

// 使用时
public void processUser(Long userId) {
findById(userId)
.ifPresentOrElse(
user -> System.out.println("找到用户: " + user.getName()),
() -> System.out.println("用户不存在")
);
}
}

// 4. 业务异常应该明确且有意义
public class PaymentService {
public void pay(Order order) throws InsufficientBalanceException, PaymentTimeoutException {
Account account = accountRepository.findById(order.getAccountId());

// 余额不足是业务异常,但这是预期的错误情况
if (account.getBalance() < order.getAmount()) {
throw new InsufficientBalanceException(
String.format("余额不足,当前余额: %.2f,需要: %.2f",
account.getBalance(), order.getAmount())
);
}

// 执行支付...
}
}

总结

异常应该用于表示异常情况,而不是控制正常的程序流程。

记住以下几点:

  1. 性能考虑:异常的创建和处理代价高昂,不适合高频场景
  2. 语义明确:异常应该表示非预期的错误,不应该用于正常业务逻辑
  3. 事务安全:捕获异常可能影响事务回滚,导致数据不一致
  4. 降低耦合:避免过度依赖特定的异常类型和底层实现
  5. API 设计:清晰的 API 应该通过返回值或方法名表达意图,而不是强制调用者处理异常

合理的做法是:

  • 预期的情况:使用返回值、Optional、条件判断
  • 非预期的错误:使用异常机制
  • 业务校验:先检查后操作,而不是先操作后捕获异常