Java對象序列化與反序列化

對象序列化的目標是將對象保存在磁盤中或者在網絡中進行傳輸。實現的機制是容許將對象轉爲與平臺無關的二進制流。java

java中對象的序列化機制是將容許對象轉爲字節序列。這些字節序列可使Java對象脫離程序存在,從而能夠保存在磁盤上,也能夠在網絡間傳輸。數組

對象的序列化是將一個Java對象寫入IO流;與此對應的,反序列化則是從IO流中恢復一個Java對象。網絡

實現序列化

若是要將一個java對象序列化,那麼對象的類須要是可序列化的。要讓類可序列化,那麼這個類須要實現以下兩個接口:性能

  • Serializable
  • Externalizable

使用Serializable序列化

實現Serializable接口很是簡單,只要讓java實現Serializable接口便可,無需實現任何方法。測試

一個類一旦實現了Serializable接口,那麼該類的對象就是可序列化的。實現類的對象的序列化可使用ObjectOutputStream,實現步驟以下:this

  • 建立ObjectOutputStream對象;
  • 調用ObjectOutputStream的writeObject方法輸出對象。

如下是一個實例:spa

package com.zhyea.test;

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

/**
 * 序列化測試類
 * 
 * @author robin
 * @date 2014年12月18日
 */
public class SerialTest {

