Java對象序列化全面總結

前言

Java容許咱們在內存中建立可複用的Java對象,但通常狀況下,這些對象的生命週期不會比JVM的生命週期更長。但在現實應用中,可能要求在JVM中止運行以後可以保存(持久化)指定的對象,並在未來從新讀取被保存的對象html

Java對象序列化就可以幫助咱們實現該功能。使用Java對象序列化,在保存對象時,會把其狀態保存爲一組字節,在將來再將這些字節組裝成對象java

必須注意地是,對象序列化保存的是對象的"狀態",即它的成員變量。由此可知,對象序列化不會關注類中的靜態變量程序員

除了在持久化對象時會用到對象序列化以外,當使用 RMI ,或在網絡中傳遞對象時,都會用到對象序列化設計模式

Java序列化API爲處理對象序列化提供了一個標準機制,該API簡單易用,但性能不是最好的數組

實例 demo

在Java中,只要一個類實現了 java.io.Serializable 接口,它就能夠被序列化(枚舉類能夠被序列化)。安全

// Gender類,表示性別
// 每一個枚舉類型都會默認繼承類java.lang.Enum,而Enum類實現了Serializable接口,因此枚舉類型對象都是默承認以被序列化的。
public enum Gender {  
    MALE, FEMALE  
} 

// Person 類實現了 Serializable 接口,它包含三個字段。另外,它還重寫了該類的 toString() 方法,以方便打印 Person 實例中的內容。
public class Person implements Serializable {  
    private String name = null;  
    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;  
    }  
 
    // 省略 set get 方法
@Override public String toString() { return "[" + name + ", " + age + ", " + gender + "]"; } }   // SimpleSerial類,是一個簡單的序列化程序,它先將Person對象保存到文件person.out中,而後再從該文件中讀出被存儲的Person對象,並打印該對象。 public class SimpleSerial { public static void main(String[] args) throws Exception { File file = new File("person.out"); ObjectOutputStream oout = new ObjectOutputStream(new FileOutputStream(file)); // 注意這裏使用的是 ObjectOutputStream 對象輸出流封裝其餘的輸出流 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); } }   // 上述程序的輸出的結果爲: arg constructor [John, 31, MALE]

 

當從新讀取被保存的Person對象時,並無調用Person的任何構造器,看起來就像是直接使用字節將Person對象還原出來的。當Person對象被保存到person.out文件後,能夠在其它地方去讀取該文件以還原對象,但必須確保該讀取程序的 CLASSPATH 中包含有 Person.class(哪怕在讀取Person對象時並無顯示地使用Person類,如上例所示),不然會拋出 ClassNotFoundException。服務器

簡單的來講,Java 對象序列化就是把對象寫入到輸出流中,用來存儲或傳輸;反序列化就是從輸入流中讀取對象。網絡

序列化一個對象首先要創造某些OutputStream對象(如FileOutputStream、ByteArrayOutputStream等),而後將其封裝在一個ObjectOutputStream對象中,在調用writeObject()方法便可序列化一個對象ide

反序列化的過程須要創造InputStream對象(如FileInputstream、ByteArrayInputStream等),而後將其封裝在ObjectInputStream中,在調用readObject()便可函數

注意對象的序列化是基於字節的,不能使用基於字符的流。

爲何一個類實現了Serializable接口,它就能夠被序列化?

使用ObjectOutputStream來持久化對象到文件中,使用了writeObject方法,該方法又調用了以下方法:

private void writeObject0(Object obj, boolean unshared) throws IOException {  
      ...
    if (obj instanceof String) {  
        writeString((String) obj, unshared);  
    } else if (cl.isArray()) {  
        writeArray(obj, desc, unshared);  
    } else if (obj instanceof Enum) {  
        writeEnum((Enum) obj, desc, unshared);  
    } else if (obj instanceof Serializable) {  
        writeOrdinaryObject(obj, desc, unshared);  
    } else {  
        if (extendedDebugInfo) {  
            throw new NotSerializableException(cl.getName() + "\n" 
                    + debugInfoStack.toString());  
        } else {  
            throw new NotSerializableException(cl.getName());  
        }  
    }  
    ...  
} 

從上述代碼可知,若是被寫對象的類型是String,或數組,或Enum,或Serializable,那麼就能夠對該對象進行序列化,不然將拋出NotSerializableException。

