首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 綜合教學

《編寫高質量代碼》第 1 章精讀:建議 11-15 詳解與實戰落地

     編輯:Java 綜合教學

《編寫高質量代碼:改善 Java 程序的 151 個建議》第 1 章精讀:建議 11-15 詳解與實戰落地

本系列深度解讀《編寫高質量代碼:改善 Java 程序的 151 個建議》(秦小波著),每篇覆蓋 5 條建議,結合源碼級分析與實戰經驗,助你內化為肌肉記憶。

建議 11:養成良好習慣,顯式聲明 serialVersionUID

為什麼要聲明?

  • 序列化/反序列化時,JVM 通過 serialVersionUID 校驗類版本一致性。
  • 未顯式聲明時,JVM 根據類結構(類名、方法、字段等)自動生成,極其敏感:任何微小修改(增刪字段、調整方法順序)都會導致 UID 變化,反序列化拋出 InvalidClassException。

顯式聲明的價值

private static final long serialVersionUID = 1867341609628930239L;
  • 版本兼容:只要 UID 不變,即使類結構變更(如新增非關鍵字段),舊數據流仍能反序列化,新字段賦默認值。
  • 跨 JVM 部署:分布式環境下,生產者/消費者版本不一致時,顯式 UID 允許向上兼容。

警示

顯式聲明 UID 可避免不一致,但切勿濫用掩蓋不兼容變更(如刪除關鍵字段、修改字段類型)。嚴重不兼容應升級 UID 強制失效,暴露問題而非隱藏問題。

建議 12:避免在序列化類構造函數中為 final 字段賦值

現象

public class Person implements Serializable {
    public final String name;
    public Person() { name = "程咬金"; } // 構造函數賦值
}

序列化後修改構造函數賦值為 "秦叔寶",反序列化結果仍為 "程咬金"。

原因

反序列化不調用構造函數。JVM 直接從字節流恢復對象實例,final 字段若為基本類型/String/數組且非構造函數賦值,會重新計算;若在構造函數賦值,JVM 發現字段未初始化則保留流中原值。

最佳實踐

// 推薦:聲明處直接賦值或靜態工廠方法
public final String name = "程咬金";
// 或
public final String name = initName();
private static String initName() { return "程咬金"; }

建議 13:避免為 final 字段進行複雜賦值

現象

通过方法返回值賦值 public final String name = initName();,修改方法返回值後反序列化不生效,原因同建議 12:反序列化不執行實例初始化代碼(包括字段初始化表達式)。

本質區別

  • 簡單賦值(字面量、常量表達式、String 常量池):屬於類初始化階段,反序列化時 JVM 會重新執行類初始化邏輯,生效。
  • 複雜賦值(方法調用、new 對象、構造函數):屬於實例初始化,反序列化跳過,不生效。

建議 14:使用私有 writeObject/readObject 精準控制序列化行為

場景

HR 系統薪資對象含「基本工資」(可對外)與「績效工資」(絕密)。RMI 傳遞給稅務系統時,必須屏蔽績效工資。

解決方案

public class Person implements Serializable {
    private transient Salary salary; // transient 阻止默認序列化

    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();           // 序列化非 transient 字段
        out.writeInt(salary.getBasePay());  // 手動寫入基本工資
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();             // 反序列化非 transient 字段
        int basePay = in.readInt();         // 手動讀取基本工資
        this.salary = new Salary(basePay, 0); // 績效工資置 0
    }
}

核心機制

  • 實現 private void writeObject(ObjectOutputStream) 和 private void readObject(ObjectInputStream),JVM 反射檢測到後會委托這兩個方法替代默認序列化/反序列化邏輯。
  • defaultWriteObject()/defaultReadObject() 處理標準字段,手動代碼處理特殊字段。
  • 讀寫順序必須完全一致(先進先出隊列模型)。

建議 15:switch-case 絕不能漏寫 break

經典事故

switch (n) {
    case 0: chinese = "零";
    case 1: chinese = "壹"; // 漏寫 break,貫穿執行
    ...
}

輸入 0 輸出 "玖"。金融系統中文大寫轉換錯誤直接導致資金損失。

防範措施

  1. 肌肉記憶:寫 case 即寫 break,形成條件反射。
  2. IDE 強制檢查:Eclipse → Preferences → Java → Compiler → Errors/Warnings → 'switch' case fall-through 設置為 Error 級別,漏寫直接紅叉報錯。
  3. 單元測試覆蓋:每個 case 至少一條測試用例。

本章小結

建議核心知識點實戰原則
11serialVersionUID 版本控制必須顯式聲明,嚴禁濫用掩蓋不兼容
12反序列化跳過構造函數final 字段聲明處直接賦值,不用構造函數
13反序列化跳過實例初始化final 字段避免方法調用/複雜表達式賦值
14私有 writeObject/readObject 委托機制敏感字段用 transient + 手動序列化控制
15switch 貫穿特性IDE 設為 Error 級別,養成必寫 break 習慣

下期預告:建議 16-20,涵蓋 equals/hashCode 契約、Cloneable 陷阱、Comparable 實現規範等核心基礎話題。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved