1. 概述
本文将详细介绍 如何在 Java 中创建自定义异常。
我们会演示如何定义和使用用户自定义的异常,涵盖 受检异常(checked) 和 非受检异常(unchecked) 两种场景。对于有经验的开发者来说,这是提升代码可读性和维护性的常用手段,尤其在复杂业务系统中,踩坑多了自然会意识到统一异常处理的重要性。
2. 为什么需要自定义异常
Java 内置的异常体系已经覆盖了大多数通用场景,比如 IOException
、NullPointerException
等。但实际开发中,仅靠这些“通用款”远远不够,尤其是在业务逻辑复杂的系统中。
引入自定义异常的主要原因包括:
- ✅ 业务逻辑异常:某些错误是业务特有的,比如“用户余额不足”、“订单状态不允许退款”。用
IllegalArgumentException
虽然也能凑合,但语义模糊,不利于排查和日志分析。 - ✅ 精细化异常处理:你想对某一类底层异常做特殊处理,但又不想影响其他情况。例如,同样是
FileNotFoundException
,可能是文件名非法导致的,也可能是路径权限问题。通过自定义异常可以区分对待。
Java 异常分为两类:
- 受检异常(checked exception):必须显式捕获或声明抛出,比如
IOException
- 非受检异常(unchecked exception):继承自
RuntimeException
,无需强制处理
接下来我们分别实现这两种自定义异常。
3. 自定义受检异常
受检异常的最大特点是:编译器会强制你处理它。如果你不 try-catch
或 throws
,代码根本编不过。
我们来看一个读取文件首行的常见场景:
try (Scanner file = new Scanner(new File(fileName))) {
if (file.hasNextLine()) return file.nextLine();
} catch(FileNotFoundException e) {
// Logging, etc
}
这段代码虽然能运行,但有个问题:当抛出 FileNotFoundException
时,你无法判断到底是“文件不存在”还是“文件名格式错误”。这对调用方来说是个黑盒 ❌。
这时候就需要一个更语义化的异常来说明问题 —— 比如 IncorrectFileNameException
。
如何定义?
✅ 只需继承 Exception
类即可:
public class IncorrectFileNameException extends Exception {
public IncorrectFileNameException(String errorMessage) {
super(errorMessage);
}
}
⚠️ 注意:必须提供一个接收 String
参数的构造函数,并传递给父类。否则你在抛出时没法带上具体的错误信息。
如何使用?
改造原来的逻辑,在发现文件名不合法时抛出自定义异常:
try (Scanner file = new Scanner(new File(fileName))) {
if (file.hasNextLine())
return file.nextLine();
} catch (FileNotFoundException e) {
if (!isCorrectFileName(fileName)) {
throw new IncorrectFileNameException("Incorrect filename : " + fileName );
}
// 其他处理...
}
看起来不错,但这里有个 经典踩坑点:我们把原始异常(FileNotFoundException
)给丢了 ❌!
这意味着上层调用者看不到完整的调用栈,日志里也查不到根因,调试起来非常痛苦。
正确姿势:保留根因
✅ 解决方法:在自定义异常中增加 Throwable
构造参数,把原始异常链起来:
public class IncorrectFileNameException extends Exception {
public IncorrectFileNameException(String errorMessage) {
super(errorMessage);
}
public IncorrectFileNameException(String errorMessage, Throwable err) {
super(errorMessage, err);
}
}
然后在 catch
块中传递原始异常:
try (Scanner file = new Scanner(new File(fileName))) {
if (file.hasNextLine()) {
return file.nextLine();
}
} catch (FileNotFoundException err) {
if (!isCorrectFileName(fileName)) {
throw new IncorrectFileNameException(
"Incorrect filename : " + fileName, err);
}
// ...
}
这样,异常堆栈中就能看到完整的链路:IncorrectFileNameException
← caused by ← FileNotFoundException
这才是生产环境该有的样子 ✅。
4. 自定义非受检异常
非受检异常(即运行时异常)不需要强制捕获,适合用于“程序逻辑错误”或“不可恢复”的场景。
比如我们想检查文件名是否包含扩展名(.txt
, .csv
等),这个判断只能在运行时完成,且一旦出错通常意味着调用方传参有问题。
定义方式
✅ 继承 RuntimeException
即可:
public class IncorrectFileExtensionException
extends RuntimeException {
public IncorrectFileExtensionException(String errorMessage) {
super(errorMessage);
}
public IncorrectFileExtensionException(String errorMessage, Throwable err) {
super(errorMessage, err);
}
}
使用示例
假设我们原来的逻辑中已经抛出了 IllegalArgumentException
,现在我们可以进一步包装成更具体的异常:
try (Scanner file = new Scanner(new File(fileName))) {
if (file.hasNextLine()) {
return file.nextLine();
} else {
throw new IllegalArgumentException("Non readable file");
}
} catch (FileNotFoundException err) {
if (!isCorrectFileName(fileName)) {
throw new IncorrectFileNameException(
"Incorrect filename : " + fileName, err);
}
} catch(IllegalArgumentException err) {
if(!containsExtension(fileName)) {
throw new IncorrectFileExtensionException(
"Filename does not contain extension : " + fileName, err);
}
}
这样做的好处是:
- 上层可以精准捕获
IncorrectFileExtensionException
做特殊处理 - 日志更清晰,定位问题更快
- API 使用者更容易理解错误含义
5. 总结
自定义异常不是炫技,而是为了:
- ✅ 提升代码可读性与可维护性
- ✅ 实现业务语义的精准表达
- ✅ 支持精细化的异常处理策略
- ✅ 保留完整的异常链,便于排查问题
关键要点回顾:
类型 | 继承类 | 是否强制处理 | 适用场景 |
---|---|---|---|
受检异常 | Exception |
✅ 是 | 业务可恢复场景,如文件名错误 |
非受检异常 | RuntimeException |
❌ 否 | 程序逻辑错误,如参数格式不合法 |
📌 最佳实践建议:
- ✅ 一定要提供
String message
和Throwable cause
两个构造函数 - ✅ 异常类命名要清晰,推荐以
Exception
结尾,如OrderNotFoundException
- ✅ 不要滥用自定义异常,避免“异常爆炸”
- ✅ 结合日志框架(如 SLF4J)记录完整上下文
文中示例代码已托管至 GitHub:https://github.com/baeldung/tutorials/tree/master/core-java-modules/core-java-exceptions