Java對象的序列化(Object Serialization)


先定義兩個簡單的類:

package comm;

import java.io.Serializable;
import java.util.Date;
import java.util.GregorianCalendar;

public class Employee implements Serializable{
    
    private static final long serialVersionUID = 8820461656542319555L;
    private String name;
    private double salay;
    private Date hireDay;
    
    public Employee(String name, double salay, int year, int month, int day) {
        super();
        this.name = name;
        this.salay = salay;
        GregorianCalendar calender = new GregorianCalendar(year, (month - 1), day);
        this.hireDay = calender.getTime();
    }
    
    public void raseSalay(double rSalay){
        this.salay += rSalay;
    }
    
    @Override
    public String toString() {
        return "Employee [name=" + name + ", salay=" + salay + ", hireDay="
                + hireDay + "]";
    }

    public String getName() {
        return name;
    }

    public double getSalay() {
        return salay;
    }

    public Date getHireDay() {
        return hireDay;
    }

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

    public void setSalay(double salay) {
        this.salay = salay;
    }

    public void setHireDay(Date hireDay) {
        this.hireDay = hireDay;
    }

}
public class Employee Class
package comm;

public class Manager extends Employee {

    private static final long serialVersionUID = 1L;

    public Manager(String name, double salay, int year, int month, int day) {
        super(name, salay, year, month, day);
        this.secretary = null;
    }
    
    private Employee secretary;

    public Employee getSecretary() {
        return secretary;
    }

    public void setSecretary(Employee secretary) {
        this.secretary = secretary;
    }

    @Override
    public String toString() {
        return "Manager [Name=" + getName()
                + ", Salay=" + getSalay() + ", HireDay="
                + getHireDay() + ", secretary=" + secretary + "]";
    }

}
public class Manager extends Employee

下面進入今天的正題:序列化和反序列化。

 


 

1、基本的用法

①、序列化(serialization)一個java對象,第一步就是構建一個ObjectOutputStream對象:

ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("e:\\enum.dat"));

現在,就可以簡單的調用ObjectOutputStream對象的writeObject()方法來序列化一個對象了,就像下面這樣(后面會介紹到Employee要實現Serializable接口):

Employee harry = new Employee("Harry Hacker", 50000, 1989, 10, 1);
out.writeObject(harry );

②、反序列化(deserialization)一個java對象,第一步則是要構建一個ObjectInputStream對象:

ObjectInputStream in = new ObjectInputStream(new FileInputStream("e:\\enum.dat"));

同樣,有了ObjectInputStream對象以后就可以調用readObject()方法來反序列化一個對象了:

Employee emp = (Employee) in.readObject();

③、實現Serializable接口。任何想進行序列化和反序列化操作的對象,其類必須要實現Serializable接口:

class Employee implements Serializable{ ... }

實際上,Serializable這個接口並沒有任何的方法和屬性,和Cloneable接口一樣。

 


 

2、一個序列化和反序列化的例子:

好,到現在為止,基本用法講完了。現在走一個看看:

package streamAndFile;

import java.io.*;
import comm.Employee;
import comm.Manager;

public class ObjectStreamTest {