即、String類型的對象、枚舉類型的對象、數組對象,都是默承認以被序列化的。

默認序列化機制

若是僅僅讓某個類實現Serializable接口,而沒有其它任何處理的話,則就是使用默認序列化機制。

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

因此,若是一個對象包含的成員變量是容器類對象,而這些容器所含有的元素也是容器類對象,那麼這個序列化的過程就會較複雜,開銷也較大。

選擇性的序列化

在現實應用中,有些時候不能使用默認序列化機制。好比,但願在序列化過程當中忽略掉敏感數據,或者簡化序列化過程。下面將介紹若干影響序列化的方法。

使用 transient 關鍵字

當類的某個字段被 transient 修飾,默認序列化機制就會忽略該字段。此處將Person類中的age字段聲明爲transient,以下所示

public class Person implements Serializable {  
    ...  
    transient private Integer age = null;  
    ...  
} 


// 再執行SimpleSerial應用程序,會有以下輸出:
arg constructor  
[John, null, MALE] 

 

使用writeObject()方法與readObject()方法

對於上述已被聲明爲 transitive 的字段 age,除了將 transient 關鍵字去掉外,是否還有其它方法能使它再次可被序列化?

方法之一就是在Person類中添加兩個方法:writeObject()與readObject(),以下所示:

public class Person implements Serializable {  
    ...  
    transient private Integer age = null;  
    ...  
 
    // writeObject()會先調用ObjectOutputStream中的defaultWriteObject()方法,該方法會執行默認的序列化機制,此時會忽略掉age字段。而後再調用writeInt()方法顯示地將age字段寫入到        
    // ObjectOutputStream中。
    private void writeObject(ObjectOutputStream out) throws IOException {  
        out.defaultWriteObject();  
        out.writeInt(age);  
    }  
 
    // readObject()的做用則是針對對象的讀取,其原理與writeObject()方法相同。
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {  
        in.defaultReadObject();  
        age = in.readInt();  
    }  
} 
// 再次執行SimpleSerial應用程序,則又會有以下輸出:
arg constructor  
[John, 31, MALE] 

 

必須注意地是,writeObject()與readObject()都是private方法,那麼它們是如何被調用的呢?

毫無疑問,使用反射。詳情能夠看看ObjectOutputStream中的writeSerialData方法,以及ObjectInputStream中的readSerialData方法。這兩個方法會在序列化、反序列化的過程當中被自動調用。且不能關閉流,不然會致使序列化操做失敗。

使用 Externalizable 接口

不管是使用 transient 關鍵字,仍是使用 writeObject() 和 readObject() 方法,其實都是基於 Serializable 接口的序列化。

Java提供了另外一個序列化接口 Externalizable,使用該接口以後,以前基於 Serializable 接口的序列化機制就將失效。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模式時,應該是指望某個類的實例應該是惟一的,但若是該類是可序列化的,那麼狀況可能略有不一樣。固然目前最好的單例實現方式是使用枚舉,若是仍是傳統的實現方式,纔會遇到這個問題。

具體參考:最簡單的設計模式——單例模式的演進和推薦寫法(Java 版)

序列化和反序列化須要注意的坑

能序列化的前提

若是一個類想被序列化,須要實現 Serializable 接口進行自動序列化,或者實現 Externalizable 接口進行手動序列化,不然強行序列化該類的對象,就會拋出 NotSerializableException 異常,這是由於,在序列化操做過程當中會對類型進行檢查,要求被序列化的類必須屬於 Enum、Array 和 Serializable 類型其中的任何一種(Externalizable也繼承了Serializable)。

JVM 是否容許反序列化,不只取決於類路徑和功能代碼是否一致,一個很是重要的一點是兩個類的序列化 ID 是否一致(就是 private static final long serialVersionUID)

transient 關鍵字的做用是控制變量的序列化,在變量聲明前加上該關鍵字,能夠阻止該變量被序列化到文件中,在被反序列化後,transient 變量的值被設爲初始值,如 int 型的是 0,對象型的是 null。

FileOutputStream 類有一個帶有兩個參數的重載 Constructor——FileOutputStream(String, boolean)。若其第二個參數爲 true 且 String 表明的文件存在,那麼將把新的內容寫到原來文件的末尾而非重寫這個文件,故不能用這個版本的構造函數來實現序列化,也就是說必須重寫這個文件,不然在讀取這個文件反序列化的過程當中就會拋出異常,致使只有第一次寫到這個文件中的對象能夠被反序列化,以後程序就會出錯。

