运行时异常与编译时异常 | JavaSE

运行时异常与编译时异常

一、学习目标

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

  • 能够区分运行时异常(Runtime Exception)与受检异常(Checked Exception)。
  • 能够解释 RuntimeException 在 Java 异常体系中的位置。
  • 能够判断一个异常是否受到 Java 编译器的异常检查。
  • 能够识别 NullPointerExceptionArithmeticExceptionArrayIndexOutOfBoundsExceptionNumberFormatException 等典型运行时异常。
  • 能够理解“编译时异常”并不是普通的语法编译错误。
  • 能够解释为什么 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 知识问答

  1. 什么是 RuntimeException?
  2. RuntimeException 在异常继承体系中的位置是什么?
  3. 什么是 Checked Exception?
  4. 什么是 Unchecked Exception?
  5. Checked Exception 为什么受到编译期检查?
  6. RuntimeException 为什么不要求强制捕获或声明?
  7. NullPointerException 属于哪一类?
  8. ArithmeticException 属于哪一类?
  9. NumberFormatException 属于哪一类?
  10. IOException 属于哪一类?
  11. ParseException 属于哪一类?
  12. “编译时异常”和普通编译错误有什么区别?
  13. int age = "18"; 属于异常还是编译错误?
  14. 为什么不应该无脑捕获所有 RuntimeException?
  15. 为什么 Checked Exception 可以看作 API 契约的一部分?
  16. Error 是否属于 Unchecked Exception?
  17. 如何通过继承体系判断一个异常是否属于 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");
    }
}

在不运行代码的情况下回答:

  1. 代码能否通过编译?
  2. 可能产生什么异常?
  3. 这个异常属于哪一类?
  4. "end" 是否能够输出?
  5. 这个问题更适合通过“修复代码”还是“无脑捕获异常”解决?

阅读:

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");
    }
}

回答:

  1. parse() 可能产生什么异常?
  2. 这个异常属于 RuntimeException 吗?
  3. 如果没有处理或者声明,该程序能否通过编译?
  4. 这里的编译失败与 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());
    }
}

要求:

  1. 不运行,预测第一个发生的异常。
  2. 判断该异常属于 Checked 还是 RuntimeException。
  3. 修复第一个问题后继续分析下一个问题。
  4. 直到所有问题都被发现。
  5. 对每个问题判断:“应该捕获”还是“应该优先修复逻辑”。

7.5 综合训练

设计一个简单的“用户信息导入”程序。

输入内容包括:

年龄字符串
成绩字符串
配置文件路径

分析:

字符串无法转换数字
数组索引错误
文件不存在
空引用

分别可能属于哪一种异常。

要求:

  • 先进行异常分类。
  • 判断哪些属于 RuntimeException。
  • 判断哪些属于 Checked Exception。
  • 写出你认为合理的处理层级。
  • 暂时不要求完整实现异常传播。

7.6 本章验收

如果能够闭卷完成下面内容,可以认为本章达到基本掌握标准:

  • [ ] 能画出 Checked / RuntimeException 在 Throwable 体系中的位置。
  • [ ] 能解释 Checked 与 Unchecked 的区别。
  • [ ] 能解释为什么 RuntimeException 不强制处理。
  • [ ] 能解释为什么 Checked Exception 会受到编译期检查。
  • [ ] 能正确判断至少 8 种常见异常的分类。
  • [ ] 能解释“编译时异常”不等于普通编译错误。
  • [ ] 能根据一个异常判断应该优先 Debug 代码还是设计处理流程。
  • [ ] 能通过 Java API 或 IDEA 查询异常继承关系。