【Java】Exception 异常体系解析 从原理到实践

Java 的 Exception 机制是程序健壮性的基石,提供了结构化、可恢复的错误处理能力。理解异常的分类、传播和处理是 Java 开发的必备技能

一、Java 异常体系总览

java 复制代码
// Java 异常继承结构
Throwable (可抛出)
├── Error (错误,不可恢复)
│   ├── OutOfMemoryError        // 内存溢出
│   ├── StackOverflowError      // 栈溢出
│   └── VirtualMachineError     // JVM 内部错误
│
└── Exception (异常,可恢复)
    ├── RuntimeException (运行时异常,非检查型)
    │   ├── NullPointerException      // 空指针
    │   ├── ArrayIndexOutOfBoundsException  // 数组越界
    │   ├── ClassCastException          // 类型转换失败
    │   ├── IllegalArgumentException    // 非法参数
    │   ├── ArithmeticException         // 算术异常(如除零)
    │   ├── NumberFormatException       // 数字格式错误
    │   └── ConcurrentModificationException  // 并发修改
    │
    └── 受检异常 (Checked Exception,必须处理)
        ├── IOException               // IO 操作失败
        ├── SQLException              // 数据库访问错误
        ├── ClassNotFoundException    // 类未找到
        ├── InterruptedException      // 线程中断
        ├── FileNotFoundException     // 文件不存在
        └── ParseException            // 解析错误

核心区别:
①Error :JVM 层面错误,程序不应捕获(如 OOM)
②RuntimeException :编码错误,可预防(如 NPE)
③Checked Exception:外部异常,必须处理(如 IOException)

二、受检异常 vs 非受检异常

对比维度 Checked Exception Unchecked Exception (RuntimeException)
继承父类 Exception(不包括 RuntimeException) RuntimeException
编译时检查 必须处理(try-catch 或 throws) 无需强制处理
设计理念 可恢复的外部错误(如文件不存在) 可预防的编程错误(如空指针)
典型例子 IOException, SQLException NullPointerException, IllegalArgumentException
处理策略 捕获并恢复,或继续抛出 修复代码 bug,通常不应捕获
官方推荐 尽量少用(Java 8+ Stream 不支持) 优先使用

Java 8 Stream 中的限制

java 复制代码
// ❌ 编译错误:Stream 的 lambda 不支持受检异常
list.stream().map(s -> {
    return new SimpleDateFormat("yyyy").parse(s); // ParseException 是受检异常
});

// ✅ 解决方案:包装为运行时异常
list.stream().map(s -> {
    try {
        return new SimpleDateFormat("yyyy").parse(s);
    } catch (ParseException e) {
        throw new RuntimeException(e); // 包装
    }
});

// 或自定义函数式接口
@FunctionalInterface
public interface FunctionWithException<T, R> {
    R apply(T t) throws Exception;
}

三、异常处理机制:try-catch-finally-throw-throws

1. try-catch 基础语法

java 复制代码
public void readFile(String path) {
    try {
        // 可能抛出异常的代码块
        FileInputStream fis = new FileInputStream(path);
        // ... 读取文件
    } catch (FileNotFoundException e) {
        // 捕获特定异常
        System.err.println("文件不存在: " + path);
        log.error("读取失败", e);
    } catch (IOException e) {
        // 捕获更广泛的异常
        System.err.println("IO 错误: " + e.getMessage());
    } catch (Exception e) {
        // 捕获所有 Exception(不推荐)
        System.err.println("未知错误");
    }
}

2. finally 块:资源释放保证

java 复制代码
public void processFile(String path) {
    FileInputStream fis = null;
    try {
        fis = new FileInputStream(path);
        // ... 处理文件
        return "success"; // 即使有 return,finally 也会执行
    } catch (IOException e) {
        throw new RuntimeException("处理失败", e);
    } finally {
        // 必须执行的代码(资源释放)
        if (fis != null) {
            try {
                fis.close(); // 关闭文件流
            } catch (IOException e) {
                log.warn("关闭流失败", e);
            }
        }
        System.out.println("无论如何都会执行"); // 即使 try 中有 return
    }
}

注意: finally 在 try 或 catch 执行后必然执行,除非:

System.exit()

JVM 崩溃

线程被杀死

3. try-with-resources(Java 7+):自动关闭资源

java 复制代码
// Java 7 前:必须手动关闭
public void readFileOld(String path) throws IOException {
    BufferedReader br = null;
    try {
        br = new BufferedReader(new FileReader(path));
        return br.readLine();
    } finally {
        if (br != null) br.close(); // 繁琐且易遗漏
    }
}

// Java 7+: 自动关闭
public void readFileNew(String path) throws IOException {
    // 实现 AutoCloseable 接口的资源自动关闭
    try (BufferedReader br = new BufferedReader(new FileReader(path))) {
        return br.readLine(); // 无需 finally,br 自动关闭
    }
}

// 多个资源
try (FileInputStream fis = new FileInputStream("input.txt");
     FileOutputStream fos = new FileOutputStream("output.txt")) {
    // 同时管理多个流
    byte[] buffer = new byte[1024];
    fis.read(buffer);
    fos.write(buffer);
} // fis 和 fos 自动关闭

