JDK1.8 java.io.Serializable接口詳解

java.io.Serializable接口是一個標誌性接口,在接口內部沒有定義任何屬性與方法。只是用於標識此接口的實現類能夠被序列化與反序列化。可是它的奧祕並不是像它表現的這樣簡單。如今從如下幾個問題入手來考慮。java

  1. 但願對象的某些屬性不參與序列化應該怎麼處理?
  2. 對象序列化以後,若是類的屬性發生了增減那麼反序列化時會有什麼影響呢?
  3. 若是父類沒有實現java.io.Serializable接口,子類實現了此接口,那麼父類中的屬性能被序列化嗎?
  4. serialVersionUID屬性是作什麼用的?必須申明此屬性嗎?若是不申明此屬性會有什麼影響?若是此屬性的值發生了變化會有什麼影響?
  5. 能干預對象的序列化與反序列化過程嗎?

在解決這些問題以前,先來看一看如何進行對象的序列化與反序列化。定義一個Animal類,並實現java.io.Serializable接口。以下代碼所示把Animal實例序列化爲文件保存在硬盤中。程序員

class Animal implements Serializable{
    /**
     * 
     */
    private static final long serialVersionUID = 8822818790694831649L;
    private String name;
    private String color;
    private String[] alias;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getColor() {
        return color;
    }
    public void setColor(String color) {
        this.color = color;
    }
    public String[] getAlias() {
        return alias;
    }
    public void setAlias(String[] alias) {
        this.alias = alias;
    }
}

對Animal對象進行序列化與反序列化的代碼以下所示:算法

// 反序列化
static void unserializable() throws FileNotFoundException, IOException, ClassNotFoundException{
    ObjectInputStream ois = null;
    try {
        ois = new ObjectInputStream(new FileInputStream("D://animal.dat"));
        Animal animal = (Animal) ois.readObject();
        System.out.println(animal);
    } finally {
        if( null != ois ){
            ois.close();
        }
    }
    
}
// 序列化
static void serializable() throws FileNotFoundException, IOException{
    ObjectOutputStream oos = null;
    try {
        oos = new ObjectOutputStream(new FileOutputStream("D://animal.dat"));
        Animal animal = new Animal();
        animal.setName("Dog");
        animal.setColor("Black");
        animal.setAlias(new String[]{"xiaoHei", "Gou", "GuaiGuai"});
        oos.writeObject(animal);
        oos.flush();
    } finally {
        if(null != oos){
            oos.close();
        }
    }
}

如今利用以上序列化與反序列化Animal對象的例子來逐步回答本文開始時提出的幾個問題。數組

1、如何讓某些屬性不參與序列化與反序列化的過程?ide

假定在Animal對象中,咱們但願alias屬性不能被序列化。這個問題很是容易解決,只須要使用transient關鍵定修飾此屬性就能夠了。對Animal類的簡單修改以下所示:工具

class Animal implements Serializable{
    /**
     * 
     */
    private static final long serialVersionUID = 8822818790694831649L;
    private String name;
    private String color;
    private transient String[] alias;

若是一個屬性被transient關鍵字修飾,那麼此屬性就不會參與對象序列化與反序列化的過程。性能

 

2、類的屬性發生了增減那麼反序列化時會有什麼影響?開發工具

假定在設計Animal類的時候因爲考慮不周全而須要添加age屬性,那麼若是在添加此以前Animal對象已序列化爲animal.dat文件,那麼在添加age屬性以後,還能不能成功的反序列化呢?新的Animal類的片斷以下所示:this

class Animal implements Serializable{
    /**
     * 
     */
    private static final long serialVersionUID = 8822818790694831649L;
    private String name;
    private String color;
    private transient String[] alias;
    private int age;

再次調用反序列化的方法,使用添加age屬性以前的animal.dat文件進行反序列化,運行結果代表仍是能正常的反序列化,只是新添加的屬性爲默認值。加密

反過來考慮,若是把animal.dat文件中存在的name屬性刪除,那麼還能使用animal.dat文件進行反序列化嗎?修改以後的Animal類以下所示:

class Animal implements Serializable{
    /**
     * 
     */
    private static final long serialVersionUID = 8822818790694831649L;
//    private String name;
    private String color;
    private transient String[] alias;
    private int age;

調用反序列化的方法,使用刪除name屬性以前的animal.dat文件進行反序列化,運行結果表時仍是能正常的反序列化。由此可知,類的屬性的增刪並不能對對象的反序列化形成影響。

 

3、繼承關係在序列化過程當中的影響?

假定有父類Living沒有實現java.io.Serializable接口,子類Human實現了java.io.Serializable接口,那麼在序列化子類時父類中的屬性能被序列化嗎?先給出Living與Human類的定義以下所示:

class Living{
    private String environment;

