跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Javajava算法

Java 常见异常全面解析:出现场景、错误排查与代码修正实战

综述由AI生成系统讲解了 Java 异常体系结构,涵盖运行时异常与受检异常的详细分析。重点剖析了 NullPointerException、ArrayIndexOutOfBoundsException、ClassCastException 等常见异常的触发场景、堆栈分析及修复方案。同时介绍了 IOException、ClassNotFoundException 的处理技巧,并总结了异常处理的最佳实践,如精准捕获、避免吞噬异常、使用 try-with-resources 及自定义异常增强业务语义等内容,帮助开发者建立系统的异常排查思维。

极客工坊发布于 2026/3/30更新于 2026/5/2730 浏览
Java 常见异常全面解析:出现场景、错误排查与代码修正实战

Java 异常体系结构图

引言

本文旨在帮助开发者系统掌握 Java 常见异常的排查与修复能力。通过深入分析异常体系结构、典型运行时异常及受检异常,结合堆栈解读与最佳实践,建立规范的异常处理思维。

一、Java 异常体系回顾

1.1 异常是什么?

异常(Exception) 是程序运行过程中出现的打断正常执行流程的事件。它本质上是一个对象,封装了错误类型、错误描述、方法调用堆栈以及可能的底层原因。

1.2 Java 异常体系结构

java.lang.Object | java.lang.Throwable
---------------------
|                    |
java.lang.Error      java.lang.Exception
-------------------------
|                    |
RuntimeException     其他 Exception
(运行时异常)       (受检异常)
  • Error:JVM 级别的严重错误,如 OutOfMemoryError、StackOverflowError,程序通常无法处理。
  • Exception:程序可处理的异常,分为两类:
    • 受检异常(Checked Exception):编译时必须处理(捕获或声明抛出),如 IOException、SQLException。
    • 运行时异常(RuntimeException):编译时不强制处理,通常由程序逻辑错误导致,如 NullPointerException、ArrayIndexOutOfBoundsException。

1.3 异常信息解读

一个典型的异常堆栈包含以下要素:

Exception java.lang.IllegalArgumentException: item quantity must be a number at io.jzheaux.pluralsight.DeliController.orderSandwich (DeliController.java:45) // … Caused by java.lang.NumberFormatException: For input string: " 3" at NumberFormatException.forInputString (NumberFormatException.java:67) at Integer.parseInt (Integer.java:647) ...
  • 异常类型:IllegalArgumentException
  • 异常消息:item quantity must be a number
  • 堆栈轨迹:从 main 开始到异常发生处的调用链
  • Caused by:底层根本原因,通常是排查的关键入口

排查技巧:遇到复杂异常时,不要只看第一行,要顺着堆栈往下找,尤其是'Caused by'部分,那里往往藏着真正的原因。

二、常见运行时异常深度剖析

运行时异常(RuntimeException)是 Java 程序中最常见的异常类型,它们通常由代码逻辑错误引起。下面我们将逐个剖析最常见的运行时异常。

2.1 NullPointerException(空指针异常)

现象描述

当应用程序试图在需要对象的地方使用 null 引用时,抛出此异常。这是 Java 中最著名的异常,占据了异常总数的很大比例。

出现场景

场景一:直接调用 null 对象的方法或属性

String text = null;
int length = text.length(); // 抛出 NullPointerException

场景二:自动拆箱时包装类型为 null

Boolean willVote = null;
if (willVote) { // 自动拆箱时抛出 NullPointerException
    System.out.println("可以投票");
}

场景三:方法参数或返回值未做空检查

void parseDocument(Document doc) {
    doc.getElements(); // 如果传入的 doc 为 null,抛出异常
}
String lookupElement(Document doc) {
    Element element = doc.findElement("span");
    return element.getValue(); // 如果 element 为 null,抛出异常
}

场景四:数组元素未初始化

Person[] people = new Person[5];
people[0].getName(); // 数组元素默认为 null,抛出异常
堆栈分析示例
Exception in thread "main" java.lang.NullPointerException
    at com.example.UserService.getUserAge(UserService.java:25)
    at com.example.UserController.main(UserController.java:12)

从堆栈可以看出,UserService.java的第 25 行调用了某个 null 对象的方法。

排查方法流程图

方法参数 → 方法返回值 → 未初始化变量 → 数组元素 → 发现 NullPointerException → 定位堆栈中第一个出现自己代码的行号 → 检查该行代码有哪些对象可能为 null → 对象来源是什么? → 检查调用方是否传入 null / 检查被调用方法是否可能返回 null / 检查变量是否已正确初始化 / 检查数组元素是否已赋值 → 修复调用方或添加空值校验