实现原理:

java 复制代码
// 编译器生成的等效代码
BufferedReader br = new BufferedReader(...);
Throwable primaryExc = null;
try {
    return br.readLine();
} catch (Throwable t) {
    primaryExc = t;
    throw t;
} finally {
    if (br != null) {
        if (primaryExc != null) {
            try {
                br.close();
            } catch (Throwable suppressedExc) {
                primaryExc.addSuppressed(suppressedExc); // Java 7+ 抑制异常
            }
        } else {
            br.close();
        }
    }
}

4. throw:主动抛出异常

java 复制代码
public void setAge(int age) {
    if (age < 0 || age > 150) {
        // 抛出运行时异常(无需声明)
        throw new IllegalArgumentException("年龄必须在 0-150 之间: " + age);
    }
    this.age = age;
}

public void process() throws IOException { // 声明抛出受检异常
    if (fileNotExist) {
        throw new FileNotFoundException("文件不存在");
    }
}

5. throws:声明可能抛出的异常

java 复制代码
// 方法签名中声明受检异常
public void readConfig() throws IOException, ParseException {
    // 可能抛出 IO 或解析异常
    FileInputStream fis = new FileInputStream("config.properties");
    parseConfig(fis);
}

// 调用者必须处理
public void init() {
    try {
        readConfig();
    } catch (IOException e) {
        log.error("读取配置失败", e);
    } catch (ParseException e) {
        log.error("解析配置失败", e);
    }
}

规则:

受检异常 必须在方法签名中声明(throws)

非受检异常 (RuntimeException)无需声明

③调用者必须捕获或继续声明受检异常

四、异常处理最佳实践

1. 优先使用非受检异常

java 复制代码
// ✅ 推荐:运行时异常
public void process(String input) {
    if (input == null) {
        throw new IllegalArgumentException("输入不能为空");
    }
}

// ❌ 不推荐:受检异常
public void process(String input) throws InvalidInputException {
    if (input == null) {
        throw new InvalidInputException("输入不能为空");
    }
}

2. 精准捕获,不要吞掉异常

java 复制代码
// ❌ 反模式:吞掉异常
try {
    // 业务代码
} catch (Exception e) {
    // 什么都没有做!异常信息丢失
}

// ❌ 反模式:只打印日志
try {
    // 业务代码
} catch (SQLException e) {
    log.error("数据库错误", e); // 记录后未抛出,上层无法感知
}

// ✅ 正确:捕获后要么恢复,要么抛出
try {
    // 业务代码
} catch (SQLException e) {
    log.error("数据库错误", e);
    throw new DataAccessException("操作失败", e); // 包装后抛出
}

// ✅ 正确:特定异常恢复
try {
    return jdbcTemplate.queryForObject(...);
} catch (EmptyResultDataAccessException e) {
    return null; // 查询不到返回 null,是合法恢复
}

3. finally 块中不要抛出异常

java 复制代码
// ❌ 危险:finally 中的异常会覆盖 try 中的异常
try {
    return riskyOperation(); // 抛出 IOException
} finally {
    cleanUp(); // 抛出 RuntimeException,会覆盖 IOException!
}

// ✅ 正确:finally 中捕获异常
try {
    return riskyOperation();
} finally {
    try {
        cleanUp();
    } catch (Exception e) {
        log.warn("清理失败", e); // 记录但不抛出
    }
}

4. 自定义异常层次结构

java 复制代码
// 业务异常基类(非受检)
public class BusinessException extends RuntimeException {
    private final String errorCode;
    
    public BusinessException(String errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
    }
    
    public String getErrorCode() { return errorCode; }
}

// 具体业务异常
public class UserNotFoundException extends BusinessException {
    public UserNotFoundException(Long userId) {
        super("USER_NOT_FOUND", "用户不存在: " + userId);
    }
}

public class InsufficientBalanceException extends BusinessException {
    public InsufficientBalanceException(Long accountId) {
        super("INSUFFICIENT_BALANCE", "账户余额不足: " + accountId);
    }
}

// 使用
@Service
public class AccountService {
    public void deduct(Long accountId, BigDecimal amount) {
        Account account = accountDao.findById(accountId);
        if (account == null) {
            throw new UserNotFoundException(accountId); // 抛出业务异常
        }
        if (account.getBalance().compareTo(amount) < 0) {
            throw new InsufficientBalanceException(accountId);
        }
        // ... 扣减逻辑
    }
}

5. 异常链(Exception Chaining)

java 复制代码
// 保留原始异常信息
try {
    // 底层操作
} catch (SQLException e) {
    // ✅ 正确:将原始异常作为 cause 传递
    throw new DataAccessException("数据库操作失败", e); 
    
    // ❌ 错误:丢失原始堆栈
    throw new DataAccessException("数据库操作失败: " + e.getMessage());
}

6. Java 7+ 多异常捕获

java 复制代码
// Java 7 前:重复代码
try {
    // ...
} catch (IOException e) {
    handleException(e);
} catch (SQLException e) {
    handleException(e);
}

