CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Java 语言基础/课时/异常处理
RawBlame
课时目录14 节

Java 语言基础

  1. 01Java 与 JVM 是什么
  2. 02基本语法与 main
  3. 03基本类型与引用类型
  4. 04条件与循环
  5. 05方法与参数
  6. 06数组
  7. 07类与封装
  8. 08继承接口与多态
  9. 09集合框架入门
  10. 10字符串
  11. 11异常处理
  12. 12文件 IO 入门
  13. 13Maven 与项目结构
  14. 14综合练习与 dsa-java 分工

异常处理

try/catch、throws、自定义异常。

30 分钟

异常处理

这一课你将能做到

  1. 画出异常类层次(Throwable → Error / Exception),说清受检异常与非受检异常的边界
  2. 正确使用 try/catch/finally、throw、throws,并用 try-with-resources 安全关闭资源
  3. 设计自定义业务异常(如 BizException)——本站后端统一异常处理体系的地基

为什么要学

程序跑着跑着会出事:数据库连不上、参数非法、文件不存在、除以零。异常机制就是 Java 的「事故处理流程」:出事时抛出一个描述问题的对象,往上传递,直到有人接住处理。

后端视角尤其重要:用户请求打过来,Service 层抛出「库存不足」,Controller 层不能让用户看到 NullPointerException 的一堆堆栈——要转成友好的 {"code": 40001, "msg": "库存不足"}。Spring 的 @RestControllerAdvice 全局异常处理,底层完全建立在今天这课的知识上。

异常处理写得好 = 故障可定位、用户看人话、系统不崩;写得差 = 到处 catch (Exception e) {} 静默吞掉,出了问题全组抓瞎。

先建立图像

Throwable(一切可抛对象的老祖宗)
├── Error              系统级灾难,别管也管不了(OutOfMemoryError、StackOverflowError)
└── Exception
    ├── RuntimeException        非受检(unchecked):编译器不强制处理
    │   ├── NullPointerException            空指针
    │   ├── ArrayIndexOutOfBoundsException  数组越界
    │   ├── ClassCastException              强转失败
    │   ├── NumberFormatException           字符串转数字失败
    │   └── ArithmeticException             整数除以 0
    └── 其他 Exception          受检(checked):编译器强制你 catch 或 throws
        ├── IOException                     IO 出错
        └── SQLException                    数据库出错

传递路线:方法内 throw → 沿调用栈向上 → 谁都没接住 → JVM 打印堆栈并终止线程

跟着做

步骤 1:体验 try/catch

新建 ExceptionDemo.java:

public class ExceptionDemo {
    public static void main(String[] args) {
        try {
            int result = 10 / 0;                    // 这里会抛 ArithmeticException
            System.out.println("这行永远执行不到");
        } catch (ArithmeticException e) {
            System.out.println("捕获到异常: " + e.getMessage());   // / by zero
        }
        System.out.println("程序继续正常运行");       // 没被异常带崩

        // 多个 catch:从子类到父类排列
        try {
            String s = null;
            s.length();
        } catch (NullPointerException e) {
            System.out.println("空指针: " + e.getMessage());
        } catch (RuntimeException e) {              // 兜底分支放最后
            System.out.println("其他运行时异常");
        }

        // 一个 catch 接多种异常(互为兄弟类型时用 |)
        try {
            int[] a = new int[2];
            a[5] = 1;
        } catch (NullPointerException | ArrayIndexOutOfBoundsException e) {
            System.out.println("空指针或越界: " + e.getClass().getSimpleName());
        }
    }
}

步骤 2:finally 的执行时机

public class FinallyDemo {
    public static void main(String[] args) {
        System.out.println(test());
    }

    static String test() {
        try {
            int r = 10 / 0;
            return "try 返回";
        } catch (ArithmeticException e) {
            return "catch 返回";         // 即使这里 return,finally 依然执行
        } finally {
            System.out.println("finally 总是执行(清理资源用)");
        }
    }
}

输出:

finally 总是执行(清理资源用)
catch 返回

finally 在「try/catch 返回值已确定之后、真正返回之前」执行——所以 finally 里写 return 会覆盖正常返回值,禁止这么写。

步骤 3:throw 与 throws

public class ThrowDemo {
    public static void main(String[] args) {
        try {
            setAge(-5);
        } catch (IllegalArgumentException e) {
            System.out.println("业务拦截: " + e.getMessage());
        }
    }

    static void setAge(int age) {
        if (age < 0 || age > 150) {
            // throw:方法体内主动抛出一个异常对象(注意是实例不是类)
            throw new IllegalArgumentException("年龄非法: " + age);
        }
        System.out.println("设置成功: " + age);
    }

    // throws:方法签名上声明「本方法可能抛出受检异常,调用方必须处理」
    static String readFile(String path) throws java.io.IOException {
        return java.nio.file.Files.readString(java.nio.file.Path.of(path));
    }
}