    public static void main(String[] args) {
        Employee harry = new Employee("Harry Hacker", 50000, 1989, 10, 1);
        Manager carl = new Manager("Carl Cracker", 8000, 1987, 12, 15);
        Manager tony = new Manager("Tony Tester", 40000, 1990, 3, 15);
     //兩個Manager公用一個秘書(Employee)
     carl.setSecretary(harry); tony.setSecretary(harry); Employee[] staff
= new Employee[3]; staff[0] = harry; staff[1] = carl; staff[2] = tony; try { //將對象序列化到文件 ObjectOutputStream objOut = new ObjectOutputStream(new FileOutputStream("employee.dat")); objOut.writeObject(staff); objOut.close(); //將對象反序列化出來 ObjectInputStream objIn = new ObjectInputStream(new FileInputStream("employee.dat")); Employee[] newStaff = (Employee[]) objIn.readObject(); objIn.close(); //修改第一個對象的屬性值,從結果中可以看出,反序列化以后的對象依然保持着原來的引用關系 newStaff[0].raseSalay(10); //打印 for(Employee e : newStaff){ System.out.println(e); } } catch (Exception e) { e.printStackTrace(); } } }

得到如下的【結果1】:

Employee [name=Harry Hacker, salay=50010.0, hireDay=Sun Oct 01 00:00:00 CST 1989]
Manager [Name=Carl Cracker, Salay=8000.0, HireDay=Tue Dec 15 00:00:00 CST 1987, secretary=Employee [name=Harry Hacker, salay=50010.0, hireDay=Sun Oct 01 00:00:00 CST 1989]]
Manager [Name=Tony Tester, Salay=40000.0, HireDay=Thu Mar 15 00:00:00 CST 1990, secretary=Employee [name=Harry Hacker, salay=50010.0, hireDay=Sun Oct 01 00:00:00 CST 1989]]

  現在來解釋一個問題:為什么我要在【結果1】中將salay=50010.0用紅色字體標注?答:因為它值得用紅色標注。

  從上面我們可以看出,ObjectOutputStream能夠檢測到對象的所有屬性並且保存(序列化,在這種上下文中保存序列化是可以通用的,讀取反序列化同樣通用)這些屬性內容。比如,當序列化一個Employee對象的時候,name,hireDay和salary屬性全部被保存。

  然而,有一個非常重要的情形需要我們認真思考:當一個對象同時被多個對象引用的時候會發生什么?

  就像Manager類對象carl中有一個Employee類型secretary屬性一樣,我們知道carl.secretary變量中實際上存放的是一個指向Employee對象的地址(也就是通常說的引用,相當於於c/c++里面的指針)。在上面的實例代碼中就存在如下的引用關系:

  要序列化這樣一個具有網狀引用關系的對象是一件具有挑戰性的事情。當然,我們知道,對secretary對象而言,我們不能夠簡單粗暴的保存一個內存地址(memory address)。序列化通常應用於網絡對象傳輸,那么,對不同的機器而言,一個內存地址是沒有意義的。

  實際上,序列化的時候對於對象的引用(object reference)是按照下面的步驟進行處理的:

  ①、序列化過程中,對每一個遇到的object reference都分配一個序列號(serial number);

  ②、當一個對象引用是第一次遇見的時候,就保存其對象數據(object data);

  ③、如果某個對象引用不是第一次遇見,就將其標記為“和serial number x對象一樣”;

就像下面這樣:

  從文件(或者是網絡文件)中反序列化的時候按照下面的步驟處理:

  ①、當第一次讀取到一個特定對象的時候,首先是構建一個對象,然后用流數據初始化它,同時記錄下serial number和內存地址之間的聯系;

  ②、當遇到標記為“和serial number x對象一樣”的對象的時候,就用serial number x對應對象的內存地址初始化這個引用;

 

  所以,從上面的過程可以看出,通過反序列化操作得到的對象和序列化之前的對象保持了一種同樣的引用關系。所以,在上面實例代碼中通過newStaff[0].raseSalay(10)修改了其salary以后,后面兩行也同樣改變為salay=50010.0

 


3、變量修飾符transient對序列化的控制。被transient修飾的變量,在使用默認序列化的時候不維護其內容。

  先定義下面兩個類:

package comm;

public class Person{

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

    public String toString() {
        return "Person [name=" + name + ", age=" + age + "]";
    }
}
public class Person
import java.io.Serializable;

public class Student implements Serializable{
    
    private static final long serialVersionUID = 1L;
    public float score;
    //變量使用transient
    public transient String intrest;
    //對變量使用transient
    public transient Person person;
    
    public Student(Person person, float score, String intrest) {
        super();
        this.person = person;
        this.score = score;
        this.intrest = intrest;
    }

    public String toString() {
        return "Student [person=" + person + ", score=" + score + ", intrest="
                + intrest + "]";
    }
}
public class Student implements Serializable

  再看看使用transient的實際效果:

package streamAndFile;

import java.io.*;

import comm.Person;
import comm.Student;

public class TransientTest {
    
    public static void main(String[] args) {
        try {
            ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("transient.dat"));
            Student stu = new Student(new Person("李雷", 23), 89, "台球");
            out.writeObject(stu);
            out.close();
            
            ObjectInputStream in = new ObjectInputStream(new FileInputStream("transient.dat"));
            Student newStu = (Student) in.readObject();
            in.close();
            
            System.out.println("stu--> " + stu);
            System.out.println("newStu--> " + newStu);
            
        } catch (Exception e) {
            e.printStackTrace();
        } 
    }
}

執行以后的結果:

stu--> Student [person=Person [name=李雷, age=23], score=89.0, intrest=台球]
newStu--> Student [person=null, score=89.0, intrest=null]

可以看出,由transient修飾的變量在序列化和反序列化的時候被忽略了。所以,其值為null。同時,還應該注意到Person類沒有實現序列化接口。由於在Strudent類中的person變量前使用了transient關鍵字,所以在序列化和反序列化的過程中被忽略,而沒有拋出NotSerializableException異常。

 


4、修改默認序列化機制(modifying the default serialization mechanism),有兩個層面:

 

層面一:在序列化類中添加writeObjectreadObject方法,分別配合defaultWriteObject()defaultReadObject()方法使用

  對象序列化機制提供了一種方式用於個性化的修改默認read和write的行為:在序列化類中用以下的方式來定義兩個方法

private void writeObject(ObjectOutputStream out) 
                throws IOException;
        
private void readObject(ObjectInputStream in) 
                throws IOException, ClassNotFoundException;

注意:兩個都是private void,同時其參數分別是ObjectOutputStreamObjectInputStream。這個和之前的方法有所不同。

  現在假設我們遇到了這樣的一種情況:像上面的Student和Person,其中Person類是寫死了的,我們沒有辦法修改了(或許是我們沒有源代碼)。但是Student可以自行修改(掌握源碼真好)。

  現在有這樣的一個需求,我序列化的時候是在有必要將Person一起進行,不然我的Student信息就不完整。

  這時有人會說,那還不簡單?!!在Student類中將person屬性前面的transient關鍵字刪掉就可以了。真的只是這么簡單嗎?難道你忘了Person類沒有實現Serializable接口的事情了嗎(沒有源碼真累,不然,直接實現一個接口還不是easy的事情)。

  現在我們就用下面的代碼介紹,沒有源代碼也能達到目的。

  ①、重寫Student類(Person類不能動了):

package comm;

import java.io.*;

public class Student implements Serializable{
    
    private static final long serialVersionUID = 1L;
    public float score;
    public String intrest;
    //對變量使用transient,防止其拋出NotSerializableException異常
    public transient Person person;
    
    public Student(Person person, float score, String intrest) {
        super();
        this.person = person;
        this.score = score;
        this.intrest = intrest;
    }
    
    private void writeObject(ObjectOutputStream out) throws IOException{
        /*
         * defaultWriteObject()方法還比較特別:只能在序列化類的writeObject方法中調用
         * 它能夠完成默認的序列化操作:對那些沒有使用transient的變量進行序列化操作
         */
        out.defaultWriteObject();
        //手動的將person的屬性寫入到序列化流中
        out.writeUTF(person.name);
        out.writeInt(person.age);
    };
    
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException{
        //完成默認的反序列化讀取
        in.defaultReadObject();    
        //手動的從反序列化流中讀取person的屬性
        String na = in.readUTF();
        int ag = in.readInt();
        //新創建一個Person對象,並用手動讀取的值進行初始化
        this.person = new Person(na, ag);
    };

    public String toString() {
        return "Student [person=" + person + ", score=" + score + ", intrest="
                + intrest + "]";
    }
}

  ②、還是使用上面的public class TransientTest做測試,會得到同樣的結果。所以,問題解決。總結到一點就是:在序列化類中重寫兩個方法,在方法中手動的寫入和讀取。從而,我們也可以看出,這兩個類的靈活性還是挺大的。

 

層面二:一個類完全可以定義它自己的序列化機制:實現Externalizable接口,要求在類中定義以下兩個方法:

public void readExternal(ObjectInputStream in) 
                throws IOException, ClassNotFoundException;
        
public void writeExternal(ObjectOutputStream out)
                throws IOException;

注意:它不同於上面說過的兩個方法,它的范圍是public。

為了說明問題,先定義兩個用於測試的類:

package comm;

import java.io.*;

/**
 * 該類實現Externalizable接口,同時重載兩個方法。對於實現了該接口的類而言,
 * 在序列化和反序列化的過程中,會用readExternal和writeExternal兩個方法
 * <b>完全負責</b>讀取和保存對象的操作,<b>包括對其父類的數據</b>。
 */
public class Car implements Externalizable{
    
    public String brand;
    /*
     * 注意:在實現了Externalizable接口的序列化類中,變量關鍵字transient
     * 將會不起任何作用。關鍵字transient是和Serializable接口有關,在對實現
     * 該接口的類進行默認序列化操作的時候,會自動忽略使用了transient關鍵字的變量。
     */
    public transient Double price;
    
    //必須要有一個無參的構造器,否則會報異常
    public Car() {}
    
    public Car(String brand, Double price) {
        super();
        this.brand = brand;
        this.price = price;
    }
    
    @Override
    public String toString() {
        return "Car [brand=" + brand + ", price=" + price + "]";
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException,
            ClassNotFoundException {
        //將類中的屬性按順序從流中讀出,然后賦值給當前對象的屬性
        this.brand = in.readUTF();
        this.price = in.readDouble();
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        //將當前對象的屬性值按順序寫入到流中
        out.writeUTF(this.brand);
        out.writeDouble(this.price);
    }
}
public class Car implements Externalizable
package comm;

import java.io.Serializable;

public class Teacher implements Serializable {
    
    private static final long serialVersionUID = 1L;
    private String name;
    //包含一個實現了Externalizable接口的屬性
    private Car car;
    
    public Teacher(String name, Car car) {
        super();
        this.name = name;
        this.car = car;
    }

    @Override
    public String toString() {
        return "Teacher [name=" + name + ", car=" + car + "]";
    }
    
}
public class Teacher implements Serializable

  Car類實現了Externalizable接口,同時實現了readExternal和writeExternal方法。為了,說明一個問題,還在Car類的price屬性上面使用了關鍵字transient。但是,要注意,這里使用transient只是為了要說明一個問題,實際上對於實現了Serializable接口的類而言,關鍵字transient不會起到任何作用。transient只是為了影響序列化的默認行為而設置的。Serializable接口完全拋棄了默認序列化機制,靠自定義實現。

  Teacher類實現了Serializable接口,其中有一個Car類型的屬性。

  

  現在,對Teacher類型的對象做序列化和反序列化操作:

package streamAndFile;

import java.io.*;
import comm.Car;
import comm.Teacher;

public class ExternalizableTest {
    
    public static void main(String[] args) {
        
        try {
            ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("externalizable.dat"));
            Car car = new Car("奧迪", 500000.0);
            Teacher teach = new Teacher("李雷", car);
            out.writeObject(teach);
            out.close();
            
            ObjectInputStream in = new ObjectInputStream(new FileInputStream("externalizable.dat"));
            Teacher newTeach = (Teacher) in.readObject();
            in.close();
            
            System.out.println(newTeach);
        } catch (Exception e) {
            e.printStackTrace();
        }         
    }
}

打印結果:

Teacher [name=李雷, car=Car [brand=奧迪, price=500000.0]]

現在,分析結果:

①、Teacher對象中有car,說明Car對象僅僅只是實現了Externalizable接口也能進行序列化和反序列化操作;

②、Car中的price屬性有值,則說明其transient關鍵字不會起到任何作用;

 

關於第二個層面的總結如下:

①、Externalizable和Serializable兩個接口,實現其中任何一個都能夠完成序列化操作。不一定非得實現Serializable接口才可以。

②、transient關鍵字是和Serializable默認序列化行為聯系在一起的,同時也是和 ObjectOutputStream out.defaultWriteObject(),ObjectInputStream in.defaultReadObject() 這兩個方法聯系在一起的。在進行默認序列化操作,以及調用out.defaultWriteObject()和in.defaultReadObject()這兩個方法進行序列化操作的時候,標注transient的變量會被序列化操作所忽略。除Serializable之外,transient關鍵字在其他地方不會起到任何作用。

③、實現了Externalizable接口的類,其序列化過程完全依靠readExternal和writeExternal這兩個方法,包括其對父類數據的處理。也就是說,實現了Externalizable接口以后,其序列化過程完全不使用默認行為了。對所有的數據處理,都必須明明白白的寫在readExternal和writeExternal這兩個方法中。

④、我們還應該注意一點,Externalizable的優先級比Serializable的優先級要高。假如,某個類同時實現了兩個接口,那么在序列化的時候只會考慮和Externalizable接口相關的性質,而不會考慮和Serializable相關的性質。

⑥、在《Core Java Volume II:Advanced Features》中說,序列化是一個比較慢的讀寫操作。但是,使用Externalizable接口會比使用Serializable接口要快35%到40%。

 


5、在序列化Singletons的時候需要注意的一個地方

  先定義一個Singletons,便於說明問題:

class Orientation implements Serializable{
    
    private static final long serialVersionUID = 1L;
    
    public static final Orientation HORIZONTAL = new Orientation(1);
    public static final Orientation VERTICAL = new Orientation(2);
            
    private int val;
    private Orientation(int val) {
        this.val = val;
    }
    
    public int getVal() {
        return val;
    }
}

  再定義一個enumeration:

enum Shrubbery{
    GROUND, CRAWLING, HANGING
}

  我們知道,Singletons的本意就是:單體。Orientation類只有一個private屬性的構造器,所以我們不能通過new來構造一個對象,只能通過Orientation.HORIZONTAL的方式獲取對象。而且,任何地方獲取到的對象都應該只同一個。包括:反序列化得到的對象。

  從序列化和非序列化的角度上說,現在我們有兩種方式來獲得一個單體對象:①、Orientation.HORIZONTAL的方式;②、通過反序列化的方式。

  那么,現在的意思就是說:即便你反序列化的數據來源於網絡,來源於其它的機器;但是,只要你序列化之前的對象時Orientation.HORIZONTAL,那么反序列化以后得到的對象和我本地的Orientation.HORIZONTAL也必須是同一個(指向相同的內存空間)。

  有人會說,本來就是這樣,難道這里還有什么小九九嗎?先看一段代碼,從結果分析一下你就知道問題所在了:

package streamAndFile;

import java.io.*;

public class ObjectStreamEnum {
    
    public static void main(String[] args) {
        
        try {
            ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("e:\\enum.dat"));
            out.writeObject(Shrubbery.CRAWLING);
       //序列化一個單體Orientation.HORIZONTAL out.writeObject(Orientation.HORIZONTAL); out.close(); ObjectInputStream in
= new ObjectInputStream(new FileInputStream("e:\\enum.dat")); Shrubbery newShr = (Shrubbery) in.readObject();
       //反序列化得到一個單體orient,那么,在本地而言,orient和Orientation.HORIZONTAL是同一個對象嗎? Orientation orient
= (Orientation) in.readObject(); in.close(); //使用enum關鍵字是可以工作的 System.out.println(newShr == Shrubbery.CRAWLING); /* * 如果沒有在Orientation中定義readResolve()方法,則會返回false。 * 在反序列化的過程中,對於單體(singleton)而言,即便其構造器是私有的, * 在readObject()的時候還是會創造一個新的對象,然后返回。 * 而,readResolve()方法會在反序列化之后調用,所以,我們可以在這個函數中 * 做一下處理,把不一樣的對象設法變成一樣的。 */ System.out.println(orient == Orientation.HORIZONTAL); } catch (Exception e) { e.printStackTrace(); } } }

打印的結果:

true
false

從第二個false可以看出,反序列化得到的單體和本地的單體並不是同一個對象。這個,太不能讓人接受了!!!但是,通過enum關鍵字得到的對象在其序列化之后還是同一個。

  那么,怎樣解決這個問題呢?答案是:在單體類中加入一個protected Object readResolve() throws ObjectStreamException方法,在這個方法中設法讓不同的對象變得相同。

  兩個對象不一樣的原因是:在readObject()方法中會構造一個新的對象,然后返回。即便,你單體只有一個private屬性的構造器(誰叫java有個反射呢)。

  解決方案的原理:如果實現了Serializable接口的類中具有一個readResolve()方法,那么這個方法會在反序列化完成之后調用。我么就可以在這個方法中做點兒手腳。

  所以,修改以后的Orientation類就是下面這樣:

package streamAndFile;

class Orientation implements Serializable{
    
    private static final long serialVersionUID = 1L;
    
    public static final Orientation HORIZONTAL = new Orientation(1);
    public static final Orientation VERTICAL = new Orientation(2);
            
    private int val;
    private Orientation(int val) {
        this.val = val;
    }
    
    public int getVal() {
        return val;
    }
    
    /**
     * if the <b>readResolve</b> method is defined, it is called after 
     * the object is deserialized. It must return an object that then 
     * becomes the return value of the <b>readObject</b> method.
     */
    protected Object readResolve() throws ObjectStreamException {
     //如果反序列化的結果是1,我就給你返回一個本地方法得到的對象Orientation.HORIZONTAL。這樣肯定就一直了。原理其實簡單。
if(val == 1) return Orientation.HORIZONTAL; if(val == 2) return Orientation.VERTICAL; return null; } }

再運行上面的ObjectStreamEnum就會得到兩個true。

  總結為一點:使用enum關鍵字就不會有這個問題,還是多使用enum。而少自己定義單體,省得麻煩。

 


6、序列化到底是個什么東西?!

   (試圖去追尋事物背后的真相是一件累和危險的事情。說錯了,往往會招人嫌棄。)

  java為序列化制定了一個特定的文件規范。序列化過程就是將對象轉換成一串特定格式的數據,該數據可以保存在本地文件中,也可以通過網絡共享和傳遞。

  反序列化過程就是按照制定好的規范,將這一串數據轉換為一個對象。

  具體的文件格式規范可以自行參考相關的文獻。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM