對象的序列化與反序列化

官方文檔理解

要使類的成員變量可以序列化和反序列化,必須實現Serializable接口。任何可序列化類的子類都是可序列化的。Serializable接口沒有提供任何方法和字段,只是標記可以序列化。

為了允許不可序列化類的子類可序列化,子類要承擔父類的public,protected和包內可訪問(default)修飾的字段。該父類必須有一個子類可訪問的無參構造器,去初始化它的屬性。

在反序列化過程中,不可序列化類的字段通過public或protected修飾的無參構造器初始化。這個構造器必須是子類可訪問的。而可序列化子類的字段從流中恢復。

這里沒有提到使用friendly修飾的構造器,應該是不確定子類和父類屬于同一包中。如果是一個包,應該也可以。因為friendly修飾的構造器也可以被同一個包下的子類訪問。

如果對一個不可序列化對象進行序列化操作時,會拋出NotSerializableException標記該類不可序列化。

如果一個類在序列化和反序列化中需要制定一些特殊操作,必須實現一下方法簽名的特殊方法:

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

writeObject()方法職責是將特定類的對象屬性輸出,這樣,相應的readObject()可以恢復。可以調用out.defaultWriteObject()使用默認保存對象屬性的機制。out.defaultWriteObject()自身不必考慮使用的變量是屬于父類還是子類。通過使用writeObject或通過使用DataOutput支持的基本數據類型的方法將各個字段寫入ObjectOutputStream來保存狀態。

readObject()主要責任是從流中讀取并恢復類的字段。它可以通過調用in.defaultReadObject()采用默認機制恢復非靜態和non-transient字段。in.defaultReadObject()使用流中的信息來將流中保存的對象的字段分配給當前對象中相應命名的字段。當類添加了新字段,它依舊可以使用。in.defaultReadObject()自身不必考慮使用的變量是屬于父類還是子類。通過使用writeObject或通過使用DataOutput支持的基本數據類型的方法將各個字段寫入ObjectOutputStream來保存狀態。

readObjectNoData():Serializable對象反序列化時,由于序列化與反序列化提供的class版本不同,序列化的class的super class不同于序列化時的class的super class;或者收到有敵意的流;或接收不完整;都會對初始化對象字段值時造成影響。針對這些情況可以在在該方法中實現這些字段的初始化。如果沒有定義readObjectNoData(),這些字段會初始化成JVM默認值。

序列化類在將對象寫入流中時,可以指定一個替代對象寫入。但是必須實現精確的方法簽名:

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

如果writeReplace()存在,序列化時會被調用。它的權限修飾符可以是任何一個,子類遵循Java權限訪問規則。

當一個類的對像從流中讀取時,指派另一個類作為返回值。必須實現精確的方法簽名:

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

該方法有著與writeReplace()類似的調用和訪問規則。

Java允許為序列化的類提供一個serialVersionUID的常量標識該類的版本。只要serialVersionUID的值不變,Java就會把它們當作相同的序列化版本。例如,一個類升級后,它的serialVersionUID類變量值保持不變,序列化機制也會把它們當成同一個類版本。

ANY-ACCESS-MODIFIER static final long serialVersionUID = 42L;

如果反序列時,發送方的類(指序列化時使用的類文件)與接受方的(指反序列化時使用的類文件)類的各自serialVersionUID不同,那么會拋出InvalidClassException異常。

JVM就會根據類的各個方面計算出一個serialVersionUID的值。不同的編譯器下會產生不同的serialVersionUID值。serialVersionUID值不同則會導致反序列化程序編譯失敗。解決辦法是顯示指定一個serialVersionUID。這樣,即使在某個對象被序列化后,它所對應的類被修改了,該對象也依然可以被正確的反序列化。

實踐

可序列化子類默認實現序列化

這應該是毋庸置疑的。因為父類實現了Serializable接口,那么子類必然可序列化。

不可序列化父類的子類可否序列化

從文檔中得知,只要父類有一個子類可訪問的無參構造器能夠初始化父類自身的字段,就可行。那么沒有訪問權限修飾符號修飾的無參構造器可行嗎?

public class FriendlyConstructorFather {
    private int number;
    FriendlyConstructorFather() {
        this.number = 12;
    }
    public int getNumber() {
        return this.number;
    }

    public void setNumber(int num) {
        this.number = num;
    }

}

public class FriendlyConstructorSon extends FriendlyConstructorFather
    implements Serializable{
    private int sonNum;
    public FriendlyConstructorSon() {

    }

    public int getSonNum() {
        return sonNum;
    }

    public void setSonNum(int number) {
        sonNum = number;
    }
}