代码修正与预防

修正方案一:参数校验

// 错误代码
void parseDocument(Document doc) {
    doc.getElements();
}
// 修正代码
void parseDocument(@NonNull Document doc) {
    if (doc == null) {
        throw new IllegalArgumentException("doc cannot be null");
    }
    doc.getElements();
}

修正方案二:使用守卫语句

// 错误代码
String lookupElement(Document doc) {
    Element element = doc.findElement("span");
    return element.getValue();
}
// 修正代码
@Nullable String lookupElement(Document doc) {
    Element element = doc.findElement("span");
    if (element == null) {
        return null; // 或者返回默认值
    }
    return element.getValue();
}

修正方案三:使用 Optional(Java 8+)

public Optional<String> lookupElement(Document doc) {
    return Optional.ofNullable(doc.findElement("span")).map(Element::getValue);
}

修正方案四:数组元素初始化

Person[] people = new Person[5];
for (int i = 0; i < people.length; i++) {
    people[i] = new Person(); // 确保每个元素都被初始化
}

预防措施:

  1. 明确空值来源:是无效值(上游问题)还是有效值(可接受 null)
  2. 使用@NonNull 和@Nullable 注解,让 IDE 帮助检查
  3. 遵循"尽早失败"原则:在方法入口处就进行参数校验
  4. 谨慎处理返回值:明确方法是否可能返回 null,并在文档中说明

2.2 ArrayIndexOutOfBoundsException(数组下标越界异常)

现象描述

当试图使用非法索引访问数组元素时抛出,非法索引包括负数、0 到数组长度减 1 范围外的值。

出现场景

场景一:索引超出数组长度

int[] numbers = {1, 2, 3};
int value = numbers[3]; // 索引 3 超出范围(有效索引 0-2)

场景二:循环条件错误

int[] scores = {85, 90, 78, 92};
for (int i = 0; i <= scores.length; i++) { // 应该是 i < scores.length
    System.out.println(scores[i]); // 最后一次循环 i=4,越界
}

场景三:索引为负数

int[] data = new int[10];
int index = -1;
data[index] = 100; // 负索引越界
堆栈分析示例
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5
    at com.example.ArrayDemo.processArray(ArrayDemo.java:15)
    at com.example.ArrayDemo.main(ArrayDemo.java:8)

异常信息直接告诉我们:试图访问索引 5,但数组长度只有 5(有效索引 0-4)。

排查方法

直接常量 → 循环变量 → 计算结果 → 发现 ArrayIndexOutOfBoundsException → 定位堆栈中的代码行 → 检查该行代码的数组访问表达式 → 索引值是如何计算的? → 检查常量是否在合法范围内 / 检查循环条件边界 / 检查计算逻辑是否有误 → 修正索引值

代码修正与预防

修正方案一:修正循环边界

// 错误代码
for (int i = 0; i <= scores.length; i++) {
    System.out.println(scores[i]);
}
// 修正代码
for (int i = 0; i < scores.length; i++) {
    System.out.println(scores[i]);
}
// 更好的方式:使用增强 for 循环
for (int score : scores) {
    System.out.println(score);
}

修正方案二:访问前检查索引

public int getElement(int[] array, int index) {
    if (array == null) {
        throw new IllegalArgumentException("array cannot be null");
    }
    if (index < 0 || index >= array.length) {
        throw new IndexOutOfBoundsException("Index " + index + " out of bounds for length " + array.length);
    }
    return array[index];
}

预防措施:

  1. 优先使用增强 for 循环处理数组遍历
  2. 使用 Arrays 工具类的方法进行数组操作
  3. 动态计算索引时,添加边界检查
  4. 考虑使用 ArrayList等集合类,它们提供了更安全的 get() 方法(也会抛出越界异常,但信息更明确)

2.3 ClassCastException(类型转换异常)

现象描述

当试图将一个对象强制转换为它不是实例的子类时抛出。这是使用继承和多态时的常见问题。

出现场景

场景一:将父类对象强制转换为子类类型

Object obj = new Object();
Integer num = (Integer) obj; // Object 不能转换为 Integer

场景二:集合中元素类型不一致

List list = new ArrayList();
list.add("Hello");
list.add(123); // 混合类型
String first = (String) list.get(0); // 正常
String second = (String) list.get(1); // 抛出 ClassCastException,123 不能转 String

