1. 程式人生 > >JDK1.8 java.io.Serializable介面詳解

JDK1.8 java.io.Serializable介面詳解

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

希望物件的某些屬性不參與序列化應該怎麼處理?
物件序列化之後,如果類的屬性發生了增減那麼反序列化時會有什麼影響呢?
如果父類沒有實現java.io.Serializable介面,子類實現了此介面,那麼父類中的屬效能被序列化嗎?
serialVersionUID屬性是做什麼用的?必須申明此屬性嗎?如果不申明此屬性會有什麼影響?如果此屬性的值發生了變化會有什麼影響?
能干預物件的序列化與反序列化過程嗎?

在解決這些問題之前,先來看一看如何進行物件的序列化與反序列化。定義一個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物件的例子來逐步回答本文開始時提出的幾個問題。

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

假定在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關鍵字修飾,那麼此屬性就不會參與物件序列化與反序列化的過程。

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

假定在設計Animal類的時候由於考慮不周全而需要新增age屬性,那麼如果在新增此之前Animal物件已序列化為animal.dat檔案,那麼在新增age屬性之後,還能不能成功的反序列化呢?新的Animal類的片段如下所示:
複製程式碼

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檔案進行反序列化,執行結果表時還是能正常的反序列化。由此可知,類的屬性的增刪並不能對物件的反序列化造成影響。

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

假定有父類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介面也不能把父類中的屬性序列化。

四、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修飾此屬性。

五、如果幹預物件的序列化與反序列化過程?

在上面例子中的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