运行时异常与编译时异常 | JavaSE
运行时异常与编译时异常
一、学习目标
完成本章后,你应该能够:
- 能够区分运行时异常(Runtime Exception)与受检异常(Checked Exception)。
- 能够解释
RuntimeException在 Java 异常体系中的位置。 - 能够判断一个异常是否受到 Java 编译器的异常检查。
- 能够识别
NullPointerException、ArithmeticException、ArrayIndexOutOfBoundsException、NumberFormatException等典型运行时异常。 - 能够理解“编译时异常”并不是普通的语法编译错误。
- 能够解释为什么 Java 对 Checked Exception 与 RuntimeException 采用不同的编译期约束。
- 能够根据异常类型初步判断程序应该优先“修复代码缺陷”还是“设计异常处理流程”。
二、核心知识
2.1 Java 异常为什么还要继续分类
上一章已经建立了异常体系:
Throwable
├── Error
└── Exception
└── RuntimeException
但在真正编写代码时,我们会发现一个重要问题:
有些异常 Java 编译器会强制要求程序员处理。
有些异常即使完全没有处理,代码仍然能够正常通过编译。
例如:
public class RuntimeDemo {
public static void main(String[] args) {
int number = 10 / 0;
System.out.println(number);
}
}
这段程序能够通过编译。
但真正运行时,会产生:
ArithmeticException
再来看另外一种情况:
import java.text.SimpleDateFormat;
public class CheckedDemo {
public static void main(String[] args) {
SimpleDateFormat sdf =
new SimpleDateFormat("yyyy/MM/dd");
sdf.parse("2026/09/02");
}
}
这里的:
parse()
可能产生:
ParseException
如果既不捕获,也不声明传播,编译器就会拒绝这段代码。
这正是两类异常最重要的区别之一。
2.2 运行时异常 RuntimeException
RuntimeException 是:
Exception
的直接子类。
核心继承关系:
Throwable
└── Exception
└── RuntimeException
RuntimeException 以及它的子类通常称为:
运行时异常
Runtime Exceptions
它们属于:
Unchecked Exceptions
也就是:
Java 编译器不会强制要求调用者捕获或者声明这些异常。
例如:
String text = null;
System.out.println(text.length());
编译器不会因为存在潜在:
NullPointerException
而拒绝编译。
但程序真正执行到:
text.length()
时就可能失败。
2.3 为什么 RuntimeException 不要求强制处理
考虑下面这些代码:
int result = 10 / number;
可能产生:
ArithmeticException
String name = null;
name.length();
可能产生:
NullPointerException
int[] arr = {1, 2, 3};
System.out.println(arr[index]);
可能产生:
ArrayIndexOutOfBoundsException
理论上,Java 编译器可以要求程序员:
每写一次数组访问
每写一次除法
每调用一次对象方法
都强制处理潜在异常。
但这样程序会变得极其臃肿。
例如如果每一次对象访问都必须:
try {
...
} catch (NullPointerException e) {
...
}
正常业务代码会被异常处理代码完全淹没。
而且很多 RuntimeException 往往意味着:
程序逻辑存在缺陷
或者输入校验不足
此时更合理的解决方案通常不是:
把异常捕获起来,让程序假装什么都没有发生。
而是:
找出导致异常的代码原因并修复。
2.4 常见运行时异常
NullPointerException
例如:
String username = null;
System.out.println(username.length());
问题本质:
没有实际对象
却试图通过引用访问对象成员
ArithmeticException
例如:
int result = 10 / 0;
整数除以 0 时会产生:
ArithmeticException
ArrayIndexOutOfBoundsException
例如:
int[] arr = {10, 20, 30};
System.out.println(arr[3]);
合法索引只有:
0
1
2
访问:
3
越界。
NumberFormatException
例如:
int number = Integer.parseInt("Java");
字符串:
Java
无法按照十进制整数进行解析。
可能产生:
NumberFormatException
ClassCastException
例如:
Object value = "Java";
Integer number = (Integer) value;
对象真实类型是:
String
却试图强制转换成:
Integer
运行时可能产生:
ClassCastException
IllegalArgumentException
当方法接收到不符合其要求的参数时,经常会使用:
IllegalArgumentException
表达参数不合法。
后续学习 API 时会频繁看到这种异常。
2.5 Checked Exception
与 RuntimeException 对应的另一类重要异常是:
Checked Exception
中文通常翻译为:
受检异常
初学教程中也经常称:
编译时异常
核心特点是:
Java 编译器会检查程序是否按照异常检查规则处理了这种异常。
如果某段代码可能产生 Checked Exception,而调用者既没有:
捕获异常
也没有:
声明异常继续传播
编译器通常会报告错误。
2.6 哪些异常属于 Checked Exception
初学阶段可以先建立这样一张图:
Throwable
├── Error
│ └── Unchecked
│
└── Exception
├── RuntimeException
│ └── Unchecked
│
└── 其他 Exception
└── 通常属于 Checked
典型 Checked Exception 包括:
IOException
FileNotFoundException
ParseException
SQLException
ClassNotFoundException
注意:
FileNotFoundException
其实属于:
IOException
体系。
继承关系类似:
Exception
└── IOException
└── FileNotFoundException
2.7 一个 Checked Exception 示例
原教学体系使用日期解析作为示例。
import java.text.SimpleDateFormat;
public class CheckedExceptionDemo {
public static void main(String[] args) {
SimpleDateFormat sdf =
new SimpleDateFormat(
"yyyy/MM/dd HH:mm:ss"
);
sdf.parse("2026/09/02 19:30:00");
}
}
parse() 声明了可能产生:
ParseException
而:
ParseException
属于 Checked Exception。
因此编译器要求程序员明确面对这个风险。
概念上只有两条主要路线:
方案一
当前代码负责处理
↓
捕获异常
方案二
当前代码暂时不处理
↓
声明可能继续向调用者传播
其中捕获方式将在下一章系统学习。
异常传播和 throws 则留到后续章节系统学习。
2.8 为什么 Checked Exception 要进行编译期检查
考虑读取文件:
读取配置文件
文件可能:
不存在
没有权限
读取失败
再考虑访问网络:
连接服务器
可能出现:
网络不可用
连接失败
数据读取中断
这些问题很多并不是:
代码一定写错了
而是程序运行所依赖的外部条件发生变化。
Java 通过 Checked Exception 强制调用方注意:
这里存在一个正常程序必须认真考虑的失败路径。
因此 Checked Exception 更像是方法契约的一部分。
调用一个可能产生 Checked Exception 的 API 时,调用者必须决定:
我自己处理
还是:
继续交给上层
2.9 “编译时异常”不是普通编译错误
这是本章最重要的术语之一。
例如:
int age = "18";
这是:
类型不匹配
代码本身违反 Java 语法或类型规则。
属于:
编译错误
再例如:
SimpleDateFormat sdf =
new SimpleDateFormat("yyyy/MM/dd");
sdf.parse("2026/09/02");
这里代码结构本身没有问题。
真正的问题是:
parse()
可能抛出 Checked Exception
Java 编译器检查后发现调用者没有按照异常检查规则处理它,于是拒绝编译。
所以:
普通编译错误
≠
Checked Exception
更加准确的理解应该是:
Checked Exception
↓
受到编译期异常检查
而不是:
异常在编译过程中真正发生
2.10 RuntimeException 和 Checked Exception 核心对比
| 对比项 | RuntimeException | Checked Exception | | ------------------------------ | ---------------------------- | -------------------------------- | | 典型中文称呼 | 运行时异常 | 受检异常 / 教学中常称编译时异常 | | 是否继承 RuntimeException | 是 | 否 | | 是否强制进行编译期异常处理检查 | 否 | 是 | | 是否必须显式捕获或声明 | 否 | 通常必须二选一 | | 常见原因 | 编程错误、非法状态、非法输入 | 文件、网络、解析等可预期失败条件 | | 示例 | NullPointerException | IOException | | 示例 | ArithmeticException | ParseException | | 示例 | NumberFormatException | ClassNotFoundException |
注意:
“RuntimeException 一定是程序员 Bug”以及“Checked Exception 一定不是 Bug”都过于绝对。
这个表格表达的是典型设计倾向,而不是所有场景的绝对规律。
三、使用方法
3.1 如何判断某个异常属于哪一类
第一种方式是:
查看继承体系
如果异常类最终继承:
RuntimeException
那么它属于运行时异常。
例如:
NumberFormatException
↓
IllegalArgumentException
↓
RuntimeException
因此:
NumberFormatException
属于 RuntimeException。
3.2 使用 IDEA 查看异常继承关系
在 IDEA 中可以:
Ctrl + 鼠标左键
进入异常类定义。
或者使用:
Ctrl + H
查看类型层次结构。
重点寻找:
extends RuntimeException
或者最终父类链中是否存在:
RuntimeException
3.3 使用 API 文档判断
例如查看:
java.io.IOException
会看到:
Object
→ Throwable
→ Exception
→ IOException
没有:
RuntimeException
所以它属于 Checked Exception。
而:
NullPointerException
的继承链中存在:
RuntimeException
因此属于运行时异常。
3.4 面对 RuntimeException 应该怎么思考
假设:
public static int divide(int a, int b) {
return a / b;
}
用户传入:
b = 0
产生:
ArithmeticException
不要第一时间形成:
catch ArithmeticException
然后什么都不做
的思维。
应该先判断:
b 为什么会变成 0?
可能需要:
输入校验
业务校验
修改程序逻辑
3.5 面对 Checked Exception 应该怎么思考
面对 Checked Exception 时,程序员必须思考:
这个层级能否真正处理问题?
如果能:
当前层处理
如果不能:
交给更合适的调用层处理
例如底层文件读取代码发现:
文件不存在
它可能并不知道应该:
弹窗
返回 HTTP 404
重试
使用默认配置
终止程序
这些往往应该由更上层的业务代码决定。
因此异常处理并不是:
哪一行出错,就必须在哪一行解决。
而是需要寻找:
最适合做恢复或响应决策的层级。
四、原理与进阶
4.1 Checked 的含义其实是“编译器检查”
Checked Exception 中的:
Checked
强调的是:
compile-time checking
也就是:
编译器会检查这种异常是否已经按照 Java 语言规则被处理或者声明。
因此更准确的中文叫法是:
受检异常
4.2 Unchecked Exception 包括什么
Java 中的 Unchecked Exception 不只有:
RuntimeException
完整概念还包括:
RuntimeException 及其子类
+
Error 及其子类
因此:
Unchecked
├── RuntimeException
└── Error
不过普通应用开发中讨论异常处理时,重点通常落在:
RuntimeException
vs
Checked Exception
4.3 为什么 NullPointerException 不设计成 Checked
假设 NullPointerException 属于 Checked Exception。
那么几乎每一次:
user.getName();
都可能要求:
显式捕获 NullPointerException
程序会充斥大量无意义的异常声明和捕获。
而大多数空引用问题更应该通过:
合理对象初始化
参数约束
状态设计
空值检查
从根源解决。
这也是 RuntimeException 免于强制编译期检查的重要原因。
4.4 Checked Exception 也是 API 契约的一部分
假设存在:
loadConfig()
如果这个方法声明可能产生:
IOException
调用者就能提前知道:
这项操作可能因为 IO 条件失败
因此异常类型并不仅仅是:
程序出错之后打印出来的信息
它还表达:
方法可能出现哪些失败模式
这就是异常作为 API 契约的重要价值。
五、实践应用
5.1 用户输入转换
Integer.parseInt(input);
可能产生:
NumberFormatException
属于 RuntimeException。
这类问题经常意味着:
用户输入格式不正确
程序可以:
提前校验
或者在系统边界进行捕获并重新输入
5.2 文件读取
文件读取可能产生:
IOException
这类外部资源失败通常无法仅通过修改某个运算表达式彻底避免。
应用程序必须设计:
失败提示
备用方案
重试
终止当前业务
等处理策略。
5.3 参数非法
如果业务要求:
年龄必须处于合理区间
却传入:
-100
很多 API 会使用:
IllegalArgumentException
表达调用者违反方法参数契约。
后续学习自定义异常时,我们还会进一步设计:
业务异常
来表达系统自己的业务错误。
六、常见问题
6.1 RuntimeException 是不是一定要等运行时才能知道?
“运行时异常”主要描述它的异常类别和编译期检查规则。
并不意味着开发工具绝对无法提前发现问题。
例如:
int value = 10 / 0;
IDE 就可能提前提示问题。
但 Java 语言的异常检查机制不会要求你像 Checked Exception 那样必须捕获或声明 RuntimeException。
6.2 Checked Exception 是不是编译时产生的异常?
不是。
真正的异常仍然是在程序执行过程中被抛出的。
“Checked”表示编译器会提前检查:
程序有没有合法处理这种潜在异常
6.3 RuntimeException 要不要 catch?
可以捕获。
但是:
可以
≠
必须
≠
应该无脑捕获
例如:
NullPointerException
如果根本原因是程序对象状态设计错误,最佳方案通常是修复程序,而不是简单吞掉异常。
6.4 Checked Exception 是不是一定应该立即 catch?
不一定。
如果当前方法没有能力做出正确处理,强行 catch 可能反而降低代码质量。
可以让异常继续向更适合处理的层级传播。
异常传播将在后续章节系统学习。
6.5 IOException 是 RuntimeException 吗?
不是。
继承关系:
Throwable
└── Exception
└── IOException
没有经过:
RuntimeException
因此属于 Checked Exception。
6.6 NumberFormatException 属于哪一类?
它最终继承:
RuntimeException
因此属于运行时异常。
6.7 为什么不能简单把所有异常都写成 Exception?
因为:
异常类型本身就是信息
如果整个系统只剩:
Exception
就会丢失:
到底发生了什么类型的问题
调用方可以针对什么问题采取措施
更加精确的异常类型通常能够让代码语义更清晰。
七、练习与验收
7.1 知识问答
- 什么是 RuntimeException?
- RuntimeException 在异常继承体系中的位置是什么?
- 什么是 Checked Exception?
- 什么是 Unchecked Exception?
- Checked Exception 为什么受到编译期检查?
- RuntimeException 为什么不要求强制捕获或声明?
- NullPointerException 属于哪一类?
- ArithmeticException 属于哪一类?
- NumberFormatException 属于哪一类?
- IOException 属于哪一类?
- ParseException 属于哪一类?
- “编译时异常”和普通编译错误有什么区别?
int age = "18";属于异常还是编译错误?- 为什么不应该无脑捕获所有 RuntimeException?
- 为什么 Checked Exception 可以看作 API 契约的一部分?
- Error 是否属于 Unchecked Exception?
- 如何通过继承体系判断一个异常是否属于 RuntimeException?
7.2 代码阅读
阅读下面代码:
public class RuntimeRead01 {
public static void main(String[] args) {
String text = null;
System.out.println("start");
System.out.println(text.length());
System.out.println("end");
}
}
在不运行代码的情况下回答:
- 代码能否通过编译?
- 可能产生什么异常?
- 这个异常属于哪一类?
"end"是否能够输出?- 这个问题更适合通过“修复代码”还是“无脑捕获异常”解决?
阅读:
import java.text.SimpleDateFormat;
public class CheckedRead01 {
public static void main(String[] args) {
SimpleDateFormat sdf =
new SimpleDateFormat("yyyy/MM/dd");
sdf.parse("2026/09/02");
}
}
回答:
parse()可能产生什么异常?- 这个异常属于 RuntimeException 吗?
- 如果没有处理或者声明,该程序能否通过编译?
- 这里的编译失败与
int age = "18";的本质是否完全相同?
7.3 手写代码
任务一:运行时异常实验
分别主动制造:
NullPointerException
ArithmeticException
ArrayIndexOutOfBoundsException
NumberFormatException
要求:
- 先预测。
- 再运行。
- 记录异常类名。
- 记录产生异常的代码行。
- 分析根本原因。
任务二:异常分类表
查阅 Java API,将下面异常按照:
Checked
RuntimeException
Error
进行分类:
IOException
FileNotFoundException
ParseException
SQLException
NullPointerException
NumberFormatException
IllegalArgumentException
StackOverflowError
OutOfMemoryError
并为每一个异常画出至少三层父类关系。
7.4 Debug
下面代码包含多个潜在问题:
public class ExceptionDebug01 {
public static void main(String[] args) {
String ageText = "十八";
int age = Integer.parseInt(ageText);
int[] scores = {90, 85, 92};
System.out.println(scores[3]);
String username = null;
System.out.println(username.length());
}
}
要求:
- 不运行,预测第一个发生的异常。
- 判断该异常属于 Checked 还是 RuntimeException。
- 修复第一个问题后继续分析下一个问题。
- 直到所有问题都被发现。
- 对每个问题判断:“应该捕获”还是“应该优先修复逻辑”。
7.5 综合训练
设计一个简单的“用户信息导入”程序。
输入内容包括:
年龄字符串
成绩字符串
配置文件路径
分析:
字符串无法转换数字
数组索引错误
文件不存在
空引用
分别可能属于哪一种异常。
要求:
- 先进行异常分类。
- 判断哪些属于 RuntimeException。
- 判断哪些属于 Checked Exception。
- 写出你认为合理的处理层级。
- 暂时不要求完整实现异常传播。
7.6 本章验收
如果能够闭卷完成下面内容,可以认为本章达到基本掌握标准:
- [ ] 能画出 Checked / RuntimeException 在 Throwable 体系中的位置。
- [ ] 能解释 Checked 与 Unchecked 的区别。
- [ ] 能解释为什么 RuntimeException 不强制处理。
- [ ] 能解释为什么 Checked Exception 会受到编译期检查。
- [ ] 能正确判断至少 8 种常见异常的分类。
- [ ] 能解释“编译时异常”不等于普通编译错误。
- [ ] 能根据一个异常判断应该优先 Debug 代码还是设计处理流程。
- [ ] 能通过 Java API 或 IDEA 查询异常继承关系。