1. 为什么说异常处理是 Java 入门绕不过的坎我做过不少 Java 基础的辅导见过最典型的画面是这样的一段看起来没什么问题的代码运行起来突然抛个NullPointerException初学者盯着控制台看半天第一反应是把整个方法体都塞进try-catch接着在 catch 里不知道写什么最后干脆空着。结果程序不崩了业务结果却莫名其妙不对。异常处理这门课难的不是记住几个关键字而是把“程序出错时应该怎么走”这件事想清楚。JAVA 异常处理基础练习题这套内容就是把异常处理拆成一个个小场景有判断题、有读代码题、有让你自己动手写异常类的编码题。它不覆盖什么高深框架只围绕 Java 语言本身的异常体系展开重点训练三个能力第一看见堆栈信息能不像看天书一样慌乱第二知道什么时候用 try-catch、什么时候用 throws 把问题抛给上层第三写完代码之后能保证资源正确关闭、异常不被悄悄吞掉。这套题适合正在学 Java 基础语法、准备考证或者刚接触真实项目但被编译器报错吓住的同学。JDK 8 以上任意版本都行用 IDEA 或者直接命令行编译都没问题。我强烈建议不要把题目当阅读理解来做而是老老实实粘到环境里运行一遍再故意改几个地方看结果。很多异常问题只有亲手踩过坑、见过那种诡异现象后面写代码才会本能地避开。2. 练习题设计的总体思路从“看见报错不慌”到“会主动抛错”2.1 知识点地图到底要练哪些东西设计这套练习题的时候我把 Java 异常处理的知识点画成了一张地图所有题目围绕这张地图展开。第一层是异常体系本身也就是Throwable下面哪部分是Error、哪部分是Exception在Exception里哪些是非受检异常、哪些是受检异常这是后面所有判断的基础。第二层是处理语法包括 try-catch-finally 的执行顺序、catch 块的排序规则、多异常捕获写法。第三层是主动抛出也就是 throw 和 throws 的区别、自定义异常类怎么写、异常从底层方法往上传播的链路。第四层是现代写法比如 try-with-resources 自动关闭资源。这几层不是平铺的而是递进的。如果一上来就让学生自定义异常他连受检异常和非受检异常都分不清写出来的异常类也没法用。但如果只讲语法不做题学生照样会在真实场景里乱用。所以这套题的顺序是先确认认知再训练处理最后训练设计。2.2 难度递进逻辑判断、理解、编码、综合练习题的难度我分成了四档。第一档是判断题和选择题题目里给一段代码问你会抛出什么异常、编译能不能通过。第二档是读代码题让你解释 catch 块顺序、finally 执行结果这类问题。第三档是编码题比如自己定义一个业务异常类然后在调用处做捕获。第四档是综合场景题把用户输入、资源关闭、业务校验、异常提示串在同一个程序里。每一档之间我特意留了“认知台阶”。比如第三档编码题里我会先给一个自定义异常继承Exception的示例因为受检异常会被编译器强制要求处理学习者在写调用代码时能立刻感受到“必须捕获”的约束。等到第四档综合题他就要自己判断哪些异常该捕获、哪些异常该继续往外抛。这个设计逻辑和我平时写真实系统的思路是一致的异常处理不是为了消灭异常而是为了让程序在最外层能给出一个清晰、安全的反馈。2.3 为什么刻意保留“编译错误”类题目这套题里有一个容易被忽视的设计不少题目故意让代码无法编译。比如 catch 顺序写反、受检异常没有处理这些在我带新人的时候是最高发的问题。初学者往往认为编译不通过就是自己“记错语法”但实际上很多编译错误的根源是对异常概念理解偏了。把这类题目放进题库就是要让大家明白编译器报错不是洪水猛兽它是最耐心的老师会明确告诉你哪个片段需要处理异常。我印象很深的是有同学问我说既然ArithmeticException是非受检异常不处理也能编译那为什么练习里还要专门去 catch 它这个问题问得特别好。能编译不代表能正确运行除以零虽然不会强制你处理但一个记账程序如果因为用户输入了 0 就直接崩溃那显然是不可接受的。受检异常是编译器逼你处理非受检异常是责任在你的判断力两道门槛都不能省。3. 核心语法速览把概念边界先理清楚3.1 异常体系与受检异常、非受检异常的分辨Java 的异常类根是Throwable下面分了两大支。一支是Error比如OutOfMemoryError、StackOverflowError这类属于 JVM 层面的严重问题普通程序不应该去捕获捕获了也基本没法恢复。另一支是Exception里面又分两类一类是RuntimeException及其子类包括NullPointerException、ArrayIndexOutOfBoundsException、ArithmeticException、NumberFormatException这些叫非受检异常编译时不强制处理另一类是RuntimeException之外的受检异常比如IOException、FileNotFoundException、InterruptedException编译器会在编译阶段强制要求处理不处理直接报错。一个特别容易混淆的地方是很多人觉得“运行时异常”就是指“运行期才会发生的异常”于是想当然地认为其他异常就是编译期发生的。其实不是这样。受检异常也可以是运行期像 IOException 那样在读取文件时产生区别在于是不是强制要求处理。非受检异常在处理上没有硬性检查但它依然会在运行期打断程序所以设计时同样要认真对待。我整理了一个简单的对比表做题之前建议先扫一眼类别典型例子编译期强制处理合适处理方式ErrorOutOfMemoryError否不捕获从程序结构上规避RuntimeExceptionNullPointerException否主动判空、参数校验其他 ExceptionIOException、FileNotFoundException是try-catch 或 throws 声明自定义受检异常账户余额不足异常是调用处捕获并做业务提示自定义运行异常参数非法异常否调用方按约定处理或不处理3.2 try-catch-finally 的执行顺序不能靠背很多初学者背了一段顺口溜说“异常来了找 catch找完就去 finallyfinally 一定会执行”。这个顺口溜说出了大概但没有覆盖真正细节。我见过太多在这种题上翻车的情况所以这里把几条硬规则先放出来。第一条try 块里没抛异常catch 块会被跳过finally 块在 try 块之后执行。第二条try 块里抛了异常会立刻跳转到第一个匹配的 catch 块后面的 try 块代码不会再执行然后继续执行 finally。第三条catch 声明顺序很重要子类必须写在父类前面。第四条如果 catch 没有捕获住异常异常会继续向上传播但 finally 依然会执行。第五条finally 里出现了 return会把 try 或 catch 块里的 return 值覆盖掉。实际代码中我最强调第五条。因为很多人写资源关闭时习惯在 finally 里做清理如果顺手 return 一个状态码很可能把真正的异常结果覆盖。等一下我给的练习题里就有这个坑现在先记住最后执行的 finally 拥有“一票否决权”就够了细节后面细看。3.3 throw 与 throws一个负责“抛出”一个负责“声明”这两个关键字只差一个字母但职责完全不同。throw 出现在方法体内部后面跟一个异常对象表示“这个地方我主动制造一个异常让程序在这里停下”。throws 出现在方法声明处后面跟异常类型表示“我这个方法自己不处理调用我的人要负责处理否则编译期就会报错”。举个例子一个取款方法检查到余额不足想提醒调用方就可以在方法内部写throw new InsufficientBalanceException(余额不足)同时方法签名写上throws InsufficientBalanceException。throw 负责制造问题throws 负责说明问题归属。这时候如果调用的地方不处理编译器是过不去的这就是受检异常带来的约束感。选择受检还是非受检其实是设计判断后面综合题会专门讨论。还要注意一点如果一个方法声明抛出了多个异常类型throws 后面用逗号分隔如果多个异常之间是父子关系只声明父类型即可。catch 块则可以用多异常捕获语法catch (IOException | SQLException e)变量 e 默认是 final 的不需要也不能给它重新赋值。3.4 try-with-resources资源关闭的正确姿势在 Java 7 之前手动关闭资源是一件痛苦的事。文件流、数据库连接、网络连接都要在 finally 里判断非空再关而且 close 本身还会再抛一个 IOException于是还得再 try-catch 一层。很多代码就这样被三层嵌套搞得没法看。try-with-resources 就是用来解决这个问题的。语法上把资源对象放进 try 后面的括号里比如try (BufferedReader reader new BufferedReader(new FileReader(config.txt)))只要是实现了AutoCloseable接口的类型都能这么写。退出 try 块时虚拟机自动调用 close而且关闭时机是在 catch 或 finally 逻辑之前不会污染业务代码。Java 9 以后还允许使用 already-final 或 effectively final 的外部资源变量不一定要在 try 括号里 new。这一点可靠友好但很多人也确实不熟悉。练习里我安排了一道原版手工关闭和 try-with-resources 重写的对照题就是为了帮大家把这条路径彻底走顺。4. 基础练习题与参考答案含讲解4.1 第一题识别异常类型题目给出下面几段独立代码请你判断每一段分别会抛出什么类型的异常并写出它的父类。// 片段 A int[] nums new int[3]; nums[3] 10; // 片段 B String s null; s.length(); // 片段 C int result 10 / 0;参考答案片段 A 是ArrayIndexOutOfBoundsException片段 B 是NullPointerException片段 C 是ArithmeticException。这三个异常都是RuntimeException的子类属于非受检异常编译阶段不会被强制处理。这道题的目的不是让大家背异常名字而是训练第一反应。数组长度为 3下标范围是 0 到 2访问 3 就是越界。对 null 调用方法必然空指针。整数除以 0得到算数异常。初学者最容易把 B 段和 C 段记混看到s.length()就怀疑是数组问题其实这里根本没数组s 本身就是 null所以是空指针。做题的时候不要只看症状要看异常的触发点。4.2 第二题多异常捕获顺序的判断题题目给出下面的代码片段问是否能正常编译如果不能请说明原因并修正try { int result Integer.parseInt(abc); } catch (Exception e) { System.out.println(捕获到 Exception); } catch (NumberFormatException e) { System.out.println(捕获到 NumberFormatException); }参考答案不能编译。因为NumberFormatException是IllegalArgumentException的子类而IllegalArgumentException是RuntimeException的子类最终它也是Exception的子类。当第一个 catch 写了Exception时编译器判定后面的NumberFormatException永远不会到达于是给出“已经被捕获”的编译错误。修正做法是把范围小的异常写在前面范围大的写在后面也就是先catch (NumberFormatException e)再catch (Exception e)。这是一个高频考点不只是笔试里爱考实际编码中如果你写了一个总异常在最前面后面所有细化异常分支都相当于死的会让排查问题变得非常困难。有人问过为什么不干脆只写一个catch (Exception e)反正都能接住这样可以但你会丢掉异常类型本身的区分度日志里只知道有异常不知道是哪一类出了错这会让你在线上排查时多花好几倍的时间。4.3 第三题finally 里的 return 覆盖问题题目写出下面方法的返回值并解释为什么。public static int test() { try { return 1; } finally { return 2; } }参考答案返回值是 2。这段代码里 try 块准备返回 1但在真正 return 之前finally 块抢先执行了而且 finally 里也有 return于是这个 return 的结果把 1 覆盖掉了。这道题属于“看着简单、做起来容易错”的典型。很多人记住了 finally 一定会执行却没有记住 finally 里的 return 有更高的优先级。更深一层是finally 不仅能覆盖正常 return还能覆盖异常。假如 try 块里抛了一个异常catch 块里已经有了处理结果结果 finally 又 return 了一个值那这个返回值会把整个异常处理链路打断上层调用方拿到的状态是完全错误的。这种代码一旦进到真实项目排错成本极高。所以我在实际开发时有一个铁律不要在 finally 里写 return也不要在 try 或 catch 里用 return 包装复杂逻辑清晰的流程比少写几行代码重要得多。4.4 第四题受检异常必须处理题目下面代码能否编译如果不处理会发生什么public class FileReadDemo { public static void main(String[] args) { java.io.FileReader reader new java.io.FileReader(data.txt); System.out.println(reader.read()); } }参考答案不能编译。new FileReader(data.txt)这个构造方法声明了throws FileNotFoundException而FileNotFoundException是受检异常。reader.read()又声明了throws IOException同样是受检异常。编译器会直接要求处理否则报错。修正方案有两种。第一种是把异常交给 main 方法继续往外抛public static void main(String[] args) throws IOException { java.io.FileReader reader new java.io.FileReader(data.txt); System.out.println(reader.read()); }不过这种方式在 main 方法里不太现实因为 main 是程序入口继续往外抛意味着 JVM 直接终止。更合理的是在当前方法内捕获并处理try { java.io.FileReader reader new java.io.FileReader(data.txt); System.out.println(reader.read()); } catch (IOException e) { System.out.println(文件读取失败 e.getMessage()); }这里可以顺带区分一下受检异常不是编译期一定发生而是编译期强制要求你“为它留下处理路径”。哪怕你只是声明了 throws也算一种处理方式。我见过不少人为了编译通过在方法上无脑加throws Exception这确实让编译通过了但把所有异常责任都推给了调用方很多层方法串起来之后异常会一路甩到最外层最后你能拿到的堆栈又长又难读。正确做法是先想清楚这个异常谁更适合处理如果当前方法有足够的上下文给出业务提示就自己 catch如果当前方法根本没有能力处理再设计 throws 让上层统一收口。4.5 第五题自定义业务异常题目银行卡取款时如果余额不足要求程序给出明确的业务提示而不是一个冷冰冰的运行时崩溃请定义一个自定义异常类并在取款方法中主动抛出。参考答案class InsufficientBalanceException extends Exception { public InsufficientBalanceException(String message) { super(message); } } class Account { private double balance; public void deposit(double amount) { balance amount; } public void withdraw(double amount) throws InsufficientBalanceException { if (amount 0) { throw new IllegalArgumentException(取款金额不能为负数); } if (amount balance) { throw new InsufficientBalanceException(余额不足当前余额 balance); } balance - amount; } public double getBalance() { return balance; } }这段代码里的关键点有两个。第一个自定义异常继承了Exception所以它是受检异常编译器要求所有调用withdraw的地方都必须处理这个异常。这一点对业务场景很有价值因为余额不足是一个业务上必须让用户感知的条件不应该被静默吞掉。第二个我特意在取款方法里还加了一层参数校验金额为负数时抛出IllegalArgumentException这是个非受检异常。为什么会这样混着用因为“金额为负数”属于调用方传参错误属于程序契约被破坏不能假装正常的业务失败而“余额不足”属于预期内的业务分支是用户可以理解、可以主动通过换卡或充值来补救的情况。两类问题的性质不同在异常类型上分开调用方才能正确区分处理级别。很多同学会问自定义异常到底继承Exception还是继承RuntimeException我的经验是如果这个异常希望强迫调用者意识到“可能失败”并采取措施比如余额不足、库存不够、重复提交订单就用受检异常如果这个异常是因为代码本身写错了、参数校验没做好比如空指针、格式错误就用非受检异常。真实项目里团队往往会约定规范但在练习阶段把这两种感受都体验一遍是最有价值的。4.6 第六题异常传播与堆栈轨迹题目看下面的代码描述异常从发生到被捕获经过了哪些层并说明堆栈轨迹是什么。public class ExceptionPropagation { public static void main(String[] args) { try { level1(); } catch (IOException e) { System.out.println(在最外层捕获 e.getMessage()); e.printStackTrace(); } } public static void level1() throws IOException { level2(); } public static void level2() throws IOException { throw new IOException(磁盘访问失败); } }参考答案异常在level2()方法中通过throw new IOException(磁盘访问失败)被制造出来。level2()的方法签名声明了throws IOException所以它不处理把异常交给调用它的level1()。level1()也声明了throws IOException同样不处理继续往上传。最终异常传到main方法的 try 块里被 catch 捕获。输出内容中最重要的是e.printStackTrace()打印的堆栈轨迹。堆栈轨迹的第一行会指明异常类型和信息下面每一行都是“方法名、所在类、源码行号”并列出一条完整的调用链。比如at ExceptionPropagation.level2、at ExceptionPropagation.level1、at ExceptionPropagation.main这就是异常从实际抛出点一级一级往上传播的证据。我看到过很多初学者踩的坑是只调用e.getMessage()不打印堆栈。这样确实能得到“磁盘访问失败”这样的信息但完全不知道这个异常是哪个文件、哪一行产生的。真实项目里异常堆栈就是程序员的第一现场宁可日志多打几行也不要为了日志美观把它截断。练习这一题时建议大家故意把某个方法改成不声明 throws观察编译器怎么提示或者把 catch 放到 main 外面的不同层观察堆栈变化。这种变形练习比单做一道题更有收获。4.7 第七题用 try-with-resources 重构资源关闭题目下面的代码使用 BufferedReader 读取文件你能找出资源关闭方面的问题吗用 try-with-resources 改写。BufferedReader reader null; try { reader new BufferedReader(new FileReader(config.txt)); System.out.println(reader.readLine()); } catch (IOException e) { e.printStackTrace(); } finally { if (reader ! null) { try { reader.close(); } catch (IOException e) { e.printStackTrace(); } } }原代码的问题在于读取文件时如果new FileReader(config.txt)就抛出了异常那么 reader 还是 nullfinally 里的非空判断可以规避但如果reader.readLine()抛了 IOExceptiontry 块立即退出进入 catch此时 reader 已经创建成功finally 里能正常关闭逻辑上是对的。不过这种写法有太多嵌套可读性差而且稍有不慎容易漏掉关闭。如果是多个资源比如还要同时操作输入流和输出流这种写法会变得极其恶劣。用 try-with-resources 改写后逻辑会简化很多try (BufferedReader reader new BufferedReader(new FileReader(config.txt))) { System.out.println(reader.readLine()); } catch (IOException e) { e.printStackTrace(); }改写后最明显的差别是不需要自己写 finally不需要非空判断也不需要再处理 close 时抛出的 IOException资源关闭由 JVM 负责。这里有一个细节要记住资源关闭的顺序与 try 括号里声明顺序相反也就是后声明的先关闭。这在依赖关系里很关键比如你有一个 HTTP 响应流依赖请求流必须先关闭响应流再关闭请求流那么声明顺序就要反过来请求流先声明响应流后声明。这道题还有一段进阶内容如果在 try 块体里抛出业务异常同时资源 close 时也抛异常那么 close 的异常会被抑制但主异常对象会把被抑制的异常记在suppressedExceptions里面。从日志看它是一层特殊的堆栈后处理。平时了解这个现象就够了不必纠结重点是别自己写 finally 手忙脚乱把主异常覆盖掉。4.8 第八题综合场景题——账号取款流程题目编写一个控制台程序模拟账号取款流程。用户输入取款金额程序判断金额格式、判断账户余额是否充足最后输出结果并保证输入资源被正确关闭。public class WithdrawDemo { public static void main(String[] args) { Account account new Account(); account.deposit(1000); try (Scanner scanner new Scanner(System.in)) { System.out.print(请输入取款金额); double amount Double.parseDouble(scanner.nextLine()); account.withdraw(amount); System.out.println(取款成功剩余余额 account.getBalance()); } catch (NumberFormatException e) { System.out.println(金额格式不正确请输入数字。); } catch (InsufficientBalanceException e) { System.out.println(业务校验失败 e.getMessage()); } } }这段代码覆盖了三种异常的处理。Double.parseDouble在用户输入非数字文本时会抛NumberFormatException这是非受检异常但在这里必须被 catch因为用户体验上要给出“格式不对”的提示。account.withdraw可能抛InsufficientBalanceException这是之前自定义的受检异常在业务上要明确提示余额不足。Scanner 实现了AutoCloseable用 try-with-resources 包裹后哪怕取款过程中抛了异常资源也能安全关闭。这道题需要重点反思的是异常处理的“分工”。格式错误和余额不足虽然都会导致取款失败但它们产生的位置不同、性质也不同。格式错误是入口校验问题理论上可以在更早的输入阶段用正则或类型判断拦截连解析都不走到余额不足是账户业务规则问题必须由 Account 类的取款方法来定义。把它们放在同一个 catch 链里是因为在控制台程序层面最终都要给用户一个友好的文字反馈但再往上层走这两种异常可能一个是系统日志的输入告警一个是写入审计记录的业务事件处理位置可能会完全不同。5. 高频报错与排查技巧实录5.1 异常被空 catch 吞掉程序安静地错下去我在练习批改中最痛心的代码就是空 catch。有的初学者为了避免控制台输出红色错误直接写出这样的代码try { int result Integer.parseInt(input); } catch (Exception e) { // 什么都不做 }程序确实不会崩但没有提示、没有日志、没有重试用户会觉得点了个按钮然后毫无反应。这种问题在练习阶段看起来只是“不够细心”到了真实项目就是线上事故。我在实际开发中给自己定了一条规则catch 块里要么给出用户可见的提示要么写日志记录完整堆栈要么重新抛出并说明原因绝不允许空块。如果是在学习阶段最简单的做法是至少保留e.printStackTrace()。虽然正式项目里应该用日志框架把堆栈输出到日志文件但这个填空功能能让你立刻看到异常来自哪里。不要用System.out.println(e.getMessage())取代它因为 getMessage 很可能为 null而且没有堆栈轨迹。5.2 finally 里的 return 把异常结果盖掉有一次我给一个模拟项目排查问题方法里明明 catch 到了某个异常并返回了业务错误码可调用方拿到的永远是 0也就是“成功”。追了半天发现在 finally 里有一行return 0;它把 catch 块里的return -1完全覆盖了。这个坑在练习题第三题里已经出现过但真实场景里它往往隐藏得更深因为它不会让程序崩溃只会让状态静默错乱。排查这类问题的思路是先看方法里有没有 finally再看 finally 里有没有 return最后看返回值有没有被 finally 逻辑改写。如果代码里有很多个 return建议先重构掉避免多条分支交叉。正常写法只让 try 和 catch 承载业务流程finally 只负责清理资源不承载任何“最后结果”。5.3 catch 顺序写反编译器报错还是逻辑失效catch 顺序问题在编译期有两种表现。如果先写了父类再写子类编译器通常会直接报错提示子类异常已经被捕获因为这是不可达的代码块。但如果你用的是同级别异常比如先catch (IOException e)再catch (SQLException e)它们没有继承关系顺序无所谓如果是RuntimeException和NumberFormatException编译器同样会报错。然而真实代码里的一个隐蔽情况是catch 顺序本身没有编译问题但写在前面的 catch 类型范围太宽导致后面的窄范围 catch 永远没机会执行。排查时可以先看 catch 列表里有没有宽泛类型在窄类型之前再去业务日志里比对异常实际类型。顺序问题最好的预防手段就是遵守一条经验法则子类在前父类在后具体异常在前通用异常在后。5.4 空指针定位三板斧NullPointerException是 Java 里出现频率最高的非受检异常。它的问题在于本身信息很少老版本 JDK 的报错往往只告诉你空指针发生在哪一行但没说哪个对象是 null。我排查时一般按三个步骤走。第一看堆栈第一行确定行号。打开对应源码行看这一行里调用方法或访问属性的对象是哪个。第二去这个对象创建的地方打断点运行时看变量面板里哪个对象实际是 null。如果是方法参数检查调用方传给它的值如果是纠结的链式调用比如order.getUser().getName()就需要拆开写降级。第三在代码结构上加防线比如对可能为 null 的对象先判空或者用Optional包装返回值让调用方明确知道这一层可能为空。这里还要提醒一点NullPointerException是非受检异常编译器不会强制你处理。所以别依赖编译器靠的是自己写代码时保持判断。练习时最常见的错误是盲目把整个方法塞进 try-catch 去接住空指针结果掩埋了真正的问题。空指针应该从源头解决而不是统一捕获后当什么都没发生。5.5 重新抛出异常时把原始堆栈弄丢在项目里经常能看到这样的封装写法catch (IOException e) { throw new ServiceException(文件读取失败); }这样写有一个很大的问题原始的IOException堆栈完全丢了等到上层排查时只能看到ServiceException根本不知道底层是哪一行、哪个文件路径出的错。正确的做法是把原始异常作为新异常的 cause 传进去catch (IOException e) { throw new ServiceException(文件读取失败, e); }这样有一个好处日志里既能保留业务语义“文件读取失败”又能通过 cause 一路追溯到最底层的 IOException 堆栈。我带的很多新人在做项目时第一次看到因果链和主异常的关系都会恍然大悟。练习题阶段虽然不需要写很复杂的异常包装但建议你们养成这个习惯别在 throw 新异常时把老异常扔掉。6. 从基础练习到真实代码的几点体会处理异常的思路其实在练习题阶段就能建立起来。比如判断题让你区分受检异常和非受检异常是在培养分类思维自定义异常题目是在培养设计意识综合题里把输入、业务、资源关闭放在一起是在锻炼全局把控。真正进入真实项目后我的体会是多了一层“契约感”。一个方法在对外暴露时异常就是它契约的一部分。声明了throws的方法调用方必须知道它可能抛什么不声明但实际抛RuntimeException的方法调用方就要从参数和文档里推断风险。好的代码在异常设计上是清晰的不会让上层拿到一个不知道该怎么处理的Exception也不会写一长串让人看了头晕的嵌套 try-catch。我对“什么时候捕获、什么时候抛出去”的经验是如果你在这个方法里能给出具体的、面向用户的反馈就实现 catch如果这个方法只是一个中间层并没有能力决定错误给谁看就继续抛出。捕获不是越早越好抛出也不是越多越好。异常处理更像责任分配每个层级只处理自己该处理的那一段剩下的传给上家。最后再分享一个小细节练习时不要把报错当成失败。每一道异常题的答案区域都应该先自己故意写错几次再回来对照解析。我之前整理这些题的时候自己每道题都跑过“错误版本”发现印象最深的就是那些报错信息本身。等你真正养成了看见堆栈就精神起来的习惯异常处理这一关才算稳稳过了。