大佬們好!我是LKJ_Coding,一枚初級馬牛,正在努力在代碼的叢林中找尋自己的方向。如果你也曾在調試中迷失,或是在文檔中翻滾,那我們一定有許多共同話題可以聊!今天,我帶着滿滿的代碼“乾貨”來和大家分享,學不學無所謂,反正我先吐槽了!

前言

  每次你寫 try-catch 的時候,心裏是不是都在想:“寫上就沒事了,編譯能過,鍋不背,放過我吧”?   但你知道嗎?你可能 catch 住了異常,但同時也 catch 掉了性能、可維護性和可觀測性。

  這篇文章,我們就來講清楚 Java 異常機制的底層哲學,從設計理念到實戰陷阱,逐個擊破。代碼多、例子實,話糙理不糙,絕不浮於表面。


一、Checked vs Unchecked:到底誰才是“合理的異常”?

在 Java 中,異常大致分三類:

異常類型 類別 是否強制處理 常見示例
IOException Checked ✅ 必須處理 文件/網絡異常
NullPointerException Unchecked ❌ 可不處理 代碼 bug
Error 嚴重錯誤 ❌ 不建議處理 OutOfMemoryError

✅ Checked 異常

編譯器強制你處理(try-catch 或 throws)

適用於:可以恢復的異常,比如數據庫連接失敗、文件不存在

public void readFile(String path) throws IOException {
    Files.readAllLines(Paths.get(path));
}

❌ Unchecked 異常(也叫 RuntimeException)

不處理也能編譯通過,一般表示程序邏輯錯誤

比如:

if (obj == null) throw new NullPointerException("對象不能為 null");

❌ Error:別碰!

JVM 層級的大問題,比如 OutOfMemoryError,你處理了也沒用……


二、try-catch-finally:這三兄弟到底怎麼配合?

try {
    // 可能拋異常的邏輯
} catch (IOException e) {
    // 捕獲並處理異常
} finally {
    // 一定會執行(即使前面 return 了)
}

finally 的經典作用:

  • 關閉資源(文件、數據庫連接)
  • 清理狀態(釋放鎖)

示例:

Connection conn = null;
try {
    conn = DriverManager.getConnection(...);
    // do something
} catch (SQLException e) {
    e.printStackTrace();
} finally {
    if (conn != null) conn.close();
}

三、try-with-resources:自動關閉的神操作!

Java 7 開始的寶藏語法,再也不用手動關閉資源了!

try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) {
    String line = reader.readLine();
} catch (IOException e) {
    e.printStackTrace();
}

原理:

  • 所有實現了 AutoCloseable 接口的資源
  • try 代碼塊結束時自動調用 close(),即使拋了異常也能保證資源釋放

優勢:

  • 更安全
  • 更簡潔
  • 更少的 boilerplate(樣板代碼)

四、自定義異常:你也可以寫個“專屬異常”!

系統內如果全靠 RuntimeException + 文案判斷,代碼會變得極其脆弱。

更優雅做法是自定義異常:

public class UserNotFoundException extends RuntimeException {
    public UserNotFoundException(String username) {
        super("用户未找到:" + username);
    }
}

然後在服務中使用:

public User findUser(String username) {
    User user = userRepo.find(username);
    if (user == null) throw new UserNotFoundException(username);
    return user;
}

好處:

  • 明確語義(只要看到異常名就知道是啥事)
  • 支持異常鏈傳遞(下節講)

五、異常鏈:別把 root cause 丟了!

在處理異常時,不要只是 e.printStackTrace(),你還要把“罪魁禍首”帶上!

try {
    doSomething();
} catch (IOException e) {
    throw new CustomBusinessException("業務執行失敗", e);
}

這裏的第二個參數 e 就是異常鏈,JVM 會在打印堆棧時把兩個異常都顯示出來。

💡 異常鏈的作用:

  • 方便排查真實 root cause
  • 更清晰地知道異常傳播鏈條
  • 日誌更完整

六、異常日誌:能不能別隻打 e.printStackTrace()?

很多人捕獲異常後就來一句:

e.printStackTrace();

這在生產環境中簡直是災難!我們應該這麼做:

logger.error("業務處理失敗:{}", businessId, e);

推薦日誌策略:

  • 統一使用 SLF4J + Logback/Log4j
  • 日誌中一定要帶異常對象本身
  • 用結構化日誌(包含業務上下文)
  • 不要 silent catch(不記錄、不處理)

七、實戰技巧彙總(一定要記牢):

場景 推薦做法
IO/數據庫異常 使用 try-with-resources
用户輸入校驗 拋出自定義異常
報錯要打印堆棧 使用 logger.error(msg, e)
catch 後繼續拋 用異常鏈 new Exception(msg, cause)
多重 catch 避免寫成 catch(Exception),要細分
不要吞異常 catch 後無日誌無拋出會掩蓋問題

八、異常處理哲學:不是 try 一把就完事

  Java 的異常設計,不是讓你“防住爆炸就 OK”,而是引導你:

  • 區分業務異常 vs 系統異常
  • 明確異常是否可恢復
  • 保證資源安全釋放
  • 日誌清晰完整

  更重要的是,讓代碼調用者知道出錯了,不是瞞着他自己扛


結語:你 catch 住的,是 bug 還是鍋?

  下次你再寫 catch(Exception e) 的時候,回頭想一想:你真的理解異常處理的初衷了嗎?你是真的在解決問題,還是隻是讓代碼“看起來沒報錯”?

  Java 的異常系統設計其實很優雅,它有層次、有哲學、有可維護性的考慮。如果你只是表面用法熟練,那只是寫 Java;但如果你理解了它的內核,那才是掌控 Java。

好啦,廢話不多説,今天的分享就到這裏!如果你覺得我這“初級馬牛”還挺有趣,那就請給我點個贊、留個言、再三連擊三連哦!讓我們一起“成精”吧!下次見,不見不散!