要知道序列化的是什麼樣兒的對象(成員)

序列化並不保存靜態變量

要想將父類對象也序列化,就須要讓父類也實現 Serializable 接口

若一個類的字段有引用對象,那麼在序列化該類的時候不只該類要實現Serializable接口,這個引用類型也要實現Serializable接口。但有時咱們並不須要對這個引用類型進行序列化,此時就須要使用transient關鍵字來修飾該引用類型保證在序列化的過程當中跳過該引用類型。

經過序列化操做,能夠實現對任何可 Serializable 對象的深度複製(deep copy),這意味着複製的是整個對象的關係網,而不只僅是基本對象及其引用

若是父類沒有實現Serializable接口,但其子類實現了此接口,那麼這個子類是能夠序列化的,可是在反序列化的過程當中會調用父類的無參構造函數,因此在其直接父類(注意是直接父類)中必須有一個無參的構造函數。

序列化的安全性

服務器端給客戶端發送序列化對象數據,序列化二進制格式的數據寫在文檔中,而且徹底可逆。一抓包就能就看到類是什麼樣子,以及它包含什麼內容。若是對象中有一些數據是敏感的,好比密碼字符串等,則要對字段在序列化時,進行加密,而客戶端若是擁有解密的密鑰,只有在客戶端進行反序列化時,才能夠對密碼進行讀取,這樣能夠必定程度保證序列化對象的數據安全。 

好比能夠經過使用 writeObject 和 readObject 實現密碼加密和簽名管理,但其實還有更好的方式。

若是須要對整個對象進行加密和簽名,最簡單的是將它放在一個 javax.crypto.SealedObject 和/或 java.security.SignedObject 包裝器中。二者都是可序列化的,因此將對象包裝在 SealedObject 中能夠圍繞原對象建立一種 「包裝盒」。必須有對稱密鑰才能解密,並且密鑰必須單獨管理。一樣,也能夠將 SignedObject 用於數據驗證,而且對稱密鑰也必須單獨管理

反序列化後,什麼時候不是同一個對象

只要將對象序列化到單一流中,就能夠恢復出與咱們寫出時同樣的對象網,並且只要在同一流中,對象都是同一個。不然,反序列化後的對象地址和原對象地址不一樣,只是內容相同

若是將一個對象序列化入某文件,那麼以後又對這個對象進行修改,而後再把修改的對象從新寫入該文件,那麼修改無效,文件保存的序列化的對象仍然是最原始的。這是由於,序列化輸出過程跟蹤了寫入流的對象,而試圖將同一個對象寫入流時,並不會致使該對象被複制,而只是將一個句柄寫入流,該句柄指向流中相同對象的第一個對象出現的位置。爲了不這種狀況,在後續的 writeObject() 以前調用 out.reset() 方法,這個方法的做用是清除流中保存的寫入對象的記錄

idea IDE 自動生成序列化版本號

安裝 serialVersionUID 插件便可。

序列化版本號的用處

參考 Stack Overflow:https://stackoverflow.com/questions/285793/what-is-a-serialversionuid-and-why-should-i-use-it 

ArrayList 序列化要注意的問題

ArrayList實現了java.io.Serializable接口,可是其 elementData 是 transient 的,可是 ArrayList 是經過數組實現的,數組 elementData 用來保存列表中的元素。經過該屬性的聲明方式知道該數據沒法經過序列化持久化。

可是若是實際測試,就會發現,ArrayList 能被完整的序列化,緣由是在writeObject 和 readObject方法中進行了序列化的實現。

這樣設計的緣由是由於 ArrayList 是動態數組,若是數組自動增加長度設爲 2000,而實際只放了一個元素,那就會序列化 1999 個 null 元素,爲了保證在序列化的時候不會將這麼多 null 元素序列化,ArrayList 把元素數組設置爲transient,可是,做爲一個集合,在序列化過程當中還必須保證其中的元素能夠被持久化,因此,經過重寫 writeObject 和 readObject 方法把其中的元素保留下來,具體作法是:

writeObject方法把elementData數組中的元素遍歷到ObjectOutputStream

readObject方法從ObjectInputStream中讀出對象並保存賦值到elementData數組
相關文章
相關標籤/搜索