public class TestFriendlyConstructor {
    public static void main(String[] agrs) {
        FriendlyConstructorSon son = new FriendlyConstructorSon();
        son.setSonNum(2);

        FileOutputStream fileOut = null;
        ObjectOutputStream objectOut = null;

        File file = new File("../file/TestFriendlyConstructor.txt");

        try {
            try {
                fileOut = new FileOutputStream(file);
                objectOut = new ObjectOutputStream(fileOut);

                objectOut.writeObject(son);
            }finally {
                objectOut.close();
            }
        }catch(IOException e) {
            e.printStackTrace();
        }

        FileInputStream fileIn = null;
        ObjectInputStream objectIn = null;

        try {
            try {
                fileIn = new FileInputStream(file);
                objectIn = new ObjectInputStream(fileIn);

                FriendlyConstructorSon resultSon =
                    (FriendlyConstructorSon) objectIn.readObject();

                System.out.println(
                    "the father's number is " + resultSon.getNumber());

                System.out.println(
                    "the son's num is " + resultSon.getSonNum());
            }finally{
                objectIn.close();
            }
        }catch(IOException e) {
            e.printStackTrace();
        }catch(ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

執行后效果:

the father's number is 12
the son's num is 2

發現是可行的,但是要求子類必須和父類在同一個包中。

readObjectNoDate方法使用情況

原始Person.java

public class Person implements Serializable{
    private int age;
    public Person() {

    }

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

    public int getAge() {
        return age;
    }
}

并且通過序列化ObjectOutputStream輸出到test.txt文件中進行保存。然后升級Person類:

public class Animal implements Serializable {
    private String name;

    public Animal() {

    }

    public String getName() {
        return name;
    }

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

    private void readObjectNoData() throws ObjectStreamException{
        this.name = "zhangsan";
    }
}

public class Person extends Animal implements Serializable {
    private int age;

    public Person() {}

    public int getAge() {
        return age;
    }

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

編譯新的Person類后,使用新的Person.class文件從test.txt反序列化加載Person對象。并且使用getName()獲取字段name值,執行輸出如下:

the age is 25
the name is zhangsan

可以看到從信息不完整的序列化流中得到了完整的Person類,這要歸功于readObjectNoData()。它初始化了name字段。如果在這種Person發生升級的情況下,沒有定義readObjectNoData()那么name字段會初始化它們的默認值。readObjectNoData()一般用于序列化對象和反序列化對象父類不同的情況,還有就是為了防止信息不完整,可以使用它來進一步保證初始化。如果了類中有自定義的readObject(),出現上述情況時,會用readObjectNoData()替代它。

這里可以注意下,雖然升級了Person,且沒有顯示指定SerializableUID。序列化機制依舊認為升級前后的Person是同一個版本。這是因為:

  • 只是修改了類的方法,不會影響反序列化。
  • 只是修改了類的static Field或transient Field,不會影響反序列化。
  • 修改了類的非static和非transient Field,會影響序列化。

如果此時升級Person時,繼承的Animal中有與原始Person相同的字段。那么readObjectNoDate()的初始化無效果,會使用它們的默認值初始化。而且不顯示指定SerializableUID會拋出InvalidClassException異常。唯一的解決辦法就是顯示指定SerializableUID,即可執行。

自定義序列化

case one

在一些特殊情況下,類中某些實例變量是敏感信息不希望被序列化,或者這些實例變量的類型不可序列化為避免發生NotSerializableException異常。可以通過關鍵詞transient修飾這些實例變量,指定類在序列化時無需理會它們。這樣一來,反序列化后得到的對象中這些字段會被初始化為默認值。

序列化注意事項:

  • 對象的類名、Field(包括基本類型、數組及對其他對象的引用)都會被序列化,對象的static Field,transient Field及方法不會被序列化;
  • 實現Serializable接口的類,如不想某個Field被序列化,可以使用transient關鍵字進行修飾;
  • 保證序列化對象的引用類型Filed的類也是可序列化的,如不可序列化,可以使用transient關鍵字進行修飾,否則會序列化失敗;
  • 反序列化時必須要有序列化對象的類的class文件,而且方法不會被序列化;
  • 當通過文件網絡讀取序列化對象的時候,必需按寫入的順序來讀取。

使用transient關鍵字修飾實例變量避免序列化非常便捷,但該變量將被完全隔離在序列化機制之外,這樣導致在反序列化恢復的對象無法取得該實例變量值。Java還提供了一種自定義序列化機制,可以讓程序控制如何序列化各實例變量,甚至完全不序列化某些實例變量(與使用transient關鍵字的效果相同)。

public class CustomSerializable implements Serializable {
    private String account;
    private transient String password;
    private int passwordCount;

    public CustomSerializable(String name, String password)
        throws Exception{
        passwordCount = password.length();
        for(int i = 0; i < passwordCount; i++) {
            char c = password.charAt(i);
            if(c < '0' && '9' < c) {
                throw new Exception("the password is not correct!");
            }
        }

        this.account = name;
        this.password = password;

    }

    private String changePassword() {
        byte[] bArray = new byte[passwordCount];
        for(int i = 0; i < passwordCount; i++) {
            bArray[i] = '*';
        }
        return new String(bArray);
    }

    private void writeObject(ObjectOutputStream out) throws IOException {
        System.out.println("custom writeObject method execute!");
        out.defaultWriteObject();
        out.writeObject(changePassword());
    }

    private void readObject(ObjectInputStream in)
        throws IOException, ClassNotFoundException {
        System.out.println("custom readObject method execute!");
        in.defaultReadObject();
        this.password = (String) in.readObject();
    }

    @Override
    public String toString() {
        return "Account: " + account + "\nPassword: " + password +
            "\nPasswrod Count: " + passwordCount;
    }
}

在代碼中,使用了defaultWrite/ReadObject()去執行默認的序列化機制。

在序列化中(自定義的writeObject()),由于password是一個敏感信息,所以使用transient修飾,將其排除在默認序列化機制外。然后針對敏感信息自定義一套序列化操作,保護信息安全。

在反序列化中(自定義的readObject()),首先使用默認反序列化機制去初始化可序列化字段,然后針對自定義序列化中的password字段,使用相應的readObject()讀取,并且賦值給反序列化對象中的同名字段。

執行效果,無自定義序列化和反序列化,transient修飾的password字段讀寫:

transient關鍵字.png

自定義序列化和反序列化后,transient修飾的password字段讀寫:

Serializable自定義序列化.png

在代碼中還看到writeObject(),readObject(),readObjectNoDate()都是private修飾的,但是序列化過程中一樣可以被外部的ObjectOut/InputStream調用。

ObjectOutputStream在執行自己的writeObject方法前會先通過反射在要被序列化的對象的類中查找有無自定義的writeObject方法,如有的話,則會優先調用自定義的writeObject方法。因為查找反射方法時使用的是getPrivateMethod,所以自定以的writeObject方法的作用域要被設置為private。通過自定義writeObject和readObject方法可以完全控制對象的序列化與反序列化。(詳情可見ObjectOutputStream中的writeSerialData方法,以及ObjectInputStream中的readSerialData方法。)

引用自Java對象序列化與反序列化

case two

Java序列化機制提供一種更徹底的序列化方式,writeReplace()readResolve()。前者是在序列化過程中替換成其他對象,后者是在反序列化中替換掉readObject()返回的實例。

注意兩者一般不同時使用,因為同時存在時,只會去執行前者,而且不會調用自定義的writeObject()readobject()。而后者執行之前回去執行writeObject()readobject(),然后替換掉readObject()返回的實例,并且拋起它。

在CustomSerializable.java中添加下列代碼:

    private Object writeReplace() throws ObjectStreamException {
        System.out.println("custom writeReplace method execute!");
        return "No Permission to access the account'password!";
    }

執行序列化讀取:

writeReplace method.png

在CustomSerializable.java中添加下列代碼:

    private Object readResolve() throws ObjectStreamException {
        System.out.println("custom readResolve method execute!");
        return "The account's password is a private!";
    }

執行序列化讀取:

readResolve Method.png

注意,writeReplace()readResolve()可以被任何權限修飾符修飾,且子類訪問這兩個方法遵循Java的權限訪問規則。這樣做的目的子類可以使用父類中已有的序列化操作,也可以覆寫它們。

一般readResolve()用于單例模式

一般類實現單例模式的用途是保證該類的實例只有一個。但是對象序列化后,通過反序列化得到的對象是重構的,也就是在堆內存中重新創建的。所以無法保障唯一實例。

public class SingletonSer implements Serializable {
    private static SingletonSer instance;
    private String name;
    private int age;
    private SingletonSer(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public static SingletonSer getInstance() {
        if(instance == null) {
            instance = new SingletonSer("FoolishDev", 25);
        }
        return instance;
    }

    private Object readResolve() throws ObjectStreamException {
        return getInstance();
    }
}

執行序列化讀取,在代碼中對序列化前后實例引用進行比較(==),得到

they are same? true

總結

Serializable反序列化時,不會去調用類的構造器,除非類的父類不可序列化,會去調用子類可訪問的無參構造器來初始化父類字段。如果父類和子類都可序列化,且各自自定義了序列化和反序列化方法,在整個序列化過程中父類和子類自定義的操作互不影響,各自執行。

Externalizable

官方文檔理解

Externalizable實例類可以實現序列化,而且承擔了自身內容存儲和恢復的責任。Externalizable子類需要實現兩個方法,writeExternal()readExternal(),通過這兩個方法可以完全控制它的對象和它父類的流的內容和格式。這兩個方法必須明確的和父類協調,處理它的狀態,同時替代了自定義的writeObject()readObject()

類的序列化可以使用Serializable和Externalizable接口。對象的持久化可以使用這兩個接口。任何一個對象要存儲,首先回去判斷有無實現Externalizable接口,如果有那么執行writeExternal(),如果沒有但是實現了Serializable接口,那么使用ObjectOutputStream。

一旦類實現了Externalizable就會替代了Serializable機制。

當一個Externalizable對象重建時,使用public no-arg 構造器進行創建實例,然后調用readExternal()。Serializable對象創建是從流中讀取信息。Externalizable類一樣可以使用writeReplace()readResolve()替換對象。

實踐

Externalizable父類構造器必須是公共的?

    public class Supertype implements Externalizable{
    private String className;
    private Date date;

    Supertype() {
        System.out.println("super no arg constructor executed!");
        className = "Super";
        date = new Date();
    }

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

    @Override
    public String toString() {
        return "Class name is " + className + ". The date is " + date.toString();
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(className);
        out.writeObject(date);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        className = (String) in.readObject();
        date = (Date) in.readObject();
    }
}


    public class Subtype extends Supertype{
    private String nickName;
    public Subtype() {
        System.out.println("sub no arg constructor executed!");
        nickName = "Sub";
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        super.writeExternal(out);
        out.writeObject(nickName);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        super.readExternal(in);
        nickName = (String) in.readObject();
    }

    public String getNickName() {
        return nickName;
    }

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

    @Override
    public String toString() {
        return super.toString() + ". The nick name is " + nickName;
    }
}

執行序列化操作后,輸出:

super no arg constructor executed!
sub no arg constructor executed!
super no arg constructor executed!
sub no arg constructor executed!
Class name is Supertype. The date is Sun Feb 26 16:10:45 CST 2017. The nick name is Subtype

發現父類的構造器可以被子類訪問即可,并沒有強制規定為public權限。如果不可被子類訪問,創建子類對象時會拋出異常java.lang.IllegalAccessError。如果子類的構造器不是public修飾,反序列時會拋出異常java.io.InvalidClassException。

而且還需要注意,即使在反序列化時調用了Externalizable類的無參構造器去初始化了字段,最后依舊會使用序列化流中的信息賦值給同名字段。

最后代碼中子類并沒有覆寫父類中的writeExternal()readExternal(),而是繼承了。這樣保證了父類字段信息不丟失。如果子類覆寫了這兩個方法,那么父類字段會使用構造器去初始化(實際上就是調用了子類的公共無參構造器,然后執行內部引用的父類構造器去初始化)。

這樣聯想到Serializable文檔中提到writeObject()readObject()無需考慮字段屬于父類還是子類。因為它們權限是private,所以是各自執行字段的初始化,互不影響。

不可序列化父類的子類實現可序列化

如何去初始化父類中的私有字段,以及子類可訪問的字段?

    public class NoExternalSuper {
    private String className;
    private Date date;

    protected NoExternalSuper() {
        System.out.println("no-externalizable super no arg constructor executed!");
        className = "Super";
        date = new Date();
    }

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

    @Override
    public String toString() {
        return "Class name is " + className + ",date is " + date.toString();
    }
}

    public class NoExternalSub
    extends NoExternalSuper implements Externalizable{
    private String nickName;
    public NoExternalSub() {
        System.out.println("no-externalizable sub no arg constructor executed!");
        nickName = "Sub";
    }

    @Override
    public void writeExternal(ObjectOutput out)
        throws IOException {
        out.writeObject(nickName);
    }

    @Override
    public void readExternal(ObjectInput in)
        throws IOException, ClassNotFoundException {
        nickName = (String) in.readObject();
    }

    @Override
    public String toString() {
        return super.toString() + ".The nick name is " + nickName;
    }
}

執行序列化操作后輸出結果:

no-externalizable super no arg constructor executed!
no-externalizable sub no arg constructor executed!
no-externalizable super no arg constructor executed!
no-externalizable sub no arg constructor executed!
Class name is Super,date is Sun Feb 26 16:36:34 CST 2017.The nick name is Sub

可以看出,父類使用自身構造器初始化自身字段(就是子類公共無參構造器引用的父類構造器)。而子類可訪問的字段當然可以在子類中做出相應序列化和反序列化操作。

總結

一個實現Externalizable接口的序列化類,必須有一個public-no-arg constructor,且會完全替代Serializable機制,讓開發者完全控制和自定義序列化和反序列化。所以需要注意協調處理父類的屬性狀態。它雖然使用起來復雜,但是性能比Serializable好。它實現持久化的方法是通過ObjectOutput和ObjectInput。

參考

Java對象序列化與反序列化

Java序列化之readObjectNoData、readResolve方法

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,119評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,382評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,038評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,853評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,616評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,112評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,192評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,355評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,869評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,727評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,928評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,467評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,165評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,570評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,813評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,585評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,892評論 2 372

推薦閱讀更多精彩內容