第35篇:Java异常处理最佳实践
📌 系列导航 :《Java 100 天进阶之路》完整目录 |
⬅️ 上一篇:第34篇:Java序列化与反序列化详解 |
➡️ 下一篇:第36篇:选择最适合自己的NIO,一探流技术
一、核心知识点
- 异常处理的基本原则:只捕获能处理的异常,不要捕获后什么都不做
- 检查型异常与运行时异常的使用场景
try-catch-finally的正确用法try-with-resources(自动关闭资源)- 自定义异常的规范
- 异常链与异常包装
- 日志记录的最佳实践
二、通俗讲解(1分钟开心学)
1. 异常处理的核心思想
- 只捕获你能处理的异常。
- 捕获后要做出有意义的响应(记录日志、返回默认值、重试、包装后重新抛出)。
2. 检查型异常 vs 运行时异常
- 检查型异常 (如
IOException):调用者必须处理或声明抛出,代表可恢复的外部错误(文件不存在、网络中断)。 - 运行时异常 (如
NullPointerException):不强制处理,代表编程错误(参数校验不足、数组越界),应当通过改进代码避免。
3. try-with-resources
自动关闭实现了 AutoCloseable 接口的资源(文件、网络连接、数据库连接),避免 finally 中手动关闭的繁琐。
4. 异常链
当你在一个异常中包装另一个异常时,可以保留原始异常信息,便于排查。
5. 日志记录
捕获异常后务必记录日志(使用 log.error("消息", e)),不要只打印 e.printStackTrace()(在正式环境可能不可见)。
6. 自定义异常
- 继承
Exception(检查型)或RuntimeException(运行时)。 - 提供多个构造方法(无参、带消息、带消息+原异常)。
生活类比 :
异常处理就像开车遇到故障。能自己修的小毛病(运行时异常)就用工具修好(改进代码)。大问题(检查型异常)需要呼叫救援(try-catch),然后记录事故(日志)。
三、实操代码案例 + 场景说明
场景:读取配置文件,处理文件不存在的异常,并确保流被正确关闭。
java
import java.io.*;
import java.util.Properties;
import java.util.logging.*;
public class ExceptionBestPractice {
private static final Logger logger = Logger.getLogger("ExceptionDemo");
// 1. 正确使用 try-with-resources 自动关闭
public static String readFirstLine(String path) {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
} catch (IOException e) {
// 记录日志,不要吞掉异常
logger.log(Level.SEVERE, "读取文件失败: " + path, e);
return null; // 或抛出运行时异常
}
}
// 2. 异常包装与链
public static Properties loadConfig(String path) throws ConfigException {
try (InputStream in = new FileInputStream(path)) {
Properties props = new Properties();
props.load(in);
return props;
} catch (IOException e) {
// 包装为自定义异常,保留原始原因
throw new ConfigException("加载配置失败: " + path, e);
}
}
// 3. 运行时异常用于参数校验
public static void setAge(int age) {
if (age < 0 || age > 150) {
throw new IllegalArgumentException("年龄必须在0~150之间,实际: " + age);
}
// 正常处理
}
// 4. 不要空 catch
public static void badPractice() {
try {
int a = 10 / 0;
} catch (ArithmeticException e) {
// 什么都不做,问题被隐藏了!这是最坏的实践
}
}
public static void main(String[] args) {
String line = readFirstLine("nonexist.txt");
System.out.println("读取结果: " + line);
try {
Properties props = loadConfig("config.properties");
} catch (ConfigException e) {
e.printStackTrace(); // 正式环境用 logger
}
try {
setAge(200);
} catch (IllegalArgumentException e) {
System.err.println(e.getMessage());
}
}
}
// 自定义检查型异常
class ConfigException extends Exception {
public ConfigException(String message, Throwable cause) {
super(message, cause);
}
public ConfigException(String message) {
super(message);
}
}
四、避坑要点
| 错误/误区 | 后果 | 正确做法 |
|---|---|---|
| 捕获异常后什么都不做(空 catch) | 掩盖问题,难以排查 | 至少记录日志 |
在 finally 中关闭资源但不处理异常 |
如果关闭失败,异常被吞没 | 使用 try-with-resources |
| 抛出检查型异常但调用者无法处理 | 传递上层,导致处处 throws |
考虑包装为运行时异常 |
在循环中 catch 异常 |
性能差,且可能陷入无限循环 | 循环外处理,或使用重试策略 |
日志只打印 e.getMessage() |
丢失栈轨迹 | 使用 log.error("msg", e) 打印完整堆栈 |
五、面试高频考点
Q1:什么时候应该捕获检查型异常,什么时候应该向上抛出?
如果当前层能恢复或做出合理响应(如重试、使用默认值),则捕获;否则向上抛出,让上层处理。对于框架代码,常包装为运行时异常抛出。
Q2:try-with-resources 的原理是什么?
实现了
AutoCloseable的资源可以在try括号中声明,编译后生成finally块调用close(),并处理异常抑制(addSuppressed)。
Q3:如何正确记录异常日志?
使用日志框架(如
java.util.logging、Log4j、SLF4J),记录完整堆栈:logger.error("操作失败", e);不要只记录e.getMessage()。
六、练习题
-
代码改错 :指出下面代码的问题并改正。
javatry { FileInputStream fis = new FileInputStream("test.txt"); fis.read(); fis.close(); } catch (IOException e) { e.printStackTrace(); } -
设计 :写一个方法
divide(int a, int b),如果 b 为 0,抛出自定义运行时异常DivideByZeroException。 -
场景:你正在开发一个网络请求库,当网络超时时应如何处理异常?
📊 你的学习进度
- 当前:第35篇 / 共44篇 · 第五阶段:工具类、异常最佳实践、序列化(第32~35篇)
- ✅ 已完成:第1~34篇
- 📖 正在学:第35篇
- ⏳ 待学习:第36~44篇
👉 📚 完整目录 & 学习指南 | 🔥 订阅本专栏,不错过每一篇
💡 本专栏每篇都包含:避坑表 + 面试高频考点 + 练习题。每天30分钟,100天拿offer!
👉 下一篇文章预告
《第36篇:选择最适合自己的NIO,一探流技术》
内容简介:BIO/NIO/AIO区别、流体系(字节/字符/缓冲/转换)、NIO核心组件(Channel/Buffer/Selector)、零拷贝。
💡 学完这篇,你将掌握Java I/O的全貌,并能根据场景选择合适的I/O模型。
📌 《Java 100 天进阶之路 | 从入门到上岗就业》 每天一篇,建议收藏 + 关注 ,一起100天拿offer!
👉 点击关注我,更新后第一时间收到推送!
📌 除了Java,我也在深挖智能物流实战(出版社WMS、托盘调度、机器学习落地)。如果你对技术在不同领域的实战感兴趣,欢迎点击我的头像,看看专栏《出版社物流WMS智能调度实战》。技术相通,思路可鉴。