Java 异常体系 | JavaSE

Java 异常体系

一、学习目标

完成本章后,你应该能够:

  1. 用自己的语言解释 Java 中的异常(Exception)是什么,以及为什么程序需要异常机制。
  2. 区分“正常业务分支”和“异常情况”。
  3. 准确画出 Throwable → Error / Exception → RuntimeException 的核心继承体系。
  4. 理解 Throwable 在 Java 异常体系中的根节点地位。
  5. 区分 ErrorException 的基本定位。
  6. 认识 RuntimeException 在异常体系中的位置。
  7. 识别 NullPointerExceptionArithmeticExceptionArrayIndexOutOfBoundsException 等常见异常。
  8. 理解异常对象能够携带异常类型、消息、调用栈和原因等诊断信息。
  9. 理解异常发生后程序控制流为什么会发生改变。
  10. 为后续学习运行时异常、受检异常、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 知识问答

  1. 什么是 Java 异常?
  2. 正常业务分支与异常情况有什么区别?
  3. Java 异常为什么比单纯返回 -1 更适合表达某些失败?
  4. 异常是否属于普通方法返回值?
  5. Java 中所有能够被抛出的对象最终继承哪个类?
  6. Throwable 有哪两个主要直接分支?
  7. Error 的定位是什么?
  8. Exception 的定位是什么?
  9. RuntimeException 与 Exception 的继承关系是什么?
  10. NullPointerException 属于哪条继承链?
  11. ArithmeticException 常见于什么情况?
  12. ArrayIndexOutOfBoundsException 为什么产生?
  13. NumberFormatException 常见于什么情况?
  14. ClassCastException 与之前学过的什么知识有关?
  15. 什么是 stack trace?
  16. 异常对象一般能够携带哪些诊断信息?
  17. Error 是否意味着程序员完全不用分析?
  18. checked 和 unchecked 的基本边界是什么?
  19. 为什么 Exception 不是 Java 所有可抛出对象的最终父类?
  20. 为什么异常体系本质上也在使用 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");
    }
}

回答:

  1. 哪些字符能够输出?
  2. 会产生什么异常?
  3. 异常产生后为什么后续代码没有沿正常路径执行?

继续阅读:

public class ExceptionRead02 {

    public static void main(String[] args) {

        String text = null;

        System.out.println(
                text.length()
        );
    }
}

回答:

  1. 产生什么异常?
  2. 它属于 Exception 体系中的哪一层?
  3. 写出完整主要继承链。

7.3 手写代码

分别从零编写能够产生下面异常的最小示例:

ArithmeticException
NullPointerException
ArrayIndexOutOfBoundsException
NumberFormatException
ClassCastException

要求:

  1. 每个案例尽量只保留触发异常所必需的代码。
  2. 在运行前先预测异常类型。
  3. 运行后读取异常堆栈。
  4. 找到异常发生的源码行号。
  5. 不使用 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()
            );
        }
    }
}

要求:

  1. 找出 Bug。
  2. 预测具体异常类型。
  3. 说明为什么不是编译语法错误。
  4. 修复代码。

继续:

Object value =
        "Java";

Integer number =
        (Integer) value;

要求:

  1. 编译能否通过?
  2. 运行会发生什么?
  3. 异常属于什么继承体系?
  4. 结合多态和类型转换解释根因。

7.5 综合训练

设计一个:

“异常实验室”

主程序依次调用五个方法:

testArray();
testNull();
testArithmetic();
testNumberFormat();
testCast();

每个方法故意制造一种不同的 RuntimeException。

要求:

  1. 当前阶段先不要处理异常。
  2. 每次只启用一个实验。
  3. 运行前预测异常。
  4. 运行后记录:
    • 异常完整类名
    • 异常消息
    • 第一处自己的代码位置
    • 调用方法
  5. 为每个异常写一句根因说明。

目标不是:

制造 Bug

而是训练:

看到 Java 异常信息后能够快速阅读和定位。


7.6 本章验收

闭卷画出:

Object
  │
  ▼
Throwable
  │
  ├───────────────┐
  ▼               ▼
Error          Exception
                  │
                  ▼
           RuntimeException

并能继续举出:

RuntimeException
├── NullPointerException
├── ArithmeticException
├── IllegalArgumentException
├── IndexOutOfBoundsException
└── ClassCastException

能够解释:

正常业务结果
vs
异常情况

能够解释:

return
vs
throw 异常后的异常控制流

能够读懂最基本的:

异常类型
异常消息
源码行号
调用栈

并明确知道:

下一章将继续解决:

RuntimeException
vs
受检异常 / “编译时异常”

则本章核心能力基本达标。