区分:throw 是动作(抛出去),throws 是声明(提前打招呼)。受检异常必须二选一:catch 住,或者 throws 往上传。

步骤 4:受检异常的传播链

public class CheckedExceptionDemo {
    public static void main(String[] args) {
        try {
            loadConfig("app.properties");
        } catch (java.io.IOException e) {
            System.out.println("读配置失败,用默认配置启动: " + e.getMessage());
        }
    }

    static String loadConfig(String name) throws java.io.IOException {
        return readAll(name);          // 受检异常逐层向上声明,最终有人处理
    }

    static String readAll(String name) throws java.io.IOException {
        // 若文件不存在,Files.readString 抛 NoSuchFileException(IOException 子类)
        return java.nio.file.Files.readString(java.nio.file.Path.of(name));
    }
}

把 throws 删掉试试——编译器直接报错「未报告的异常错误,必须对其进行捕获或声明以便抛出」。这就是「受检」的含义:编译期强制你表态。

步骤 5:自定义业务异常

// 业务异常基类:非受检(继承 RuntimeException),避免调用链上层层 throws 污染
class BizException extends RuntimeException {
    private final int code;          // 错误码,对接前端的 code 字段

    public BizException(int code, String message) {
        super(message);
        this.code = code;
    }

    public int getCode() { return code; }
}

class InsufficientStockException extends BizException {
    public InsufficientStockException(long productId, int shortage) {
        super(40001, "商品 " + productId + " 库存不足,缺 " + shortage + " 件");
    }
}

public class BizExceptionDemo {
    public static void main(String[] args) {
        try {
            createOrder(1001L, 5, 3);      // 想买 5 件,只有 3 件
        } catch (BizException e) {
            // 模拟 Spring 全局异常处理:把异常翻译成统一响应体
            System.out.printf("{\"code\": %d, \"msg\": \"%s\"}%n", e.getCode(), e.getMessage());
        }
    }

    static void createOrder(long productId, int want, int stock) {
        if (want > stock) {
            throw new InsufficientStockException(productId, want - stock);
        }
        System.out.println("下单成功");
    }
}

输出:{"code": 40001, "msg": "商品 1001 库存不足,缺 2 件"}。这就是本站后端异常体系的微缩模型:Service 抛业务异常,全局处理器统一翻译。

步骤 6:try-with-resources

import java.io.BufferedReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class TwrDemo {
    public static void main(String[] args) {
        Path path = Path.of("demo.txt");
        try {
            Files.writeString(path, "第一行\n第二行\n");     // 先造个文件(JDK 11+ API)
        } catch (IOException e) {
            System.out.println("写文件失败: " + e.getMessage());
        }

        // try-with-resources:括号里声明的资源自动关闭(JDK 7+)
        try (BufferedReader reader = Files.newBufferedReader(path)) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println("读到: " + line);
            }
        } catch (IOException e) {
            System.out.println("读文件失败: " + e.getMessage());
        }
        // 不需要 finally 里写 reader.close()——编译器自动生成,且异常时也保证关闭
    }
}

要求:资源类必须实现 AutoCloseable。多个资源用分号隔开,关闭顺序与声明相反。

讲清楚要点

  1. 受检 vs 非受检的分界:受检(IO、SQL 等外部依赖故障)编译器强制处理——「可预见、可恢复」;非受检(NPE、越界等编程错误)编译器不管——「修代码而不是 catch」。
  2. catch 要具体,不要一把梭:catch (Exception e) 会把 NPE 等编程 bug 也吞掉。只 catch 预期内的异常,未知异常让它炸出来暴露问题。
  3. 绝对禁止空 catch:catch (Exception e) {} 让故障凭空消失,线上排查时堆栈什么都没有。最低限度也要 log.error("xx失败", e)(学习阶段 e.printStackTrace())。
  4. finally 负责清理,try-with-resources 是它的现代替代:新代码一律 try-with-resources。
  5. 异常别用来控制正常流程:用异常当 if 用(循环里靠异常跳出)性能差且难读;异常是「异常」路径的机制。
  6. 异常信息要带上下文:throw new IllegalArgumentException("年龄非法: " + age) 远好于 throw new IllegalArgumentException("参数错误")——排错时前者一眼定位。