场景三:不正确的向下转型

Animal animal = new Dog();
Cat cat = (Cat) animal; // Dog 不能转换为 Cat
堆栈分析示例
Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String
    at com.example.GenericDemo.processList(GenericDemo.java:22)
    at com.example.GenericDemo.main(GenericDemo.java:15)

异常信息明确告诉我们:试图将 Integer 转换为 String,类型不兼容。

排查方法

集合 → 方法返回值 → 从外部系统获取 → 发现 ClassCastException → 定位堆栈中的转换代码行 → 检查被转换对象的实际类型 → 对象来源是什么? → 检查集合中元素类型是否一致 / 检查返回类型的实际实现 / 检查序列化/反序列化过程 → 使用泛型或 instanceof 检查

代码修正与预防

修正方案一:使用泛型

// 错误代码
List list = new ArrayList();
list.add("Hello");
String s = (String) list.get(0);
// 修正代码:使用泛型
List<String> list = new ArrayList<>();
list.add("Hello");
String s = list.get(0); // 无需强制转换

修正方案二:使用 instanceof 检查

Object obj = getSomeObject();
if (obj instanceof String) {
    String str = (String) obj; // 安全转换
    // 处理字符串
} else if (obj instanceof Integer) {
    Integer num = (Integer) obj; // 安全转换
    // 处理整数
}

修正方案三:Java 16+ 的 Pattern Matching for instanceof

Object obj = getSomeObject();
if (obj instanceof String str) { // 这里可以直接使用 str 变量
    System.out.println(str.length());
} else if (obj instanceof Integer num) {
    System.out.println(num + 10);
} else {
    // 处理其他情况
}

预防措施:

  1. 始终使用泛型确保集合类型安全
  2. 在向下转型前使用 instanceof 检查
  3. 遵循里氏替换原则,避免不必要的向下转型
  4. 考虑使用多态,而不是频繁的类型转换

2.4 ArithmeticException(算术异常)

现象描述

当发生异常的算术条件时抛出,最常见的是整数除零。

出现场景

场景一:整数除零

int result = 10 / 0; // 抛出 ArithmeticException

场景二:取模运算除零

int remainder = 10 % 0; // 抛出 ArithmeticException

注意:浮点数除零不会抛出异常,会返回 Infinity 或 NaN

double result = 10.0 / 0.0; // 返回 Infinity,不会抛出异常
堆栈分析示例
Exception in thread "main" java.lang.ArithmeticException: / by zero
    at com.example.Calculator.divide(Calculator.java:10)
    at com.example.Calculator.main(Calculator.java:5)

异常信息直接告诉我们问题:除零。

排查方法

直接常量 → 变量 → 方法返回值 → 发现 ArithmeticException → 定位堆栈中的除法/取模代码 → 检查分母/右操作数的值 → 分母来源是什么? → 修改常量为非零值 / 检查变量赋值逻辑 / 检查返回值的范围 → 添加除零检查

代码修正与预防

修正方案一:检查除数

public int divide(int a, int b) {
    if (b == 0) {
        throw new IllegalArgumentException("除数不能为 0");
    }
    return a / b;
}

修正方案二:使用 Optional 处理可能为 0 的情况

public Optional<Integer> safeDivide(int a, int b) {
    if (b == 0) {
        return Optional.empty();
    }
    return Optional.of(a / b);
}

修正方案三:使用浮点数运算(如果业务允许)

double result = 10.0 / 0.0; // 返回 Infinity,不会抛出异常
if (Double.isInfinite(result)) {
    // 处理无穷大情况
}

预防措施:

  1. 在进行除法或取模前,始终检查除数是否为 0
  2. 考虑使用 BigDecimal进行精确计算,它提供了更好的异常处理
  3. 从用户输入获取除数时,必须进行验证

三、其他运行时异常与受检异常

3.1 NumberFormatException(数字格式异常)

现象描述

当尝试将字符串转换为数字类型,但字符串格式不合法时抛出。

出现场景

场景一:字符串包含非数字字符

int num = Integer.parseInt("123abc"); // 抛出 NumberFormatException

场景二:字符串包含空格或特殊符号

int num = Integer.parseInt(" 123 "); // 抛出 NumberFormatException,空格未处理

场景三:数字超出类型范围

int num = Integer.parseInt("2147483648"); // 超出 int 最大值,抛出异常

场景四:空字符串或 null

