☕ 第五章 异常

5.2 运行时异常与编译时异常

📄 一句话概括

Exception 家谱里的两个岔路口正式收网:RuntimeException 一派(运行时异常)编译器挥手放行,运行时才炸——多半是你程序自己的 bug;另一派(编译时异常)编译器当场拦下,逼你写好预案才准编译通过——全是外部世界不可控的风险。一句话:自己的 bug 改代码,外部风险写预案。

1. 为什么要分两派?

5.1 的家谱里,Exception 底下有两支。本节把这条线讲透——划分标准不是"严重程度",而是"这事该怪谁":

编译器就像安检员:运行时异常是你自己揣的雷(它假设你会自己拆);编译时异常是外部来的不明包裹(必须申报处理方案才放行)。

2. 运行时异常(unchecked):编译器放行,运行才炸

定义:RuntimeException 及其所有子类。 又叫"未检异常"(unchecked——编译器不检查)。

老朋友点名(全部亲手炸过):

异常类 触发场景 出身地
ArithmeticException 整数除以 0 5.1 Demo01
NullPointerException 钥匙是 null 还去用 3.2
ArrayIndexOutOfBoundsException 索引超出范围 2.7
ClassCastException 转型没验货 4.4
NumberFormatException 把"abc"转数字 会认即可

这派的共同点:

  1. 编译能过——编译器一个都不拦;
  2. 多半是逻辑 bug——空指针说明没判 null,越界说明边界没算对;
  3. 正确的根治方式是改代码(判空、检查索引、instanceof 验货),而不是到处 try-catch 把 bug 接住假装没事。
📝 "不强制处理" ≠ "不用处理"

编译器放行只是不逼你。真到该接的场合(比如用户输入导致的 NumberFormatException——外部输入你控制不了)照样要 try-catch。判断标准还是那句:能靠改代码根除的改代码,控制不了外部因素的写预案。

3. 编译时异常(受检):编译器当场拦下

定义:Exception 里除了 RuntimeException 之外的所有异常。 又叫"受检异常 / 检查性异常"(checked——编译器要检查)。

常见几位(会认即可):

异常类 场景
ClassNotFoundException 要加载的类找不到
FileNotFoundException 要读的文件不存在
IOException 读写数据时出意外(文件/网络)
ParseException 文本按格式解析失败(如日期)
InterruptedException 线程睡觉时被打断

体验一次"编译器逼你写预案"——在 IDEA05 里新建 Demo02,抄这一行:

public class Demo02 {
    public static void main(String[] args) {
        Class.forName("java.lang.String");   // 按名字加载类
    }
}

运行?根本轮不到运行——编译直接红字:

Unhandled exception: java.lang.ClassNotFoundException

Class.forName 这个方法天生可能遇到"类找不到",而类找不到是编译时异常——编译器逼你二选一(IDEA 会用红波浪线 + Alt+Enter 提示这两个选项):

  1. try-catch 接住:自己写预案处理——5.3 学;
  2. throws 往上甩:在方法签名声明"我这处理不了,谁调用谁负责"——5.3 学。

先感受关键结论:同样一行代码,运行时异常编译器看都不看,编译时异常直接拦下——两派的待遇差异,一眼见分晓。

4. 两派完整对照表

维度 运行时异常 编译时异常(受检)
家谱位置 RuntimeException 及其子类 Exception 里其余的
别名 未检异常 unchecked 受检异常 / 检查性异常 checked
编译器态度 放行,不强制处理 当场拦下,必须 try-catch 或 throws
什么时候暴露 运行时才炸 编译期就逼你面对
事出何方 程序自己的 bug 外部条件不可控
根治方式 改代码(判空/查边界/验货) 写预案(接住或上抛)
典型代表 除零、空指针、越界、转型失败 文件找不到、类找不到、IO 出错

5. 动手:亲眼看编译器拦人

在 IDEA05 的 Demo02 里完成三个实验:

三个 30 秒实验

  1. 被编译器拦一次:抄第 3 节的 Class.forName 行,看红波浪线和 Unhandled exception 报错——Alt+Enter 看 IDEA 给的两个预案选项,先不点。
  2. 运行时异常不被拦:在旁边写一行 int r = 10 / 0;——编译器毫无反应(能编译)。两派待遇同屏对比。
  3. 拆一次雷:把 10 / 0 改成 int b = 0; int r = args.length > 0 ? 10 / b : 10; 之前先想想——不管怎么绕,除零风险还在代码里,编译器照样不管。运行时异常的安全只能靠你自己改逻辑。

6. 常见误区

⚠️ 误区 1:编译时异常 = 编译错误

两码事。编译错误是语法不对(少分号),改语法就好;编译时异常语法完全正确,编译器拦你是因为你没写意外预案——它要的是 try-catch 或 throws,不是改语法。

⚠️ 误区 2:运行时异常不需要处理

是"不强制",不是"不该"。该判空的判空、该验货的验货——根治靠改代码;实在控制不了的外部因素(如用户输入),照样要接。

⚠️ 误区 3:写了 try-catch 运行时异常就不会发生了

try-catch 只是接住意外后的处理方案,不阻止意外发生。除零不会因为包了 try-catch 就变成能除。

⚠️ 误区 4:Exception 就是 RuntimeException

RuntimeException 只是 Exception 的一个分支。判断口诀:是不是 RuntimeException 或它的子类?是 → 运行时异常;不是(但仍属于 Exception)→ 编译时异常。

⚠️ 误区 5:编译时异常比运行时异常更严重

分派标准是"怪谁",不是"谁重"。空指针(运行时)可能让整个服务挂掉;文件找不到(编译时)换个路径就好。严重程度和处理方式是两码事。

7. 自测题(先自己答,再展开)

❓ Q1:两派异常的划分标准是什么?

怪谁。RuntimeException 一派是程序自己的 bug(代码写对就不会发生),编译器放行;其余是外部世界不可控的风险(代码再好也可能发生),编译器强制你写预案。

❓ Q2:`ClassCastException` 和 `ClassNotFoundException` 只差几个字母,怎么快速分派?

看家谱:前者是 RuntimeException 的子类 → 运行时异常,编译器不管;后者直属于 Exception(不在 RuntimeException 之下)→ 编译时异常,不写预案编译不过。背家谱,别猜名字。

❓ Q3:为什么编译器对两派态度不同?这样设计合理吗?

合理。运行时异常是纯代码 bug,编译器就算逼你 catch,你接住一个空指针也毫无意义——正确做法是改代码;而文件/网络这类外部风险无法靠改代码消除,只能靠预案兜底——不写预案的程序等于裸奔上路。

❓ Q4:`Unhandled exception: java.lang.ClassNotFoundException` 是编译错误吗?

不是编译错误(语法全对),是受检异常没写预案的报错。解法两个:try-catch 接住,或方法签名加 throws 往上甩——5.3 正式学。本质区别:编译错误改语法,这个写预案。

❓ Q5(毕业题):给下面 6 个异常分派,并说明编译器各自的态度——

NullPointerException、FileNotFoundException、ArithmeticException、ParseException、ClassCastException、IOException 运行时异常(编译器放行,运行才炸):NullPointerException、ArithmeticException、ClassCastException——全在 RuntimeException 家。编译时异常(编译器拦下,逼你写预案):FileNotFoundException、ParseException、IOException——Exception 直系,不在 RuntimeException 家。判断永远走家谱:RuntimeException 的子孙 → 放行;Exception 其余 → 拦下。

8. 下一步学什么


相关笔记