Serializable & Parcelable

對象序列化的簡單介紹

所謂對象的序列化其實就是把JVM運行過程當中生成的對象經過特殊的處理手段轉換爲字節形式的文件。轉換以後就能夠將其永久保存到磁盤中,或者以字節流進行網絡傳輸。java

在Android中使用Intent傳遞數據時,基本數據類型能夠直接傳遞,而比較複雜的引用類型的數據就須要先將對象序列化再進行傳遞。數據庫

序列化的轉換隻是將對象的屬性進行序列化,不針對方法進行序列化。數組

Android中有兩種實現序列化的方法,一種是實現Java提供的Serializable接口,另外一種是Android提供的Parcelable接口。bash

使用Serializable 序列化對象

Serializable是Java提供的接口(interface),它裏面沒有任何的屬性跟方法,純粹就是起到個標識的做用。若是想讓某個類下的對象可以序列化須要先實現Serializable接口。markdown

例如,咱們想讓一個Person類的對象可以序列化,這個類就須要被聲明爲:網絡

public class Person implements Serializable{

    private String name;
    private int age;
    
    ...
}
複製代碼

以後咱們就能夠將Person類的對象序列化寫入文件中永久保存了,這個環節你須要ObjectOutStream的幫助:學習

// 構造一個指定具體文件的ObjectOutStream ,path爲文件的路徑
ObjectOutputStream out = new ObjectOutputStream(Files.newOutPutStream(path));

//實例化對象
Person peter = new Person("peter" , 18);
Person mike = new Person("mike" , 20);

// 寫入對象
out.writeObject(peter);
out.writeObject(mike);

複製代碼

上面代碼就完成了寫入對象的操做,要想讀回對象的話須要用到ObjectInputStreamui

ObjectInputStream in = new ObjectInputStream(Files.newInPutStream(path));

// 讀取 peter
Person p1 = (Person) in.readObject(); 

// 讀取mike
Person p2 = (Person) in.readObject(); 
複製代碼

注意!讀取對象的順序與寫入對象的順序是一致的。this

若是序列化對象的屬性是基本數據類型的則會以二進制形式保存數據,若是屬性也是一個對象那麼它會被writeObject()再次寫入,直到全部屬性都是基本數據類型爲止。spa

還有一點,若是寫入的兩個對象裏引用了同一個對象,當讀取回這兩個對象時它們引用的對象仍是同一個,而不會是兩個內容相同倒是不一樣引用的對象。這歸功於在讀寫對象時會爲每一個對象記錄一個惟一序列號。

使用transient關鍵字忽略某些屬性

在實際中某些屬性是不須要被序列化的,例如數據庫鏈接對象就不必序列化,爲了實現某些屬性不被序列化,咱們能夠給這些屬性加上一個transient修飾標記符,那麼這些屬性在序列化時就會被自動忽略。

public class Person implements Serializable{

    private String name;
    private int age;
    
    // 不須要序列化的屬性
    private transient Connection mConn;
    ...
}
複製代碼

關於序列化版本

有時候咱們會將序列化的對象從一臺JVM傳到另外一臺JVM上運行,爲保證讀取的對象與寫入的對象一致,JVM在寫入對象的時候爲類分配了一個serialVersionUID屬性.

serialVersionUID屬性用來標識當前序列化對象的類版本,若是咱們沒有手動指定它,JVM會根據類的信息自動生成一個UID。但若是是兩臺JVM互傳數據時爲保證類的一致性,咱們最好本身手動聲明這個屬性:

public class Person implements Serializable{

    // 序列化的版本,本身定義具體數據來實現每次的版本更新
    private static final long serialVersionUID = 1L;

    private String name;
    private int age;
    
    // 不須要序列化的屬性
    private transient Connection mConn;
    ...
}
複製代碼

自定義序列化細節

到如今爲止咱們序列化對象的方法只是直接調用了Java的API,序列化的過程所有由Java幫咱們默認實現。可是有些狀況咱們須要在序列化時進行一些特殊處理,例如某些表示狀態的屬性序列化時不須要保存而反序列化成對象時但願可以被賦值,顯然transient關鍵字不能幫咱們實現,這時候咱們就須要自定義序列化的細節。

ObjectOutputStreamObjectInputStream在序列化與反序列化時會檢查咱們的類是否聲明瞭以下幾個方法:

  • void writeObject(ObjectOutputStream oos) throws IOException 序列化對象時調用的方法
  • void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException 反序列化對象時調用的方法
  • Object writeReplace() throws ObjectStreamException ObjectOutPutStream 序列化對象以前調用的方法,在這裏能夠替換真正被序列化的對象
  • Object readResolve() throws ObjectStreamException 在反序列化對象後調用的方法,在這裏能夠替換反序列化後獲得的對象

以上方法若是你本身聲明瞭那麼就執行你自定義的方法,不然使用系統默認的方法。至於自定義方法的權限修飾符private protected public都無所謂,由於使用ObjectXXXputStream使用反射調用的。他們在序列化與反序列化的調用流程以下圖。

