源本科技 | 码上会

Java 自定义异常

2026/01/23
74
0

学习目标

  • 理解为何需要自定义异常,以及它在业务系统中的价值

  • 掌握创建检查型非检查型自定义异常的方法

  • 能够根据业务场景选择合适的异常类型

  • 实现清晰、可维护的异常处理结构,分离业务逻辑与错误处理

  • 避免使用模糊错误信息,提升程序可读性与调试效率


为什么需要自定义

在实际开发中,标准异常(如 IllegalArgumentException)往往无法准确表达业务语义。例如:

public class PoorExample {
    public static void main(String[] args) {
        int age = 15;
        if (age < 18) {
            System.out.println("Error"); // 模糊、无上下文、难调试
        }
    }
}

上述做法的问题:

  • 错误信息不明确,无法快速定位问题

  • 没有抛出异常对象,调用方无法捕获或处理

  • 业务逻辑与错误提示混杂,违反单一职责原则

  • 在大型系统中难以追踪和监控特定业务错误

解决方案:使用自定义异常,将业务规则错误封装为有意义的异常类型。


基本结构

在 Java 中,自定义异常通过继承 ExceptionRuntimeException 实现。

通用模板

// 检查型自定义异常
class MyCheckedException extends Exception {
    public MyCheckedException(String message) {
        super(message);
    }
}

// 非检查型自定义异常
class MyUncheckedException extends RuntimeException {
    public MyUncheckedException(String message) {
        super(message);
    }
}

可选增强:

  • 添加错误码字段(如 errorCode

  • 提供无参构造器

  • 支持嵌套异常(Throwable cause


示例 1

检查型自定义异常

适用于可恢复的业务错误,如用户输入不符合规则、外部资源不可用等。

场景:年龄验证(注册限制)

// 自定义检查型异常
class InvalidAgeException extends Exception {
    public InvalidAgeException(String message) {
        super(message);
    }
}

public class AgeValidator {
    // 方法声明可能抛出检查型异常
    public static void validateAge(int age) throws InvalidAgeException {
        if (age < 18) {
            throw new InvalidAgeException("用户年龄必须满 18 岁才能注册");
        }
        System.out.println("年龄验证通过: " + age + " 岁");
    }

    public static void main(String[] args) {
        try {
            validateAge(16);
        } catch (InvalidAgeException e) {
            System.err.println("注册失败: " + e.getMessage());
        }
    }
}
注册失败: 用户年龄必须满 18 岁才能注册

优势

  • 编译器强制调用者处理该异常(try-catchthrows

  • 明确表达了“这是一个需要处理的业务规则”

  • 便于上层做不同策略(如提示用户、记录日志、返回错误码)


示例 2

非检查型自定义异常

适用于编程错误或不可恢复的逻辑错误,如非法状态、参数校验失败等。

场景:除零保护

// 自定义非检查型异常
class DivisionByZeroException extends RuntimeException {
    public DivisionByZeroException(String message) {
        super(message);
    }
}

public class Calculator {
    public static int safeDivide(int dividend, int divisor) {
        if (divisor == 0) {
            throw new DivisionByZeroException("除数不能为零");
        }
        return dividend / divisor;
    }

    public static void main(String[] args) {
        try {
            int result = safeDivide(10, 0);
            System.out.println("结果: " + result);
        } catch (DivisionByZeroException e) {
            System.err.println("计算错误: " + e.getMessage());
        }
    }
}
计算错误: 除数不能为零

注意:

  • 编译器不要求处理此异常(但建议捕获以避免程序崩溃)

  • 更适合用于“本不该发生的错误”,属于防御性编程的一部分


自定义异常对比

特性

检查型异常(Checked)

非检查型异常(Unchecked)

父类

Exception

RuntimeException

编译期检查

✅ 必须处理(try-catchthrows

❌ 不强制处理

典型用途

可恢复的业务错误(如文件未找到、余额不足)

编程错误或非法状态(如空指针、参数无效)

调用者负担

较高(必须显式处理)

较低(可选择性处理)

设计哲学

“错误是预期的一部分”

“错误是 bug,应尽早暴露”

如何选择

  • 用检查型异常:当调用者有能力且应该处理该错误时(如重试、提示用户、切换备用方案)。

  • 用非检查型异常:当错误表示程序缺陷,不应被忽略,且通常无法合理恢复时。


增强自定义异常

添加错误码

便于国际化或多端响应

class BusinessException extends RuntimeException {
    private final String errorCode;

    public BusinessException(String errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
    }

    public String getErrorCode() {
        return errorCode;
    }
}

// 使用
throw new BusinessException("USER_AGE_INVALID", "年龄不符合注册要求");

支持嵌套异常

保留原始上下文

class DataAccessException extends Exception {
    public DataAccessException(String message, Throwable cause) {
        super(message, cause);
    }
}

// 使用
try {
    // 数据库操作
} catch (SQLException e) {
    throw new DataAccessException("数据库查询失败", e);
}

最佳实践

  1. 命名规范:异常类名以 Exception 结尾(如 InsufficientBalanceException)。

  2. 语义清晰:异常名称应准确反映业务含义,避免泛化(如 MyException)。

  3. 避免过度设计:不是每个错误都需要自定义异常,优先使用标准异常。

  4. 文档化:在方法注释中说明可能抛出的自定义异常及其触发条件。

  5. 统一异常体系:在项目中建立基类异常(如 BaseBusinessException),便于全局处理。


重点总结

  • 自定义异常让业务错误显式化、结构化、可追踪

  • 检查型异常适用于可恢复的外部错误,非检查型适用于内部逻辑错误

  • 通过继承 ExceptionRuntimeException 创建自定义异常

  • 合理使用自定义异常可显著提升代码的健壮性、可读性和可维护性

  • 现代应用常结合全局异常处理器(如 Spring 的 @ControllerAdvice)统一响应格式


思考题

  1. 在一个电商系统中,“库存不足”应该设计为检查型还是非检查型异常?为什么?

  2. 如果多个自定义异常具有相似结构(如都包含错误码),如何避免重复代码?

  3. 能否在自定义异常中添加业务数据(如用户 ID、订单号)?这样做是否合理?