Android進階之路——Serializable序列化

簡介

序列化 (Serialization)是將對象的狀態(tài)信息轉換為可以存儲或傳輸的形式的過程。在序列化期間,對象將其當前狀態(tài)寫入到臨時或持久性存儲區(qū)。以后,可以通過從存儲區(qū)中讀取或反序列化對象的狀態(tài),重新創(chuàng)建該對象?!俣劝倏?。

在Android中序列化最常見的使用場景就是緩存數據了?,F在的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生成的版本號的能不一致,類的結構也可能發(fā)生變化等這些因素都可能導致序列化時候的版本號和反序列化時的版本號不止一次導致運行時拋出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接口時靜態(tài)變量(被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
}

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

靜態(tài)成員返回序列化時會取內存中的值,被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也可以被序列化。這時sex、nameage這三個字段都會被序列化。

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

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異常?;蛘呤褂?/code>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序列化機制中可以用來干預序列化過程的五個方法,他們具體能感謝什么繼續(xù)往下看。

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主要是用來處理當類發(fā)生結構性的變化時處理數據初始化的,這么說可能有點抽象,我們還拿上面的案例來說明。

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上了,然后發(fā)現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相對于第一版本中緩存的數據發(fā)生了結構性的變化,當使用第二版的User反序列化第一版的User信息時父類Person中的sex就沒辦法初始化了,只能使用boolean類型的默認值,也就是false了。那如何才能在反序列化過程中修改sex的值呢?就可以通過readObjectNoData方法來完成。

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

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

writeReplace

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

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

上面重寫了writeReplace方法,并新建一個User對象,其name賦值為當前對象的name,this即表示當前對象。其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,及代表著可以是任意類型的權限修飾符,例如private、protectedpublic。但是因為這兩個方法主要的作用是用來處理當前類對象的序列化與反序列化,所以通常推薦使用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都可以實現序列化,那么他們有什么區(qū)別呢?該如何選擇呢?

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

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

郵箱:eonliu1024@gmail.com

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

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,238評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 98,430評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,134評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,893評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,653評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,136評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,212評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,372評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 48,888評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,738評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,939評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,482評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,179評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,588評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,829評論 1 283
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,610評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,916評論 2 372