Android進階之路——Serializable序列化

簡介

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

在Android中序列化最多見的使用場景就是緩存數據了。如今的App中基本須要緩存數據,例如緩存用戶登陸信息。git

// 用來保存用戶信息
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接口就能夠了。github

Serializable

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

package java.io;
public interface Serializable {
}
複製代碼

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

若是想要使User可被序列化只要實現Serializable接口便可。ide

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字段了。this

總結:加密

  • 儘可能顯示聲明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

郵箱:eonliu1024@gmail.com

Github: github.com/Eon-Liu

CSDN:blog.csdn.net/EonLiu

相關文章
相關標籤/搜索