Java异常机制详解
Java 异常机制详解
本章位置:Java 核心基础
学习目标:理解 Java 异常体系,掌握try-catch-finally、throw、throws、自定义异常、异常链等核心内容,并知道在实际项目中如何正确处理异常。
一、什么是异常
程序运行时,不可能永远按照正常情况执行。
例如:
用户输入了错误的数据
数组访问越界
字符串转数字失败
除数为 0
文件不存在
数据库连接失败
网络请求失败
这些运行过程中出现的问题,可以统一理解为:
异常
英文:
Exception
二、为什么需要异常机制
如果没有异常机制,程序遇到问题时很容易直接崩溃。
例如:
public class Demo {
public static void main(String[] args) {
int a = 10;
int b = 0;
int result = a / b;
System.out.println(result);
System.out.println("程序结束");
}
}
运行时:
10 / 0
会出现异常。
后面的:
System.out.println("程序结束");
不会继续执行。
三、异常机制的作用
Java 异常机制可以帮助程序:
发现问题
描述问题
传递问题
处理问题
例如:
try {
int result = 10 / 0;
} catch (ArithmeticException e) {
System.out.println("除数不能为 0");
}
程序不会直接因为这个问题结束。
四、异常不是错误提示那么简单
很多初学者会把异常理解成:
控制台上的红色报错
实际上异常是一个:
对象
例如:
ArithmeticException
NullPointerException
NumberFormatException
这些本质上都是类。
程序发生异常时:
JVM 会创建异常对象
并把异常对象抛出来
五、Java 异常体系
Java 中异常体系最顶层:
Throwable
结构可以简单理解:
Throwable
│
├─ Error
│
└─ Exception
│
├─ RuntimeException
│ ├─ NullPointerException
│ ├─ ArithmeticException
│ ├─ ArrayIndexOutOfBoundsException
│ ├─ NumberFormatException
│ └─ ClassCastException
│
└─ 其他 Exception
├─ IOException
├─ SQLException
└─ ...
六、Throwable
Throwable 是 Java 异常体系的顶层父类。
它主要有两个重要子类:
Error
Exception
七、Error
Error 表示:
比较严重的问题
例如:
JVM 内存不足
栈溢出
虚拟机内部错误
常见:
OutOfMemoryError
StackOverflowError
八、Error 一般不由普通业务代码处理
例如:
服务器内存已经完全耗尽
这种问题不是简单:
try-catch
就能真正解决的。
通常需要:
修改程序
调整 JVM 参数
优化内存使用
检查递归
调整服务器资源
所以初学阶段重点学习:
Exception
而不是 Error。
九、Exception
Exception 表示程序运行过程中:
可以预料
可以捕获
可以处理
的问题。
例如:
用户输入错误
文件读取失败
数字格式错误
数据库访问异常
十、异常的两大分类
Exception 可以重点分成:
运行时异常
编译时异常
也经常叫:
非受检异常
受检异常
英文:
Unchecked Exception
Checked Exception
十一、运行时异常
继承:
RuntimeException
的异常通常称为:
运行时异常
例如:
NullPointerException
ArithmeticException
ArrayIndexOutOfBoundsException
NumberFormatException
ClassCastException
十二、运行时异常特点
运行时异常:
编译器不强制要求处理
例如:
int result = 10 / 0;
编译时可能没有问题。
真正运行到这里时才出现:
ArithmeticException
十三、编译时异常
不是 RuntimeException 子类的某些 Exception:
编译器要求必须处理
例如:
IOException
SQLException
如果不处理:
代码无法通过编译
十四、编译时异常不是说编译时一定出错
“编译时异常”这个名字容易误解。
它不是说:
异常发生在编译阶段
而是:
编译器会检查这种异常是否被处理。
异常真正发生依然通常是在:
程序运行阶段
十五、常见运行时异常
1. ArithmeticException
例如:
int result = 10 / 0;
2. NullPointerException
例如:
String name = null;
System.out.println(
name.length()
);
3. ArrayIndexOutOfBoundsException
int[] array = {
10,
20,
30
};
System.out.println(
array[3]
);
4. NumberFormatException
int number =
Integer.parseInt(
"abc"
);
5. ClassCastException
例如错误向下转型:
Animal animal =
new Cat();
Dog dog =
(Dog) animal;
十六、异常默认处理机制
如果程序没有主动处理异常:
JVM 会处理
大致过程:
1. 创建异常对象
2. 打印异常信息
3. 打印异常位置
4. 当前线程结束
例如:
Exception in thread "main"
java.lang.ArithmeticException: / by zero
十七、异常信息怎么看
例如:
java.lang.NumberFormatException:
For input string: "abc"
重点看:
异常类型
异常原因
代码位置
十八、异常堆栈
异常后通常还会看到:
at com.example.Demo.test(Demo.java:20)
at com.example.Demo.main(Demo.java:10)
这叫:
异常堆栈
英文:
Stack Trace
排查问题时非常重要。
十九、try-catch
最基本的异常处理方式:
try {
可能发生异常的代码;
} catch (异常类型 变量) {
异常处理代码;
}
二十、try-catch 基础案例
public class Demo {
public static void main(String[] args) {
try {
int result =
10 / 0;
System.out.println(
result
);
} catch (
ArithmeticException e
) {
System.out.println(
"除数不能为 0"
);
}
System.out.println(
"程序继续执行"
);
}
}
输出:
除数不能为 0
程序继续执行
二十一、try 中的执行规律
例如:
try {
System.out.println("A");
int result = 10 / 0;
System.out.println("B");
} catch (ArithmeticException e) {
System.out.println("C");
}
输出:
A
C
为什么没有:
B
因为:
try 中一旦发生异常
当前 try 后续代码不会继续执行
程序会直接去寻找匹配的:
catch
二十二、catch 参数
例如:
catch (ArithmeticException e) {
}
其中:
ArithmeticException
表示捕获的异常类型。
e
表示接收到的异常对象。
二十三、getMessage()
获取异常的简单描述:
System.out.println(
e.getMessage()
);
例如:
/ by zero
二十四、printStackTrace()
打印完整异常堆栈:
e.printStackTrace();
开发排错时非常常见。
但正式项目中通常会配合:
日志框架
例如:
SLF4J
Logback
而不是所有地方直接 printStackTrace()。
二十五、不要把异常直接吃掉
不推荐:
try {
// 业务代码
} catch (Exception e) {
}
什么都不做。
这种做法叫:
吞异常
问题是:
程序出错了
但日志中完全没有信息
后续排查非常困难
二十六、多个 catch
一个 try 可以对应多个 catch。
例如:
try {
String text = "abc";
int number =
Integer.parseInt(
text
);
int result =
10 / number;
} catch (
NumberFormatException e
) {
System.out.println(
"数字格式错误"
);
} catch (
ArithmeticException e
) {
System.out.println(
"算术异常"
);
}
二十七、多个 catch 的匹配规律
Java 会根据实际异常类型:
从上往下寻找第一个匹配的 catch
找到后:
执行对应 catch
其他 catch:
不会执行
二十八、父类异常要放后面
例如:
try {
} catch (Exception e) {
} catch (RuntimeException e) {
}
会有问题。
因为:
Exception
已经可以捕获:
RuntimeException
后面的 catch 永远没有机会执行。
正确:
try {
} catch (
NumberFormatException e
) {
} catch (
RuntimeException e
) {
} catch (
Exception e
) {
}
原则:
子类异常放前面
父类异常放后面
二十九、catch Exception 可以吗
可以:
catch (Exception e) {
}
它可以捕获很多异常。
但不建议:
任何地方都只写 Exception
如果已经知道具体异常:
NumberFormatException
IOException
SQLException
更推荐捕获:
具体异常
这样代码表达更清晰。
三十、Java 7 多异常捕获
如果多个异常处理方式完全一样:
catch (
NumberFormatException
| ArithmeticException e
) {
System.out.println(
"输入或计算错误"
);
}
使用:
|
连接多个异常类型。
三十一、finally
finally 表示:
无论是否发生异常,通常都会执行的代码。
语法:
try {
} catch (Exception e) {
} finally {
}
三十二、finally 基础案例
try {
System.out.println(
"执行 try"
);
} catch (Exception e) {
System.out.println(
"执行 catch"
);
} finally {
System.out.println(
"执行 finally"
);
}
三十三、finally 常见用途
以前经常用于:
关闭文件流
关闭数据库连接
释放资源
例如:
finally {
// 关闭资源
}
现代 Java 对很多资源推荐使用:
try-with-resources
后面会讲。
三十四、finally 真的百分百一定执行吗
一般业务流程中可以理解为:
通常都会执行
但也有特殊情况。
例如:
System.exit(0);
直接终止 JVM。
这种情况下:
finally
可能无法正常执行。
所以不要死记:
finally 绝对 100% 执行
三十五、try-finally
try 也可以只配:
finally
例如:
try {
// 代码
} finally {
// 清理资源
}
但是:
异常并没有被真正处理
它仍然可能继续向上抛。
三十六、return 和 finally
例如:
public static int test() {
try {
return 10;
} finally {
System.out.println(
"finally"
);
}
}
调用:
System.out.println(
test()
);
通常先执行:
finally
再完成返回。
三十七、不要在 finally 中 return
例如:
try {
return 10;
} finally {
return 20;
}
这种代码非常不推荐。
因为:
finally 中的 return
可能覆盖 try 或 catch 中的 return
甚至影响异常传播
实际开发中:
不要在 finally 里写 return。
三十八、throw
throw 用于:
主动抛出一个异常对象
例如:
throw new RuntimeException(
"发生异常"
);
三十九、主动抛异常的意义
有些业务问题并不是 JVM 自动发现的。
例如:
年龄不能小于 0
余额不足
订单不存在
用户没有权限
库存不足
这些都需要程序主动判断。
例如:
if (age < 0) {
throw new IllegalArgumentException(
"年龄不能小于 0"
);
}
四十、IllegalArgumentException
表示:
参数不合法
例如:
public void setAge(
int age
) {
if (age < 0
|| age > 150) {
throw new IllegalArgumentException(
"年龄必须在 0~150 之间"
);
}
this.age = age;
}
四十一、throw 后面的代码
例如:
if (age < 0) {
throw new IllegalArgumentException(
"年龄错误"
);
// System.out.println("后续代码");
}
throw 执行后:
当前正常流程被中断
异常开始向上寻找处理位置。
四十二、throws
throws 写在:
方法声明
后面。
作用:
声明这个方法可能抛出某些异常
例如:
public void readFile()
throws IOException {
}
四十三、throw 和 throws 区别
throw
写在:
方法体内部
作用:
真正抛出一个异常对象
例如:
throw new IOException();
throws
写在:
方法声明位置
作用:
告诉调用者
这个方法可能抛出异常
例如:
public void test()
throws IOException {
}
四十四、throw 和 throws 对比
| 对比 | throw | throws |
|---|---|---|
| 位置 | 方法体内部 | 方法声明后 |
| 作用 | 真正抛异常 | 声明可能异常 |
| 后面跟什么 | 异常对象 | 异常类型 |
| 数量 | 一次一个对象 | 可声明多个类型 |
四十五、throws 多个异常
例如:
public void test()
throws IOException,
SQLException {
}
调用者需要:
继续抛
或者处理
四十六、异常传播
例如:
public static void methodC() {
int result = 10 / 0;
}
A 调 B:
public static void methodB() {
methodC();
}
main 调 B:
public static void main(
String[] args
) {
methodB();
}
如果中间都不处理:
异常会沿着方法调用栈向上传播
四十七、异常传播过程
可以理解:
main()
↓
methodB()
↓
methodC()
↓
发生异常
↑
methodB 没处理
↑
main 没处理
↑
JVM 处理
四十八、在哪里处理异常
一个很重要的问题:
异常应该在哪里 catch?
不是:
哪里发生就一定在哪里 catch
而应该看:
哪一层真正有能力决定怎么处理。
例如底层 DAO:
数据库连接失败
底层可能只知道:
数据库访问失败
真正返回什么提示给用户:
Service / Controller
可能更清楚。
四十九、不要为了“消除红线”乱 catch
例如:
try {
service.pay();
} catch (Exception e) {
System.out.println(
"出错了"
);
}
如果只是为了让代码“不报错”,没有真正处理:
记录
转换
补偿
重试
返回错误
这不算好的异常处理。
五十、受检异常的处理方式
编译器要求处理的异常一般有两种方式:
1. try-catch
2. throws
五十一、方式一:try-catch
例如:
try {
// 可能发生 IOException 的代码
} catch (IOException e) {
// 处理
}
五十二、方式二:继续 throws
例如:
public void test()
throws IOException {
// 可能抛 IOException
}
把处理责任交给:
调用者
五十三、File 读取中的异常概念
例如:
Files.readString(
Path.of(
"data.txt"
)
);
读取文件时可能出现:
IOException
因此需要:
try-catch
或者:
throws IOException
五十四、try-with-resources
Java 7 提供:
try-with-resources
用于自动关闭资源。
语法:
try (
资源对象
) {
使用资源;
}
离开 try 后:
资源自动关闭
五十五、为什么 try-with-resources 更好
以前:
FileInputStream input = null;
try {
input =
new FileInputStream(
"a.txt"
);
} finally {
if (input != null) {
input.close();
}
}
比较繁琐。
现在:
try (
FileInputStream input =
new FileInputStream(
"a.txt"
)
) {
// 使用 input
}
更简洁。
五十六、哪些资源能自动关闭
实现:
AutoCloseable
接口的资源,可以使用:
try-with-resources
例如:
InputStream
OutputStream
Reader
Writer
数据库 Connection
Statement
ResultSet
很多资源都支持。
五十七、自定义异常
实际业务中,Java 自带异常不一定能准确描述业务问题。
例如:
余额不足
库存不足
用户不存在
订单已取消
登录失败
可以定义:
自定义异常
五十八、自定义运行时异常
例如:
public class BalanceNotEnoughException
extends RuntimeException {
public BalanceNotEnoughException(
String message
) {
super(message);
}
}
五十九、使用自定义异常
public void pay(
BigDecimal balance,
BigDecimal amount
) {
if (
balance.compareTo(amount)
< 0
) {
throw new BalanceNotEnoughException(
"余额不足"
);
}
System.out.println(
"支付成功"
);
}
六十、自定义异常为什么常继承 RuntimeException
现代业务开发中很多:
业务异常
会选择继承:
RuntimeException
原因之一:
调用链更干净
不用每一层都强制 throws
例如 Spring 项目中经常看到:
BusinessException
extends RuntimeException
六十一、自定义异常完整写法
public class BusinessException
extends RuntimeException {
private final int code;
public BusinessException(
int code,
String message
) {
super(message);
this.code = code;
}
public int getCode() {
return code;
}
}
使用:
throw new BusinessException(
40001,
"库存不足"
);
六十二、异常类可以携带业务信息
例如:
错误码
错误消息
业务模块
附加数据
例如:
private int code;
private String message;
后面 SpringBoot 全局异常处理中会很常见。
六十三、异常链
有时底层异常需要包装成更适合业务的异常。
例如:
try {
// 数据库访问
} catch (SQLException e) {
throw new RuntimeException(
"查询用户失败",
e
);
}
这里把原始异常:
e
继续作为:
cause
保留下来。
六十四、为什么要保留 cause
错误写法:
catch (SQLException e) {
throw new RuntimeException(
"查询失败"
);
}
原始 SQLException 信息丢失很多。
更好:
throw new RuntimeException(
"查询失败",
e
);
这样排查时还能看到:
真正底层原因
六十五、getCause()
可以获取:
导致当前异常的原始异常
例如:
Throwable cause =
e.getCause();
六十六、异常包装
异常包装常用于:
底层技术异常
↓
转换成业务层异常
例如:
SQLException
↓
UserQueryException
或者:
IOException
↓
FileStorageException
这样上层代码不用了解太多底层实现细节。
六十七、断言 assert 简单了解
Java 提供:
assert
例如:
assert age >= 0;
但是:
默认 JVM 不一定启用断言
所以不要把它用作:
正式业务参数校验
业务校验更适合:
if + throw
六十八、异常和 if 判断的区别
并不是所有错误都应该:
throw Exception
例如:
年龄是否成年
订单是否已支付
库存是否为 0
有些只是:
正常业务分支
应该使用:
if
六十九、什么时候更适合异常
异常更适合:
程序无法按当前正常流程继续
例如:
用户不存在
数据非法
权限不足
数据库失败
文件读取失败
而不是把异常当成普通流程控制工具。
七十、不要使用异常代替普通判断
不推荐:
try {
int number =
array[index];
} catch (
ArrayIndexOutOfBoundsException e
) {
// 用异常判断索引是否合法
}
更合理:
if (
index >= 0
&& index < array.length
) {
int number =
array[index];
}
七十一、业务异常和系统异常
项目中经常可以粗略分成:
业务异常
系统异常
七十二、业务异常
例如:
余额不足
库存不足
用户不存在
订单状态错误
验证码失效
权限不足
这些通常是:
业务规则导致的问题
七十三、系统异常
例如:
数据库连接失败
Redis 访问失败
第三方接口超时
磁盘异常
网络异常
这类问题通常:
不是用户正常操作导致
七十四、异常日志
实际项目中异常通常需要:
记录日志
例如:
log.error(
"查询用户失败",
e
);
而不是:
System.out.println(
e.getMessage()
);
七十五、为什么不能只打印 e.getMessage()
例如:
System.out.println(
e.getMessage()
);
可能只有:
Connection refused
缺少:
异常类型
调用链
代码位置
原始 cause
所以排查严重问题时通常需要:
完整异常堆栈
七十六、日志级别简单了解
常见:
TRACE
DEBUG
INFO
WARN
ERROR
异常通常会根据严重程度使用:
WARN
ERROR
后面学习 SpringBoot 日志时再深入。
七十七、不要重复记录同一个异常
例如:
DAO 记录一遍
Service 再记录一遍
Controller 再记录一遍
全局异常处理再记录一遍
同一个异常可能在日志中出现四遍。
实际项目中需要统一约定:
在哪一层记录
记录哪些上下文
是否继续抛
避免日志污染。
七十八、异常信息不要泄露敏感数据
例如:
密码
身份证号
银行卡
Token
数据库密码
完整 SQL 参数中的敏感字段
不要直接拼进异常信息或日志。
例如不推荐:
throw new RuntimeException(
"登录失败,密码="
+ password
);
七十九、异常消息应该有上下文
不推荐:
throw new RuntimeException(
"失败"
);
更好:
throw new RuntimeException(
"读取用户配置文件失败"
);
再结合原始 cause:
throw new RuntimeException(
"读取用户配置文件失败",
e
);
八十、异常处理的基本原则
可以先记住:
能处理就处理
不能处理就继续抛
不要无意义吞掉
保留原始原因
不要滥用 Exception
不要滥用 try-catch
八十一、参数校验案例
public static void register(
String username,
int age
) {
if (
username == null
|| username.isBlank()
) {
throw new IllegalArgumentException(
"用户名不能为空"
);
}
if (
age < 0
|| age > 150
) {
throw new IllegalArgumentException(
"年龄不合法"
);
}
System.out.println(
"注册成功"
);
}
八十二、数字输入异常案例
import java.util.Scanner;
public class InputDemo {
public static void main(
String[] args
) {
Scanner scanner =
new Scanner(
System.in
);
System.out.print(
"请输入年龄:"
);
String text =
scanner.nextLine();
try {
int age =
Integer.parseInt(
text
);
System.out.println(
"年龄:"
+ age
);
} catch (
NumberFormatException e
) {
System.out.println(
"请输入正确的整数"
);
}
scanner.close();
}
}
八十三、循环输入直到正确
import java.util.Scanner;
public class InputLoopDemo {
public static void main(
String[] args
) {
Scanner scanner =
new Scanner(
System.in
);
while (true) {
System.out.print(
"请输入整数:"
);
String text =
scanner.nextLine();
try {
int number =
Integer.parseInt(
text
);
System.out.println(
"输入成功:"
+ number
);
break;
} catch (
NumberFormatException e
) {
System.out.println(
"格式错误,请重新输入"
);
}
}
scanner.close();
}
}
八十四、ATM 业务异常案例
自定义异常:
public class BalanceNotEnoughException
extends RuntimeException {
public BalanceNotEnoughException(
String message
) {
super(message);
}
}
账户类:
import java.math.BigDecimal;
public class Account {
private BigDecimal balance;
public Account(
BigDecimal balance
) {
this.balance =
balance;
}
public void withdraw(
BigDecimal amount
) {
if (
amount == null
|| amount.compareTo(
BigDecimal.ZERO
) <= 0
) {
throw new IllegalArgumentException(
"取款金额必须大于 0"
);
}
if (
balance.compareTo(
amount
) < 0
) {
throw new BalanceNotEnoughException(
"账户余额不足"
);
}
balance =
balance.subtract(
amount
);
}
public BigDecimal getBalance() {
return balance;
}
}
八十五、调用 ATM 业务
Account account =
new Account(
new BigDecimal(
"1000.00"
)
);
try {
account.withdraw(
new BigDecimal(
"1200.00"
)
);
} catch (
BalanceNotEnoughException e
) {
System.out.println(
e.getMessage()
);
}
八十六、用户登录异常案例
定义:
public class LoginException
extends RuntimeException {
public LoginException(
String message
) {
super(message);
}
}
业务:
public static void login(
String username,
String password
) {
if (
!"admin".equals(
username
)
) {
throw new LoginException(
"用户不存在"
);
}
if (
!"123456".equals(
password
)
) {
throw new LoginException(
"密码错误"
);
}
System.out.println(
"登录成功"
);
}
八十七、异常分层思想
真实项目经常类似:
Controller
↓
Service
↓
Mapper / DAO
↓
Database
底层可能发生:
SQLException
Service 可能转换为:
BusinessException
Controller 或全局异常处理器再统一:
返回前端错误信息
八十八、SpringBoot 中异常机制会怎么用
后面学习 SpringBoot 时,经常会看到:
throw new BusinessException(
"用户不存在"
);
然后使用:
全局异常处理器
统一捕获。
例如概念上:
@RestControllerAdvice
配合:
@ExceptionHandler
统一返回:
错误码
错误消息
当前阶段只需要理解:
Java 异常机制
是 SpringBoot 全局异常处理的基础
八十九、方法重写中的 throws 规则
子类重写父类方法时:
不能抛出比父类声明更宽泛的受检异常
例如父类:
public void test()
throws IOException {
}
子类可以:
@Override
public void test()
throws FileNotFoundException {
}
但不能随便扩大成:
throws Exception
九十、RuntimeException 在重写中的特点
运行时异常通常不受上述受检异常限制那么严格。
例如:
throw new IllegalArgumentException();
很多地方可以直接抛。
九十一、finally 和资源释放
传统资源:
InputStream input = null;
如果不用 try-with-resources:
finally
中关闭时也可能:
再次发生异常
这也是为什么现代 Java 更推荐:
try-with-resources
九十二、try-with-resources 多资源
例如:
try (
InputStream input =
...;
OutputStream output =
...
) {
// 使用资源
}
多个资源用:
;
分隔。
关闭顺序通常与创建顺序相反。
九十三、异常抑制简单了解
try-with-resources 中:
业务代码异常
+
close() 也异常
Java 会保留主要异常,并记录被抑制异常。
可通过:
getSuppressed()
获取。
初学阶段知道即可。
九十四、异常设计不要太细碎
例如:
UsernameEmptyException
UsernameTooLongException
UsernameContainsIllegalCharException
UsernameAlreadyExistsException
如果每个小问题都建一个异常类:
异常类数量可能爆炸
实际项目常通过:
BusinessException
+
错误码
+
错误消息
统一描述业务异常。
九十五、统一业务异常示例
public class BusinessException
extends RuntimeException {
private final String code;
public BusinessException(
String code,
String message
) {
super(message);
this.code = code;
}
public String getCode() {
return code;
}
}
使用:
throw new BusinessException(
"USER_NOT_FOUND",
"用户不存在"
);
九十六、异常错误码
实际项目中常见:
40001 参数错误
40002 用户不存在
40003 权限不足
50001 系统异常
相比只返回:
"出错了"
更方便前后端协作。
九十七、不要把技术异常直接返回用户
例如数据库报错:
Communications link failure
不应该直接返回前端用户。
用户应该看到:
系统繁忙,请稍后重试
详细技术信息:
写入服务器日志
九十八、异常处理中的安全问题
错误信息不要暴露:
数据库表名
服务器路径
SQL 语句
内部 IP
Token
密码
堆栈详情
给普通用户。
这些信息可以进入:
受控日志
但不应该直接展示在页面。
九十九、异常和事务
后面学习 Spring 事务时:
异常类型
会直接影响:
事务是否回滚
Spring 默认情况下:
RuntimeException
通常会触发事务回滚。
这也是很多业务异常继承:
RuntimeException
的原因之一。
详细规则后面事务章节再学。
一百、常见异常快速表
| 异常 | 常见原因 |
|---|---|
NullPointerException |
对 null 调用方法/访问成员 |
ArithmeticException |
整数除 0 |
ArrayIndexOutOfBoundsException |
数组索引越界 |
NumberFormatException |
字符串转数字失败 |
ClassCastException |
类型强转错误 |
IllegalArgumentException |
参数不合法 |
IOException |
IO 操作失败 |
FileNotFoundException |
文件不存在 |
SQLException |
数据库访问异常 |
一百零一、NullPointerException 排查思路
看到:
NullPointerException
不要只想着:
加 try-catch
更重要的是找:
谁是 null
为什么是 null
为什么本不应该为 null
例如:
user.getName();
可能是:
user == null
应该检查:
数据是否查询到
参数是否为空
对象是否初始化
一百零二、数组越界排查
看到:
ArrayIndexOutOfBoundsException
检查:
数组长度
访问索引
循环条件
通常错误:
i <= array.length
正确:
i < array.length
一百零三、NumberFormatException 排查
例如:
Integer.parseInt(
text
);
检查:
text 是否为空
是否包含空格
是否真的是数字
是否超出 int 范围
例如:
text = text.trim();
再根据业务判断是否转换。
一百零四、ClassCastException 排查
例如:
Dog dog =
(Dog) animal;
检查实际对象类型:
if (
animal instanceof Dog dog
) {
}
不要盲目强转。
一百零五、异常调试顺序
看到异常时,建议:
1. 看异常类型
2. 看异常消息
3. 看第一条属于自己代码的堆栈
4. 找到对应源码行
5. 查看当时变量值
6. 分析为什么进入异常状态
7. 修复根因
不要只:
复制整段报错到搜索框
而完全不看堆栈。
一百零六、IDEA 中查看异常
IDEA 控制台中通常:
文件名.java:行号
可以点击。
例如:
Demo.java:25
直接跳到:
发生异常的源码位置
排错非常方便。
一百零七、IDEA Debug 配合异常排查
可以在异常前设置:
断点
然后 Debug。
重点观察:
变量值
调用栈
条件结果
对象状态
有时候比大量打印:
System.out.println()
更高效。
一百零八、IDEA Exception Breakpoint 简单了解
IDEA 支持:
异常断点
例如针对:
NullPointerException
设置断点。
异常一发生:
调试器立即停住
在复杂项目中很实用。
一百零九、不要 catch Throwable
一般不要:
catch (Throwable e) {
}
因为这连:
Error
都可能捕获。
普通业务代码一般处理:
Exception
或更具体的异常即可。
一百一十、不要 catch Error
例如:
catch (OutOfMemoryError e) {
}
普通业务层通常没有真正恢复能力。
严重 JVM 问题应该:
定位根因
监控报警
合理恢复/重启
而不是简单吞掉。
一百一十一、异常不要过度捕获
例如:
try {
// 500 行业务代码
} catch (Exception e) {
}
范围太大时:
很难知道究竟哪里失败
更好的原则:
try 包住真正可能发生异常
且你准备处理的代码
一百一十二、异常不要过度细分 try
另一种极端:
try {
a();
} catch (...) {}
try {
b();
} catch (...) {}
try {
c();
} catch (...) {}
如果这些属于一个完整事务流程:
过度拆分也会破坏逻辑
异常处理需要结合:
业务边界
事务边界
恢复能力
设计。
一百一十三、检查型异常和非检查型异常总结
Checked Exception
特点:
编译器强制处理
常见:
IOException
SQLException
处理:
try-catch
或者 throws
Unchecked Exception
主要:
RuntimeException
编译器:
不强制处理
常见:
NullPointerException
IllegalArgumentException
NumberFormatException
一百一十四、什么时候自定义 Checked Exception
理论上可以:
class MyException
extends Exception
这种异常会要求调用者:
强制处理
适合:
你真的希望调用者明确处理
且有恢复能力
的场景。
普通 Web 业务开发中:
自定义 RuntimeException
通常更常见。
一百一十五、自定义 Checked Exception
public class MyCheckedException
extends Exception {
public MyCheckedException(
String message
) {
super(message);
}
}
方法:
public void test()
throws MyCheckedException {
throw new MyCheckedException(
"测试异常"
);
}
调用者必须处理或继续抛。
一百一十六、异常构造方法常见写法
自定义异常建议至少提供:
public BusinessException(
String message
) {
super(message);
}
以及:
public BusinessException(
String message,
Throwable cause
) {
super(
message,
cause
);
}
这样方便保留异常链。
一百一十七、完整 BusinessException 示例
public class BusinessException
extends RuntimeException {
private final String code;
public BusinessException(
String code,
String message
) {
super(message);
this.code = code;
}
public BusinessException(
String code,
String message,
Throwable cause
) {
super(
message,
cause
);
this.code = code;
}
public String getCode() {
return code;
}
}
一百一十八、完整业务案例:转账
需求:
转账金额必须大于 0
余额必须足够
账户不能为空
账户:
import java.math.BigDecimal;
public class Account {
private final String name;
private BigDecimal balance;
public Account(
String name,
BigDecimal balance
) {
this.name = name;
this.balance =
balance;
}
public void subtract(
BigDecimal amount
) {
balance =
balance.subtract(
amount
);
}
public void add(
BigDecimal amount
) {
balance =
balance.add(
amount
);
}
public BigDecimal getBalance() {
return balance;
}
public String getName() {
return name;
}
}
一百一十九、转账业务方法
public static void transfer(
Account from,
Account to,
BigDecimal amount
) {
if (
from == null
|| to == null
) {
throw new BusinessException(
"ACCOUNT_NULL",
"转账账户不能为空"
);
}
if (
amount == null
|| amount.compareTo(
BigDecimal.ZERO
) <= 0
) {
throw new BusinessException(
"AMOUNT_ERROR",
"转账金额必须大于 0"
);
}
if (
from.getBalance()
.compareTo(
amount
) < 0
) {
throw new BusinessException(
"BALANCE_NOT_ENOUGH",
"账户余额不足"
);
}
from.subtract(amount);
to.add(amount);
}
一百二十、调用转账业务
try {
transfer(
accountA,
accountB,
new BigDecimal(
"500.00"
)
);
System.out.println(
"转账成功"
);
} catch (
BusinessException e
) {
System.out.println(
"转账失败:"
+ e.getMessage()
);
}
一百二十一、try-catch 模板
try {
// 可能发生异常的代码
} catch (
SomeException e
) {
// 处理异常
}
一百二十二、try-catch-finally 模板
try {
// 业务代码
} catch (
SomeException e
) {
// 处理异常
} finally {
// 清理操作
}
一百二十三、throw 模板
if (条件不合法) {
throw new IllegalArgumentException(
"参数不合法"
);
}
一百二十四、throws 模板
public void read()
throws IOException {
}
一百二十五、自定义异常模板
public class BusinessException
extends RuntimeException {
public BusinessException(
String message
) {
super(message);
}
}
一百二十六、异常链模板
try {
// 底层调用
} catch (
SomeException e
) {
throw new BusinessException(
"业务处理失败",
e
);
}