異常處理是 Java 程式健壯性的基石,但許多開發者陷入「能編譯通過就好」的誤區。本文透過一段包含六個嚴重問題的程式碼片段,深度剖析常見反模式,並提供重構後的標準寫法。
OutputStreamWriter out = ...;
Connection conn = ...;
try { // ③ try 區塊過大
Statement stat = conn.createStatement();
ResultSet rs = stat.executeQuery("select uid, name from user");
while (rs.next()) {
out.println("ID:" + rs.getString("uid") // ⑥ 輸出可能不完整
+ ",姓名:" + rs.getString("name"));
}
conn.close(); // ③ 資源釋放位置錯誤
out.close();
}
catch (Exception ex) { // ② 捕獲過寬泛異常
ex.printStackTrace(); // ① 拋棄異常、④ 缺乏錯誤上下文
}
位置:catch 區塊僅 printStackTrace()
危害:
修正策略(四選一):
throw ex; 保持堆疊追蹤throw new BusinessException("查詢失敗", ex);位置:catch (Exception ex)
危害:
RuntimeException(如 NullPointerException),掩蓋程式邏輯錯誤修正:分別捕獲具體異常
catch (SQLException sqle) { /* 資料庫錯誤處理 */ }
catch (IOException ioe) { /* IO 錯誤處理 */ }
位置:conn.close()、out.close() 在 try 區塊末尾
危害:若前面程式碼拋出異常,close() 永不執行 → 連線洩漏、檔案鎖定、記憶體耗盡
修正:使用 try-with-resources (Java 7+) 或 finally 區塊
// 推薦:try-with-resources 自動關閉實作 AutoCloseable 的資源
try (Connection conn = dataSource.getConnection();
Statement stat = conn.createStatement();
ResultSet rs = stat.executeQuery(sql);
OutputStreamWriter out = new OutputStreamWriter(new FileOutputStream(file), "UTF-8")) {
while (rs.next()) {
out.write(...);
}
} // 編譯器自動產生 finally 關閉所有資源,且正確處理關閉時的異常抑制
位置:ex.printStackTrace() 僅輸出堆疊
危害:
修正:使用結構化日誌(SLF4J + Logback/Log4j2)
log.error("查詢用戶列表失敗, userId={}, sql={}", userId, sql, ex);
// 輸出:ERROR [http-nio-8080-exec-1] c.s.UserService - 查詢用戶列表失敗, userId=123, sql=select...
// java.sql.SQLException: Connection timeout
位置:整段業務邏輯包在單一 try
危害:
修正:最小化 try 區塊,僅包含可能拋出目標異常的操作
// 好:精準包裹資料庫操作
List users;
try (Connection conn = ds.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
users = executeQuery(ps);
}
// 好:精準包裹檔案寫入
try (Writer out = new OutputStreamWriter(new FileOutputStream(file), "UTF-8")) {
writeUsers(out, users);
}
位置:迴圈中寫入,異常導致中斷無標記
危害:下游系統收到截斷資料,誤以為完整,導致錯誤決策
修正:
public void exportUsers(File outputFile) throws ExportException {
String sql = "SELECT uid, name FROM user ORDER BY uid";
// 1. 讀取資料(獨立 try 區塊)
List users;
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
users = new ArrayList<>();
while (rs.next()) {
users.add(new User(rs.getLong("uid"), rs.getString("name")));
}
} catch (SQLException e) {
log.error("讀取用戶資料失敗, sql={}", sql, e);
throw new ExportException("資料庫查詢失敗", e);
}
// 2. 寫入檔案(獨立 try 區塊,緩衝後寫入)
try (BufferedWriter out = Files.newBufferedWriter(outputFile.toPath(), StandardCharsets.UTF_8)) {
out.write("ID,姓名\n");
for (User u : users) {
out.write(u.getId() + "," + u.getName() + "\n");
}
out.flush(); // 確保寫入磁碟
} catch (IOException e) {
log.error("寫出用戶檔案失敗, file={}", outputFile, e);
// 嘗試刪除不完整檔案
Files.deleteIfExists(outputFile.toPath());
throw new ExportException("檔案寫入失敗", e);
}
}
catch (Exception)throw new BizEx(msg, cause))finally 中拋出異常(會掩蓋原始異常)良好的異常處理不只是「不讓程式當機」,而是讓錯誤可見、可追蹤、可恢復、可審計。將這六大反模式內化為肌肉記憶,配合現代 Java 語法,能顯著提升系統穩定性與維運效率。