Java 异常机制是一种"分离正常逻辑与错误处理"的编程方式,合理使用可以让程序更健壮、更易维护。核心机制 :
try-catch-finally
和throws
Java 异常的继承体系
Java 中所有异常和错误都继承自 Throwable
类。其主要分为Error 和Exception两大类:异常是"可恢复的问题",错误是"不可恢复的灾难"
php
Throwable
|
-------------------------
| |
Error Exception
|
-------------------------
| |
Checked Exception Unchecked Exception
(RuntimeException 及其子类)
1. Error
(错误)
-
表示 JVM 无法处理的严重问题,通常程序无法恢复。
-
常见例子:
OutOfMemoryError
:内存不足StackOverflowError
:栈溢出(如无限递归)
-
⚠️ 程序一般不捕获
Error
,也无法恢复。
2. Exception
(异常)
-
表示程序可以处理的异常情况。
-
分为两类:检查异常(Checked)和未检查异常(Unchecked)
-
检查异常 vs 未检查异常
对比项 | 检查异常(Checked) | 未检查异常(Unchecked) |
---|---|---|
编译器是否强制处理 | ✅ 是(必须 try-catch 或 throws) | ❌ 否 |
继承自 | Exception (非 RuntimeException) |
RuntimeException |
表示问题类型 | 外部环境问题(如文件不存在、网络断开) | 程序逻辑错误(如空指针、越界) |
是否应该捕获 | ✅ 应该处理或提示用户 | ❌ 应通过代码避免,而不是捕获 |
常见例子 | FileNotFoundException , SQLException ,IOException |
NullPointerException , IllegalArgumentException |
3、异常处理的 5 个关键字
关键字 | 作用 |
---|---|
try |
包裹可能出错的代码 |
catch |
捕获并处理特定类型的异常 |
finally |
无论是否出错都执行(用于资源清理) |
throw |
主动抛出一个异常 |
throws |
声明方法可能抛出的异常类型 |
异常处理流程图
csharp
try
↓
执行业务代码
↓
是否发生异常?
↙ ↘
是 否
↓ ↓
catch块 finally块
↓ ↓
处理异常 清理资源
↓ ↓
方法结束
示例代码:
java
// 方法1:使用try-catch处理异常
private void readFileWithTryCatch(String filename) {
FileInputStream fis = null;
try {
fis = new FileInputStream(filename);
int data = fis.read();
appendOutput(" 读取到的数据: " + data);
} catch (FileNotFoundException e) {
appendOutput(" 文件未找到异常: " + e.getMessage());
} catch (IOException e) {
appendOutput(" IO异常: " + e.getMessage());
} finally {
if (fis != null) {
try {
fis.close();
appendOutput(" 文件流已关闭");
} catch (IOException e) {
appendOutput(" 关闭文件流时发生错误: " + e.getMessage());
}
}
}
}
```
// 方法2:使用throws声明异常
private void readFileWithThrows(String filename) throws IOException {
FileInputStream fis = null;
try {
fis = new FileInputStream(filename);
int data = fis.read();
appendOutput(" 读取到的数据: " + data);
} finally {
if (fis != null) {
fis.close();
appendOutput(" 文件流已关闭");
}
}
}
// 方法3:问题示例 - 同时使用throws和catch
private void readFileQuestionable(String filename) throws IOException {
FileInputStream fis = null;
try {
fis = new FileInputStream(filename);
int data = fis.read();
appendOutput(" 读取到的数据: " + data);
} catch (FileNotFoundException e) {
appendOutput(" 文件未找到异常: " + e.getMessage());
// 这里捕获了异常但没有重新抛出,调用者不知道发生了异常
} catch (IOException e) {
appendOutput(" IO异常: " + e.getMessage());
// 这里捕获了异常但没有重新抛出,调用者不知道发生了异常
} finally {
if (fis != null) {
fis.close(); // 释放资源,可能抛出IOException异常
appendOutput(" 文件流已关闭");
}
}
}
// 方法4:使用try-with-resources(推荐方式)
✅ Java 7+ 推荐使用 **try-with-resources** 简化资源管理
private void readFileWithTryWithResources(String filename) {
try (FileInputStream fis = new FileInputStream(filename)) {
int data = fis.read();
appendOutput(" 读取到的数据: " + data);
} catch (FileNotFoundException e) {
appendOutput(" 文件未找到异常: " + e.getMessage());
} catch (IOException e) {
appendOutput(" IO异常: " + e.getMessage());
}
// 不需要finally块,资源会自动关闭
appendOutput(" 文件流已自动关闭");
}
4、常见的异常类型
未检查异常(RuntimeException)
异常 | 原因 |
---|---|
NullPointerException |
访问 null 对象的字段或方法 |
ArrayIndexOutOfBoundsException |
数组下标越界 |
ClassCastException |
类型转换错误 |
IllegalArgumentException |
传递了非法参数 |
NumberFormatException |
字符串转数字失败 |
ArithmeticException |
除以零等算术错误 |
检查异常
异常 | 原因 |
---|---|
IOException |
输入输出错误(如文件读写) |
FileNotFoundException |
文件不存在 |
SQLException |
数据库操作错误 |
ParseException |
字符串解析失败(如日期) |
5、自定义异常
当 Java 内置异常无法满足需求时,可以自定义异常。
scala
// 自定义检查异常
public class InvalidAgeException extends Exception {
public InvalidAgeException(String message) {
super(message);
}
}
// 使用
public void setAge(int age) throws InvalidAgeException {
if (age < 0 || age > 150) {
throw new InvalidAgeException("年龄必须在 0-150 之间");
}
this.age = age;
}
也可以继承 `RuntimeException` 创建未检查异常。
public class InvalidAgeException extends RuntimeException {
public InvalidAgeException(String message) {
super(message);
}
}
public void setAge(int age) { // 不需要 throws
if (age < 0 || age > 150) {
throw new InvalidAgeException("年龄必须在 0-150 之间");
}
this.age = age;
}
7、最佳实践与建议
- 优先使用 try-with-resources 管理资源(如流、连接)
- 不要捕获所有异常 (避免
catch (Exception e)
) - 不要忽略异常 (避免空的
catch
块) - 记录异常日志 (使用
log.error("msg", e)
) - 不要在 finally 中使用
return
(会覆盖 try 的返回值) - 未检查异常应通过代码预防,而不是捕获
- 检查异常应合理处理或向上抛出
8、 finally中使用return会怎么样?
-
结论先行:不要在 finally 中使用
return
,虽然语法上允许,但 在finally
中使用return
是非常危险且不推荐的,原因如下:- 如果
finally
块中有return
语句,会覆盖try
和catch
中的返回值 - 会吞掉异常 (如果
catch
中抛出异常,finally
的return
会让异常消失) - 破坏程序逻辑,难以调试
- 如果
示例代码
typescript
//1.覆盖try中的return
public static String test() {
try {
System.out.println("try block");
return "from try";
} finally {
System.out.println("finally block");
return "from finally";
}
}
public static void main(String[] args) {
System.out.println(test());
}
/*
`finally` 中的 `return`直接结束方法,返回 "from finally",原本 `try` 中要返回的 `"from try"` 被丢弃
输出结果:
try block
finally block
from finally
*/
//2.吞掉try中的异常
public static String badExample() {
try {
throw new RuntimeException("error");
} finally {
return "success"; // 异常被吞了!
}
}
这个方法不会抛出异常,而是返回 `"success"`,严重掩盖了错误!
//3.特殊情况:try 中有 return,finally 中修改了返回值(针对引用类型)
public static StringBuilder testRef() {
StringBuilder sb = new StringBuilder("hello");
try {
return sb;
} finally {
sb.append(", world"); // 修改对象内容
// 注意:这里没有 return 新值
}
}
返回值仍然是 `sb`,但内容被修改了,最终结果是 `"hello, world"`。
正确做法:finally 用于清理资源,不要 return
typescript
public static String goodExample() {
String result = "default";
try {
result = "from try";
return result;
} finally {
// 只做资源清理,不要 return
System.out.println("cleanup resources");
// ✅ 正确:关闭文件、连接等
// resource.close();
}
}
此时返回值是 `"from try"`,`finally` 不影响返回结果。
9、 finally中的代码一定会执行吗?
答案不一定,以前面试美团还是阿里问过,记不清了,finally
块中的代码几乎总是会执行 ,这是 Java 异常处理机制的核心设计之一。它的主要目的就是确保关键的清理操作(如关闭文件、释放资源、断开连接等)能够被执行。
finally
一定会执行的常见情况
示例 :
csharp
//1.try 中有 return
public static String test() {
try {
return "try";
} finally {
System.out.println("finally 执行了");
}
}
//输出:`finally 执行了` → 然后返回 `"try"`
//2.try 中抛出异常
try {
throw new RuntimeException("error");
} finally {
System.out.println("finally 还是执行了");
}
//输出:`finally 还是执行了` → 然后抛出异常
finally
不会执行的几种特殊情况
尽管 finally
的设计目标是"无论如何都要执行",但在以下极少数情况下,它不会执行:
JVM 退出或崩溃
如果在 try
或 catch
中调用了 System.exit(0)
或 Runtime.getRuntime().halt()
,JVM 直接终止,finally
不会执行。
csharp
try {
System.out.println("try");
System.exit(0); // JVM 退出
} finally {
System.out.println("finally"); // ❌ 不会执行!
}
线程在 try/catch 中被强制终止
- 如果线程在
try
或catch
中被Thread.stop()
(已废弃)或interrupt()
强行中断,且导致 JVM 不正常运行,finally
可能不会执行。 - 虽然
interrupt()
本身不会阻止finally
执行,但如果线程状态异常,可能影响执行。
3. JVM 崩溃或操作系统故障
- 如发生严重错误(
OutOfMemoryError
、StackOverflowError
等)导致 JVM 崩溃。 - 或操作系统死机、断电等硬件/系统级故障。
csharp
try {
while (true) {
// 无限循环占满 CPU,可能导致系统无响应
}
} finally {
System.out.println("finally"); // ❌ 可能永远不执行
}
在 finally 之前发生 native 代码死锁或无限循环
- 如果
try
块中调用了 native 方法(JNI),并且该方法陷入死锁或无限循环,JVM 无法继续执行finally
。
finally
块在绝大多数情况下都会执行 ,这是它设计的初衷。finally
的承诺是:"只要 JVM 还活着,并且程序能继续运行,我就会执行。
9、什么是 try-with-resources?
try-with-resources
是一种特殊的 try
语句,它允许你声明一个或多个资源 ,并在 try
块执行完毕后自动关闭这些资源,无论是否发生异常。
✅ 核心目标:自动、安全地释放资源(如文件流、网络连接、数据库连接等)。
基本语法
csharp
try (资源声明) {
// 使用资源
} catch (ExceptionType e) {
// 处理异常(可选)
} finally {
// 清理代码(可选)
}
前提条件:资源必须实现 AutoCloseable
接口
Java 中常见的可关闭资源都实现了 AutoCloseable
或其子接口 Closeable
:
InputStream
/OutputStream
Reader
/Writer
Socket
Connection
(数据库连接)Statement
/ResultSet
示例对比:传统方式 vs try-with-resources
ini
//1.传统方式(繁琐且易出错)
FileInputStream fis = null;
try {
fis = new FileInputStream("data.txt");
int data = fis.read();
System.out.println(data);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fis != null) {
try {
fis.close(); // 必须手动关闭,且 close() 也可能抛异常
} catch (IOException e) {
e.printStackTrace();
}
}
}
//问题:代码冗长,容易忘记关闭资源,`close()` 异常需要额外处理
//2. ✅ 使用 try-with-resources(简洁安全)
try (FileInputStream fis = new FileInputStream("data.txt")) {
int data = fis.read();
System.out.println(data);
} catch (IOException e) {
e.printStackTrace();
}
//无需手动调用 `close()` ,JVM 会自动调用!
//3.多个资源的使用
try (
FileInputStream fis = new FileInputStream("input.txt");
FileOutputStream fos = new FileOutputStream("output.txt")
) {
int data;
while ((data = fis.read()) != -1) {
fos.write(data);
}
} catch (IOException e) {
e.printStackTrace();
}
//多个资源用分号 `;` 分隔,关闭顺序是**声明的逆序**(先 `fos`,再 `fis`)。
资源是如何自动关闭的?
try
块执行完毕(正常结束或抛出异常)- JVM 自动调用每个资源的
close()
方法 - 关闭顺序:从最后一个声明的资源开始,逆序关闭
- 如果
close()
抛出异常,它会被抑制(suppressed) ,主异常(try块中的异常)仍然会被抛出
异常处理机制(重要!)
如果 try
块和 close()
都抛出异常,主异常是 try
块中的异常 ,close()
的异常会被"抑制",但可以通过 getSuppressed()
获取。
php
try (MyResource res = new MyResource()) {
throw new RuntimeException("try 块异常");
} catch (Exception e) {
System.out.println("主异常: " + e.getMessage());
for (Throwable t : e.getSuppressed()) {
System.out.println("被抑制的异常: " + t.getMessage());
}
}
自定义可关闭资源
你可以创建自己的 AutoCloseable
资源:
csharp
public class MyResource implements AutoCloseable {
public MyResource() {
System.out.println("资源打开");
}
public void doWork() {
System.out.println("执行任务");
}
@Override
public void close() throws Exception {
System.out.println("资源关闭");
}
}
// 使用
try (MyResource res = new MyResource()) {
res.doWork();
}
输出:
资源打开
执行任务
资源关闭
总结
项目 | 说明 |
---|---|
目的 | 自动管理资源,避免资源泄漏 |
要求 | 资源必须实现 AutoCloseable 或 Closeable |
优点 | 代码简洁、安全、不易出错 |
适用场景 | 所有需要手动关闭的资源(I/O、数据库、网络等) |
Java 版本 | Java 7+ |
💡 一句话理解 :
try-with-resources
就像是"用完即扔 "的智能容器------你只管使用资源,JVM 会在try
块结束后自动帮你关闭它,无论是否发生异常。