序列化與反序列化流程

此四個方法你能夠根據須要任意替換成本身的方法,不過通常都是都是讀寫成對替換的,下面看咱們如何用自定義方法實現序列化:

public class Person implements Serializable{

    // 序列化的版本,本身定義具體數據來實現每次的版本更新
    private static final long serialVersionUID = 1L;

    private String name;
    private int age;
    
    // 不須要序列化的屬性
    private transient Connection mConn;

    public Person(String name,int age){
        this.name = name;
        this.age = age;  
    } 

    private void writeObject(ObjectOutputStream oos)  throws IOException {
        // 默認的序列化對象方法
        out.defaultWriteObject();
        //咱們自定義添加的東西
        out.writeInt(100);
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
          // 默認反序列化方法
         in.defaultReadObject();
         // 讀出咱們自定義添加的東西
         int flag = in.readInt();
         System.out.println(flag);
    }

    private Object writeReplace(){
         // 替換真正序列化的對象
         return new Person(name,age);
    }

    private Object readResolve(){
        // 替換反序列化後的對象
        return new Person(name,age);
    }
}
複製代碼

此處你可能對writeReplace()readResolve()方法的用處有疑問,在下面序列化代理中會見識到它們的用處。

關於反序列化須要注意的

從字節流中讀取的數據後反序列化的對象並非經過構造器建立的,那麼不少依賴於構造器保證的約束條件在對象反序列化時都沒法保證。好比一個設計成單例的類若是可以被序列化就能夠分分鐘克隆出多個實例...

序列化代理

在知道了Java在反序列化時並非經過構造器建立的對象,那麼別人只須要解析你序列化後的字節碼就可以垂手可得的獲取你的內容,不只如此,再利用一樣的序列化格式生成任意的字節碼送你你的程序分分鐘就攻破你的程序。

爲解決該隱患,大神們推薦咱們使用靜態內部類做爲代理來進行類的序列化:

public class Person implements Serializable{

    // 序列化的版本,本身定義具體數據來實現每次的版本更新
    private static final long serialVersionUID = 1L;

    private String name;
    private int age;
    
    // 不須要序列化的屬性
    private transient Connection mConn;

    public Person(String name,int age){
        this.name = name;
        this.age = age;  
    } 

    // 把真正要序列化的對象替換成PersonProxy 代理
    private Object writeReplace() {
        return new PersonProxy (this);
    }

    // 由於真正被序列化的對象是PersonProxy 代理對象,因此Person的readObject()方法永遠不會執行
    // 執行的是PersonProxy 代理對象的readObject()方法
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
        // 若是該方法被執行說明有流氓入侵,直接拋異常
        throw new InvalidObjectException("proxy requied");
    }

    static class PersonProxy implements Serializable{
        private String name;
        private int age;

        public PersonProxy(Person person){
            this.name = person.name;
            this.age = person.age;
        }

        // 把讀取出來的代理對象再替換回Person對象
        private Object readResolve(){
            return new Person(name,age);
        }
    }
}
複製代碼

使用Parcelable 序列化對象

Parcelable 雖然也是序列化對象的方法,可是它跟java提供的Serializable 在使用上有着極大的差異。

Android設計 Parcelable 的目的是讓其支持進程間通訊的功能,所以它不具有相似Serializable的版本功能,因此Parcelable 不適合永久存儲。

實現Parcelable 接口須要知足兩個條件:

  1. 實現Parcelable 接口下的兩個方法describeContents()writeToParcel(Parcel out,int flags)

  2. 聲明一個非空的靜態屬性CREATOR且類型爲Parcelable.Creator <T>

例如咱們想讓person類實現Parcelable 接口:

public class Person implements Parcelable {

     private int age;

     // 定義當前傳送的 Parcelable實例包含的特殊對象的類別
     public int describeContents() {
         // 通常狀況咱們用不到,直接爲0就行 
         return 0;
     }

     // 在該方法中將對象的屬性寫入字節流
     public void writeToParcel(Parcel out, int flags) {
         out.writeInt(age);
     }

     // 該靜態屬性會從Parcel 字節流中生成Parcelable類的實例
     public static final Parcelable.Creator<MyParcelable> CREATOR
             = new Parcelable.Creator<Person>() {
         
         // 該方法接收Parcel解析成對應的實例
         public Person createFromParcel(Parcel in) {
             return new Person(in);
         }
        
         // 根據size建立對應數量的實例數組
         public Person[] newArray(int size) {
             return new Person[size];
         }
     };
     
     private Person(Parcel in) {
         age= in.readInt();
     }
 }
複製代碼

到此Person就具有了序列化的條件。至於讀和寫就看具體的需求了,最簡單的使用方法能夠利用Intent傳遞。

讓我驚訝的是Parcelable 的使用並無那麼簡單,它牽扯出了一大堆進程間通訊相關的問題,待學習到進程間通訊時須要再從新梳理一遍。

相關文章
相關標籤/搜索