// Java 7+:多异常统一捕获
try {
    // ...
} catch (IOException | SQLException e) {
    handleException(e);
}

// 注意:只有 final 的变量可用
try {
    // ...
} catch (final Exception e) { // e 必须是 final 或 effectively final
    logger.log(e);
    throw e; // 重新抛出
}

7. try-with-resources 的最佳实践

java 复制代码
// Java 9+: 可以使用 final 或 effectively final 资源
BufferedReader br1 = new BufferedReader(...);
BufferedReader br2 = new BufferedReader(...);
try (br1; br2) { // 直接使用已声明的变量
    // ...
}

// 资源关闭顺序:与声明顺序相反
// 声明:A → B → C
// 关闭:C → B → A

8. 异常日志记录规范

java 复制代码
// ✅ 记录有意义的上下文
try {
    processUser(user);
} catch (Exception e) {
    log.error("处理用户失败, userId={}, userName={}, reason={}", 
              user.getId(), user.getName(), e.getMessage(), e); // 最后一个参数是异常
}

// ❌ 避免无意义日志
log.error("出错啦"); // 没有上下文
log.error("error: " + e); // 未打印堆栈

五、Java 7+ 异常新特性

1. Suppressed Exceptions(抑制异常)

java 复制代码
try (Resource resource = new Resource()) {
    resource.doWork(); // 抛出 WorkException
} catch (WorkException e) {
    // Java 7+: 获取被抑制的异常(close 方法抛出的异常)
    for (Throwable suppressed : e.getSuppressed()) {
        System.out.println("被抑制异常: " + suppressed);
    }
}

2. 更精准的重新抛出

java 复制代码
// Java 7 前:只能声明 throws Exception
public void process() throws Exception {
    try {
        // ...
    } catch (Exception e) {
        throw e; // 编译器要求 throws Exception(太宽泛)
    }
}

// Java 7+: 编译器推断实际抛出的异常类型
public void process() throws IOException, SQLException {
    try {
        // ...
    } catch (Exception e) {
        throw e; // 编译器知道可能是 IOException 或 SQLException
    }
}

六、终极最佳实践清单

Do(应该做的)

①优先抛出非受检异常(RuntimeException)

②使用 try-with-resources 管理资源

③保持异常链(throw new MyException("msg", cause))

④自定义业务异常层次

⑤finally 中只做资源清理,不抛异常

⑥只捕获能处理的异常,其他继续抛出

⑦异常日志包含完整上下文

Don't(不应该做的)

①不要吞掉异常(空 catch 块)

②不要用异常控制流程(性能差)

③不要捕获 Throwable/Error(可能捕获 OOM)

④不要在 finally 中使用 return(会覆盖 try 的 return)

⑤不要抛出 Exception 基类(信息不足)

⑥不要在循环中频繁抛出异常(性能杀手)

⑦不要在构造函数中抛出受检异常(工厂模式替代)

七、性能考虑

1.异常处理有性能开销,不应用于正常的控制流

2.创建异常对象成本较高(需要收集堆栈信息)

3.在性能关键路径上,优先使用条件检查而非异常

java 复制代码
// 性能较差:使用异常
try {
    return array[index];
} catch (ArrayIndexOutOfBoundsException e) {
    return defaultValue;
}

// 性能较好:使用条件检查
if (index >= 0 && index < array.length) {
    return array[index];
} else {
    return defaultValue;
}

总结

Java 异常机制的核心要点:

1.分类清晰 :Error(不可恢复)、受检异常(必须处理)、非受检异常(可不处理)

2.处理灵活 :try-catch-finally、throws、throw

3.资源安全 :try-with-resources 自动管理资源

4.扩展性强 :支持自定义异常

5.信息丰富:异常链、堆栈跟踪、suppressed异常

正确使用异常机制可以:

1.提高代码的健壮性可维护性

2.提供清晰的错误信息和调试线索

3.实现优雅的错误处理和资源清理

4.分离正常逻辑错误处理逻辑

相关推荐
谷哥的小弟1 小时前
Spring Framework源码解析——GenericTypeResolver
java·spring·源码
sheji34162 小时前
【开题答辩全过程】以 基于Springboot的超市仓库管理系统设计与实现为例,包含答辩的问题和答案
java·spring boot·后端
Pyeako2 小时前
python网络爬虫
开发语言·爬虫·python·requsets库
diegoXie2 小时前
【Python】 中的 * 与 **:Packing 与 Unpacking
开发语言·windows·python
我命由我123452 小时前
Android 开发问题:在无法直接获取或者通过传递获取 Context 的地方如何获取 Context
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
好好沉淀2 小时前
IDEA 取消 Save Actions 自动删除未用导入(前端开发避坑)
java·ide·intellij-idea
qq_12498707532 小时前
基于SpringBoot学生学习历史的选课推荐系统的设计与实现(源码+论文+部署+安装)
java·spring boot·后端·学习·毕业设计·毕设
qq_479875433 小时前
C++ 鸭子类型” (Duck Typing)
开发语言·c++
廋到被风吹走3 小时前
【Spring】事务管理深度解析|从原理到实战
java·spring