int num = Integer.parseInt(""); // 抛出 NumberFormatException
Integer.parseInt(null); // 抛出 NullPointerException,注意这里是 NPE
堆栈分析示例
Exception in thread "main" java.lang.NumberFormatException: For input string: " 123 "
    at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
    at java.base/java.lang.Integer.parseInt(Integer.java:654)
    at java.base/java.lang.Integer.parseInt(Integer.java:786)
    at com.example.UserInput.processAge(UserInput.java:12)
排查方法
graph TD
    A[开始] --> B{输入是否为 null?}
    B -- 是 --> C[抛出 NullPointerException]
    B -- 否 --> D{去除前后空格}
    D --> E{是否为空?}
    E -- 是 --> F[抛出 NumberFormatException]
    E -- 否 --> G{匹配正则 \d+?}
    G -- 否 --> H[抛出 NumberFormatException]
    G -- 是 --> I[成功解析]
代码修正与预防

修正方案一:数据清洗

// 错误代码
String input = " 123 ";
int value = Integer.parseInt(input); // 抛出异常
// 修正代码
String input = " 123 ";
input = input.trim(); // 去除前后空格
if (!input.isEmpty()) {
    try {
        int value = Integer.parseInt(input);
    } catch (NumberFormatException e) {
        // 处理异常
    }
}

修正方案二:使用正则表达式预验证

public int parsePostalCode(String input) {
    // 预验证:必须是 5 位数字
    if (input == null || !input.matches("\\d{5}")) {
        throw new IllegalArgumentException("邮政编码必须是 5 位数字");
    }
    return Integer.parseInt(input); // 此时已保证安全
}

修正方案三:使用 Apache Commons Lang 的 NumberUtils

import org.apache.commons.lang3.math.NumberUtils;
String input = "123";
int value = NumberUtils.toInt(input, 0); // 失败时返回默认值 0,不抛出异常

修正方案四:Java 8+ 的 Optional + 异常处理

public Optional<Integer> tryParseInt(String input) {
    try {
        return Optional.of(Integer.parseInt(input.trim()));
    } catch (NumberFormatException e) {
        return Optional.empty();
    }
}

预防措施:

  1. 始终对输入进行清洗(trim、去除非数字字符)
  2. 解析前验证格式,特别是来自外部系统的数据
  3. 考虑使用专门的验证框架如 Hibernate Validator
  4. 使用 try-catch 包围解析代码,优雅处理异常

3.2 IllegalArgumentException(非法参数异常)

现象描述

当向方法传递了不合法或不适当的参数时抛出。这通常表示调用者的责任。

出现场景

场景一:参数值超出允许范围

public void setAge(int age) {
    if (age < 0 || age > 150) {
        throw new IllegalArgumentException("年龄必须在 0-150 之间");
    }
    this.age = age;
}

场景二:参数格式错误

public void setEmail(String email) {
    if (email == null || !email.contains("@")) {
        throw new IllegalArgumentException("邮箱格式不正确");
    }
    this.email = email;
}

场景三:参数为 null 但方法不允许

public void processData(@NonNull Data data) {
    if (data == null) {
        throw new IllegalArgumentException("data cannot be null");
    }
    // 处理数据
}
排查方法
  1. 查看异常消息,通常会说明参数需要满足什么条件
  2. 检查调用代码,确认传入的参数值
  3. 验证参数来源,判断是输入错误还是上游数据问题
代码修正
// 在方法开头进行参数校验
public void registerUser(String username, String email, int age) {
    // 参数校验集中处理
    if (username == null || username.trim().isEmpty()) {
        throw new IllegalArgumentException("用户名不能为空");
    }
    if (email == null || !email.matches("^[A-Za-z0-9+_.-]+@(.+)$")) {
        throw new IllegalArgumentException("邮箱格式不正确");
    }
    if (age < 0 || age > 150) {
        throw new IllegalArgumentException("年龄无效");
    }
    // 业务逻辑
}

3.3 IllegalStateException(非法状态异常)

现象描述

当方法在非法或不适当的时间被调用时抛出。通常表示被调用者的状态不适合执行请求的操作。

出现场景

场景一:对象未正确初始化

public class ConnectionPool {
    private boolean initialized = false;
    public void connect() {
        if (!initialized) {
            throw new IllegalStateException("连接池未初始化");
        }
        // 建立连接
    }
}

场景二:迭代器越界

List<String> list = Arrays.asList("A", "B");
Iterator<String> it = list.iterator();
it.next(); // A
it.next(); // B
it.next(); // 抛出 NoSuchElementException,但有时会被包装为 IllegalStateException
排查方法
  1. 阅读异常消息,了解当前对象应该处于什么状态
  2. 检查对象初始化或配置代码,确保在调用前已正确设置
  3. 检查操作顺序,确认是否按正确步骤调用
