1. 程式人生 > 實用技巧 >java物件序列化知識點總結-動力節點

java物件序列化知識點總結-動力節點

物件序列化的概念

所謂物件序列化就是指將一個儲存的物件變成一個二進位制的資料流進行傳輸。但並不是所有類的物件都可以進行序列化操作,如果一個物件需要被序列化,則物件所在的類必須實現Serializable介面。但是在此介面中並沒有定義任何方法,所以此介面和Cloneable介面一樣都是作為標示接口出現的。

public class Person implements Serializable{
    private String name;
    private int age;
    public Persion(String name,int age){
        this.name=name;
        this.age=age;
    }
    public String toString(){
        return "姓名"+this.name+",年齡"+this.age;
    }
}

此時,Person類的物件已經允許被序列化操作了,即變成了二進位制的byte流。

如果想要序列化,需要使用ObjectOutputStream類完成。如果想要反序列化,則需要ObjectInputStream類完成。

物件序列化:把Java物件轉換為位元組序列的過程

ObjectOutputStream主要是序列化物件的使用,也是一個OutputStream的子類。

pbulic class ObjectOutputStreamDemo{
    public static void main(String[] agrs)throws Exception{
    Person per=new Person("張三",30);
    ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream(new File("D:"+File.separator+"person.ini")));
    oos.writeObject(per);//序列化物件
    oos.close();
    }
}

反序列化物件:把位元組序列恢復為Java物件的過程

ObjectInputStream就可以完成物件的反序列化功能,方法如下:

  • 構造:public ObjectInputStream(InputStream) throws IoException
  • 讀取物件:public final Objecte readObject() throws IOException,ClassNoFoundException
pbulic class ObjectInputStreamDemo{
    public static void main(String[] agrs)throws Exception{
    ObjectinputStream ois=new ObjectInputStream(new FileInputStream(new File("D:"+File.separator+"person.ini")));
    Object obj=ois.readObject();//讀取序列化的物件
    ois.close();
    if(obj instanceof Person){
        Person per=(Person)obj;
    }
    }
}

Serializable的作用

為什麼一個類實現了Serializable介面,它就可以被序列化呢?在上面的示例中,使用ObjectOutputStream來持久化物件,在該類中有如下程式碼:

public class SimpleSerial {
 
    public static void main(String[] args) throws Exception {
        File file = new File("person.out");
 
        ObjectOutputStream oout = new ObjectOutputStream(new FileOutputStream(file));
        Person person = new Person("John", 101, Gender.MALE);
        oout.writeObject(person);
        oout.close();
 
        ObjectInputStream oin = new ObjectInputStream(new FileInputStream(file));
        Object newPerson = oin.readObject(); // 沒有強制轉換到Person型別
        oin.close();
        System.out.println(newPerson);
    }
}

從上述程式碼可知,如果被寫物件的型別是String,或陣列,或Enum,或Serializable,那麼就可以對該物件進行序列化,否則將丟擲NotSerializableException。

序列化機制

如果僅僅只是讓某個類實現Serializable介面,而沒有其它任何處理的話,則就是使用預設序列化機制。使用預設機制,在序列化物件時,不僅會序列化當前物件本身,還會對該物件引用的其它物件也進行序列化,同樣地,這些其它物件引用的另外物件也將被序列化。

在現實應用中,有些時候不能使用預設序列化機制。比如,希望在序列化過程中忽略掉敏感資料,或者簡化序列化過程。為此需要為某個欄位宣告為transient,那麼序列化機制就會忽略被transient修飾的欄位。

transient關鍵字

實際上在進行物件序列化的時候,序列化的是類中的屬性,每個類的物件只有屬性是不同的,但是如果現在有某個屬性不希望被序列化下來的話,則可以使用transient關鍵字。

private transient String name;
private int age;

writeObject()與readObject()

對於上被宣告為transitive的欄位age,除了將transitive關鍵字去掉之外,是否還有其它方法能使它再次可被序列化?方法之一就是在Person類中新增兩個方法:writeObject()與readObject(),如下所示:

