背景

本文是《Java 后端从小白到大神》修仙系列第十二篇,正式进入Java后端世界,本篇文章主要聊Java基础中的异常处理。若想详细学习请点击首篇博文,我们开始吧。

文章概览

  1. 异常处理概述
  2. 异常体系结构
  3. 异常处理机制
  4. 异常处理最佳实践
  5. 自定义异常
  6. 异常处理性能考虑

异常处理概述

异常是程序运行过程中发生的意外情况,可能导致程序中断或错误。Java 提供了一套完整的异常处理机制,用于捕获、处理和传递异常,确保程序能够优雅地处理错误情况。

什么是异常?

异常是程序执行过程中发生的事件,它中断了正常的指令流。在 Java 中,所有异常都是 Throwable 类的子类。

异常体系结构

Java 的异常体系结构是一个层次分明的类层次结构,所有异常都继承自 Throwable 类。

异常体系图

graph TD
    Throwable --> Error
    Throwable --> Exception
    Exception --> RuntimeException
    Exception --> IOException
    Exception --> SQLException
    RuntimeException --> NullPointerException
    RuntimeException --> ArithmeticException
    RuntimeException --> ArrayIndexOutOfBoundsException
    RuntimeException --> IllegalArgumentException
    Error --> OutOfMemoryError
    Error --> StackOverflowError

异常分类

  1. Error(错误)

    • 严重的系统级错误,程序无法恢复
    • OutOfMemoryErrorStackOverflowError
    • 一般不需要捕获和处理
  2. Exception(异常)

    • 程序运行时的异常情况
    • 分为检查型异常和非检查型异常
  3. 检查型异常(Checked Exception)
    也叫:编译时异常

    • 什么时候出现:编译期就强制检查
    • 必须做什么:必须 try-catch 或 throws 声明,否则编译不通过
    • 典型例子:IOException、SQLException
    • 继承关系:extends Exception
    • 但不是 RuntimeException 的子类

    一句话记忆:编译器盯着你,不处理不让运行

  4. 非检查型异常(Unchecked Exception)
    也叫:运行时异常

    • 什么时候出现:运行时才可能触发
    • 必须做什么不强制处理,代码可直接运行
    • 典型例子:NullPointerException、ArrayIndexOutOfBoundsException
    • 继承关系:extends RuntimeException

    一句话记忆: 编译器不管你,代码写错才炸

异常处理机制

1. try-catch 块

用于捕获并处理代码块中的异常。

1
2
3
4
5
6
7
8
9
try {
    // 可能抛出异常的代码
    int result = 10 / 0; // 抛出 ArithmeticException
} catch (ArithmeticException e) {
    // 处理异常
    System.out.println("错误:除数不能为零!");
    // 打印异常信息
    e.printStackTrace();
}

说明

  • try 块:包含可能抛出异常的代码
  • catch 块:捕获并处理指定类型的异常
  • e:异常对象,包含异常信息

2. 多个 catch 块

处理不同类型的异常,子类异常需在前。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
try {
    int[] arr = new int[5];
    arr[10] = 50; // 抛出 ArrayIndexOutOfBoundsException
} catch (ArrayIndexOutOfBoundsException e) {
    // 处理数组越界异常
    System.out.println("数组越界!");
} catch (Exception e) {
    // 处理其他所有异常
    System.out.println("其他异常:" + e.getMessage());
}

注意

  • 异常捕获顺序很重要,子类异常必须放在父类异常之前
  • 否则子类异常将永远不会被捕获

3. finally 块

