【Java八股|第8篇】Java 异常体系详解
前言
这一篇我们来学习 Java 面试中非常重要的一个基础知识点:异常体系。
在平时写项目时,我们经常会遇到各种异常,比如:
- 空指针异常
- 数组越界异常
- 类型转换异常
- SQL 异常
- 文件找不到异常
- 参数校验异常
- 业务异常
如果异常处理不好,接口可能直接报 500,前端看到一堆错误信息,用户体验很差。
在 Spring Boot 项目中,我们还经常会配合全局异常处理:
@RestControllerAdvice
public class GlobalExceptionHandler {
}
所以理解 Java 异常体系,不只是为了面试,也直接关系到后端项目开发。
一、Java 异常体系结构
Java 中异常的顶级父类是:
Throwable
Throwable 下面主要分成两大类:
Throwable
├── Error
└── Exception
可以简单理解为:
| 类型 | 说明 |
|---|---|
Error |
严重错误,程序一般无法处理 |
Exception |
异常,程序可以捕获和处理 |
常见结构:
Throwable
├── Error
│ ├── OutOfMemoryError
│ └── StackOverflowError
│
└── Exception
├── RuntimeException
│ ├── NullPointerException
│ ├── IndexOutOfBoundsException
│ └── ClassCastException
│
└── IOException
二、Error 是什么?
Error 表示严重错误,一般不是业务代码能处理的。
常见 Error 有:
OutOfMemoryError
StackOverflowError
1. OutOfMemoryError
内存溢出。
比如程序创建了太多对象,导致 JVM 内存不够:
List<byte[]> list = new ArrayList<>();
while (true) {
list.add(new byte[1024 * 1024]);
}
这可能导致:
OutOfMemoryError
2. StackOverflowError
栈溢出。
常见原因是递归没有结束条件:
public class Test {
public static void main(String[] args) {
method();
}
public static void method() {
method();
}
}
不断调用自己,最终可能出现:
StackOverflowError
Error 一般不建议在业务代码中捕获处理,而是要从程序设计、JVM 参数、内存使用等角度解决。
三、Exception 是什么?
Exception 表示异常,是程序可以捕获和处理的问题。
比如:
try {
int result = 10 / 0;
} catch (Exception e) {
System.out.println("程序出现异常:" + e.getMessage());
}
这里的除零异常可以被捕获。
Exception 又可以分成两类:
运行时异常
编译时异常
更准确地说:
| 类型 | 说明 |
|---|---|
RuntimeException 及其子类 |
运行时异常,也叫非受检异常 |
| 除 RuntimeException 外的 Exception | 编译时异常,也叫受检异常 |
四、运行时异常 RuntimeException
运行时异常指的是 RuntimeException 及其子类。
常见运行时异常包括:
| 异常 | 含义 |
|---|---|
NullPointerException |
空指针异常 |
IndexOutOfBoundsException |
下标越界异常 |
ClassCastException |
类型转换异常 |
ArithmeticException |
算术异常 |
IllegalArgumentException |
参数非法异常 |
NumberFormatException |
数字格式化异常 |
运行时异常的特点是:
编译器不会强制你处理。
比如:
public class Test {
public static void main(String[] args) {
String str = null;
System.out.println(str.length());
}
}
这段代码可以编译通过,但运行时会报:
NullPointerException
五、常见运行时异常示例
1. NullPointerException
String str = null;
System.out.println(str.length());
原因是 str 是 null,却调用了它的方法。
避免方式:
if (str != null) {
System.out.println(str.length());
}
或者在比较字符串时写成:
"success".equals(str)
这样可以减少空指针风险。
2. IndexOutOfBoundsException
List<String> list = new ArrayList<>();
list.add("Java");
System.out.println(list.get(1));
列表只有一个元素,下标只有 0。
访问下标 1 就会越界。
3. ClassCastException
Object obj = "hello";
Integer num = (Integer) obj;
obj 实际是字符串,却强转成 Integer,会出现类型转换异常。
4. ArithmeticException
int result = 10 / 0;
整数除以 0 会报算术异常。
5. NumberFormatException
String str = "abc";
Integer num = Integer.parseInt(str);
abc 不是合法数字,所以转换失败。
六、编译时异常是什么?
编译时异常也叫受检异常。
它的特点是:
编译器会强制你处理。
比如文件读取:
FileInputStream fis = new FileInputStream("a.txt");
这段代码可能出现文件找不到异常:
FileNotFoundException
编译器会要求你处理。
处理方式有两种:
- 使用
try-catch - 使用
throws抛出
七、try-catch 处理异常
示例:
try {
FileInputStream fis = new FileInputStream("a.txt");
} catch (FileNotFoundException e) {
System.out.println("文件不存在");
}
这里如果文件不存在,就会进入 catch。
try-catch 的作用是:
捕获异常,并在当前方法中处理它。
八、throws 抛出异常
如果当前方法不想处理异常,可以继续往外抛。
public void readFile() throws FileNotFoundException {
FileInputStream fis = new FileInputStream("a.txt");
}
调用这个方法的人,需要继续处理这个异常。
public static void main(String[] args) {
try {
readFile();
} catch (FileNotFoundException e) {
System.out.println("文件不存在");
}
}
throws 的意思是:
当前方法可能会抛出这个异常,但当前方法不处理,交给调用者处理。
九、throw 和 throws 的区别
这也是面试中经常问的点。
| 对比 | throw | throws |
|---|---|---|
| 位置 | 方法体内部 | 方法声明上 |
| 作用 | 主动抛出一个异常对象 | 声明方法可能抛出异常 |
| 后面跟什么 | 异常对象 | 异常类型 |
| 数量 | 一次抛一个 | 可以声明多个 |
1. throw 示例
public void checkAge(Integer age) {
if (age == null || age < 0) {
throw new IllegalArgumentException("年龄不合法");
}
}
这里是主动抛出异常对象。
2. throws 示例
public void readFile() throws IOException {
FileInputStream fis = new FileInputStream("a.txt");
}
这里是声明当前方法可能抛出 IOException。
简单记忆:
throw:真正抛异常
throws:声明可能抛异常
十、finally 的作用
finally 中的代码一般无论是否发生异常都会执行。
示例:
try {
int result = 10 / 0;
} catch (Exception e) {
System.out.println("出现异常");
} finally {
System.out.println("finally 执行");
}
输出:
出现异常
finally 执行
finally 常用于释放资源,比如关闭流、释放连接等。
FileInputStream fis = null;
try {
fis = new FileInputStream("a.txt");
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
不过现在更推荐使用 try-with-resources。
十一、try-with-resources
try-with-resources 可以自动关闭资源。
示例:
try (FileInputStream fis = new FileInputStream("a.txt")) {
int data = fis.read();
System.out.println(data);
} catch (IOException e) {
e.printStackTrace();
}
只要资源实现了 AutoCloseable 接口,就可以放在 try() 中自动关闭。
这样比手动在 finally 中关闭资源更简洁。
十二、自定义异常
在项目中,我们经常会自定义业务异常。
比如:
public class BusinessException extends RuntimeException {
public BusinessException(String message) {
super(message);
}
}
使用:
public void saveUser(User user) {
if (user == null) {
throw new BusinessException("用户不能为空");
}
if (user.getUsername() == null) {
throw new BusinessException("用户名不能为空");
}
}
为什么继承 RuntimeException?
因为业务异常通常不希望每一层都强制 try-catch,而是交给全局异常处理统一处理。
十三、Spring Boot 中的全局异常处理
在 Spring Boot 项目中,可以使用:
@RestControllerAdvice
统一处理异常。
示例:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public Result handleBusinessException(BusinessException e) {
return Result.error(e.getMessage());
}
@ExceptionHandler(Exception.class)
public Result handleException(Exception e) {
e.printStackTrace();
return Result.error("系统异常,请联系管理员");
}
}
这样 Controller 中就不需要到处写 try-catch。
比如:
@PostMapping("/users")
public Result save(@RequestBody User user) {
userService.save(user);
return Result.success();
}
如果 Service 中抛出 BusinessException,会被全局异常处理类统一捕获并返回:
{
"code": 0,
"msg": "用户名不能为空",
"data": null
}
这也是项目中非常常见的写法。
十四、异常处理的实际建议
在项目中处理异常时,可以注意下面几点:
- 不要直接把异常堆栈返回给前端
- Controller 层不要到处写重复的
try-catch - 业务错误可以使用自定义业务异常
- 全局异常处理统一返回结果
- 捕获异常后不要什么都不做
- 不能处理的异常不要乱吞掉
- 涉及资源关闭时优先使用
try-with-resources - 日志中可以记录异常详细信息,但响应前端要友好
十五、面试标准回答
如果面试官问:
Java 异常体系是什么?
可以这样回答:
Java 异常体系的顶级父类是 Throwable,它下面主要分为 Error 和 Exception。
Error 表示严重错误,比如 OutOfMemoryError、StackOverflowError,一般不是业务代码能处理的。
Exception 表示异常,程序可以捕获和处理。Exception 又可以分为运行时异常和编译时异常。RuntimeException 及其子类属于运行时异常,编译器不会强制处理,比如空指针异常、下标越界异常、类型转换异常等。除了 RuntimeException 之外的很多异常属于编译时异常,编译器会强制使用 try-catch 或 throws 处理,比如 IOException。
实际项目中,业务异常通常会自定义异常类,并配合 Spring Boot 的全局异常处理统一返回结果。
十六、常见问题总结
1. Error 和 Exception 有什么区别?
Error 是严重错误,一般程序无法处理。
Exception 是异常,程序可以捕获和处理。
2. 运行时异常和编译时异常有什么区别?
运行时异常编译器不强制处理。
编译时异常编译器会强制处理,要么 try-catch,要么 throws。
3. throw 和 throws 有什么区别?
throw 用在方法体中,表示主动抛出异常对象。
throws 用在方法声明上,表示当前方法可能抛出异常。
4. finally 一定会执行吗?
大多数情况下会执行。
但如果 JVM 直接退出,比如执行:
System.exit(0);
finally 可能不会执行。
5. 自定义业务异常一般继承什么?
一般继承:
RuntimeException
这样可以配合全局异常处理统一处理,不需要每层都强制捕获。
十七、总结
这一篇主要学习了 Java 异常体系。
Java 异常顶级父类是 Throwable,下面分为 Error 和 Exception。Error 表示严重错误,通常不由业务代码处理;Exception 表示异常,可以被程序捕获和处理。
Exception 又可以分为运行时异常和编译时异常。运行时异常编译器不强制处理,编译时异常必须处理。
在实际项目中,我们经常会自定义业务异常,并配合 Spring Boot 全局异常处理,让接口返回更加统一、友好。
下一篇我们继续学习 final、finally、finalize 的区别。
更多推荐



所有评论(0)