你可能会懵的地方

  • 为什么自定义异常继承 RuntimeException 而不是 Exception? 继承 Exception 是受检异常,调用链上每个方法都得 throws 或 catch,层层污染。业务异常属于「可预期的错误流程」,让它非受检,只在最外层(Spring 全局处理器)统一接住。

  • finally 一定执行吗? 几乎总是,除了三种极端:System.exit() 强退、JVM 崩溃、守护线程被直接终止。另一个考点:finally 里 return 会「吃掉」try/catch 的返回值,所以规范禁止。

  • catch 顺序有什么讲究? 子类在前父类在后。catch (Exception e) 放第一个会让后面所有 catch 变成不可达代码,编译报错。兄弟类型之间顺序随意,可用 | 合并。

  • Error 和 Exception 什么区别? Error 是 JVM 级致命错误(内存耗尽、栈溢出),程序设计上不捕获不处理;Exception 是程序可处理的异常。别写 catch (Error e),也别指望 catch OutOfMemoryError 能救回系统。

  • throw 之后的方法代码还执行吗? 不执行。throw 会立即中断当前方法,异常沿调用栈向上找 catch;如果没有任何方法捕获,最终到 main,线程终止并打印堆栈。

  • RuntimeException 为什么编译器不检查? Java 设计者的取舍:NPE、越界这类是代码 bug,到处 throws 会让签名全是噪音;修复 bug 才是正解。所以 IDE 提示「method throws NullPointerException」是提醒你写防御代码,不是让你 catch。

错法对照

错法问题正解
catch (Exception e) {} 空捕获故障静默消失至少打印堆栈 e.printStackTrace() / 记日志
catch 里只打印不处理继续跑后续逻辑基于错误状态恢复/重试/向上抛,明确策略
finally 里写 return覆盖 try/catch 的返回值finally 只做清理
手写 finally { reader.close(); }啰嗦且 close 本身可能抛异常try-with-resources
catch (Exception e) 放在第一个 catch后续 catch 不可达,编译错子类异常在前,父类兜底在后
受检异常既不 catch 又不 throws编译错误二选一
用异常控制正常分支性能差、语义混乱正常流程用 if/return
吞掉异常再返回 null调用方 NPE,问题更隐蔽抛异常或返回 Optional/默认值

卡点急救

  1. 编译报「未报告的异常错误 XXX,必须对其进行捕获或声明」:受检异常没处理。要么 try/catch,要么方法签名加 throws。IDEA 中 Alt+Enter 一键给出两个选项。
  2. catch 了却好像没 catch 住:检查异常类型——抛出的是子类,catch 的是不相关的类;或异常发生在 try 块之前。打印 e.getClass().getName() 确认真实类型。
  3. 看不到完整报错信息:只打了 e.getMessage()(可能为 null)。改打 e.printStackTrace() 或日志 log.error("ctx", e),堆栈第一行是异常类名+消息,往下是调用链。
  4. try-with-resources 编译错「资源必须实现 AutoCloseable」:该类没有 close 语义,手工管理;或换用支持自动关闭的标准类(Files、流、连接)。
  5. 自定义异常没被 catch 到:确认 catch 的类型是抛出类型的父类或本身;catch (BizException e) 能接住子类 InsufficientStockException,反之不行。

面试常问

  • 受检异常和非受检异常的区别?(必考) 受检:Exception 非 RuntimeException 分支,编译器强制 catch/throws,代表外部依赖故障,典型 IOException/SQLException;非受检:RuntimeException 和 Error,编译器不检查,代表编程错误或系统灾难,典型 NPE/越界/StackOverflowError。

  • finally 一定执行吗?什么时候不执行? 除 System.exit、JVM 崩溃、守护线程被杀外都执行。finally 在 return 值确定后、真正返回前执行;finally 中的 return 会覆盖原返回值,禁止使用。

  • throw 和 throws 的区别? throw 是语句,在方法体内抛出异常对象,一个 throw 抛一个实例;throws 是方法签名声明,说明该方法可能抛出的受检异常类型,可声明多个,由调用方处理。

  • 异常处理影响性能吗? 抛异常要填充堆栈快照,成本高(微秒级);try 本身几乎零成本。所以不要用异常做流程控制,高频路径避免靠异常「探边界」。

  • Spring 里怎么统一处理异常?(后端实战题) @RestControllerAdvice + @ExceptionHandler(BizException.class) 全局拦截,把业务异常翻译成统一响应体 {code, msg};未知异常兜底成 500 并记录日志,用户看不到原始堆栈。

  • return 和 finally 的执行顺序? try 中 return:先计算返回值并存好 → 执行 finally → 返回存好的值。若 finally 中改了返回的引用对象内容(不是换对象),改动能体现出来。

自测三问

  1. 下面三个异常哪些是受检的:IOException、NullPointerException、SQLException?编译器分别怎么要求?
  2. finally 里写 return 会导致什么?为什么规范禁止?
  3. 设计「余额不足」业务异常:继承谁?带什么字段?在哪一层抛、哪一层接?

小结一句话

异常是向上传递的事故对象:受检的编译器逼你表态、非受检的是代码 bug 要修不要吞;throw 抛、throws 声明、try-with-resources 管清理、业务异常继承 RuntimeException——后端的统一错误码体系就从这起步。

延伸

  • Oracle:异常(Exceptions)
  • 下一课:文件 IO 入门——Path/Files 现代 API、逐行读取、try-with-resources 实战,为日志和配置处理打底。
加载本节练习…
← 上一课字符串下一课 →文件 IO 入门