乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Java概述与环境搭建

    • Java概述与环境搭建
    • Java语言概述
    • 解释型语言与编译型语言对比
    • JDK安装与配置
    • JDK、JRE、JVM 详解
    • HelloWorld程序详解
    • IDE 介绍
  • 第2章 标识符与基本数据类型

    • 章节导读
    • 变量概述
    • 常量概述
    • 基本类型与包装类
    • 字节型 byte
    • 短整型 short
    • 整型 int
    • 长整型 long
    • 单精度浮点型 float
    • 双精度浮点型 double
    • 字符型 char
    • 布尔型 boolean
    • 类型转换
  • 第3章 运算符与表达式

    • 章节导读
    • 算术运算符
    • 赋值运算符
    • 关系运算符
    • 逻辑运算符
    • 位运算符
    • 条件运算符
    • 运算符优先级
    • 表达式
  • 第4章 流程控制

    • 章节导读
    • 常见的程序运行流程
    • if-else 选择结构
    • switch 多分支选择
    • while 循环
    • do-while 循环
    • for 循环
    • break 与 continue
  • 第5章 数组

    • 章节导读
    • 一维数组
    • 多维数组
    • Arrays 工具类
  • 第6章 类与对象

    • 章节导读
    • 类与对象
    • 方法定义与调用
    • 构造方法
    • 封装
    • 访问修饰符
    • package 与 import
    • static 关键字
    • this 关键字
    • 参数传递 详解
    • 枚举
    • 成员内部类
    • 局部内部类
    • 静态内部类
    • 匿名内部类
  • 第7章 接口与继承

    • 章节导读
    • 继承
    • super 关键字
    • final 关键字
    • 多态
    • 向上转型与向下转型
    • 抽象类
    • 接口
    • 抽象类与接口对比
  • 第8章 注解

    • 章节导读
    • 注解基础
    • 元注解详解
    • 自定义注解
  • 第9章 常用类

    • 章节导读:Java 常用类
    • Object 类:万类之祖
    • 包装类:基本类型的对象化
    • String:不可变的字符串
    • StringBuffer:线程安全的可变字符串
    • StringBuilder:可变的字符串构建器
    • Math:数学运算工具类
    • Random:伪随机数生成器
    • 大数值运算 详解
    • 日期时间API 详解
  • 第10章 异常机制

    • 章节导读
    • 异常体系与分类
    • try-catch-finally
    • try-with-resources
    • throws 与 throw
    • 自定义异常
  • 第11章 泛型

    • 章节导读
    • 泛型基础
    • 通配符与PECS原则
    • 类型擦除
  • 第12章 集合框架

    • 章节导读
    • 集合框架概述
    • ArrayList
    • LinkedList
    • HashMap 详解
    • LinkedHashMap 详解
    • TreeMap 详解
    • HashSet
    • TreeSet 详解
    • TreeSet 与 Comparable
    • Collections 工具类详解
  • 第13章 IO流

    • 章节导读
    • IO流概述
    • 字节流
    • 字符流
    • 缓冲流
    • 转换流 详解
    • 序列化 详解
    • NIO与Files 详解
    • NIO与Files工具类
  • 第14章 多线程与并发

    • 第十六章 多线程与并发 —— 章节导读
    • 线程基础详解
    • synchronized 详解
    • Lock 与显式锁详解
    • volatile 详解
    • wait 与 notify 详解
    • ThreadLocal详解
    • 原子类详解
    • 并发工具类详解
    • 线程池详解
  • 第15章 反射

    • 章节导读
    • 反射概述与 Class 对象
    • Constructor 与对象创建
    • Field与Method详解
    • 反射应用详解
  • 第16章 JDK8新特性

    • 章节导读
    • Lambda 表达式
    • Stream API 基础
    • Stream API 高级详解
    • Optional 详解
    • 新日期时间API详解
  • 第17章 JDK9-11新特性

    • 章节导读
    • 模块化系统 — Project Jigsaw(JDK 9)
    • var 局部变量类型推断(JDK 10)
    • 集合工厂方法与增强(JDK 9 / 10 / 11)
    • 接口增强:private 方法(JDK 9)
    • Stream API 增强(JDK 9)
    • Optional 增强(JDK 9 / 10 / 11)
    • String 新增方法(JDK 11)
    • HTTP Client 与 Files 增强(JDK 11)
    • 直接运行 Java 源文件 — JEP 330(JDK 11)
  • 第18章 JDK12-17新特性

    • 章节导读
    • Switch 表达式(JDK 12 预览 / JDK 14 正式)
    • 文本块 Text Blocks(JDK 13 预览 / JDK 15 正式)
    • Records 记录类(JDK 14 预览 / JDK 16 正式)
    • 密封类 Sealed Classes(JDK 15 预览 / JDK 17 正式)
    • instanceof 模式匹配(JDK 14 预览 / JDK 16 正式)
    • Switch 模式匹配 — Pattern Matching for switch(JDK 17 预览 / JDK 21 正式)
    • Helpful NPE 与 String 增强(JDK 12 / JDK 14 / JDK 15)
    • Stream 增强(JDK 12 / JDK 16)
    • 日期时间增强 — Day Period 支持(JDK 16)
  • 第19章 JDK18-21新特性

    • 章节导读
    • 虚拟线程(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 序列集合(JDK 21 正式)
    • Switch 模式匹配(JDK 17 预览 / JDK 18 第二预览 / JDK 20 第四预览 / JDK 21 正式)
    • Record 模式匹配(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 未命名模式与变量(JDK 21 预览 / JDK 22 正式)
  • 第20章 JDK 22-25 新特性

    • 章节导读
    • 字符串模板(JDK 22 预览 / JDK 23 第二预览 / JDK 24 第三预览)
    • Stream Gatherers(JDK 22 预览 / JDK 24 第二预览)
    • 隐式声明类与实例方法(JDK 23 预览 / JDK 24 第二预览)
    • 原始类型模式匹配(JDK 24 预览)
  • 附录

    • Java 核心知识点
    • Java SE 专业术语
    • Java特性索引(JDK 8 → 25)

异常体系与分类

本节定位:系统掌握 Java 异常机制的顶层设计——Throwable 继承体系。理解 Error、受检异常(Checked Exception)、非受检异常(RuntimeException)三个分支的设计意图与使用场景,为后续的异常处理打下理论基础。


场景引入

周一早会,飞翔科技架构师白歌在代码审查中发现实习生李眉的代码里到处是 catch (Exception e)。

"李眉,你为什么把所有异常都捕获成 Exception?"白歌皱着眉头问。

"因为……反正都是异常嘛,抓了不就行了?"李眉小声回答。

大翔接过话:"异常是分门别类的,就像医院分内科外科。头疼吃感冒药,骨折打石膏,你得先搞清楚异常的类型,才能对症下药。"


Throwable 体系全景图

Java 中所有异常和错误的根类是 java.lang.Throwable。整个体系分为三个分支:Error、受检异常、非受检异常。


三大分支详解

一、Error:JVM 的"不可抗力"

Error 表示 JVM 层面的严重问题,通常是程序无法处理的。原则:不要捕获 Error。

Error 类型触发原因能否恢复
OutOfMemoryError堆内存耗尽,无法分配新对象通常不可恢复
StackOverflowError方法递归调用过深,栈空间耗尽通常不可恢复
NoClassDefFoundError编译时存在的类在运行时找不到检查 classpath 可能恢复
NoSuchMethodError调用的方法在运行时不存在(版本不匹配)修正 jar 包版本

原则:Error 是 JVM 的"不可抗力",就像地震、海啸,程序不应该也不需要在代码层面处理。

// ❌ 错误示范:捕获 OutOfMemoryError
try {
    byte[] huge = new byte[Integer.MAX_VALUE];
} catch (OutOfMemoryError e) {
    // OOM 后 JVM 状态不确定,继续运行更危险
    System.out.println("内存不够了,但程序继续...");
}
// 这段代码不仅无法真正解决问题,还可能让程序在不可预测的状态下继续执行
// ✅ 正确做法:预防而非治疗
// 1. 合理设置 JVM 参数(-Xmx, -Xms)
// 2. 避免内存泄漏(及时解除引用)
// 3. 使用软引用/弱引用
// 4. 如果真的 OOM,让程序崩溃并重启是更安全的选择

二、受检异常(Checked Exception):编译器强制检查

受检异常 = Exception 的子类,但不是 RuntimeException 的子类。

编译器强制要求程序处理这类异常:要么 try-catch,要么 throws 声明。如果不处理,编译不通过。

常见受检异常触发场景飞翔科技场景
IOExceptionI/O 操作失败学生数据文件读写失败
FileNotFoundException文件不存在导入的 Excel 学生名单找不到
SQLException数据库操作异常查询学生信息时数据库连接断开
ClassNotFoundException动态加载类失败加载数据库驱动类时出错
InterruptedException线程被中断异步发送成绩通知时线程被中断
// 受检异常:编译器强制处理
public void importStudents(String filePath) {
    // 编译错误!未处理 IOException
    FileReader reader = new FileReader(filePath);  // FileNotFoundException
}
// 必须处理:
// 方式一:try-catch
public void importStudents(String filePath) {
    try {
        FileReader reader = new FileReader(filePath);
        // 读取文件逻辑...
    } catch (FileNotFoundException e) {
        System.err.println("文件不存在: " + filePath);
    }
}

// 方式二:throws 声明
public void importStudents(String filePath) throws IOException {
    FileReader reader = new FileReader(filePath);
    // 读取文件逻辑...
}

三、非受检异常(Unchecked Exception / RuntimeException)

非受检异常 = RuntimeException 及其子类。

编译器不强制要求处理,但会在运行时抛出。通常源于程序逻辑错误。

常见非受检异常触发原因典型代码
NullPointerException对 null 引用调用方法str.length() 当 str == null
ArrayIndexOutOfBoundsException数组索引越界arr[arr.length]
IllegalArgumentException方法参数不合法Thread.sleep(-1)
NumberFormatException字符串格式无法转为数字Integer.parseInt("abc")
ArithmeticException算术运算异常int x = 1 / 0
ClassCastException类型转换失败(String) new Object()
// 编译器不强制处理,但运行时可能炸
public double calculateAvgScore(int[] scores) {
    int sum = 0;
    for (int i = 0; i <= scores.length; i++) {  // 注意:<=
        sum += scores[i];  // 最后一次循环:ArrayIndexOutOfBoundsException
    }
    return (double) sum / scores.length;
}

受检异常 vs 非受检异常的哲学之争

受检异常(Checked):"编译器帮你发现潜在问题,你必须提前想好怎么处理"
非受检异常(Unchecked):"这些是你代码逻辑的问题,修好代码就行,不要硬 capture"
对比维度受检异常非受检异常
编译器检查强制检查,不处理编译不通过不检查
设计意图可预见的可恢复的异常程序逻辑错误、不可预见
典型场景IO 失败、数据库异常、网络异常NPE、数组越界、类型转换
处理策略捕获并恢复,或向上抛修复代码逻辑
是否可恢复通常可以恢复通常不应恢复

核心原理:异常对象的构造与捕获流程

异常对象从创建到被捕获的全过程

关键步骤:

  1. 创建异常对象:new FileNotFoundException("...")
  2. 填充调用栈:JVM 在构造异常对象时调用 fillInStackTrace(),记录当前线程的方法调用链
  3. 栈展开(Stack Unwinding):JVM 沿着调用栈从当前方法开始向上搜索
  4. 匹配 catch:检查每个方法的异常表(Exception Table),找到匹配的 catch 块
  5. 执行处理:跳转到 catch 块,将异常对象传递给 catch 参数
  6. 未捕获终止:如果整个调用栈都没有匹配的 catch,线程终止,JVM 打印异常栈

字节码层面的异常表

每个方法在编译后都会携带一个异常表(Exception Table),这是 JVM 实现 try-catch 的底层机制。

public void readStudentFile(String path) {
    try {
        FileReader reader = new FileReader(path);   // ①
        BufferedReader br = new BufferedReader(reader);  // ②
        String line = br.readLine();   // ③
        System.out.println(line);      // ④
    } catch (FileNotFoundException e) {   // ⑤
        System.err.println("文件不存在");
    } catch (IOException e) {              // ⑥
        System.err.println("读取失败");
    }
}

编译后生成的异常表(伪代码):

fromtotargettype
①④⑤FileNotFoundException
①④⑥IOException
①④⑥any(finally 块)

编译原理详解:当异常在 from-to 范围内被抛出,如果异常类型与 type 匹配,JVM 将控制权跳转到 target 处的指令。这就是 "从上到下,精确匹配" 规则的字节码级实现。


飞翔科技实战:学生数据导入的异常分类

场景

小崔正在开发学生数据导入功能。用户上传 Excel 文件,系统解析后批量写入数据库。

架构师白歌帮他梳理可能出现的异常:

public class StudentImporter {

    /**
     * 导入学生数据
     * @param filePath Excel 文件路径
     * @throws StudentImportException 导入失败的自定义异常
     */
    public void importStudentFromExcel(String filePath) throws StudentImportException {
        // === 可能的异常分类 ===

        // ① 受检异常:文件不存在 → 必须处理
        File file = new File(filePath);
        if (!file.exists()) {
            throw new StudentImportException("文件不存在: " + filePath,
                new FileNotFoundException(filePath));
        }

        // ② 受检异常:数据库连接失败 → 必须处理
        try (Connection conn = DataSource.getConnection()) {
            // ③ 非受检异常:空指针 → 代码逻辑问题
            List<String> rows = ExcelParser.parse(filePath);
            for (String row : rows) {
                Student student = parseRow(row);
                // ④ 非受检异常:参数不合法 → 提前校验
                if (student.getName() == null || student.getName().isEmpty()) {
                    throw new IllegalArgumentException("学生姓名不能为空");
                }
                saveToDatabase(conn, student);
            }
        } catch (SQLException e) {
            throw new StudentImportException("数据库操作失败", e);
        }
    }
}
编号异常类型分类处理策略
①FileNotFoundException受检异常包装为业务异常抛出
②SQLException受检异常包装为业务异常抛出
③NullPointerException非受检异常提前判空,不让它发生
④IllegalArgumentException非受检异常提前校验参数,按需抛出

易错场景

反例一:用 catch (Exception) 一锅端

小崔初学时最喜欢的写法:

// ❌ 错误:捕获所有异常,掩盖了真实的错误类型
try {
    studentDao.save(student);
    emailService.sendNotification(student);
    fileService.exportReport(student);
} catch (Exception e) {
    // 简单打印,无法区分是数据库错误、邮件错误还是文件错误
    e.printStackTrace();
}

问题:

  1. 无法区分异常类型,无法做针对性恢复
  2. 可能捕获了 RuntimeException 掩盖代码 bug
  3. 违反了"只捕获你知道怎么处理的异常"原则
// ✅ 正确:分别处理不同类型的异常
try {
    studentDao.save(student);
    emailService.sendNotification(student);
    fileService.exportReport(student);
} catch (SQLException e) {
    // 数据库异常:记录日志,通知 DBA
    logger.error("数据库保存学生失败: {}", student.getId(), e);
    throw new StudentServiceException("保存学生信息失败", e);
} catch (MessagingException e) {
    // 邮件异常:降级处理,不影响主流程
    logger.warn("邮件通知发送失败,学生已保存: {}", student.getId(), e);
} catch (IOException e) {
    // 文件异常:记录日志,稍后重试
    logger.error("导出报告失败: {}", student.getId(), e);
    retryQueue.add(new ExportTask(student));
}

反例二:捕获 Error

// ❌ 错误:不应该捕获 Error
try {
    recursiveMethod(0);  // 可能 StackOverflowError
} catch (Error e) {      // 捕获 Error 是极其危险的行为
    System.out.println("继续运行");
}
// ✅ 正确:不要捕获 Error,让 JVM 处理
// 如果确实需要捕获,只能作为最后的防线(如记录日志后重新抛出)
try {
    recursiveMethod(0);
} catch (Error e) {
    logger.fatal("JVM 发生严重错误,即将退出", e);
    throw e;  // 必须重新抛出!
}

反例三:混淆受检和非受检

// ❌ 错误:对所有异常都用 try-catch 包起来
public String getStudentName(int id) {
    try {
        return studentDao.findById(id).getName();
    } catch (NullPointerException e) {  // 应该提前判空,而不是捕获 NPE
        return "未知";
    }
}
// ✅ 正确:用代码逻辑防御非受检异常
public String getStudentName(int id) {
    Student student = studentDao.findById(id);
    return student != null ? student.getName() : "未知";
}

面试考点

Q1:受检异常和非受检异常的本质区别是什么?

受检异常(Checked Exception)是 Exception 的子类(但不含 RuntimeException 及其子类),编译器强制要求程序必须处理(try-catch 或 throws),否则编译不通过。设计意图是提示"这里有一个可以预见且可能恢复的异常情况",如文件不存在可以提示用户重新选择。非受检异常(Unchecked Exception)是 RuntimeException 及其子类,编译器不强制处理,通常由程序逻辑错误导致(如 NPE、数组越界),正确的处理方式不是捕获而是修复代码本身。

Q2:Error 和 Exception 有什么区别?

Error 表示 JVM 层面的严重问题(如 OutOfMemoryError、StackOverflowError),通常是程序无法处理的,不应该被捕获。Exception 表示程序可以处理的异常,分为受检和非受检。简单记忆:Error 是"天灾",Exception 是"人祸"——天灾挡不住,人祸可以防。

Q3:什么场景下使用受检异常,什么场景使用非受检异常?

受检异常用于"可预见且可恢复"的场景:文件不存在、网络超时、数据库连接失败——调用方有义务处理这些情况。非受检异常用于"程序逻辑错误"场景:空指针、参数非法、状态不一致——调用方无法通过代码逻辑恢复,应该修复代码本身。Spring 框架选择了"全部使用非受检异常"的策略(DataAccessException 继承 RuntimeException),但 JDK 标准库仍大量使用受检异常。

Q4:catch (Exception e) 有什么风险?

① 捕获了 RuntimeException,掩盖了代码 bug(如 NPE),让程序在错误状态下继续运行;② 无法区分异常类型,无法做针对性处理;③ 可能意外捕获 ThreadDeath 等不应捕获的异常(尽管 ThreadDeath 继承 Error)。原则是:只捕获你知道怎么处理的异常,不知道的让它向上抛。

Q5:为什么 finally 块中的代码几乎总是执行?

finally 由 JVM 保证执行,机制是在编译时将 finally 块的代码复制到每个出口(正常 return、异常抛出)之前。唯一不执行的情况:① 在 try 或 catch 中调用了 System.exit();② 守护线程中所有非守护线程都结束了;③ JVM 崩溃。注意,即使 try 中有 return,finally 也会在 return 之前执行(这是字节码层面的保证)。

上一页
章节导读
下一页
try-catch-finally