代码修正
public class FileProcessor {
    private boolean opened = false;
    public void open() {
        // 打开文件
        opened = true;
    }
    public void readData() {
        if (!opened) {
            throw new IllegalStateException("必须先调用 open() 方法打开文件");
        }
        // 读取数据
    }
}

3.4 IOException(输入输出异常)

现象描述

当输入输出操作失败或中断时抛出。这是最典型的受检异常,处理文件、网络、流操作时经常遇到。

出现场景

场景一:文件不存在(FileNotFoundException)

FileReader fr = new FileReader("nonexistent.txt"); // 抛出 FileNotFoundException

场景二:读取流时连接断开

InputStream in = socket.getInputStream();
int data = in.read(); // 如果连接已关闭,可能抛出 IOException

场景三:写入磁盘空间不足

FileOutputStream fos = new FileOutputStream("largefile.bin");
byte[] data = new byte[1024];
fos.write(data); // 如果磁盘空间不足,抛出 IOException
堆栈分析示例
java.io.FileNotFoundException: nonexistent.txt (系统找不到指定的文件)
    at java.base/java.io.FileInputStream.open0(Native Method)
    at java.base/java.io.FileInputStream.open(FileInputStream.java:219)
    at java.base/java.io.FileInputStream.<init>(FileInputStream.java:157)
    at com.example.FileReaderDemo.main(FileReaderDemo.java:8)
排查方法

FileNotFoundException → EOFException → SocketException → 其他 IO 错误 → 发现 IOException → 查看异常消息中的具体原因 → 原因类型? → 检查文件路径和存在性 / 检查是否提前到达文件末尾 / 检查网络连接状态 / 检查磁盘空间、权限等 → 修正路径或创建文件

代码修正与预防

修正方案一:使用 try-with-resources 确保资源关闭

// 错误代码:可能忘记关闭资源
public String readFile(String path) throws IOException {
    FileReader fr = new FileReader(path);
    BufferedReader br = new BufferedReader(fr);
    return br.readLine(); // 没有关闭资源,可能造成内存泄漏
}
// 修正代码:使用 try-with-resources
public String readFile(String path) throws IOException {
    try (FileReader fr = new FileReader(path);
         BufferedReader br = new BufferedReader(fr)) {
        return br.readLine(); // 自动关闭
    }
}

修正方案二:检查文件存在性

public void processFile(String path) {
    File file = new File(path);
    if (!file.exists()) {
        System.err.println("文件不存在:" + path);
        return; // 或者抛出更友好的异常
    }
    try (BufferedReader br = new BufferedReader(new FileReader(file))) {
        // 处理文件
    } catch (IOException e) {
        System.err.println("读取文件时发生错误:" + e.getMessage());
        e.printStackTrace();
    }
}

修正方案三:多层异常处理

public void copyFile(String src, String dest) {
    try (FileInputStream in = new FileInputStream(src);
         FileOutputStream out = new FileOutputStream(dest)) {
        byte[] buffer = new byte[1024];
        int length;
        while ((length = in.read(buffer)) > 0) {
            out.write(buffer, 0, length);
        }
    } catch (FileNotFoundException e) {
        System.err.println("源文件不存在或目标目录无法写入:" + e.getMessage());
    } catch (IOException e) {
        System.err.println("复制过程中发生 IO 错误:" + e.getMessage());
    }
}

预防措施:

  1. 始终使用 try-with-resources或确保 finally 中关闭资源
  2. 操作前检查文件和目录状态
  3. 为 IO 操作提供有意义的错误消息
  4. 考虑重试机制,特别是网络相关的 IO 操作

3.5 ClassNotFoundException(类未找到异常)

现象描述

当应用程序试图通过字符串名加载类,但在类路径中找不到该类的定义时抛出。

出现场景

场景一:Class.forName() 加载类

Class.forName("com.mysql.jdbc.Driver"); // 如果驱动 jar 不在类路径中,抛出异常

场景二:类加载器加载类

ClassLoader.getSystemClassLoader().loadClass("com.example.MissingClass");

场景三:使用反射创建实例

Object obj = Class.forName("com.example.DynamicClass").newInstance();
堆栈分析示例
java.lang.ClassNotFoundException: com.mysql.jdbc.Driver
    at java.base/java.net.URLClassLoader.findClass(URLClassLoader.java:476)
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:589)
    at java.base/java.lang.Class.forName0(Native Method)
    at java.base/java.lang.Class.forName(Class.java:398)
