Java之Serializable/Externalizable序列化和反序列化

Java之Serializable/Externalizable序列化和反序列化

文章鏈接:

知識點:

  1. 序列化和反序列化介紹;
  2. 爲什麼需要序列化和反序列化;
  3. Serializable接口序列化和反序列化;
  4. Externalizable接口序列化和反序列化;
  5. 兼容性問題;
  6. 序列化和反序列化得到的對象問題;
  7. 新名詞記錄{Serializable;Externalizable;序列化版本兼容問題-serialVersionUID;}

概述

對象剛入門編程的人都知道,因爲在Java中,任何事物都是對象。

我們在編程時,想要什麼對象,new一個出來使用就OK了。這是因爲Java平臺允許我們在內存中創建可複用的Java對象,但一般情況下,只有當JVM處於運行時,這些對象纔可能存在,即這些對象的生命週期不會比JVM的生命週期更長。隨着JVM被shutdown,這些new出來的對象便會隨之被釋放而消失。

但是在現實中,有可能需要保存一份用戶瀏覽過的一組數據以便用戶打開應用直接查看,或者是將這組數據在有網之後提交到後臺去。

那麼如果我要在JVM停止之後,還想得到我需要的數據對象呢?但在現實應用中,就可能要求在JVM停止運行之後能夠保存(持久化)指定的對象,並在將來重新讀取被保存的對象。Java對象序列化就能夠幫助我們實現該功能。

關於Android中activity數據持久化的文章請看

這就要使用到序列化和反序列化了。

序列化和反序列化:*Java的對象序列化是指將那些實現了Serializable接口的對象轉換成一個字節序列,並能夠在以後將這個字節序列完全恢復爲原來的對象。對象的序列化是基於字節,不能使用io流中以字符讀取操作的Reader和Writer。*

對象序列化保存的是對象的”狀態”,即它的成員變量。由此可知,對象序列化不會關注類中的靜態變量。反之亦然。支持序列化和反序列化的基本類型有:String,Array,Enum和Serializable,如果非以上的一種,那麼會拋出NotSerializableException異常。

因爲枚舉類默認繼承java.lang.Enum類,而此類又是實現了Serializable接口,所以枚舉類型對象都是可以被序列化的。

對於序列化和飯序列化要使用到的io操作類主要有:FileOutputStream,ByteArrayOutputStream,ObjectOutputStream,FileInputstream,ByteArrayInputStream,ObjectInputStream等等,而我們要調用的是writeObject()方法和readObject()方法。

爲什麼要序列化:這一過程甚至可通過網絡進行,這意味着序列化機制能自動彌補不同操作系統之間的差異。

Serializable序列化和反序列化

下面是具體的實例操作:
首先需要建立一個實體類,創建UserBean.java類,實現Serializable接口。

package com.yaojt.sdk.java.bean;

import java.io.Serializable;
public class UserBean implements Serializable {

    //串行化版本統一標識符
    private static final long serialVersionUID = 1L;

    private String userName;
    private String password;
    private int age;

    public UserBean(String userName, String password, int age) {
        this.userName = userName;
        this.password = password;
        this.age = age;
    }

    //一系列的setter和getter方法,省略了
}

然後對此類進行序列化和反序列化操作:

public void serializableTest() {
        UserBean userBean = new UserBean("yaojt", "123456", 25);
        try {
            FileOutputStream fileOutputStream = new FileOutputStream(file);
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
            objectOutputStream.writeObject(userBean);
            objectOutputStream.flush();
            objectOutputStream.close();
            fileOutputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            FileInputStream fileInputStream = new FileInputStream(file);
            ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
            UserBean userBean1 = (UserBean) objectInputStream.readObject();
            CommonLog.logInfo("Serializable,objectOutput反序列化", userBean1.toString());
            //結果:Serializable,objectOutput反序列化: :{userName:yaojt, password:123456}
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

以上的方法,是利用文件流進行的序列化和反序列化操作。

這裏始終要記得:當io流不需要使用到時,一定要進行關閉流操作,否則很可能引起內存泄漏。我這裏只是簡單的在try{}catch{}裏頭進行io流的關閉(爲了簡潔明瞭),比較正確的做法是要在finally代碼塊裏頭進行關閉操作。

下面將使用字節數組流進行序列化和反序列化的操作。

public void writeSerializableByArrayTest() {
        ByteArrayOutputStream byteArrayOutputStream = null;
        try {
            byteArrayOutputStream = new ByteArrayOutputStream();
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
            UserBean userBean = new UserBean("tanksu", "123456", 25);
            objectOutputStream.writeObject(userBean);
            objectOutputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
            ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
            UserBean userBean = (UserBean) objectInputStream.readObject();
            CommonLog.logInfo("serializable,byteArray反序列化", userBean.toString());
            //serializable,byteArray反序列化: :{userName:tanksu, password:123456}
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }

    }

通過以上幾個操作,就可以進行對象的序列化和反序列化了。我們可以把序列化的數據用來做什麼都可以了。

注意:在反序列化中,必須要顯示的調用UserBean實體類,否則會拋出ClassNotFoundException異常。

使用默認機制,在序列化對象時,不僅會序列化當前對象本身,還會對該對象引用的其它對象也進行序列化,同樣地,這些其它對象引用的另外對象也將被序列化,以此類推。


但是這裏有一點,就是我並不想序列化password字段,因爲這是一個敏感的字段。我需要保護起來,不讓他進行序列化,那麼反序列化就得不到該字段的數值了。

其實這裏是可以做的,需要一個關鍵字transient來修飾不想要序列化的字段就OK了。

    //利用transient關鍵字修飾,改字段對序列化不可見
    private transient String password;

然後我們就可以在反序列化之後看到password的字段爲null了。

//結果:Serializable,objectOutput反序列化: :{userName:yaojt, password:null}

當然,我們還可以自定義序列化來實現上面對某些字段的不序列化操作,而不是使用transient關鍵字。

在userbean的類中,重寫writeObject()和readObject()方法,然後再實現我們想要不被序列化的字段就OK了。重寫方法中,顯示的調用了out的writeObject()方法,即使字段被transient修飾,也會序列化此字段。

注意:out.defaultWriteObject();和in.defaultReadObject();必須寫在最前面,然後再去實現我們想要的操作。在下面的代碼中,transient關鍵字就不起作用了。

private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        out.writeObject(password);
    }

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

Externalizable類進行序列化

當然,出了繼承類進行序列化和反序列化,我們還可以繼承Externalizable來進行序列化和反序列化。Externalizable進行序列化和反序列化會比較麻煩,因爲需要重寫序列化和反序列化的方法,序列化的細節需要手動完成。當讀取對象時,會調用被序列化類的無參構造器去創建一個新的對象,然後再將被保存對象的字段的值分別填充到新對象中。因此,實現Externalizable接口的類必須要提供一個無參的構造器,且它的訪問權限爲public。

下面的類就是實現Externalizable類進行的序列化和反序列化操作。

首先定義另外一個實體類UserBean2.java,繼承自Externalizable類。需要重寫writeExternal()和readExternal()方法,及實現細節。

package com.yaojt.sdk.java.bean;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
public class UserBean2 implements Externalizable {

    //串行化版本統一標識符
    private static final long serialVersionUID = 2L;

    private String userName;
    private String password;
    private int age;

    public UserBean2() {
    }

    public UserBean2(String userName, String password, int age) {
        this.userName = userName;
        this.password = password;
        this.age = age;
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(userName);
        out.writeObject(password);
        out.writeInt(age);
    }

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

    //一系列的setter和getter方法,省略了

}

說明:在上面的實體類中,我們可以看到重寫的兩個方法,分別是序列化和反序列化的方法。我們要做的就是在兩個方法裏面分別對寫入每一個需要序列化的字段。ObjectOutput和ObjectInput裏面有一系列寫入和讀出基本數據類型的方法,可按需進行選用。

具體的使用如下:

public void externalizableTest() {
        try {
            FileOutputStream fileOutputStream = new FileOutputStream(file);
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
            UserBean2 userBean = new UserBean2("fishing", "123456", 25);
            userBean.writeExternal(objectOutputStream);
            objectOutputStream.close();

            ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
            UserBean2 userBean2 = new UserBean2();
            userBean2.readExternal(objectInputStream);
            CommonLog.logInfo("externalizable類,序列化和反序列化", userBean2.toString());
            //結果:externalizable類,序列化和反序列化: :{userName:fishing, password:123456}
            objectInputStream.close();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

序列化和反序列化得到的對象是不是同一個?

我們在序列化和反序列化的時候,得到的前後對象是不是相同的呢?答案是否定的。

無論是實現Serializable接口,或是Externalizable接口,當從I/O流中讀取對象時,readResolve()方法都會被調用到。實際上就是用readResolve()中返回的對象直接替換在反序列化過程中創建的對象。

如果想要返回一個單例對象,那麼改怎麼來做呢?
只要我們重寫readResolve()方法,構造函數私有化,然後返回唯一的實例。如下所示:

ublic static class InstanceHolder {
        private static final UserBean userBean = new UserBean("tanksu", "999999", 12);
    }

    private Object readResolve() throws ObjectStreamException {
        return InstanceHolder.userBean;
    }

    private UserBean(String userName, String password, int age) {
        this.userName = userName;
        this.password = password;
        this.age = age;
    }

兼容性問題

串行化版本統一標識符-serialVersionUID
java通過一個名爲UID(stream unique identifier)來控制,這個UID是隱式的,它通過類名,方法名等諸多因素經過計算而得,理論上是一一映射的關係,也就是唯一的。如果UID不一 樣的話,就無法實現反序列化了,並且將會得到InvalidClassException。在繼承上面兩個接口時,我們並沒有看到要實現個UID。默認地,系統幫我們實現了這一個操作。實際上,更推薦自己顯示的寫一個UID會更好。

向上兼容:指老的版本能夠讀取新的版本序列化的數據流。因爲在java中serialVersionUID是唯一控制着能否反序列化成功的標誌,只要這個值不一樣,就無法反序列化成功。但只要這個值相同,無論如何都將反序列化,在這個過程中,對於向上兼容性,新數據流中的多餘的內容將會被忽略;對於向下兼容性而言,舊的數據流中所包含的所有內容都 將會被恢復,新版本的類中沒有涉及到的部分將保持默認值。利用這一特性,可以說,只要我們認爲的保持serialVersionUID不變,向上兼容性是 自動實現的。

向下兼容:老的版本能夠讀取新的數據序列流。有些新的字段可能會沒有值,所以需要由一個版本號來進行區分維護,以適應讀取不同的數據流的要求。

//串行化版本統一標識符
    private static final long serialVersionUID = 1L;

//初始化version版本號
    private final long version = 2L;

    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        if (version == 1L){ //1版本
            out.writeObject(password);
        }else if (version == 2L){ //2版本
            out.writeObject(password);
        }else {
            throw new InvalidClassException(); //拋出異常了
        }
    }

需要滿足2個條件
1. serialVersionUID保持一致;
2. 事先實現版本識別標誌字段,例如final long version = 2L;

如果拋出異常,就可以提示用戶進行升級操作。


總結

序列化和反序列化是一個很有用的用戶緩存數據的方式之一。可以跨平臺進行傳輸數據,只要保持UID不變。序列化和反序列化主要有兩個序列化接口可以實現,Serializable和Externalizable,可以重寫他們的方法進行手動序列化和反序列化操作。對於敏感的字段,可以使用transient關鍵字進行修飾,那麼該字段不會被序列化。對於最後的兼容性問題,是一個比較麻煩的事,需要更好的理解序列化和反序列化的操作。

以上就是所有內容,如有任何問題,請及時與我聯繫,謝謝。

發佈了135 篇原創文章 · 獲贊 70 · 訪問量 51萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章