public class Person implements Serializable {
    ...
    transient private Integer age = null;
    ... 
 
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        out.writeInt(age);
    }
 
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        age = in.readInt();
    }
}

在writeObject()方法中會先呼叫ObjectOutputStream中的defaultWriteObject()方法,該方法會執行預設的序列化機制,此時會忽略掉age欄位。然後再呼叫writeInt()方法顯示地將age欄位寫入到ObjectOutputStream中。readObject()的作用則是針對物件的讀取,其原理與writeObject()方法相同。

再次執行SimpleSerial應用程式,則又會有如下輸出:

arg constructor
[John, 31, MALE]

必須注意地是,writeObject()與readObject()都是private方法,那麼它們是如何被呼叫的呢?毫無疑問,是使用反射。詳情可見ObjectOutputStream中的writeSerialData方法,以及ObjectInputStream中的readSerialData方法。

Externalizable介面

無論是使用transient關鍵字,還是使用writeObject()和readObject()方法,其實都是基於Serializable介面的序列化。JDK中提供了另一個序列化介面Externalizable,使用該介面之後,之前基於Serializable介面的序列化機制就將失效。此時將Person類修改成如下:

public class Person implements Externalizable {
 
    private String name = null;
 
    transient private Integer age = null;
 
    private Gender gender = null;
 
    public Person() {
        System.out.println("none-arg constructor");
    }
 
    public Person(String name, Integer age, Gender gender) {
        System.out.println("arg constructor");
        this.name = name;
        this.age = age;
        this.gender = gender;
    }
 
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        out.writeInt(age);
    }
 
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        age = in.readInt();
    }
 
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
 
    }
 
    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
 
    }
     
}

此時再執行SimpleSerial程式之後會得到如下結果:

arg constructor
none-arg constructor
[null, null, null]

從該結果,一方面可以看出Person物件中任何一個欄位都沒有被序列化。另一方面這此序列化過程呼叫了Person類的無參構造器。

Externalizable繼承於Serializable,當使用該介面時,序列化的細節需要由程式設計師去完成。如上所示的程式碼,由於writeExternal()與readExternal()方法未作任何處理,那麼該序列化行為將不會儲存/讀取任何一個欄位。這也就是為什麼輸出結果中所有欄位的值均為空。

另外,若使用Externalizable進行序列化,當讀取物件時,會呼叫被序列化類的無參構造器去建立一個新的物件,然後再將被儲存物件的欄位的值分別填充到新物件中。這就是為什麼在此序列化過程中Person類的無參構造器會被呼叫。由於這個原因,實現Externalizable介面的類必須要提供一個無參的構造器,且它的訪問許可權為public。
對上述Person類作進一步的修改,使其能夠對name與age欄位進行序列化,但要忽略掉gender欄位,如下程式碼所示:

public class Person implements Externalizable {
 
    private String name = null;
 
    transient private Integer age = null;
 
    private Gender gender = null;
 
    public Person() {
        System.out.println("none-arg constructor");
    }
 
    public Person(String name, Integer age, Gender gender) {
        System.out.println("arg constructor");
        this.name = name;
        this.age = age;
        this.gender = gender;
    }
 
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        out.writeInt(age);
    }
 
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        age = in.readInt();
    }
 
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(name);
        out.writeInt(age);
    }
 
    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        name = (String) in.readObject();
        age = in.readInt();
    }
     
}

執行SimpleSerial之後會有如下結果:

arg constructor
none-arg constructor
[John, 31, null]

readResolve()

當使用Singleton模式時,應該是期望某個類的例項應該是唯一的,但如果該類是可序列化的,那麼情況可能會略有不同。為了能在序列化過程仍能保持單例的特性,可以在單例類中新增一個readResolve()方法,在該方法中直接返回單例物件,如下所示:

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

無論是實現Serializable介面,或是Externalizable介面,當從I/O流中讀取物件時,readResolve()方法都會被呼叫到。實際上就是用readResolve()中返回的物件直接替換在反序列化過程中建立的物件,而被建立的物件則會被垃圾回收掉。