Android進階之路——Serializable序列化

簡介

序列化 (Serialization)是將對象的狀態信息轉換爲可以存儲或傳輸的形式的過程。在序列化期間,對象將其當前狀態寫入到臨時或持久性存儲區。以後,可以通過從存儲區中讀取或反序列化對象的狀態,重新創建該對象。——百度百科。

在Android中序列化最常見的使用場景就是緩存數據了。現在的App中基本需要緩存數據,例如緩存用戶登錄信息。

// 用來保存用戶信息
public class User {
    private String name;
    private int age;
    
    // getter/setter
}

// 用戶信息
User user = new User("Eon Liu", 18);
ObjectOutputStream oos = null;
try {
    // 緩存路徑(需要開啓存儲權限)
    File cache = new File(Environment.getExternalStorageDirectory(), "cache.txt");
    oos = new ObjectOutputStream(new FileOutputStream(cache));
    // 將用戶信息寫到本地文件中
    oos.writeObject(user);
} catch (IOException e) {
    e.printStackTrace();
} finally {
    // 關閉流
    if (oos != null) {
        try {
            oos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

通常在登錄成功之後我們將用戶的信息解析成一個類似User的對象,然後將其保存在SDCard中。這一過程就需要用到序列化。上面代碼我們並沒有對User進行可序列化的處理,所以在保存過程中就會拋出java.io.NotSerializableException: com.eonliu.sample.serialization.User這樣的Java異常。因爲在writeObject方法中對需要存儲的類進行了校驗,如果沒有實現Serializable接口就會拋出這個異常信息。處理這種異常也很簡單,只要使User類實現Serializable接口就可以了。

Serializable

Serializable是Java中提供的序列化接口。

package java.io;
public interface Serializable {
}

Serializable是一個空接口,它僅僅是用來標識一個對象是可序列化的。

如果想要使User可被序列化只要實現Serializable接口即可。

public class User implements Serializable {

    private static final long serialVersionUID = 8279379322154244252L;
    private String name;
    private int age;
    
    // getter/setter
}

可以看到User類實現了Serializable接口,這時User就可以被序列化了。並且還多了一個serialVersionUID字段。那麼這個字段是幹什麼用的呢?

serialVersionUID的作用及注意事項

serialVersionUID是用來標記User類版本用的。其聲明的格式是任意訪問權限修飾符 static final long serialVersionUID = longValue; 因爲其作用是標識每個類的版本,所以最好使用private控制serialVersionUID的訪問權限僅在當前類有用,不會被其他子類繼承使用。

如果不顯示聲明serialVersionUID那麼JVM會根據類的信息生成一個版本號,由於不同的JVM生成的版本號的能不一致,類的結構也可能發生變化等這些因素都可能導致序列化時候的版本號和反序列化時的版本號不止一次導致運行時拋出InvalidClassException異常。所以最佳實踐還是在序列化時顯示的指定serialVersionUID字段。其值是一個long類型的數值。這個值在Android Studio中默認是不能自動生成的,可以打開Perferences-Editor-Code Style-Inspections-Serialization issues-Serializable class without serialVersionUID,這樣在實現Serializable接口是如果沒有聲明serialVersionUID字段編譯器就會給出警告⚠️,根據警告提示就可以自動生成serialVersionUID字段了。

總結:

  • 儘量顯示聲明serialVersionUID字段。
  • 最好使用private修飾serialVersionUID字段。
  • 儘量使用Android Studio或者其他工具生成serialVersionUID的值。
  • 不同版本的類的serialVersionUID值儘量保持一致,不要隨意修改,否則反序列化時會拋出InvalidClassException異常,反序列化失敗。

不可被序列化的字段

有時候可能要序列化的對象中存在某些字段不需要被序列化。例如用戶密碼,爲了保證安全我們不需要將密碼字段進行序列化,那如何能做到這一點呢?實現Serializable接口時靜態變量(被static修飾的變量)不會被序列化、另外被transient關鍵字修飾的變量也是不會被序列化的。

public class User implements Serializable {

    private static final long serialVersionUID = 8279379322154244252L;

    private String name;
    private int age;
    private transient String password;
    
    // getter/setter
}

因爲靜態變量不能被序列化,所以serialVersionUID需要聲明爲static的,另外password被聲明爲transient也不會被序列化。

靜態成員返回序列化時會取內存中的值,被transient修飾的成員變量使用其類型的默認值,例如password的默認值則爲null

繼承或組合關係中的序列化

public class Person {
    private boolean sex;
    
    // getter/setter
}

public class User extends Person implements Serializable {
    private static final long serialVersionUID = 8279379322154244252L;
    private String name;
    private int age;
    private transient String password;
    
    // getter/setter
}

父類Person沒有實現Serializable接口,單其子類實現了Serializable接口,所以父類的信息不回被序列化,當我們保存User信息時,父類的sex字段是不會被保存的。反序列化時sex會使用boolean類型的默認值false

另外當父類沒有實現Serializable接口時,必須有一個可用的無參數構造函數,例如上面的Person代碼並沒有顯示聲明構造,JVM會生成一個無參數構造函數,但是如果我們將其代碼改成如下形式:

public class Person {

    private boolean sex;

    public Person(boolean sex) {
        this.sex = sex;
    }
    
    // getter/setter
}

這裏顯示聲明瞭Person的構造函數,其參數爲sex,這也是Person的唯一構造函數了。因爲根據Java機制,當顯示聲明構造函數時JVM就不會生成無參數的構造函數。這樣就會導致反序列化時候無法構造Person對象,拋出java.io.InvalidClassException: com.eonliu.sample.serialization.User; no valid constructor異常。

我們對上面的代碼稍作修改。

當父類實現了Serializable接口時,其子類也可以被序列化。

public class Person implements Serializable {
    private static final long serialVersionUID = 2622760185052917383L;
    private boolean sex;
    
    // getter/setter
}

public class User extends Person {
    private static final long serialVersionUID = 8279379322154244252L;
    private String name;
    private int age;
    private transient String password;
    
    // getter/setter
}

當父類Person實現了Serializable接口時,則子類User也可以被序列化。這時sexnameage這三個字段都會被序列化。

還有一種情況就是當我們序列化的類中有一個成員變量是一個自定義類的情形。

public class Car {
    private String product;
    
    // getter/setter
}

public class Person implements Serializable {
    private static final long serialVersionUID = 2622760185052917383L;
    private boolean sex;
    
    // getter/setter
}

public class User extends Person {
    private static final long serialVersionUID = 8279379322154244252L;
    private String name;
    private int age;
    private transient String password;
    private Car car;
    // getter/setter
}

User中有一個成員變量爲Car類型,因爲Car沒有實現Serializable接口,所以會導致User序列化失敗,拋出java.io.NotSerializableException: com.eonliu.sample.serialization.Car異常,這時解決辦法有兩個,一個是使用transient修飾Car字段,使其在序列化時被忽略。另一個辦法就是Car實現Serializable接口,使其擁有可序列化功能。

總結:

  • 繼承關係中,父類實現Serializable接口,則父類和子類都可被序列化。

  • 集成關係中,父類沒有實現Serializable接口,則父類信息不會被序列化,子類實現Serializable接口則只會序列化子類信息。

  • 如果被序列化的類中有Class類型的字段則這個Class需要實現Serializable接口,否則序列化時候回拋出``java.io.NotSerializableException異常。或者使用transient`將其標記爲不需要被序列化。

  • 如果父類沒有實現Serializable接口,則必須要有一個可用的無參數構造函數。否則拋出java.io.InvalidClassException: com.eonliu.sample.serialization.User; no valid constructor異常。

自定義序列化過程

Serializable接口預留了幾個方法可以用來實現自定義序列化過程。

private void writeObject(java.io.ObjectOutputStream out)throws IOException
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;
private void readObjectNoData() throws ObjectStreamException;
ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException

上面五個方法就是Java序列化機制中可以用來干預序列化過程的五個方法,他們具體能感謝什麼繼續往下看。

writeObject&readObject

writeObjectreadObject這兩個方法從名字可以看出來,就是用來讀寫對象的,在序列化過程中我們需要把對象信息通過ObjectOutputStream保存在存儲介質上,反序列化的時候就是通過ObjectInputStream從存儲介質上將對象信息讀取出來,然後在內存中生成一個新的對象。這兩個方法就可以用來定義這一過程。

// 序列化
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
    // 寫入性別信息(sex是Person的字段信息)
    out.writeBoolean(isSex());
    // 寫入年齡信息
    out.writeInt(age);
}
// 反序列化
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
    // 恢復性別信息
    setSex(in.readBoolean());
    // 恢復年齡信息
    age = in.readInt();
}

首先這兩個方法要成對出現,否則一個都不要寫。在readObject中read的次序要與在writeObject中write的次序保持一致,否則可能會導致反序列化的數據出現混亂的現象。另外我們這兩個方法不關心父類是否實現了Serializable接口,如上面代碼所示,out.writeBoolean(isSex());中的sex字段就是來自父類Person的,即使Person沒有實現Serializable接口這個序列化也會正常運行。

如果不需要自定義過程可以使用out.defaultWriteObject();來實現默認的序列化過程,使用in.defaultReadObject();實現默認的反序列化過程。

重寫這兩個方法可以自定義序列化和反序列的過程、例如可以自己定義那些字段可以序列化,哪些不被序列化,也可以對字段進行加密、解密的操作等。如果使用默認的序列化、反序列化的過程我們也可以在其過程的前後插入其他的邏輯代碼來完成其他的任務。

readObjectNoData

readObjectNoData主要是用來處理當類發生結構性的變化時處理數據初始化的,這麼說可能有點抽象,我們還拿上面的案例來說明。

public class User implements Serializable {

    private static final String TAG = "SerializationActivity";
    private static final long serialVersionUID = -5795919384959747554L;
    private String name;
    private int age;
    private transient String password;
    private Car car;

    // getter/setter
}

第一版本User類如上所示,這時候序列化User對象將其保存在SDCard上了,然後發現User取消性別字段,無法滿足需求,於是就有了下一版。

public class Person implements Serializable {
    private static final long serialVersionUID = -3824243371733653209L;
    private boolean sex;

    ...
}

public class User extends Person implements Serializable {
	...
}

在第二版本中User類繼承了Person,同時也有用了性別的屬性。此時User相對於第一版本中緩存的數據發生了結構性的變化,當使用第二版的User反序列化第一版的User信息時父類Person中的sex就沒辦法初始化了,只能使用boolean類型的默認值,也就是false了。那如何才能在反序列化過程中修改sex的值呢?就可以通過readObjectNoData方法來完成。

當反序列化過程中類發生了結構性的變化時readObjectNoData方法就會被調用,解決上面的問題我們就可以在Person中重寫readObjectNoData方法來對sex進行初始化操作。

private void readObjectNoData() throws ObjectStreamException {
    sex = true;
}

writeReplace

writeReplace方法會在writeObject方法之前被調用,它返回一個Object,用來替換當前需要序列化的對象,並且在其內部可以用this來調用當前對象的信息。

// 返回值Object則是真正被序列化的對象
private Object writeReplace() throws ObjectStreamException {
    // 新創建一個User對象
    User user = new User();
    // 新User的name爲當前對象的name值
    user.name = this.name;
    // 新User的age爲20
    user.age = 20;
    // 返回新User對象
    return user;
}

上面重寫了writeReplace方法,並新建一個User對象,其name賦值爲當前對象的namethis即表示當前對象。其age賦值爲20,然後返回新的user對象,之後writeObject方法就會被調用,將在writeReplace方法中返回的user對象進行序列化。在反序列化中的得到user信息與writeReplace方法中新建的user信息一致。

writeReplace方法中我們可以對其對象信息做一些過濾或者添加,甚至可以返回其他類型的對象都是可以的。只不過反序列化的過程也要做響應的轉換。

readResolve

readResolve方法會在readObject方法之後調用,返回值也是Object,它表示反序列化最終的對象。在其方法內部可以使用this表示最終反序列化對象。

private Object readResolve() throws ObjectStreamException {
    User user = new User();
    user.name = this.name;
    user.age = 20;
    return user;
}

這裏的實現代碼與writeReplace方式一致,也很好理解,就不過多解釋了。瞭解其運行機制之後至於怎麼用大家就可以腦洞大開了。

在上面瞭解到writeReplacereadResolve的訪問修飾符爲ANY-ACCESS-MODIFIER,及代表着可以是任意類型的權限修飾符,例如privateprotectedpublic。但是因爲這兩個方法主要的作用是用來處理當前類對象的序列化與反序列化,所以通常推薦使用private修飾,以防止其子類重寫。

Externalizable

Externalizable是Java提供的一個Serializable接口擴展的接口。

public interface Externalizable extends java.io.Serializable {
    void writeExternal(ObjectOutput out) throws IOException;
    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

使用也很簡單,與Serializable類似。

public class User implements Externalizable {
    private static final long serialVersionUID = -5795919384959747554L;
    private String name;
    private int age;
    
    ...
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        Log.d(TAG, "writeExternal: ");
        out.writeInt(age);
    }

    @Override
    public void readExternal(ObjectInput in) throws ClassNotFoundException, IOException {
        Log.d(TAG, "readExternal: ");
        age = in.read();
    }
}

Serializable的去別就是實現Externalizable接口必須重啓writeExternalreadExternal兩個方法,其功能就是實現序列化和反序列化的過程。與Serializable中的writeObjectreadObject功能一樣。另外使用Externalizable實現序列化需要提供一個public的無參構造函數,否則在反序列化的過程中拋出java.io.InvalidClassException: com.eonliu.sample.serialization.User; no valid constructor異常。

Serializable vs Externalizable

SerializableExternalizable都可以實現序列化,那麼他們有什麼區別呢?該如何選擇呢?

  • Serializable只是標記接口,其序列化過程都交給了JVM處理,使用相比Externalizable更簡單。
  • Externalizable並不是標記接口,實現它就必須重寫兩個方法來實現序列化和反序列化,相對複雜一點。
  • 由於Serializable把序列化和反序列化的過程都交給了JVM,所以在個別情況可能其效率不如Externalizable

所以通常情況下使用Serializable來實現序列化和反序列化過程即可。只有充分的瞭解到使用Externalizable實現其序列化和反序列化會使其效率有所提升才或者需要完全自定義序列化和反序列化過程才考慮使用Externalizable

郵箱:[email protected]

Github: https://github.com/Eon-Liu

CSDN:https://blog.csdn.net/EonLiu

發佈了2 篇原創文章 · 獲贊 1 · 訪問量 1310
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章