    public static void main(String[] args) {
        ObjectOutputStream oos = null;
        try {
            oos = new ObjectOutputStream(new FileOutputStream("D:\\object.txt"));
            Person robin = new Person("robin", 29);
            oos.writeObject(robin);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (null != oos) {
                try {
                    oos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

/**
 * 序列化測試用對象
 * 
 * @author robin
 * @date 2014年12月18日
 */
class Person implements Serializable{
    
    private static final long serialVersionUID = -6412852654889352693L;
    
    /**
     * 姓名
     */
    private String name;
    /**
     * 年齡
     */
    private int age;

    public Person() {
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = 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;
    }
}

如上的代碼實現了將一個Person對象保存在了磁盤的一個文本文件object.txt上。運行程序在D盤上生成了一個object.txt文件。如下是文件內容:code

image

有亂碼(字節流轉字符流致使的),但仍不影響咱們分辨出裏面是否是咱們保存的對象。xml

接下來須要反序列化將Person對象從磁盤上讀出。相應的反序列化須要使用的類是ObjectInputStream,反序列化步驟以下:對象

  • 建立ObjectInputStream對象;
  • 使用ObjectInputStream的readObject方法取出對象。

接下來,重構下咱們的代碼,實現反序列化,以下:

package com.zhyea.test;

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

/**
 * 序列化測試類
 * 
 * @author robin
 * @date 2014年12月18日
 */
public class SerialTest {

    public static void main(String[] args) {
        Person robin = new Person("robin", 29);
        String savePath = "D:\\object.txt";

        SerialTest test = new SerialTest();

        try {
            test.serialize(robin, savePath);
            Person person = (Person) test.deSerialize(savePath);
            System.out.println("Name:" + person.getName() + "   Age:"
                    + person.getAge());
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    /**
     * 實現序列化
     * 
     * @param obj
     *            要被序列化保存的對象
     * @param path
     *            保存地址
     * @throws IOException
     */
    public void serialize(Object obj, String path) throws IOException {
        ObjectOutputStream oos = null;
        try {
            oos = new ObjectOutputStream(new FileOutputStream(path));
            oos.writeObject(obj);
        } finally {
            if (null != oos)
                oos.close();
        }
    }

    /**
     * 反序列化取出對象
     * 
     * @param path
     *            被序列化對象保存的位置
     * @return
     * @throws IOException
     * @throws ClassNotFoundException
     */
    public Object deSerialize(String path) throws IOException,
            ClassNotFoundException {
        ObjectInputStream ois = null;
        try {
            ois = new ObjectInputStream(new FileInputStream(path));
            return ois.readObject();
        } finally {
            if (null != ois)
                ois.close();
        }
    }

}

/**
 * 序列化測試用對象
 * 
 * @author robin
 * @date 2014年12月18日
 */
class Person implements Serializable {

    private static final long serialVersionUID = -6412852654889352693L;

    /**
     * 姓名
     */
    private String name;
    /**
     * 年齡
     */
    private int age;

    public Person() {
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = 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;
    }
}

關於對象序列化與反序列化還有幾點須要注意:

  • 反序列化無需經過構造器初始化對象;
  • 若是使用序列化機制向文件中寫入了多個對象,那麼取出和寫入的順序必須一致;
  • Java對類的對象進行序列化時,若類中存在對象引用(且值不爲null),也會對類的引用對象進行序列化。

使用transient

在一些特殊場景下,好比銀行帳戶對象,出於保密考慮,不但願對存款金額進行序列化。或者類的一些引用類型的成員是不可序列化的。此時可使用transient關鍵字修飾不想被或者不能被序列化的成員變量。

繼續調整咱們的代碼來作演示:

package com.zhyea.test;

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

/**
 * 序列化測試類
 * 
 * @author robin
 * @date 2014年12月18日
 */
public class SerialTest {

    public static void main(String[] args) {
        Person robin = new Person("robin", 29);
        School school = new School("XX學校");
        
        Teacher tRobin = new Teacher(robin);
        tRobin.setSchool(school);
        tRobin.setSalary(12.0);
        
        String savePath = "D:\\object.txt";

        SerialTest test = new SerialTest();

        try {
            test.serialize(savePath, tRobin);
            
            Teacher t = (Teacher) test.deSerialize(savePath);
            System.out.println("Name:" + t.getPerson().getName()
                             +" Salary:" + t.getSalary());
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    /**
     * 實現序列化
     * 
     * @param obj
     *            要被序列化保存的對象
     * @param path
     *            保存地址
     * @throws IOException
     */
    public void serialize(String path, Object ... obj) throws IOException {
        ....
    }

    /**
     * 反序列化取出對象
     * 
     * @param path
     *            被序列化對象保存的位置
     * @return
     * @throws IOException
     * @throws ClassNotFoundException
     */
    public Object deSerialize(String path) throws IOException,
            ClassNotFoundException {
        ...
    }

}

/**
 * Teacher類
 * @author robin
 * @date 2014年12月18日
 */
class Teacher implements Serializable{
    
    private static final long serialVersionUID = -8751853088437904443L;
    
    private Person person;
    private transient School school;
    private transient double salary;
    
    public Teacher(Person person){
        this.person = person;
    }
    
    /*略去get、set,請自行補充*/
}

/**
 * School類,不可序列化
 * 
 * @author robin
 * @date 2014年12月18日
 */
class School{
    private String name;
    
    public School(String name){
        this.name = name;
    }

    /*略去get、set,請自行補充*/
}

/**
 * Person類,可序列化
 * 
 * @author robin
 * @date 2014年12月18日
 */
class Person implements Serializable {

     ....
}

在不對Teacher類的school成員添加transient標識的狀況下,若school值不爲null,會報NotSerializableException。異常信息以下:
image

在不對Teacher類的salary成員添加transient標識的時候,會如實輸出salary的值,添加後則只會輸出salary的默認初始值即0.0。

image

須要注意的是transient只能修飾屬性(filed),不能修飾類或方法。

自定義序列化

transient提供了一種簡潔的方式將被transient修飾的成員屬性徹底隔離在序列化機制以外。這樣子當然不錯,可是Java還提供了一種自定義序列化機制讓開發者更自由地控制如何序列化各個成員屬性,或者不序列化某些屬性(與transient效果相同)。

在須要自定義序列化和反序列化的類中須要提供如下方法:

  • private void writeObject(ObjectOutputStream out)
  • private void readObject(ObjectInputStream in)
  • private void readObjectNoData()

先說下前兩個方法writeObject和readObject,這兩個方法和ObjectOutputStream及ObjectInputStream裏對應的方法名稱相同。實際上,儘管這兩個方法是private型的,可是仍然是在被序列化(或反序列化)階段被外部類ObjectOutputStream(或ObjectInputStream)調用。僅以序列化爲例,ObjectOutputStream在執行本身的writeObject方法前會先經過反射在要被序列化的對象的類中(有點繞口是吧)查找有無自定義的writeObject方法,若有的話,則會優先調用自定義的writeObject方法。由於查找反射方法時使用的是getPrivateMethod,因此自定以的writeObject方法的做用域要被設置爲private。經過自定義writeObject和readObject方法能夠徹底控制對象的序列化與反序列化。

以下是示例代碼:

package com.zhyea.test;

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

import com.sun.xml.internal.ws.encoding.soap.DeserializationException;

/**
 * 序列化測試類
 * 
 * @author robin
 * @date 2014年12月18日
 */
public class SerialTest {

    public static void main(String[] args) {
        Person robin = new Person("robin", 29);
        
        String savePath = "D:\\object.txt";

        SerialTest test = new SerialTest();

        try {
            test.serialize(savePath, robin);
            Person person = (Person) test.deSerialize(savePath);
            
            System.out.println("Name:" + person.getName()
                             +" Age:" + person.getAge());
            
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } 
    }

    /**
     * 實現序列化
     * 
     * @param obj
     *            要被序列化保存的對象
     * @param path
     *            保存地址
     * @throws IOException
     */
    public void serialize(String path, Person ... obj) throws IOException {
        ObjectOutputStream oos = null;
        ...
    }

    /**
     * 反序列化取出對象
     * 
     * @param path
     *            被序列化對象保存的位置
     * @return
     * @throws IOException
     * @throws ClassNotFoundException
     */
    public Object deSerialize(String path) throws IOException,
            ClassNotFoundException {
        ...
    }

}

/**
 * Person類,可序列化
 * 
 * @author robin
 * @date 2014年12月18日
 */
class Person implements Serializable {

    private static final long serialVersionUID = -6412852654889352693L;

    /**
     * 姓名
     */
    private String name;
    /**
     * 年齡
     */
    private int age;

    public Person() {}

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    /*  略去get和set,請自行實現  */
    
    
    private void writeObject(ObjectOutputStream out) throws IOException{
        out.writeObject(name);
        out.writeInt(age + 1);
        
        System.out.println("my write");
    }
    
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException{
        this.name = "zhangsan";
        this.age = 30;
        System.out.println("my read");
    }
}

如下是輸出結果:

image

關於readObjectNoData,在網上找了以下一段說明:

readObjectNoData 
  原始狀況
    pojo
        public class Person implements Serializable {               
            private int age;                
            public Person() {  }              
            //setter getter...      
        }
    序列化
         Person p = new Person();             
         p.setAge(10);             
         ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("c:/person.ser"));             
         oos.writeObject(p);            
         oos.flush();             
         oos.close();
  類結構變化後, 序列化數據不變     pojo
      Animal
        implements Serializable
        顯式編寫readObjectNoData
        public class Animal implements Serializable {                  
                private String name;                 
                public Animal() {  }                 
                //setter getter...                 
                private void readObjectNoData() {                       
                    this.name = "zhangsan";                 
                }          
        }
      Person 
        extends Animal
        public class Person extends Animal implements Serializable {                  
                private int age;                  
                public Person() {  }                 
                // setter getter...         
        }
    反序列化
      ObjectInputStream ois = new ObjectInputStream(new FileInputStream("c:/person.ser"));       
      Person sp = (Person) ois.readObject();       
      System.out.println(sp.getName());
      readObject時, 會調用readObjectNoData

readObjectNoData在我理解看來像是一種異常處理機制,用來在序列化的流不完整的狀況下返回正確的值。

使用 writeReplace和readResolve

writeReplace和readResolve是一種更完全的序列化的機制,它甚至能夠將序列化的目標對象替換爲其它的對象。

可是與writeObject和readObject不一樣的是,這兩者不是必需要一塊兒使用的,並且儘可能應分開使用。若一塊兒使用的話,只有writeReplace會生效。

代碼能夠說明一切,首先是writeReplace:

package com.zhyea.test;

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;


/**
 * 序列化測試類
 * 
 * @author robin
 * @date 2014年12月18日
 */
public class SerialTest {

    public static void main(String[] args) {
        Person robin = new Person("robin", 29);        
        String savePath = "D:\\object.txt";
        SerialTest test = new SerialTest();
        try {
            //序列化
            test.serialize(savePath, robin);
            //反序列化
            String person = (String) test.deSerialize(savePath);
            
            System.out.println(person);
            
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } 
    }

    /**
     * 實現序列化
     * 
     * @param obj
     *            要被序列化保存的對象
     * @param path
     *            保存地址
     * @throws IOException
     */
    public void serialize(String path, Person ... obj) throws IOException {
        ObjectOutputStream oos = null;
        ....
    }

    /**
     * 反序列化取出對象
     * 
     * @param path
     *            被序列化對象保存的位置
     * @return
     * @throws IOException
     * @throws ClassNotFoundException
     */
    public Object deSerialize(String path) throws IOException,
            ClassNotFoundException {
        ....
    }

}

/**
 * Person類,可序列化
 * 
 * @author robin
 * @date 2014年12月18日
 */
class Person implements Serializable {

    private static final long serialVersionUID = -6412852654889352693L;

    /**
     * 姓名
     */
    private String name;
    /**
     * 年齡
     */
    private int age;

    public Person() {}

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    /*   set和get方法請自行添加    */
    
    
    private Object writeReplace() throws ObjectStreamException{
        System.out.println("my writeReplace");
        return "robin";
    }
    
    private Object readResolve() throws ObjectStreamException{
        System.out.println("my readResolve");
        return "zhangsan";
    }
    
    private void writeObject(ObjectOutputStream out) throws IOException{
        ....
    }
    
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException{
        ....
    }
}

如下是運行結果:

image

在Person類中,保留了以前的writeObject和readObject方法,而且還添加了readResolve方法。可是從運行結果能夠看出來,這3個方法都沒有被調用,只有writeReplace方法被使用。能夠理解爲當使用writeReplace時,其餘的自定義方法都不會被調用,即writeReplace的優先級最高。

如今註釋掉writeReplace方法,再次執行,結果以下:

image

此次writeObject,readObject和readResolve方法都被調用。readResolve方法緊跟着readObject方法被調用且最終返回的值是readResolve返回的值,readObject裏反序列化生成的對象被拋棄。

此外還有一點須要說明:writeReplace和readResolve可使用任何做用域,這意味着子類也能夠調用超類的這兩個方法。可是若是子類還有不一樣的序列化及反序列化需求,這就須要子類重寫這個方法,有些時候這樣作是沒有必要的。所以通常狀況下將這兩個方法的做用域設置爲private。

使用Externalizable

一開始有提到過實現Externalizable接口也能夠實現類的序列化。使用這種方法,能夠由開發者徹底決定如何序列化和反序列化目標對象。Externalizable接口提供了writeExternal和readExternal兩個方法。

實際上這種方法和前面的自定義序列化方法很類似,只是Externalizable強制自定義序列化。在使用了Externalizable的類中仍可使用writeReplace和readResolve方法。使用Externalizable進行序列化較之使用Serializable性能略好,可是複雜度較高。

版本問題

執行序列化和反序列化時有可能會遇到JRE版本問題。尤爲是在網絡的兩端進行通訊時,這種狀況更爲多見。

爲了解決這種問題,Java容許爲序列化的類提供一個serialVersionUID的常量標識該類的版本。只要serialVersionUID的值不變,Java就會把它們看成相同的序列化版本。

若是不顯式定義serialVersionUID,那麼JVM就會計算出一個serialVersionUID的值。不一樣的編譯器下會產生不一樣的serialVersionUID值。serialVersionUID值不一樣則會致使編譯失敗。可使用jdk的bin目錄下的serial.exe查看可序列化類的serialVersionUID,指令以下:

serial Person

若是對類的修改確實會致使反序列化失敗,則應主動調整serialVersionUID的值。致使類的反序列化失敗的修改有如下幾種情形:

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

序列化注意事項

關於對象的序列化,總結下注意事項:

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