排查方法

是 → 否 → 发现 ClassNotFoundException → 查看缺少的类名 → 确认该类属于哪个库/JAR → 该类是否应该存在? → 检查类路径配置 → 检查类名拼写或版本兼容性 → 添加缺失的 JAR 到类路径

代码修正与预防

修正方案一:确保 JAR 包在类路径中

  • 对于 Maven 项目:检查 pom.xml 中的依赖
  • 对于普通项目:确认 JAR 文件在 classpath 下

修正方案二:捕获并处理异常

try {
    Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
    // 提供友好的错误信息
    throw new RuntimeException("MySQL 驱动未找到,请检查是否添加了 mysql-connector-java 依赖", e);
}

修正方案三:使用 ServiceLoader 模式(Java 6+)

ServiceLoader<Driver> drivers = ServiceLoader.load(Driver.class);
for (Driver driver : drivers) {
    // 自动发现所有驱动实现
}

四、综合实战与最佳实践

4.1 复杂异常排查案例

案例:银行转账系统中的异常链
public class BankingService {
    public void transfer(String fromAccount, String toAccount, double amount) throws BusinessException {
        try {
            Account from = accountRepository.findByNumber(fromAccount);
            Account to = accountRepository.findByNumber(toAccount);
            if (from == null || to == null) {
                throw new IllegalArgumentException("账户不存在");
            }
            from.withdraw(amount);
            to.deposit(amount);
            transactionLog.log(fromAccount, toAccount, amount);
        } catch (IllegalArgumentException e) {
            throw new BusinessException("转账参数错误", e);
        } catch (InsufficientBalanceException e) {
            throw new BusinessException("余额不足", e);
        } catch (Exception e) {
            throw new BusinessException("转账失败,请稍后重试", e);
        }
    }
}
异常排查思路

当看到类似下面的异常堆栈时:

com.example.BusinessException: 转账失败,请稍后重试
    at com.example.BankingService.transfer(BankingService.java:45)
    at com.example.BankingController.main(BankingController.java:18)
Caused by: java.sql.SQLException: Connection timed out
    at com.mysql.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:2189)
    at com.mysql.jdbc.ConnectionImpl.<init>(ConnectionImpl.java:795)
    at com.mysql.jdbc.NonRegisteringDriver.connect(NonRegisteringDriver.java:329)
    at java.sql.DriverManager.getConnection(DriverManager.java:664)
    at com.example.AccountRepository.findByNumber(AccountRepository.java:22)
    ... 5 more

排查步骤:

  1. 看顶层异常:BusinessException,但消息太泛化,请稍后重试 没有实质信息
  2. 看 Caused by:SQLException: Connection timed out,这才是真正原因
  3. 追溯源头:AccountRepository.java:22 处的数据库连接超时
  4. 根本原因:数据库连接失败

解决方案:

  • 检查数据库服务是否运行
  • 检查网络连接
  • 检查数据库连接池配置
  • 添加重试机制

教训:包装异常时不要丢失原始信息,提供具体的错误消息有助于排查。

4.2 异常处理最佳实践总结