无论是否发生异常,都会执行的代码块(常用于资源清理)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
FileInputStream file = null;
try {
    file = new FileInputStream("test.txt");
    // 读取文件操作
} catch (FileNotFoundException e) {
    System.out.println("文件未找到!");
} finally {
    // 无论是否异常,都会执行
    System.out.println("执行 finally 块");
    // 关闭资源
    if (file != null) {
        try {
            file.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

作用

  • 确保资源被正确释放
  • 清理临时变量
  • 执行必须完成的操作

4. throw 关键字

主动抛出异常(需在方法内处理或声明)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void checkAge(int age) {
    if (age < 18) {
        // 主动抛出异常
        throw new IllegalArgumentException("年龄未满18岁!");
    }
    System.out.println("年龄验证通过");
}

// 调用示例
try {
    checkAge(15);
} catch (IllegalArgumentException e) {
    System.out.println("错误:" + e.getMessage());
}

注意

  • throw 用于方法体内
  • 抛出的是异常对象
  • 可以抛出任何 Throwable 类型的异常

5. throws 关键字

声明方法可能抛出的异常(由调用者处理)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 方法声明可能抛出 IOException
public void readFile() throws IOException {
    FileReader file = new FileReader("test.txt");
    // 读取文件操作
    file.close();
}

// 调用示例
try {
    readFile();
} catch (IOException e) {
    System.out.println("IO异常:" + e.getMessage());
}

说明

  • throws 用于方法签名
  • 声明方法可能抛出的异常类型
  • 可以声明多个异常,用逗号分隔

6. try-with-resources(Java 7+)

自动关闭资源(资源需实现 AutoCloseable 接口)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 自动关闭资源
try (FileInputStream fis = new FileInputStream("test.txt")) {
    int data = fis.read();
    while (data != -1) {
        System.out.print((char) data);
        data = fis.read();
    }
} catch (IOException e) {
    System.out.println("IO异常:" + e.getMessage());
}
// 无需显式关闭,资源自动关闭

优点

  • 代码更简洁
  • 避免资源泄漏
  • 自动处理资源关闭

支持的资源

  • 所有实现 AutoCloseable 接口的类
  • FileInputStreamFileOutputStreamConnectionStatement

异常处理最佳实践

1. 只捕获你能处理的异常

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 不推荐
try {
    // 业务逻辑
} catch (Exception e) {
    // 捕获所有异常但不处理
    e.printStackTrace();
}

// 推荐
try {
    // 业务逻辑
} catch (SpecificException e) {
    // 针对性处理特定异常
    handleSpecificException(e);
} catch (AnotherSpecificException e) {
    // 处理另一种特定异常
    handleAnotherException(e);
}

2. 合理使用异常类型

1
2
3
4
5
6
7
8
9
// 不推荐
if (age < 18) {
    throw new Exception("年龄未满18岁");
}

// 推荐
if (age < 18) {
    throw new IllegalArgumentException("年龄未满18岁");
}

3. 提供有意义的异常信息

1
2
3
4
5
// 不推荐
throw new IllegalArgumentException("参数错误");

// 推荐
throw new IllegalArgumentException("年龄必须大于等于18岁,当前值:" + age);

4. 避免在循环中抛出异常

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 不推荐
for (int i = 0; i < 1000; i++) {
    try {
        if (i == 500) {
            throw new Exception("遇到错误");
        }
    } catch (Exception e) {
        // 处理异常
    }
}

// 推荐
for (int i = 0; i < 1000; i++) {
    if (i == 500) {
        // 处理错误
        break;
    }
}

5. 正确使用 finally 块

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 不推荐
String readFile() {
    FileInputStream fis = null;
    try {
        fis = new FileInputStream("test.txt");
        // 读取文件
        return "文件内容";
    } catch (IOException e) {
        // return 永远是方法最后一步
        // 1. 异常进来了
        // 2. 确定要返回:"错误"
        return "错误";
    } finally {
        // finally 永远在 return 前面跑
        // 3. 【先执行这里!,再执行 2】
        System.out.println("执行 finally");
    }
}

// 推荐
String readFile() {
    FileInputStream fis = null;
    try {
        fis = new FileInputStream("test.txt");
        // 读取文件
        return "文件内容";
    } catch (IOException e) {
        System.out.println("IO异常:" + e.getMessage());
        return "错误";
    } finally { 
        // 清理资源
        if (fis != null) {
            try {
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

自定义异常

创建继承自 ExceptionRuntimeException 的类。

1. 自定义检查型异常

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 自定义检查型异常类
// 先调用父类构造,作用:给父类自己的那部分的成员变量赋初值,但不创建新对象!,只有new 构造函数的时候才创建对象。
// 再调用子类构造,作用:给子类自己的字段赋值、做额外初始化,但不创建新对象!,只有new 构造函数的时候才创建对象。
// 子类对象大小 = 父类字段 + 子类字段
class ValidationException extends Exception {
    public ValidationException() {
        super();
    }
    
    public ValidationException(String message) {
        super(message);
    }
    
    public ValidationException(String message, Throwable cause) {
        super(message, cause);
    }
}

// 使用示例
void validateUser(String username, String password) throws ValidationException {
    if (username == null || username.isEmpty()) {
        throw new ValidationException("用户名不能为空");
    }
    if (password == null || password.length() < 6) {
        throw new ValidationException("密码长度不能少于6位");
    }
}

// 调用时捕获
try {
    validateUser("", "123");
} catch (ValidationException e) {
    System.out.println("验证失败:" + e.getMessage());
}

2. 自定义非检查型异常

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 自定义非检查型异常类
class BusinessException extends RuntimeException {
    private int errorCode;
    
    public BusinessException(String message) {
        super(message);
    }
    
    public BusinessException(String message, int errorCode) {
        super(message);
        this.errorCode = errorCode;
    }
    
    public int getErrorCode() {
        return errorCode;
    }
}

// 使用示例
void processOrder(int orderId) {
    if (orderId <= 0) {
        throw new BusinessException("订单ID无效", 400);
    }
    // 处理订单
}

// 调用时可以选择捕获
processOrder(-1); // 直接抛出异常

3. 自定义异常的最佳实践

  • 提供多个构造方法
  • 包含错误码等额外信息
  • 保持异常类的简洁性
  • 合理命名异常类

异常处理性能考虑

1. 异常的性能影响

  • 异常处理比正常流程慢
  • 异常会产生堆栈跟踪,消耗内存
  • 频繁抛出异常会影响程序性能

2. 性能优化建议

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 不推荐 - 使用异常控制流程
public int divide(int a, int b) {
    try {
        return a / b;
    } catch (ArithmeticException e) {
        return 0;
    }
}

// 推荐 - 先检查条件
public int divide(int a, int b) {
    if (b == 0) {
        return 0;
    }
    return a / b;
}

3. 异常处理的性能优化

  • 只在真正异常的情况下使用异常
  • 避免在循环中抛出异常
  • 合理使用 try-catch 块的范围
  • 优先使用检查型异常处理可恢复的错误
  • 使用非检查型异常处理不可恢复的错误

异常处理常见问题

1. 空指针异常(NullPointerException)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 常见错误
String str = null;
System.out.println(str.length()); // 抛出 NullPointerException

// 避免方法
String str = null;
if (str != null) {
    System.out.println(str.length());
}

// Java 8+ 可选
Optional<String> optionalStr = Optional.ofNullable(str);
optionalStr.ifPresent(s -> System.out.println(s.length()));

2. 数组越界异常(ArrayIndexOutOfBoundsException)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 常见错误
int[] arr = new int[5];
System.out.println(arr[10]); // 抛出 ArrayIndexOutOfBoundsException

// 避免方法
int[] arr = new int[5];
int index = 10;
if (index >= 0 && index < arr.length) {
    System.out.println(arr[index]);
}

3. 类型转换异常(ClassCastException)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 常见错误
Object obj = "Hello";
Integer num = (Integer) obj; // 抛出 ClassCastException

// 避免方法
Object obj = "Hello";
if (obj instanceof Integer) {
    Integer num = (Integer) obj;
} else {
    System.out.println("类型不匹配");
}

总结

异常处理是 Java 编程中非常重要的一部分,它可以帮助我们:

  1. 优雅处理错误:捕获并处理异常,避免程序崩溃
  2. 提高代码可读性:使错误处理逻辑清晰可见
  3. 增强代码健壮性:处理各种异常情况,提高程序稳定性
  4. 便于调试:通过异常信息快速定位问题

掌握异常处理的核心概念和最佳实践,对于编写高质量的 Java 代码至关重要。在实际开发中,应该根据具体场景选择合适的异常处理方式,既要保证程序的正确性,又要考虑代码的性能和可维护性。