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

Java 序列化核心:ObjectInputStream/ObjectOutputStream 完整指南

     編輯:Java 相關知識

核心作用與適用場景

ObjectOutputStream / ObjectInputStream 提供物件序列化與反序列化能力,支援基本類型與實現 Serializable 或 Externalizable 介面的物件持久化存儲、網絡傳輸、進程間通訊。

關鍵前提條件

只有實現 java.io.Serializable 或 java.io.Externalizable 介面的類,其實例才能被序列化/反序列化。
  • Serializable:標記介面,無方法,JVM 自動處理序列化細節
  • Externalizable:需手動實現 writeExternal/readExternal,完全控制格式,效能更高

核心 API 概覽

ObjectOutputStream 主要方法

方法說明
writeObject(Object obj)寫入物件(含圖引用處理)
writeUnshared(Object obj)寫入「非共享」物件,反序列化時強制新建實例
defaultWriteObject()在 writeObject 中調用,執行預設序列化邏輯
putFields()獲取 PutField 手動控制欄位寫入
writePrimitive 系列writeInt、writeLong、writeUTF 等基本類型
flush()/close()刷新緩衝區、關閉流(會自動 flush)
reset()重置已寫入物件引用表,後續寫入視為新物件
useProtocolVersion(int)指定序列化協議版本

ObjectInputStream 主要方法

方法說明
readObject()讀取物件(自動處理引用共享)
readUnshared()讀取「非共享」物件,強制新建實例
defaultReadObject()在 readObject 中調用,執行預設反序列化
readFields()獲取 GetField 手動控制欄位讀取
readPrimitive 系列readInt、readLong、readUTF 等
registerValidation(ObjectInputValidation, int)註冊驗證回調,反序列化完成後執行
available()/skipBytes()流操作輔助

完整演示程式

定義可序列化類別

class Box implements Serializable {
    private static final long serialVersionUID = 1L;  // 強烈建議顯式聲明
    private int width;
    private int height;
    private String name;

    public Box(String name, int width, int height) {
        this.name = name; this.width = width; this.height = height;
    }
    @Override public String toString() {
        return "[" + name + ": (" + width + ", " + height + ") ]";
    }
}

序列化寫入

public static void testWrite() {
    try (ObjectOutputStream out = new ObjectOutputStream(
            new FileOutputStream("box.tmp"))) {
        
        // 基本類型
        out.writeBoolean(true);
        out.writeByte((byte) 65);
        out.writeChar('a');
        out.writeInt(20131015);
        out.writeFloat(3.14F);
        out.writeDouble(1.414D);

        // 集合物件
        Map<String, String> map = new HashMap<>();
        map.put("one", "red");
        map.put("two", "green");
        map.put("three", "blue");
        out.writeObject(map);

        // 自定義物件
        Box box = new Box("desk", 80, 48);
        out.writeObject(box);
        
    } catch (Exception ex) { ex.printStackTrace(); }
}

反序列化讀取

public static void testRead() {
    try (ObjectInputStream in = new ObjectInputStream(
            new FileInputStream("box.tmp"))) {
        
        System.out.printf("boolean:%b%n", in.readBoolean());
        System.out.printf("byte:%d%n", in.readByte() & 0xff);
        System.out.printf("char:%c%n", in.readChar());
        System.out.printf("int:%d%n", in.readInt());
        System.out.printf("float:%f%n", in.readFloat());
        System.out.printf("double:%f%n", in.readDouble());

        // 讀取 Map
        @SuppressWarnings("unchecked")
        Map<String, String> map = (Map<String, String>) in.readObject();
        map.forEach((k, v) -> System.out.printf("%-6s -- %s%n", k, v));

        // 讀取 Box
        Box box = (Box) in.readObject();
        System.out.println("box: " + box);
        
    } catch (Exception e) { e.printStackTrace(); }
}

執行結果

boolean:true
byte:65
char:a
int:20131015
float:3.140000
double:1.414000
three -- blue
two -- green
one -- red
box: [desk: (80, 48) ]

進階主題與最佳實踐

1. serialVersionUID 的重要性

  • 未顯式聲明時,JVM 根據類結構自動計算,類修改會導致 InvalidClassException
  • 強烈建議 顯式聲明 private static final long serialVersionUID = 1L;
  • 相容性修改(新增欄位)保持 UID 不變;不相容修改(刪除欄位、改變類型)需更新 UID

2. transient 關鍵字

private transient String password;  // 不參與序列化,反序列化後為 null
private transient Logger logger;    // 非序列化資源

3. 自定義序列化邏輯

private void writeObject(ObjectOutputStream out) throws IOException {
    out.defaultWriteObject();  // 執行預設邏輯
    out.writeObject(encrypt(password));  // 額外處理敏感欄位
}

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    in.defaultReadObject();
    this.password = decrypt((String) in.readObject());
}

4. Externalizable 完全控制

public class Data implements Externalizable {
    private int value;
    @Override public void writeExternal(ObjectOutput out) throws IOException {
        out.writeInt(value * 2);  // 自定義編碼
    }
    @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        this.value = in.readInt() / 2;
    }
}

5. 繼承與序列化

  • 父類實現 Serializable → 子類自動可序列化
  • 父類未實現 → 反序列化時父類無參建構子會被調用初始化父類欄位
  • 若父類無無參建構子 → 拋出 InvalidClassException

6. 安全性風險與防範

反序列化任意類別是重大安全漏洞入口(如 Apache Commons Collections Gadget 鏈)。

防範措施:

  • JDK 9+ 使用 ObjectInputFilter 白名單過濾
  • 避免反序列化不可信數據
  • 採用 JSON/Protobuf 等安全格式替代

6. 效能優化建議

  • 大量物件序列化:reset() 定期清理引用表防止記憶體洩漏
  • 高效能場景:改用 Externalizable 或 Protobuf/Kryo
  • 緩衝區調優:new BufferedOutputStream(new FileOutputStream(file), 64*1024)

總結

ObjectOutputStream/InputStream 是 Java 內建序列化機制的核心,掌握以下要點即可靈活應用:

  1. 實現 Serializable 並聲明 serialVersionUID
  2. 掌握 writeObject/readObject 配合 defaultWriteObject/readObject 自定義邏輯
  3. 理解 transient、Externalizable、writeUnshared/readUnshared 進階用法
  4. 務必重視反序列化安全風險,生產環境優先考慮替代方案
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved