Java异常处理:用好try-catch,代码更健壮
一、从常见的错误写法说起
try {
// 一堆业务代码
} catch (Exception e) {
e.printStackTrace();
}刚开始觉得这样写很安全,至少程序不会直接崩溃。但用久了就会发现几个问题:
控制台全是红色错误信息,用户根本看不懂
业务逻辑和异常处理混在一起,很难维护
真正有用的错误信息没有被好好利用
想复用代码时,被各种throws和catch搞得很头疼
我自己也踩过不少坑,慢慢总结出了一套处理异常的方法。今天就来分享怎么用好try-catch,写出更好维护、更好排查的代码。
二、try-catch到底是怎么工作的
先看个基本例子:
public void readFile(String filename) {
try {
FileInputStream in = new FileInputStream(filename); // 可能出错
int b;
while ((b = in.read()) != -1) {
// 处理文件内容
}
System.out.println("读取完成");
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("方法结束");
}这里有几个关键点:
try里面的代码只要有一行抛出异常,后面的代码就不会执行了
程序直接跳到对应的catch块
catch执行完后,如果没有再抛出异常,就继续执行try-catch后面的代码
所以如果文件读取到一半出错,System.out.println("读取完成")这行就不会执行,但System.out.println("方法结束")还是会执行的。
三、什么时候该catch,什么时候该throw
上面的写法是在当前方法里处理异常。但有时候这样并不合适。
比如你在DAO层读取文件或数据库,真正知道该怎么给用户反馈错误信息的应该是上层的Service或Controller。
这种情况下,我建议不要在底层直接catch,而是把异常抛出去:
public void readFile(String filename) throws IOException {
try (FileInputStream in = new FileInputStream(filename)) {
int b;
while ((b = in.read()) != -1) {
// 处理文件内容
}
}
}上层可以选择:
统一记录日志
转成业务异常
给用户友好的提示
记住:如果你不知道怎么处理这个异常,就不要假装自己能处理。
四、处理多种异常类型
实际项目中,一个try块里经常会有多种可能的异常。
1. 用多个catch分别处理
try {
URL url = new URL("https://example.com/config.json");
URLConnection conn = url.openConnection();
try (InputStream in = conn.getInputStream()) {
// 读取配置
}
} catch (MalformedURLException e) {
// URL格式错误
System.err.println("配置地址不合法");
} catch (UnknownHostException e) {
// 域名解析失败
System.err.println("无法解析服务器域名");
} catch (IOException e) {
// 其他IO问题
System.err.println("读取配置失败");
}两个技巧:
具体的异常放在前面,通用的异常放在后面
这样可以针对不同的错误给出不同的提示
2. 多种异常用同样的方式处理
如果几种异常的处理方式一样,可以这样写:
try {
loadResource("config.json");
} catch (FileNotFoundException | UnknownHostException e) {
// 这两种情况都算"资源找不到"
System.err.println("资源不可用");
} catch (IOException e) {
System.err.println("读取资源失败");
}注意:这里的e变量是final的,不能重新赋值。
五、包装异常传递上下文
有时候会遇到这种情况:
底层抛出SQLException、IOException
上层并不关心具体细节,只想知道业务子系统出了什么问题
这时可以包装一层自己的异常:
public class ConfigException extends Exception {
public ConfigException(String message, Throwable cause) {
super(message, cause);
}
}
public Properties loadConfig(Path path) throws ConfigException {
try (InputStream in = Files.newInputStream(path)) {
Properties props = new Properties();
props.load(in);
return props;
} catch (IOException e) {
// 包装成业务异常
throw new ConfigException("加载配置失败:" + path, e);
}
}上层既能拿到业务描述,也能追溯原始错误:
try {
loadConfig(Paths.get("config/app.properties"));
} catch (ConfigException e) {
System.err.println(e.getMessage()); // 业务提示
Throwable root = e.getCause(); // 原始异常
root.printStackTrace(); // 排查用
}我的习惯是:一旦跨越业务边界(比如从DAO到Service),就用自定义异常包装底层异常。
六、资源关闭:finally和try-with-resources
1. finally的用法
传统的资源关闭方式:
FileInputStream in = null;
try {
in = new FileInputStream("data.txt");
// 处理文件
} catch (IOException e) {
e.printStackTrace();
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}要点:
无论是否抛出异常,finally都会执行
即使catch里又抛出异常,finally还是会执行
重要提醒:finally里不要写return、throw这样的语句:
public static int parseInt(String s) {
try {
return Integer.parseInt(s);
} finally {
return 0; // 危险!会吞掉异常和正常返回值
}
}这个方法永远返回0,而且不会报错。
2. 推荐使用try-with-resources
现在我都用这种方式:
public List<String> readAllLines(String filename) throws IOException {
List<String> lines = new ArrayList<>();
try (Scanner in = new Scanner(Paths.get(filename))) {
while (in.hasNextLine()) {
lines.add(in.nextLine());
}
} // 这里自动调用close()
return lines;
}好处:
自动关闭资源,不会忘记
代码更简洁
可以同时处理多个资源
七、堆栈跟踪:不只是错误信息
平时看到的红色报错就是堆栈跟踪,它记录了:
异常类型
错误信息
方法调用链
最简单的用法:
try {
// 可能出错的操作
} catch (Exception e) {
e.printStackTrace(); // 开发时有用
}生产环境建议用日志记录:
catch (Exception e) {
logger.error("处理订单失败", e); // 日志包含完整堆栈
}八、实用经验总结
1. 异常不是if的替代品
不要这样写:
// 不好的写法
try {
stack.pop();
} catch (EmptyStackException e) {
// 什么都不做
}问题:
性能差:抛异常比普通判断慢很多
语义不对:异常应该用于"意外情况"
应该这样写:
if (!stack.empty()) {
stack.pop();
}2. 不要每行代码都包try-catch
// 不好的写法
try {
n = stack.pop();
} catch (EmptyStackException e) { /* ... */ }
try {
out.writeInt(n);
} catch (IOException e) { /* ... */ }如果整个操作失败就算失败,应该:
try {
for (int i = 0; i < 100; i++) {
int n = stack.pop();
out.writeInt(n);
}
} catch (EmptyStackException e) {
logger.warn("栈元素不足");
} catch (IOException e) {
logger.error("写入文件失败");
}3. 不要吞掉异常
见过太多这样的代码:
try {
doSomething();
} catch (Exception e) {
// 什么都不做
}表面安全,实际埋雷。至少应该做其中一件事:
记录日志
转成业务异常往上抛
写注释说明为什么忽略
4. throws不丢人
有些人觉得方法签名里写throws不好看,就想把所有异常都catch掉。
我建议:如果这层不知道怎么处理,就老实往上抛。上一层知道业务该怎么做。
public void importUsers(Path path) throws IOException {
List<String> lines = Files.readAllLines(path);
// 解析并保存
}5. 用标准工具做参数校验
Java自带的Objects很好用:
public void putData(int index, Object value) {
Objects.checkIndex(index, data.length); // 检查索引
Objects.requireNonNull(value, "值不能为空"); // 检查null
data[index] = value;
}好处:异常类型和信息都符合JDK标准。
6. 不要给用户看堆栈信息
开发时可以这样:
e.printStackTrace();但生产环境不要给用户看完整的堆栈信息,因为:
可能包含敏感信息
用户看不懂
应该:
日志里记录完整堆栈
给用户友好的错误提示
九、完整示例:配置加载服务
import java.io.IOException;
import java.nio.file.*;
import java.util.Properties;
import java.util.Objects;
import java.util.logging.Logger;
public class ConfigService {
private static final Logger logger = Logger.getLogger(ConfigService.class.getName());
public static class ConfigException extends Exception {
public ConfigException(String message, Throwable cause) {
super(message, cause);
}
}
private final Path configPath;
public ConfigService(Path configPath) {
this.configPath = Objects.requireNonNull(configPath, "配置文件路径不能为空");
}
public Properties loadConfig() throws ConfigException {
if (!Files.exists(configPath)) {
throw new ConfigException("配置文件不存在:" + configPath, null);
}
try {
Properties props = new Properties();
try (var in = Files.newInputStream(configPath)) {
props.load(in);
}
return props;
} catch (IOException e) {
logger.severe("读取配置文件失败:" + e.getMessage());
throw new ConfigException("读取配置文件失败:" + configPath, e);
}
}
public static void main(String[] args) {
ConfigService service = new ConfigService(Paths.get("config/app.properties"));
try {
Properties props = service.loadConfig();
System.out.println("配置加载成功");
} catch (ConfigException e) {
System.err.println("系统启动失败,请检查配置"); // 给用户的提示
e.printStackTrace(); // 开发人员看的详细信息
}
}
}这个例子包含了:
try-with-resources自动关闭资源
包装异常传递业务信息
给用户友好提示,同时记录详细日志
十、下一步思考
掌握了这些基础后,可以进一步思考:
Web项目中,可以用全局异常处理统一转换异常
多线程环境下,异常处理有什么不同
除了异常信息,还要记录哪些上下文帮助排查问题
下次再想写catch (Exception e) { e.printStackTrace(); }时,可以先想一想:我真的需要在这里处理吗?
希望这些经验能帮你写出更好的Java代码,少踩一些坑。用好异常处理,你的程序会更健壮、更好维护。
本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!