Java 釋放了記憶體管理負擔,但檔案、資料庫連線等資源仍需手動釋放。正如手動記憶體管理易出錯,手動資源管理亦衍生出諸多錯誤模式——本文聚焦分離清理錯誤模式。
第二種策略隱患極大:代碼膨脹、新人加入、新增執行路徑,極易遺漏釋放邏輯,導致資源洩漏。
所謂「分離清理」,指釋放代碼散落在各執行路徑,且多為複製貼上後未同步修改(即 Rogue Tile/流氓磚 模式)。
基類 TableWalker 遍歷資料表,walk() 結束時關閉連線:
public void walk() throws SQLException {
Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery(queryString);
while (rs.next()) execute(rs);
con.close(); // 僅在正常路徑關閉
}
TablePrinter 繼承後列印名字運作正常。但若拋出 SQLException,連線關閉前異常終止——成熟代碼中 SQLException 罕見,錯誤潛伏極久。
新增 TableFixer 更新電話號碼,同樣繼承 TableWalker。若嘗試用同一連線建立兩個 walker,首個走訪結束即關閉連線,第二個便操作已關閉連線——新手極易犯此錯,尤其當「單連線單 walker」不變量未文檔化、未測試時。
錯誤直覺:發現路徑缺釋放,直接補上?錯! 這會產生更多 Rogue Tile(如 try/catch 重複 close()),重構愈發困難。
正解:採用策略一——將獲取與釋放放入同一方法。Andrew Hunt 與 Dave Thomas 在《The Pragmatic Programmer》提倡「有始有終」:每個方法負責釋放自己獲取的資源。
重構範例:將 con.close() 移至 Example.main() 的 finally 區塊,確保無路徑漏網:
Connection con = null;
try {
con = DriverManager.getConnection(url, "Fernanda", "J8");
new TablePrinter(con, "Names").walk();
} catch (SQLException e) {
throw new RuntimeException(e.toString());
} finally {
try { con.close(); }
catch (Exception e) { throw new RuntimeException(e.toString()); }
}
此時 TableWalker 完全不用管連線關閉,消除「多 walker 重複關閉」與「異常路徑漏關」雙重風險。
| 模式 | 分離清理 |
|---|---|
| 症狀 | 資源洩漏或過早釋放 |
| 成因 | 部分執行路徑未「恰好一次」釋放資源 |
| 治療/預防 | 將釋放代碼移至獲取資源的同一方法 |