Java异常处理详解:掌握try-catch-finally与try-with-resources,避开空指针等踩坑点
之前跟你们聊过 Java 接口多实现、泛型这些知识点,也安利过 Cursor 编辑器,今天咱们回归 Java 基础,说说 “异常处理”—— 这玩意儿看着简单,不少人写代码时却总爱忽略,结果线上出了 bug 都不知道咋排查。小索奇之前维护一个老项目,就因为前人没处理空指针异常,用户操作时突然闪退,查日志才发现是某个对象没初始化,那排查过程别提多闹心了。
先搞懂啥是异常?简单说就是代码运行时出的 “意外”,比如你想读取一个文件,结果文件被删了;或者你想把字符串转整数,结果字符串是 “abc”,这些都会触发异常。要是不处理,程序就会直接崩溃,给用户的体验也太差了,对吧?
Java 里处理异常主要靠 try-catch-finally 这三兄弟,咱们一个个说。try 块里放可能出异常的代码,比如:
try {
// 可能出异常的代码:把字符串转整数
int num = Integer.parseInt ("abc");
}
要是 try 块里真出了异常,就会跳转到 catch 块处理,比如给用户弹个提示:
catch (NumberFormatException e) {
// 处理 “字符串转整数失败” 的异常
System.out.println ("请输入正确的数字格式!");
// 还能打印异常日志,方便排查问题
e.printStackTrace ();
}
这里要注意,catch 块要抓具体的异常,别一上来就用 Exception(所有异常的父类)。比如上面的例子,明明是 NumberFormatException,你却写 catch (Exception e),虽然也能处理,但以后排查时,你就不知道具体是哪种异常了 —— 小索奇之前就见过有人把所有异常都用 Exception 抓,出了问题光看日志都分不清是空指针还是数组越界,排查效率低得要命。
然后是 finally 块,不管 try 块里有没有异常,它都会执行。最常用的场景就是关闭资源,比如读取文件后关闭流:
try {
FileReader reader = new FileReader ("test.txt");
// 读取文件的操作
} catch (FileNotFoundException e) {
System.out.println ("文件找不到!");
} finally {
// 不管有没有异常,都关闭流
reader.close (); // 这里要注意,reader 可能没初始化,会报错
}
哎,这里又有个坑!要是 try 块里初始化 reader 时就出了异常(比如文件找不到),reader 就没被创建,这时候在 finally 里调用 reader.close (),又会触发空指针异常。咋解决呢?Java 7 之后出了 try-with-resources 语法,能自动关闭资源,不用手动写 finally:
try (FileReader reader = new FileReader ("test.txt")) {
// 读取文件的操作
} catch (FileNotFoundException e) {
System.out.println ("文件找不到!");
}
看到没?只要把资源初始化放在 try 后面的括号里,程序结束后会自动关闭,再也不用操心 finally 里的关闭逻辑了。小索奇现在写文件操作、数据库连接的代码,全用 try-with-resources,省了不少代码,还避免了资源泄漏的问题。
除了系统自带的异常,咱们还能自定义异常。比如你做一个电商项目,用户余额不足时要抛异常,这时候就可以写个自定义异常:
// 自定义 “余额不足” 异常,继承 RuntimeException
class InsufficientBalanceException extends RuntimeException {
// 构造方法,传异常信息
public InsufficientBalanceException (String message) {
super (message);
}
}
用的时候直接抛就行:
public void pay (double amount) {
double balance = 100; // 假设用户余额 100
if (amount > balance) {
// 余额不足,抛自定义异常
throw new InsufficientBalanceException ("余额不足,还差" + (amount - balance) + "元!");
}
}
自定义异常要注意,尽量继承 RuntimeException(运行时异常),别继承 Exception(编译时异常)。因为编译时异常会强制你用 try-catch 处理,要是每个业务异常都得写 try-catch,代码会变得特别啰嗦;而运行时异常可以选择性处理,比如在全局异常处理器里统一处理,更灵活。小索奇做项目时,所有业务异常都是自定义的 RuntimeException,再用 Spring 的 @ControllerAdvice 写个全局处理器,不管哪抛异常,都能统一返回友好的提示,特别方便。
说到这儿可能有人会问:“那我是不是把所有代码都用 try-catch 包起来就行?” 当然不是!比如空指针异常,最好的处理方式是提前判断,而不是等它抛出来再抓。比如:
// 不好的写法:等空指针异常再处理
try {
String name = user.getName ();
} catch (NullPointerException e) {
e.printStackTrace ();
}
// 好的写法:提前判断 user 是不是 null
if (user != null) {
String name = user.getName ();
} else {
System.out.println ("用户对象不能为空!");
}
异常处理的核心是 “预防为主,处理为辅”,能提前避免的异常就别等它发生,对吧?
你们平时写代码时,有没有因为没处理异常踩过坑?比如程序突然崩溃,或者日志里全是模糊的 Exception 信息?可以在评论区跟小索奇聊聊,咱们一起把异常处理这块儿的细节抠得更细~
搜索即兴小索奇,点击关注,加入社区群聊,获取更多好用工具和资源