4.2.1 捕获特定异常,而不是通用异常
// 不好的做法
try {
    // 业务代码
} catch (Exception e) {
    // 捕获所有异常,掩盖了真正的问题
}
// 好的做法
try {
    // 业务代码
} catch (FileNotFoundException e) {
    // 处理文件不存在
} catch (IOException e) {
    // 处理其他 IO 错误
}
4.2.2 避免空的 catch 块
// 绝对不要这样做
try {
    riskyOperation();
} catch (Exception e) {
    // 空的 catch 块,异常被吞噬
}
// 至少记录异常
try {
    riskyOperation();
} catch (Exception e) {
    logger.error("操作失败", e); // 记录日志
    throw e; // 或者重新抛出
}
4.2.3 使用 try-with-resources 自动关闭资源
// Java 7 之前的方式
FileInputStream fis = null;
try {
    fis = new FileInputStream("file.txt"); // 处理文件
} finally {
    if (fis != null) {
        try {
            fis.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
// Java 7+ 推荐的方式
try (FileInputStream fis = new FileInputStream("file.txt")) {
    // 处理文件
} // 自动关闭
4.2.4 使用自定义异常增强业务语义
// 自定义业务异常
public class InsufficientBalanceException extends Exception {
    private double currentBalance;
    private double requiredAmount;
    public InsufficientBalanceException(double current, double required) {
        super(String.format("余额不足:当前余额%.2f,需要%.2f", current, required));
        this.currentBalance = current;
        this.requiredAmount = required;
    }
    // getters...
}
// 使用
public void withdraw(double amount) throws InsufficientBalanceException {
    if (balance < amount) {
        throw new InsufficientBalanceException(balance, amount);
    }
    balance -= amount;
}
4.2.5 方法重写时遵守异常声明规则
  • 子类方法可以抛出与父类相同的异常、子类异常,或不抛出异常
  • 子类方法不能抛出比父类更宽泛的受检异常
class Parent {
    public void process() throws IOException {}
}
class Child extends Parent {
    @Override
    public void process() throws FileNotFoundException {} // 允许,FileNotFoundException 是 IOException 的子类
    // @Override
    // public void process() throws Exception { } // 不允许,Exception 比 IOException 更宽泛
}
4.2.6 记录异常时包含上下文信息
try {
    processOrder(orderId, userId);
} catch (OrderException e) {
    // 记录有用的上下文信息
    logger.error("处理订单失败:orderId={}, userId={}", orderId, userId, e);
    throw e;
}
4.2.7 不要用异常控制正常的程序流程
// 不好的做法:用异常控制流程
try {
    Integer.parseInt(userInput); // 是数字,继续处理
} catch (NumberFormatException e) {
    // 不是数字,执行其他逻辑
}
// 好的做法:使用条件判断
if (userInput.matches("\\d+")) {
    int value = Integer.parseInt(userInput); // 是数字,继续处理
} else {
    // 不是数字,执行其他逻辑
}
4.2.8 异常处理的黄金法则总结
原则说明
精准捕获捕获具体的异常类型,而不是笼统的 Exception
绝不吞噬空的 catch 块是万恶之源,至少要记录日志
及时释放使用 try-with-resources 或 finally 确保资源释放
保留原始异常包装异常时要把原异常作为 cause 传入
提供上下文异常消息要包含有助于排查的信息
区分异常类型可恢复用受检异常,程序错误用运行时异常
文档化用 javadoc 的@throws 说明方法可能抛出的异常

4.3 Java 7+ 多异常捕获

从 Java 7 开始,可以使用 | 在一个 catch 块中捕获多个异常类型,减少代码重复:

try {
    // 可能抛出多种异常的代码
} catch (IOException | SQLException e) {
    // 统一处理 IO 和 SQL 异常
    logger.error("数据访问错误", e);
    throw e; // Java 7+ 支持更精确的重抛类型检查
}

注意:多异常捕获时,catch 参数隐式为 final,不能修改。

4.4 异常处理与事务管理

在企业级应用中,异常处理与事务管理密切相关。通常:

  • 运行时异常触发事务回滚
  • 受检异常不自动触发事务回滚(在 Spring 中可通过 rollbackFor 配置)
@Service
public class AccountService {
    @Transactional(rollbackFor = {BusinessException.class, RuntimeException.class})
    public void transferMoney(String from, String to, double amount) throws BusinessException {
        try {
            // 转账逻辑
        } catch (InsufficientBalanceException e) {
            // 业务异常,触发事务回滚
            throw new BusinessException("转账失败", e);
        }
    }
}

五、总结

知识体系回顾

通过本文的学习,我们全面覆盖了:

  1. 异常基础:体系结构、受检与非受检异常、异常信息解读
  2. 运行时异常:
    • NullPointerException:空引用访问 → 前置检查、Optional
    • ArrayIndexOutOfBoundsException:数组越界 → 边界检查、增强 for 循环
    • ClassCastException:类型转换错误 → instanceof 检查、泛型
    • ArithmeticException:算术异常 → 除零检查
    • NumberFormatException:数字格式错误 → 输入清洗、预验证
    • IllegalArgumentException/IllegalStateException:参数/状态错误 → 前置校验
  3. 受检异常:
    • IOException及其子类 → try-with-resources、文件存在性检查
    • ClassNotFoundException → 检查类路径、依赖管理
  4. 排查方法:堆栈分析、Caused by 追踪、异常链理解
  5. 最佳实践:精准捕获、避免吞噬、及时释放、保留上下文等 8 大原则

异常排查思维导图

graph TD
    A[遇到异常] --> B{1. 看类型:是什么异常?}
    B --> C{2. 看消息:说了什么?}
    C --> D{3. 看堆栈:第一行在哪?}
    D --> E{4. 看原因:有 Caused by?}
    E --> F{5. 想来源:值从哪来?}
    F --> G{6. 想方案:怎么修复?}
    G --> H[结束]

异常排查思维导图

目录

  1. 引言
  2. 一、Java 异常体系回顾
  3. 1.1 异常是什么?
  4. 1.2 Java 异常体系结构
  5. 1.3 异常信息解读
  6. 二、常见运行时异常深度剖析
  7. 2.1 NullPointerException(空指针异常)
  8. 现象描述
  9. 出现场景
  10. 堆栈分析示例
  11. 排查方法流程图
  12. 代码修正与预防
  13. 2.2 ArrayIndexOutOfBoundsException(数组下标越界异常)
  14. 现象描述
  15. 出现场景
  16. 堆栈分析示例
  17. 排查方法
  18. 代码修正与预防
  19. 2.3 ClassCastException(类型转换异常)
  20. 现象描述
  21. 出现场景
  22. 堆栈分析示例
  23. 排查方法
  24. 代码修正与预防
  25. 2.4 ArithmeticException(算术异常)
  26. 现象描述
  27. 出现场景
  28. 堆栈分析示例
  29. 排查方法
  30. 代码修正与预防
  31. 三、其他运行时异常与受检异常
  32. 3.1 NumberFormatException(数字格式异常)
  33. 现象描述
  34. 出现场景
  35. 堆栈分析示例
  36. 排查方法
  37. 代码修正与预防
  38. 3.2 IllegalArgumentException(非法参数异常)
  39. 现象描述
  40. 出现场景
  41. 排查方法
  42. 代码修正
  43. 3.3 IllegalStateException(非法状态异常)
  44. 现象描述
  45. 出现场景
  46. 排查方法
  47. 代码修正
  48. 3.4 IOException(输入输出异常)
  49. 现象描述
  50. 出现场景
  51. 堆栈分析示例
  52. 排查方法
  53. 代码修正与预防
  54. 3.5 ClassNotFoundException(类未找到异常)
  55. 现象描述
  56. 出现场景
  57. 堆栈分析示例
  58. 排查方法
  59. 代码修正与预防
  60. 四、综合实战与最佳实践
  61. 4.1 复杂异常排查案例
  62. 案例:银行转账系统中的异常链
  63. 异常排查思路
  64. 4.2 异常处理最佳实践总结
  65. 4.2.1 捕获特定异常,而不是通用异常
  66. 4.2.2 避免空的 catch 块
  67. 4.2.3 使用 try-with-resources 自动关闭资源
  68. 4.2.4 使用自定义异常增强业务语义
  69. 4.2.5 方法重写时遵守异常声明规则
  70. 4.2.6 记录异常时包含上下文信息
  71. 4.2.7 不要用异常控制正常的程序流程
  72. 4.2.8 异常处理的黄金法则总结
  73. 4.3 Java 7+ 多异常捕获
  74. 4.4 异常处理与事务管理
  75. 五、总结
  76. 知识体系回顾
  77. 异常排查思维导图
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • Web 团队构建 App:Capacitor 选型指南
  • 2023 年网络安全发展趋势与工程师成长路径
  • Eino ADK 体系篇:ChatModelAgent 核心机制与实战解析
  • 算法优选:位运算技巧
  • 30 天 CTF 入门:Web 安全与杂项速成计划
  • NVM 环境配置问题排查指南:NVM_SYMLINK 错误及 Node 命令未找到
  • 滑动窗口算法:无重复字符的最长子串(数组模拟哈希表)
  • Python 数据清洗实战:缺失值、异常值与格式处理
  • VR 虚拟实验室构建:学生与 AI 协同探索科学规律
  • 基于 DeepFace 和 OpenCV 的情绪分析器实现
  • 基于 Python、MySQL 与 Web 构建私有 Apple 设备监控面板
  • Qwen3-VL-2B 部署教程:4090D 单卡 WebUI 配置详解
  • 前端 PWA:构建离线可用与可安装的 Web 应用
  • RTX 4070 本地部署 Stable Diffusion 教程:环境搭建与 4K 人像生成
  • 9 篇必读的大模型前沿论文
  • AI 赋能软件测试全流程实战指南
  • llama-cpp-python 常见问题解决指南
  • 嵌入式 C/C++ 面试:STL 容器与算法详解
  • Linux 入门:常用命令、软件安装与项目部署指南
  • MySQL MVCC 多版本并发控制原理

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online