    public String getEnvironment() {
        return environment;
    }

    public void setEnvironment(String environment) {
        this.environment = environment;
    }
}

class Human extends Living implements Serializable{

    /**
     * 
     */
    private static final long serialVersionUID = -4389621464687273122L;
    
    private String name;
    private double weight;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public double getWeight() {
        return weight;
    }
    public void setWeight(double weight) {
        this.weight = weight;
    }
    @Override
    public String toString() {
        return getEnvironment() + " : " + name + ", " + weight;
    }
}

經過代碼序列化Human對象獲得human.dat文件,再今後文件中進行反序列化得出結果爲:

null : Wg, 130.0

也可使用文件編輯工具Notepad++,查看human.dat文件以下所示:

image

在這個文件中看不到任何與父類中的environment屬性相同的內容,說明這個屬性並無被序列化。

修改父類Living,使之實現java.io.Serialazable接口,父類修改以後代碼片斷以下所示:

class Living implements Serializable{

序列化Human對象再次獲得human.dat文件,再今後文件中反序列化得出結果爲:

human environment : Wg, 130.0

再次經過Notepad++,查看human.dat文件以下所示:

image

從這個文件中也能夠清楚的看到父類Living中的environment屬性被成功的序列化。

由此可得出結論在繼承關係中若是父類沒有實現java.io.Serializable接口,那麼在序列化子類時即便子類實現了java.io.Serializable接口也不能把父類中的屬性序列化。

 

4、serialVersionUID屬性

在使用Eclipse之類的IDE開發工具時,若是類實現了java.io.Serializable接口,那麼IDE會警告讓生成以下屬性:

private static final long serialVersionUID = 8822818790694831649L;

這個屬性必須被申明爲static的,最好是final不可修改的。此屬性被用於序列化與反序列化過程當中的類信息校驗,若是此屬性的值在序列化以後發生了變化,那麼可序列化的文件就不能再反序列化,會拋出InvalidClassException異常。以下所示,在序列化之生修改此屬性,運行代碼的結果:

// 序列化之生手動修改了serialVersionUID屬性
    private static final long serialVersionUID = 1822818790694831649L;
//    private static final long serialVersionUID = 8822818790694831649L;

這時反序列化會出現以下的異常信息:

java.io.InvalidClassException: j2se.Animal; local class incompatible: stream classdesc serialVersionUID = 8822818790694831649, local class serialVersionUID = 1822818790694831649
    at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:621)
    at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1623)
    at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1518)
    at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1774)
    at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351)
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:371)
    at j2se.SerializableTest.unserializable(SerializableTest.java:58)
    at j2se.SerializableTest.animalUnSerializable(SerializableTest.java:50)
    at j2se.SerializableTest.main(SerializableTest.java:26)

因而可知,若是序列化以後修改了serialVersionUID屬性,那麼序列化的文件就不能再成功的反序列化。

固然,在工做中也見過不少程序員並不在乎IDE警告,不會爲類申明serialVersionUID屬性,由於這個屬性也不是必須的。經過把類的serialVersionUID屬性刪除也能夠成功的序列化與反序列化,若是類沒有顯式的申明serialVersionUID屬性,那麼JVM會依據類的各方面信息自動生成serialVersionUID屬性值,可是因爲不一樣的JVM生成serialVersionUID的原理存在差別。因此強烈建議程序員顯式申明serialVersionUID屬性,並強烈建議使用private static final修飾此屬性。

 

5、若是干預對象的序列化與反序列化過程?

在上面例子中的Animal類中定義了一個由transient關鍵字修飾的alias變量,因爲被transient修飾因此它不會被序列化。可是但願在序列化的過程當中把alias數組的各個元素序列化,並在反序列化過程把數組中的元素還原到alias數組中。java.io.Serializable接口雖然沒有定義任何方法,可是能夠經過在要序列化的類中的申明以下準確簽名的方法:

   /**
     * 序列化對象時調用此方法完成序列化過程
     * @param o
     * @throws IOException
     */
    private void writeObject(ObjectOutputStream o) throws IOException{
        
    }
    /**
     * 反序列化對象時調用此方法完成反序列化過程
     * @param o
     * @throws IOException
     * @throws ClassNotFoundException
     */
    private void readObject(ObjectInputStream o) throws IOException, ClassNotFoundException{
        
    }
    /**
     * 反序列化的過程當中若是沒有數據時調用此方法
     * @throws ObjectStreamException
     */
    private void readObjectNoData() throws ObjectStreamException{
        
    }

在Animal類中能夠申明以上的方法,以下所示:

class Animal implements Serializable{
    /**
     * 
     */
    private static final long serialVersionUID = 8822818790694831649L;
    private String name;
    private String color;
    private transient String[] alias;
    private int age;
    
    /**
     * 序列化對象時調用此方法完成序列化過程
     * @param o
     * @throws IOException
     */
    private void writeObject(ObjectOutputStream o) throws IOException{
        o.defaultWriteObject(); // 默認寫入對象的信息
        o.writeInt(alias.length);// 寫入alias元素的個數
        for(int i=0;i<alias.length;i++){
            o.writeObject(alias[i]);// 寫入alias數組中的每個元素
        }
    }
    /**
     * 反序列化對象時調用此方法完成反序列化過程
     * @param o
     * @throws IOException
     * @throws ClassNotFoundException
     */
    private void readObject(ObjectInputStream o) throws IOException, ClassNotFoundException{
        // 讀取順序與寫入順序一致
        o.defaultReadObject(); // 默認讀取對象的信息
        int length = o.readInt(); // 讀取alias元素的個數
        alias = new String[length];
        for(int i=0;i<length;i++){
            alias[i] = o.readObject().toString(); // 讀取元素存入數組
        }
    }

到目前爲止,咱們已能夠自定義對象的序列化與反序列化的過程。好比經過如下程序序列化對象,獲得animal.dat文件。

static void animalSerializable(){
        Animal animal = new Animal();
        animal.setName("Dog");
        animal.setColor("Black");
        animal.setAge(100);
        animal.setAlias(new String[]{"xiaoHei", "Gou", "GuaiGuai"});
        serializable(animal, "D://animal.dat");
    }

經過Notepad++打開animal.dat文件以下圖所示:

image

能夠從上圖中發現,實際上能夠序列化的文件中找到部分對象信息。如今咱們但願能把信息加密以後再序列化,並在反序列化時自動解密。在java.io.Serializable接口的實現類中還能夠定義以下的方法,用於替換序列化過程當中的對象與解析反序列化過程當中的對象。

/**
     * 在writeObject方法以前調用,經過此方法替換序列化過程當中須要替換的內部。
     * @return
     * @throws ObjectStreamException
     */
    Object writeReplace() throws ObjectStreamException{
        
    }
    
    /**
     * 在readObject方法以前調用,用於把writeReplace方法中替換的對象還原
     * @return
     * @throws ObjectStreamException
     */
    Object readResolve() throws ObjectStreamException{
        
    }

在Animal對象的序列化與反序列化的過程當中能夠利用以上的兩個方法進行加密與解密,以下所示:

/**
     * 在writeObject方法以前調用,經過此方法替換序列化過程當中須要替換的內部。
     * @return
     * @throws ObjectStreamException
     */
    Object writeReplace() throws ObjectStreamException{
        try {
            Animal animal = new Animal();
            String key = String.valueOf(serialVersionUID); // 簡單使用erialVersionUID作爲對稱算法的密鑰 
            animal.setAge(getAge() << 2); // 對於整數就簡單的處理爲向左移動兩位
            animal.setName(DesUtil.encrypt(getName(), key)); // 加密
            animal.setColor(DesUtil.encrypt(getColor(), key));
            String[] as = new String[getAlias().length];
            for(int i=0;i<as.length;i++){
                as[i] = DesUtil.encrypt(getAlias()[i], key);
            }
            animal.setAlias(as);
            return animal;
        } catch (Exception e) {
            throw new InvalidObjectException(e.getMessage());
        }
    }
    
    /**
     * 在readObject方法以前調用,用於把writeReplace方法中替換的對象還原
     * @return
     * @throws ObjectStreamException
     */
    Object readResolve() throws ObjectStreamException{
        try {
            Animal animal = new Animal();
            String key = String.valueOf(serialVersionUID);
            animal.setAge(getAge() >> 2);
            animal.setName(DesUtil.decrypt(getName(), key)); // 解密
            animal.setColor(DesUtil.decrypt(getColor(), key));
            String[] as = new String[getAlias().length];
            for(int i=0;i<as.length;i++){
                as[i] = DesUtil.decrypt(getAlias()[i], key);
            }
            animal.setAlias(as);
            return animal;
        } catch (Exception e) {
            throw new InvalidObjectException(e.getMessage());
        }
    }

再次使用Notepad++打開animal.dat文件以下圖所示,在其中就不會再存在Animal對象的信息。

image

因此綜上所述,對象的序列化與反序列化過程是徹底可控的,利用writeReplace與writeObject方法控制序列化過程,readResolve與readObject方法控制反序列化過程。在序列化過程當中與反序列化過程當中方法的調用順序以下所示:

序列化過程:writeReplace –> writeObject

反序列化過程:readObject –> readResolve

相關文章
相關標籤/搜索