Java 异常体系 | JavaSE
Java 异常体系
一、学习目标
完成本章后,你应该能够:
- 用自己的语言解释 Java 中的异常(Exception)是什么,以及为什么程序需要异常机制。
- 区分“正常业务分支”和“异常情况”。
- 准确画出
Throwable → Error / Exception → RuntimeException的核心继承体系。 - 理解
Throwable在 Java 异常体系中的根节点地位。 - 区分
Error与Exception的基本定位。 - 认识
RuntimeException在异常体系中的位置。 - 识别
NullPointerException、ArithmeticException、ArrayIndexOutOfBoundsException等常见异常。 - 理解异常对象能够携带异常类型、消息、调用栈和原因等诊断信息。
- 理解异常发生后程序控制流为什么会发生改变。
- 为后续学习运行时异常、受检异常、try-catch、throw、throws 和自定义异常建立完整知识框架。
二、核心知识
2.1 什么是异常
异常(Exception)可以先简单理解为:
程序在编译或运行过程中遇到的、需要通过异常机制表示和处理的问题。
例如:
int[] arr = {10, 20, 30};
System.out.println(
arr[3]
);
数组只有:
0
1
2
三个合法索引。
访问:
3
会产生:
ArrayIndexOutOfBoundsException
再例如:
System.out.println(
10 / 0
);
整数除数不能为 0。
运行时会出现:
ArithmeticException
再例如:
String name = null;
System.out.println(
name.length()
);
此时:
name
没有引用正常 String 对象。
调用:
name.length()
会产生:
NullPointerException
2.2 程序中的问题不只有代码写错
异常可能来源于很多方面。
例如:
数组越界
除数为 0
空引用
非法类型转换
文件不存在
磁盘读写失败
网络断开
连接超时
日期解析失败
数据库访问失败
业务数据非法
所以:
异常机制不是为了处理一种具体 Bug,而是 Java 用来表达和传播异常情况的一整套语言与类库机制。
2.3 异常与普通业务分支有什么区别
假设用户输入年龄:
18
程序判断:
if (age >= 18) {
System.out.println("成年");
} else {
System.out.println("未成年");
}
这里:
成年
未成年
都是正常业务结果。
不是异常。
再比如:
订单状态:
待支付
已支付
已发货
已完成
它们同样属于:
正常业务状态。
不能把:
业务结果不符合某个条件
全部理解成 Java 异常。
2.4 什么情况下更像异常
例如:
方法要求年龄必须在 0~150
却传进了 -100
或者:
程序必须读取配置文件
但配置文件不存在
或者:
程序尝试连接数据库
但连接失败
这些情况往往意味着:
正常执行路径无法按照预期继续。
此时异常机制就有价值。
2.5 为什么不能全部通过特殊返回值表示失败
早期程序设计中,可以通过:
return -1;
表示:
失败
例如:
int findUser() {
return -1;
}
问题在于:
-1 到底表示什么?
用户不存在?
网络失败?
数据库失败?
参数非法?
调用者还可能:
忘记检查这个特殊值
异常机制提供了另一种表达方式:
正常返回值
和
异常执行路径
分开。
2.6 Java 异常不是普通 return
这是一个非常关键的概念。
正常返回:
return value;
意味着:
方法正常完成
↓
把结果返回调用者
而异常发生时:
异常被抛出
↓
当前正常执行路径被打断
↓
Java 沿调用关系寻找能够处理它的位置
所以异常真正代表的是:
一种异常控制流(exceptional control flow)。
它可以携带失败信息传递给上层,但它本身不是普通意义上的 return 返回值。
三、使用方法
3.1 先看一段异常程序
public class ExceptionDemo {
public static void main(String[] args) {
System.out.println("程序开始");
int result = 10 / 0;
System.out.println(result);
System.out.println("程序结束");
}
}
程序执行:
程序开始
然后执行:
10 / 0
产生:
ArithmeticException
于是正常控制流被中断。
后面的:
System.out.println(result);
以及:
System.out.println("程序结束");
不会按照原来的正常路径继续执行。
3.2 异常也是对象
这是理解 Java 异常体系的关键。
例如:
ArithmeticException
NullPointerException
IOException
SQLException
不是某种神秘的 JVM 文本。
它们都是:
Java 类。
运行时发生异常时,会有对应的:
异常对象。
异常对象可以携带:
- 异常具体类型
- 详细消息
- 发生位置
- 方法调用关系
- 原因 cause
- 调用栈 stack trace
3.3 Throwable:整个异常体系的根类
Java 中能够被:
throw
抛出的对象,必须属于:
Throwable
或者它的子类。
Throwable 位于:
java.lang
包。
因此通常不需要手动 import。
核心体系:
java.lang.Object
│
▼
java.lang.Throwable
│
├──────────────┐
▼ ▼
Error Exception
│
▼
RuntimeException
这是整个 Java 异常体系最重要的一张图。
必须记住。
3.4 Throwable 的两个直接核心分支
Throwable 下面主要有:
Error
Exception
可以初步理解:
Throwable
├── Error
└── Exception
3.5 Error 是什么
Error 表示:
Java 运行环境或系统层面的严重问题,合理的普通应用程序通常不应该试图通过常规业务代码捕获并恢复。
例如:
VirtualMachineError
StackOverflowError
OutOfMemoryError
LinkageError
其中一个你以后很容易遇到:
StackOverflowError
例如没有正确终止条件的递归:
public class ErrorDemo {
public static void test() {
test();
}
public static void main(String[] args) {
test();
}
}
函数不断调用自己:
test()
↓
test()
↓
test()
↓
test()
↓
...
最终可能耗尽线程栈空间,出现:
StackOverflowError
3.6 Error 是不是程序员完全不用管
不能这样理解。
更准确地说:
一般业务代码通常不应该把 Error 当成普通、可恢复的业务异常去随意 catch。
但是开发人员依然需要:
定位
监控
分析
修复根因
调整资源
检查代码
检查 JVM / 依赖 / 部署环境
例如:
OutOfMemoryError
出现后,你当然不能说:
“这是 JVM 的事,我不用管。”
开发者往往需要排查:
内存泄漏?
堆设置太小?
对象创建失控?
缓存无限增长?
一次加载太多数据?
所以:
通常不应尝试常规捕获恢复
不等于:
完全不用分析和处理根因
3.7 Exception 是什么
Exception 同样继承:
Throwable
它通常表示:
合理的应用程序可能希望处理的异常情况。
例如:
文件不存在
输入数据非法
日期解析失败
网络 IO 出现问题
程序出现空指针
数组越界
注意:
Exception
内部还有进一步分类。
3.8 RuntimeException
在 Exception 下面,有一个非常重要的类:
RuntimeException
继承关系:
Throwable
│
└── Exception
│
└── RuntimeException
它是:
大量运行时异常的父类。
例如:
ArithmeticException
NullPointerException
IllegalArgumentException
IndexOutOfBoundsException
ClassCastException
都属于 RuntimeException 体系。
3.9 常见运行时异常体系
可以先认识:
RuntimeException
│
├── ArithmeticException
│
├── NullPointerException
│
├── IllegalArgumentException
│
├── IndexOutOfBoundsException
│ │
│ ├── ArrayIndexOutOfBoundsException
│ └── StringIndexOutOfBoundsException
│
├── ClassCastException
│
└── NumberFormatException
具体“运行时异常与编译时异常”的编译规则将在下一章:
04-02
系统展开。
本章只要求:
先知道 RuntimeException 在整个体系中的位置。
3.10 ArithmeticException
典型场景:
int result = 10 / 0;
产生:
ArithmeticException
注意:
double result = 10.0 / 0.0;
浮点运算遵循不同的 IEEE 754 语义。
不要简单把:
除 0
统一理解为“一定 ArithmeticException”。
当前最典型的教学案例是:
10 / 0
整数除零。
3.11 NullPointerException
例如:
String text = null;
System.out.println(
text.length()
);
出现:
NullPointerException
它意味着:
程序在需要对象的地方使用了 null。
常见情况:
null 调用实例方法
null 访问实例字段
把 null 当数组访问
对 null 数组读取 length
NPE 是 Java 开发中最常见的错误之一。
3.12 ArrayIndexOutOfBoundsException
例如:
int[] numbers =
{10, 20, 30};
System.out.println(
numbers[3]
);
合法:
0
1
2
非法:
3
产生:
ArrayIndexOutOfBoundsException
它属于:
IndexOutOfBoundsException
→ RuntimeException
→ Exception
→ Throwable
整个异常对象依然在统一继承体系中。
3.13 NumberFormatException
例如:
int number =
Integer.parseInt("abc");
字符串:
abc
无法解析成合法整数。
会产生:
NumberFormatException
这也是:
RuntimeException
的子类。
3.14 ClassCastException
前面学习多态时:
Animal animal =
new Cat();
如果错误地强转成:
Dog dog =
(Dog) animal;
运行时就可能出现:
ClassCastException
这说明异常知识并不是孤立章节。
它会把前面的:
数组
字符串
类型转换
多态
API
全部重新串起来。
3.15 异常继承体系为什么重要
因为以后:
catch (...)
能不能捕获某种异常,与:
类型继承关系
直接相关。
例如:
NullPointerException
↓
RuntimeException
↓
Exception
↓
Throwable
因此从类型系统角度:
NullPointerException
也是:
RuntimeException
也是:
Exception
也是:
Throwable
这就是面向对象中的:
继承 + 多态
在异常机制中的真实应用。
四、原理与进阶
4.1 异常发生后控制流发生了什么
正常代码:
main
↓
methodA
↓
methodB
↓
methodC
如果 methodC() 正常完成:
methodC 返回
↓
methodB 继续
↓
methodA 继续
↓
main 继续
但是如果:
methodC
抛出了一个异常,并且当前方法没有处理:
methodC
↓
异常向调用方传播
↓
methodB
↓
methodA
↓
main
Java 会沿着动态调用链寻找:
与该异常类型匹配的处理器。
这叫:
异常传播
详细机制将在:
04-03
04-04
继续学习。
4.2 什么是调用栈
例如:
main()
↓
service()
↓
calculate()
可以抽象成:
┌─────────────┐
│ calculate() │ ← 当前执行
├─────────────┤
│ service() │
├─────────────┤
│ main() │
└─────────────┘
如果:
calculate()
发生异常:
ArithmeticException
异常信息中通常会记录:
异常发生在哪一行
由哪个方法调用
上一级是谁
再上一级是谁
形成:
stack trace
即:
堆栈跟踪信息。
4.3 为什么 stack trace 非常重要
假设控制台出现:
java.lang.ArithmeticException: / by zero
at Calculator.divide(Calculator.java:12)
at OrderService.calculate(OrderService.java:25)
at Main.main(Main.java:8)
你能够直接看到:
异常类型
↓
ArithmeticException
异常消息
↓
/ by zero
真正出错位置
↓
Calculator.java:12
调用来源
↓
OrderService.java:25
更上层调用
↓
Main.java:8
这就是原始课程所说:
异常是定位 Bug 的关键信息。
但工程能力不是:
看到红字就慌
而应该训练成:
读异常类型
↓
读异常消息
↓
找第一处自己的业务代码位置
↓
沿调用链分析上下文
↓
定位真正原因
4.4 Throwable 中的重要诊断信息
所有异常和 Error 最终都继承自:
Throwable
因此可以获得一系列统一诊断能力。
例如:
getMessage()
获取详细消息。
getCause()
获取异常原因。
getStackTrace()
获取调用栈信息。
printStackTrace()
打印异常堆栈。
这些 API 后面学习捕获异常时会大量使用。
4.5 checked 与 unchecked:先建立总框架
Java 异常体系还有另一个重要分类:
checked exception
unchecked exception
从语言规范角度:
RuntimeException 及其子类
+
Error 及其子类
属于:
unchecked
其他需要编译器执行异常检查的 Throwable 类型属于:
checked
对于初学阶段,通常主要把:
Exception 中不是 RuntimeException 的异常
理解为:
受检异常 / 课程传统称“编译时异常”
下一章:
04-02 · 运行时异常与编译时异常
会专门讲:
- 为什么叫“编译时异常”
- 它是不是编译阶段才发生
- checked exception 到底检查什么
- 为什么 RuntimeException 不强制声明
- throws / try-catch 编译约束
本章不要提前混在一起。
4.6 Throwable 为什么不等于 Exception
很多初学者会说:
Java 所有异常的父类是 Exception
这是不准确的。
完整体系是:
Throwable
├── Error
└── Exception
因此:
Exception
不是整个 Throwable 体系的根。
真正的根是:
Throwable
4.7 Error 也属于 Throwable
所以从语言系统角度:
Error
也是可以被 JVM 抛出的 Throwable。
但它与普通 Exception 的设计意图不同。
请记住:
Throwable
= Java 可抛出对象的根体系
Error
= 严重异常状况分支
Exception
= 应用程序可能希望处理的异常分支
五、实践应用
5.1 用户输入
假设用户应该输入:
年龄整数
结果输入:
abc
如果程序执行:
Integer.parseInt(input)
就可能得到:
NumberFormatException
异常机制可以帮助程序识别:
当前正常数据转换没有成功。
后面可以进一步设计:
捕获异常
↓
提示用户
↓
重新输入
5.2 文件系统
程序需要读取:
config.txt
但是文件不存在。
这并不是:
if / else 的普通业务状态
那么简单。
文件 API 会通过相应异常类型将问题报告给调用者。
后续 IO 章节会频繁与异常机制结合。
5.3 网络通信
假设:
客户端
↓
TCP
↓
服务器
通信过程中可能发生:
服务器未启动
连接中断
网络故障
数据读取失败
这些问题会通过:
IOException
SocketException
等异常体系表达。
所以异常是:
IO、网络、多线程、反射等后续 JavaSE 技术的基础设施。
5.4 参数校验
假设:
setAge(-100)
在设计良好的 API 中,可以通过:
IllegalArgumentException
表示:
调用者传入了不合法参数。
以后自定义异常章节还会学习:
业务异常
例如:
余额不足
库存不足
用户状态非法
如何建立更准确的错误模型。
六、常见问题
6.1 异常是不是就是语法错误
不是。
例如:
int result = 10 / 0;
如果这里除数是变量:
int result = 10 / divisor;
代码完全可能编译成功。
运行时当:
divisor = 0
才产生异常。
所以:
语法错误
和:
运行时异常
不是同一个概念。
6.2 Exception 是不是整个异常体系的父类
不是。
真正顶层:
Throwable
体系:
Throwable
├── Error
└── Exception
6.3 Error 是不是 Exception 的子类
不是。
二者是:
Throwable
下面不同的分支:
Throwable
├── Error
└── Exception
6.4 RuntimeException 是不是和 Exception 平级
不是。
关系:
Exception
↓
RuntimeException
RuntimeException 是 Exception 的子类。
6.5 NullPointerException 属于什么体系
完整关系:
Object
↓
Throwable
↓
Exception
↓
RuntimeException
↓
NullPointerException
6.6 Error 是不是完全不需要程序员管
不是。
准确说:
合理的普通应用通常不应该把 Error 当成可以常规捕获并恢复的业务异常。
但出现 Error 后仍可能需要:
- 日志监控
- 故障诊断
- 内存分析
- 资源配置
- 修复代码
- 排查环境
6.7 异常是不是一种普通返回值
不是。
普通 return:
正常控制流
异常:
异常控制流
+
Throwable 对象携带错误信息
它确实能把失败原因传播给上层,但不是普通 return。
6.8 出现异常是不是一定要 try-catch
不是所有异常都应该立刻:
try {
} catch (...) {
}
异常处理还有:
向上声明
继续传播
当前层捕获
边界层统一处理
转换异常
恢复处理
等设计。
这些会在后面的:
04-02
04-03
04-04
04-05
逐步展开。
6.9 catch(Exception e) 是不是万能方案
从类型范围看,它能捕获大量 Exception 子类。
但工程中不能形成:
try {
...
} catch (Exception e) {
}
然后什么都不做的习惯。
这叫:
吞异常。
会让:
真正失败原因
悄悄消失。
正确的异常处理策略将在后续章节系统学习。
6.10 为什么不在这一章直接把 try-catch 全讲完
因为冻结课程已经明确拆分:
04-01 Java异常体系
04-02 运行时异常与编译时异常
04-03 try-catch-finally
04-04 throw、throws 与异常传播
04-05 自定义异常与异常处理策略
当前章节的核心任务只有:
先把整个异常世界的地图建立起来。
否则所有概念一次塞进来,反而难以形成结构。
七、练习与验收
7.1 知识问答
- 什么是 Java 异常?
- 正常业务分支与异常情况有什么区别?
- Java 异常为什么比单纯返回
-1更适合表达某些失败? - 异常是否属于普通方法返回值?
- Java 中所有能够被抛出的对象最终继承哪个类?
- Throwable 有哪两个主要直接分支?
- Error 的定位是什么?
- Exception 的定位是什么?
- RuntimeException 与 Exception 的继承关系是什么?
- NullPointerException 属于哪条继承链?
- ArithmeticException 常见于什么情况?
- ArrayIndexOutOfBoundsException 为什么产生?
- NumberFormatException 常见于什么情况?
- ClassCastException 与之前学过的什么知识有关?
- 什么是 stack trace?
- 异常对象一般能够携带哪些诊断信息?
- Error 是否意味着程序员完全不用分析?
- checked 和 unchecked 的基本边界是什么?
- 为什么
Exception不是 Java 所有可抛出对象的最终父类? - 为什么异常体系本质上也在使用 Java 的继承和多态?
7.2 代码阅读
不运行下面代码:
public class ExceptionRead01 {
public static void main(String[] args) {
System.out.println("A");
int result =
10 / 0;
System.out.println("B");
System.out.println(result);
System.out.println("C");
}
}
回答:
- 哪些字符能够输出?
- 会产生什么异常?
- 异常产生后为什么后续代码没有沿正常路径执行?
继续阅读:
public class ExceptionRead02 {
public static void main(String[] args) {
String text = null;
System.out.println(
text.length()
);
}
}
回答:
- 产生什么异常?
- 它属于 Exception 体系中的哪一层?
- 写出完整主要继承链。
7.3 手写代码
分别从零编写能够产生下面异常的最小示例:
ArithmeticException
NullPointerException
ArrayIndexOutOfBoundsException
NumberFormatException
ClassCastException
要求:
- 每个案例尽量只保留触发异常所必需的代码。
- 在运行前先预测异常类型。
- 运行后读取异常堆栈。
- 找到异常发生的源码行号。
- 不使用 try-catch 隐藏异常。
7.4 Debug
分析:
public class DebugDemo {
public static void main(String[] args) {
String[] names =
{"Tom", "Jerry"};
for (int i = 0;
i <= names.length;
i++) {
System.out.println(
names[i].toUpperCase()
);
}
}
}
要求:
- 找出 Bug。
- 预测具体异常类型。
- 说明为什么不是编译语法错误。
- 修复代码。
继续:
Object value =
"Java";
Integer number =
(Integer) value;
要求:
- 编译能否通过?
- 运行会发生什么?
- 异常属于什么继承体系?
- 结合多态和类型转换解释根因。
7.5 综合训练
设计一个:
“异常实验室”
主程序依次调用五个方法:
testArray();
testNull();
testArithmetic();
testNumberFormat();
testCast();
每个方法故意制造一种不同的 RuntimeException。
要求:
- 当前阶段先不要处理异常。
- 每次只启用一个实验。
- 运行前预测异常。
- 运行后记录:
- 异常完整类名
- 异常消息
- 第一处自己的代码位置
- 调用方法
- 为每个异常写一句根因说明。
目标不是:
制造 Bug
而是训练:
看到 Java 异常信息后能够快速阅读和定位。
7.6 本章验收
闭卷画出:
Object
│
▼
Throwable
│
├───────────────┐
▼ ▼
Error Exception
│
▼
RuntimeException
并能继续举出:
RuntimeException
├── NullPointerException
├── ArithmeticException
├── IllegalArgumentException
├── IndexOutOfBoundsException
└── ClassCastException
能够解释:
正常业务结果
vs
异常情况
能够解释:
return
vs
throw 异常后的异常控制流
能够读懂最基本的:
异常类型
异常消息
源码行号
调用栈
并明确知道:
下一章将继续解决:
RuntimeException
vs
受检异常 / “编译时异常”
则本章核心能力基本达标。