面試官:三年工作經驗,你連序列化都說不明白?

什麼是序列化、反序列化

  • 序列化:把Java對象轉換為位元組序列的過程。
  • 反序列化:把位元組序列恢復為Java對象的過程。

序列化的作用

  • 1、可以把對象的位元組序列永久地保存到硬盤上,通常存放在一個文件中;(持久化對象)
  • 2、也可以在網絡上傳輸對象的位元組序列;(網絡傳輸對象)

序列化在Java中的用法

在Java中序列化的實現:將需要被序列化的類實現Serializable接口,該接口沒有需要實現的方法,實現該接口只是為了標註該對象是可被序列化的,然後使用一個輸出流(如:FileOutputStream)來構造一個ObjectOutputStream(對象輸出流)對象,接着,使用ObjectOutputStream對象的writeObject(Object obj)方法就可以將參數為obj的對象寫出(即保存其狀態),要恢復的話則用ObjectInputStream(對象輸入流)。

如下為序列化、反序列化簡單案例Test01

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class Test01 {
    public static void main(String[] args) {
        //序列化操作
        serializable();
        //反序列化操作
        deserialization();
    }

    private static void serializable() {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"))) {
            Person person = new Person();
            person.setName("張三");
            person.setAge(20);
            oos.writeObject(person);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void deserialization() {
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"))) {
            Person person = (Person) ois.readObject();
            System.out.println(person);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

//目標類實現Serializable接口
class Person implements Serializable {
    private static final long serialVersionUID = -2052381772192998351L;
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

上面案例中只是簡單的進行了對象序列化和反序列化,但是序列化和反序列化過程中有很多值得思考的細節問題,例如:

1、序列化版本號(serialVersionUID)問題
2、靜態變量序列化
3、父類的序列化與 transient 關鍵字
4、自定義序列化規則
5、序列化存儲規則

1、序列化版本號(serialVersionUID)問題

在寫Java程序中有時我們經常會看到類中會有一個序列化版本號:serialVersionUID。這個值有的類是1L或者是自動生成的。

  private static final long serialVersionUID = 1L;

或者

  private static final long serialVersionUID = -2052381772192998351L;

當在反序列化時JVM需要判斷需要轉化的兩個類是不是同一個類,於是就需要一個序列化版本號。如果在反序列化的時候兩個類的serialVersionUID不一樣則JVM會拋出java.io.InvalidClassException的異常;如果serialVersionUID一致則表明可以轉換。

如果可序列化類未顯式聲明 serialVersionUID,則序列化運行時將基於該類的各個方面計算該類的默認 serialVersionUID 值。不過,強烈建議 所有可序列化類都顯式聲明 serialVersionUID 值,原因是計算默認的 serialVersionUID 對類的詳細信息具有較高的敏感性,根據編譯器實現的不同可能千差萬別,這樣在反序列化過程中可能會導致意外的 InvalidClassException,所以這種方式不支持反序列化重構。所謂重構就是可以對類增加或者減少屬性字段,也就是說即使兩個類並不完全一致,他們也是可以轉換的,只不過如果找不到對應的字段,它的值會被設為默認值。

因此,為保證 serialVersionUID 值跨不同 java 編譯器實現的一致性或代碼重構時,序列化類必須聲明一個明確的 serialVersionUID 值。還強烈建議使用 private 修飾符顯示聲明 serialVersionUID(如果可能),原因是這種聲明僅應用於直接聲明類 — serialVersionUID 字段作為繼承成員沒有用處。數組類不能聲明一個明確的 serialVersionUID,因此它們總是具有默認的計算值,但是數組類沒有匹配 serialVersionUID 值的要求。

還有一個常見的值是1L(或者其他固定值),如果所有類都這麼寫那還怎麼區分它們,這個字段還有什麼意義嗎?有的!首先如果兩個類有了相同的反序列化版本號,比如1L,那麼表明這兩個類是支持在反序列化時重構的。但是會有一個明顯的問題:如果兩個類是完全不同的,但是他們的序列化版本號都是1L,那麼對於JVM來說他們也是可以進行反序列化重構的!這這顯然是不對的,但是回過頭來說這種明顯的,愚蠢的錯誤在實際開發中是不太可能會犯的,如果不是那麼嚴謹的話用1L是個不錯的選擇。

一般的情況下這個值是顯式地指定為一個64位的哈希字段,比如你寫了一個類實現了java.io.Serializable接口,在idea里會提示你加上這個序列化id。這樣做可以區分不同的類,也支持反序列化重構。

總結如下:

serialVersionUID 區分不同類 支持相同類的重構
不指定 YES NO
1L NO YES
64位哈希值 YES YES

簡單而言,從嚴謹性的角度來說,指定64位哈希值>默認值1L>不指定serialVersionUID值,具體怎麼使用就看你的需求啦。

2、靜態變量序列化

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class Test02 {
    public static void main(String[] args) {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
             ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"))) {
            //初始時avgAge為77
            Person person = new Person();
            person.setName("張三");
            person.setAge(20);
            oos.writeObject(person);

            //序列化後修改avgAge為80
            Person.avgAge = 80;

            Person person1 = (Person) ois.readObject();
            //再讀取,通過person1.avgAge輸出新的值,通過實例對象訪問靜態變量本來就很反常
            System.out.println(person1.avgAge);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

//目標對象實現Serializable接口
class Person implements Serializable {
    private static final long serialVersionUID = -2052381772192998351L;
    private String name;
    private int age;
    public static int avgAge = 77;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

執行結果顯示如下:

我們看到Test02.java將對象序列化後,修改靜態變量的數值再將序列化對象讀取出來,然後通過讀取出來的對象獲得靜態變量的數值並打印出來,最後的輸出是 10,之所以打印 10 的原因在於序列化時,並不保存靜態變量,這其實比較容易理解,序列化保存的是對象的狀態,靜態變量屬於類的狀態,因此 序列化並不保存靜態變量 。

3、父類的序列化與transient關鍵字

情境 :一個子類實現了 Serializable 接口,它的父類都沒有實現 Serializable 接口,序列化該子類對象,然後反序列化後輸出父類定義的某變量的數值,該變量數值與序列化時的數值不同。

解決 : 要想將父類對象也序列化,就需要讓父類也實現 Serializable 接口 。如果父類不實現的話的,就需要有默認的無參的構造函數 。在父類沒有實現 Serializable 接口時,虛擬機是不會序列化父對象的,而一個 Java 對象的構造必須先有父對象,才有子對象,反序列化也不例外。所以反序列化時,為了構造父對象,只能調用父類的無參構造函數作為默認的父對象。因此當我們取父對象的變量值時,它的值是調用父類無參構造函數後的值。如果你考慮到這種序列化的情況,在父類無參構造函數中對變量進行初始化,否則的話,父類變量值都是默認聲明的值,如 int 型的默認是 0,string 型的默認是 null。

transient 關鍵字的作用是控制變量的序列化,在變量聲明前加上該關鍵字,可以阻止該變量被序列化到文件中,在被反序列化後,transient 變量的值被設為初始值,如 int 型的是 0,對象型的是 null。

3-1、特性使用案例:

我們熟悉使用 transient 關鍵字可以使得字段不被序列化,那麼還有別的方法嗎?根據父類對象序列化的規則,我們可以將不需要被序列化的字段抽取出來放到父類中,子類實現 Serializable 接口,父類不實現,根據父類序列化規則,父類的字段數據將不被序列化,形成類圖如下圖所示。

上圖中可以看出,attr1、attr2、attr3、attr5 都不會被序列化,放在父類中的好處在於當有另外一個 Child 類時,attr1、attr2、attr3 依然不會被序列化,不用重複書寫 transient 關鍵字,代碼簡潔。

4、自定義序列化規則

在序列化和反序列化過程中需要特殊處理的類必須使用下列準確簽名來實現特殊方法:

 private void writeObject(java.io.ObjectOutputStream oos) throws IOException;
 private void readObject(java.io.ObjectInputStream oin) throws IOException, ClassNotFoundException;
 private void readObjectNoData() throws ObjectStreamException;

writeObject 方法負責寫入特定類的對象的狀態,以便相應的 readObject 方法可以恢復它。通過調用 oos.defaultWriteObject 可以調用保存 Object 的字段的默認機制。該方法本身不需要涉及屬於其超類或子類的狀態。通過使用 writeObject 方法或使用 DataOutput 支持的用於基本數據類型的方法將各個字段寫入 ObjectOutputStream,狀態可以被保存。

readObject 方法負責從流中讀取並恢復類字段。它可以調用 oin.defaultReadObject 來調用默認機制,以恢復對象的非靜態和非瞬態(非 transient 修飾)字段。defaultReadObject方法使用流來分配保存在流中的對象的字段當前對象中相應命名的字段。這用於處理類演化後需要添加新字段的情形。該方法本身不需要涉及屬於其超類或子類的狀態。通過使用 writeObject 方法或使用 DataOutput 支持的用於基本數據類型的方法將各個字段寫入 ObjectOutputStream,狀態可以被保存。

在序列化流不列出給定類作為將被反序列化對象的超類的情況下,readObjectNoData 方法負責初始化特定類的對象狀態。這在接收方使用的反序列化實例類的版本不同於發送方,並且接收者版本擴展的類不是發送者版本擴展的類時發生。在序列化流已經被篡改時也將發生;因此,不管源流是「敵意的」還是不完整的,readObjectNoData 方法都可以用來正確地初始化反序列化的對象。

readObjectNoData()應用示例:

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

//先對舊的類對象進行序列化
public class Test03Old {
    public static void main(String[] args) {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"))) {
            Person person = new Person();
            person.setAge(20);
            oos.writeObject(person);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class Person implements Serializable {
    private int age;

    public void setAge(int age) {
        this.age = age;
    }

    public int getAge() {
        return this.age;
    }
}

import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.io.Serializable;

//用新的類規範來反序列化
public class Test03New {
    public static void main(String[] args) {
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"))) {
            Person person = (Person) ois.readObject();
            System.out.println(person.getName());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

//新的類繼承了Animal,這是已經序列化的舊對象裏面所沒有的內容,
//所以實現readObjectNoData,可以彌補這種因臨時擴展而無法兼容反序列化的缺陷
class Person extends Animal implements Serializable {
    private int age;

    public void setAge(int age) {
        this.age = age;
    }

    public int getAge() {
        return this.age;
    }
}

class Animal implements Serializable {
    private String name;

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }

    private void readObjectNoData() {
        this.name = "張三";
    }
}

將對象寫入流時需要指定要使用的替代對象的可序列化類,應使用準確的簽名來實現此特殊方法:

ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;

此 writeReplace 方法將由序列化調用,前提是如果此方法存在,而且它可以通過被序列化對象的類中定義的一個方法訪問。因此,該方法可以擁有私有 (private)、受保護的 (protected) 和包私有 (package-private) 訪問。子類對此方法的訪問遵循 java 訪問規則。

在從流中讀取類的一個實例時需要指定替代的類應使用的準確簽名來實現此特殊方法。

ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;

此 readResolve 方法遵循與 writeReplace 相同的調用規則和訪問規則。

TIP: readResolve常用來反序列單例類,保證單例類的唯一性

例如:

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class Test04Old {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"))) {
            oos.writeObject(Brand.NIKE);
        }
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"))) {
            Brand b = (Brand) ois.readObject();
            // 答案顯然是false
            System.out.println(b == Brand.NIKE);
        }
    }
}

class Brand implements Serializable {
    private int val;

    private Brand(int val) {
        this.val = val;
    }

    // 兩個枚舉值
    public static final Brand NIKE = new Brand(0);
    public static final Brand ADDIDAS = new Brand(1);
}

答案很顯然是false,因為Brand.NIKE是程序中創建的對象,而b是從磁盤中讀取並恢復過來的對象,兩者明顯來源不同,因此必然內存空間是不同的,引用(地址)顯然也是不同的;

但這不是我們想看到的,因為我們把Brand設計成枚舉類型,不管是程序中創建的還是從哪裡讀取的,其必須應該和枚舉常量完全相等,這才是枚舉的意義啊!


而此時readResolve就派上用場了,我們可以這樣實現readResolve:

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamException;
import java.io.Serializable;

public class Test04New {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"))) {
            oos.writeObject(Brand.NIKE);
        }
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"))) {
            Brand b = (Brand) ois.readObject();
            // 答案顯然是true
            System.out.println(b == Brand.NIKE);
        }
    }
}

class Brand implements Serializable {
    private int val;

    private Brand(int val) {
        this.val = val;
    }

    // 兩個枚舉值
    public static final Brand NIKE = new Brand(0);
    public static final Brand ADDIDAS = new Brand(1);

    private Object readResolve() throws ObjectStreamException {
        if (val == 0) {
            return NIKE;
        }
        if (val == 1) {
            return ADDIDAS;
        }
        return null;
    }
}

改造以後,不管來源如何,最終得到的都將是程序中Brand的枚舉值了!因為readResolve的代碼在執行時已經進入了程序內存環境,因此其返回的NIKE和ADDIDAS都將是Brand的靜態成員對象;

因此保護性恢復的含義就在此:首先恢復的時候沒有改變其值(val的值沒有改變)同時恢復的時候又能正常實現枚舉值的對比(地址也完全相同);

4-1、對敏感字段加密

情境:服務器端給客戶端發送序列化對象數據,對象中有一些數據是敏感的,比如密碼字符串等,希望對該密碼字段在序列化時,進行加密,而客戶端如果擁有解密的密鑰,只有在客戶端進行反序列化時,才可以對密碼進行讀取,這樣可以一定程度保證序列化對象的數據安全。

解決:在序列化過程中,虛擬機會試圖調用對象類里的 writeObject 和 readObject 方法,進行用戶自定義的序列化和反序列化,該方法必須要被聲明為private,如果沒有這樣的方法,則默認調用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。用戶自定義的 writeObject 和 readObject 方法可以允許用戶控制序列化的過程,比如可以在序列化的過程中動態改變序列化的數值。基於這個原理,可以在實際應用中得到使用,用于敏感字段的加密工作,如下代碼展示了這個過程。

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class Test05 {
    public static void main(String[] args) {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
             ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"))) {
            oos.writeObject(new Account());

            Account account = (Account) ois.readObject();
            System.out.println("解密後的字符串:" + account.getPassword());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

class Account implements Serializable {
    private String password = "123456";

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    private void writeObject(ObjectOutputStream out) {
        try {
            ObjectOutputStream.PutField putFields = out.putFields();
            System.out.println("原密碼:" + password);
            //模擬加密
            password = "encryption";
            putFields.put("password", password);
            System.out.println("加密後的密碼" + password);
            out.writeFields();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void readObject(ObjectInputStream in) {
        try {
            ObjectInputStream.GetField readFields = in.readFields();
            Object object = readFields.get("password", "");
            System.out.println("要解密的字符串:" + object.toString());
            //模擬解密,需要獲得本地的密鑰
            password = "123456";
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

上述代碼中的 writeObject 方法中,對密碼進行了加密,在 readObject 中則對 password 進行解密,只有擁有密鑰的客戶端,才可以正確的解析出密碼,確保了數據的安全。執行上述代碼後控制台輸出如下圖所示。

4-2、序列化SDK中不可序列化的類型

4-1、對敏感字段加密案例使用 writeObject 和 readObject 進行了對象屬性值加解密操作,有時我們想將對象中的某一字段序列化,但它在SDK中的定義卻是不可序列化的類型,這樣的話我們也必須把他標註為 transient 才能保證正常序列化,可是不能序列化又怎麼恢復呢?這就用到了上面提到的 writeObject 和 readObject 方法,進行自定義序列化操作了。

示例:java.awt.geom包中的Point2D.Double類就是不可序列化的,因為該類沒有實現Serializable接口

import java.awt.geom.Point2D;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class Test06 {
    public static void main(String[] args) {
        LabeledPoint label = new LabeledPoint("Book", 5.00, 5.00);
        try {
            // 寫入前
            System.out.println(label);
            ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("object.txt"));
            //通過對象輸出流,將label寫入流中
            out.writeObject(label);
            out.close();
            // 寫入後
            System.out.println(label);
            ObjectInputStream in = new ObjectInputStream(new FileInputStream("object.txt"));
            LabeledPoint label1 = (LabeledPoint) in.readObject();
            in.close();
            // 讀出並加1.0後
            System.out.println(label1);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class LabeledPoint implements Serializable {
    private String label;
    //因為不可被序列化,所以需要加transient關鍵字
    transient private Point2D.Double point;

    public LabeledPoint(String str, double x, double y) {
        label = str;
        //此類Point2D.Double不可被序列化
        point = new Point2D.Double(x, y);
    }

    //因為Point2D.Double不可被序列化,所以需要實現下面兩個方法
    private void writeObject(ObjectOutputStream oos) throws IOException {
        oos.defaultWriteObject();
        oos.writeDouble(point.getX());
        oos.writeDouble(point.getY());
    }

    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        ois.defaultReadObject();
        double x = ois.readDouble() + 1.0;
        double y = ois.readDouble() + 1.0;
        point = new Point2D.Double(x, y);
    }

    @Override
    public String toString() {
        return "LabeledPoint{" +
                "label='" + label + '\'' +
                ", point=" + point +
                '}';
    }
}

執行結果如圖所示:

4-1、序列化SDK中不可序列化的類型案例中,你會發現調用了defaultWriteObject()和defaultReadObject()。它們做的是默認的序列化進程,就像寫/讀所有的non-transient和 non-static字段(但他們不會去做serialVersionUID的檢查)。通常說來,所有我們想要自己處理的字段都應該聲明為transient。這樣的話 defaultWriteObject/defaultReadObject 便可以專註於其餘字段,而我們則可為這些特定的字段(指transient)定製序列化。使用那兩個默認的方法並不是強制的,而是給予了處理複雜應用時更多的靈活性。

5、序列化存儲規則

5-1、存儲兩次相同對象

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class Test07 {
    public static void main(String[] args) {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
             ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"))) {
            //試圖將對象兩次寫入文件
            Account account = new Account();
            account.setPassword("123456");
            oos.writeObject(account);
            oos.flush();
            System.out.println(new File("object.txt").length());
            oos.writeObject(account);
            System.out.println(new File("object.txt").length());

            //從文件依次讀出兩個對象
            Account account1 = (Account) ois.readObject();
            Account account2 = (Account) ois.readObject();

            //判斷兩個引用是否指向同一個對象
            System.out.println(account1 == account2);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

class Account implements Serializable {
    private String password;

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

上述代碼中對同一對象兩次寫入文件,打印出寫入一次對象後的存儲大小和寫入兩次後的存儲大小,然後從文件中反序列化出兩個對象,比較這兩個對象是否為同一對象。一般的思維是,兩次寫入對象,文件大小會變為兩倍的大小,反序列化時,由於從文件讀取,生成了兩個對象,判斷相等時應該是輸入 false 才對,但是最後結果輸出如圖下圖所示。

我們看到,第二次寫入對象時文件只增加了 5 位元組,並且兩個對象是相等的,因為Java 序列化機製為了節省磁盤空間,具有特定的存儲規則,當寫入文件的為同一對象時,並不會再將對象的內容進行存儲,而只是再次存儲一份引用,上面增加的 5 位元組的存儲空間就是新增引用和一些控制信息的空間。反序列化時,恢復引用關係,使得上述代碼中的 account1 和 account2 指向唯一的對象,二者相等,輸出 true。該存儲規則極大的節省了存儲空間

5-2、存儲兩次相同對象,更改屬性值

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class Test08 {
    public static void main(String[] args) {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
             ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"))) {
            Account account = new Account();
            account.setPassword("123456");
            oos.writeObject(account);
            oos.flush();
            account.setPassword("456789");
            oos.writeObject(account);

            //從文件依次讀出兩個對象
            Account account1 = (Account) ois.readObject();
            Account account2 = (Account) ois.readObject();

            System.out.println(account1.getPassword());
            System.out.println(account2.getPassword());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

class Account implements Serializable {
    private String password;

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

執行結果如下圖:

上述代碼的目的是希望將 account 對象兩次保存到 object.txt 文件中,寫入一次以後修改對象屬性值再次保存第二次,然後從 object.txt 中再依次讀出兩個對象,輸出這兩個對象的 password 屬性值。上述代碼的目的原本是希望一次性傳輸對象修改前後的狀態。

結果兩個輸出的都是 123456, 原因就是第一次寫入對象以後,第二次再試圖寫的時候,虛擬機根據引用關係知道已經有一個相同對象已經寫入文件,因此只保存第二次寫的引用,所以讀取時,都是第一次保存的對象。這也驗證了5-1、存儲兩次相同對象案例的現象,相同對象存在只會存儲引用,不再進行對象存儲,所以第二次修改的屬性未變化。讀者在使用一個文件多次 writeObject 需要特別注意這個問題。

 

Tags: