序列化與反序列化(Serializable與Parcelable)

前言:這幾天在考慮寫點啥筆記的時候,看到了有關於序列化的代碼 SerializableParcelable 接口實現,突然意識到自己很久沒關注這塊的知識了,所以又給重新整理了一下相關的內容,感覺這個東西還是很有用的。希望今天整理的這篇 序列化與反序列化(Serializable與Parcelable) 文章對小夥伴有所幫助。

概念簡介

序列化 :由於存在於內存中的變量都是暫時的,無法長期駐存,爲了把對象的狀態保持下來,把變量從內存中變成可存儲或傳輸的過程就叫做序列化。
反序列化 :反序列化恰恰是序列化的反向操作,也就是說,把已存在在磁盤或者其他介質中的對象,反序列化(讀取)到內存中,以便後續操作,而這個過程就叫做反序列化。

概括性來說序列化是指將對象實例的狀態存儲到存儲媒體(磁盤或者其他介質)的過程。在此過程中,先將對象的公共字段和私有字段以及類的名稱(包括類所在的程序集)轉換爲字節流,然後再把字節流寫入數據流。在隨後對對象進行反序列化時,將創建出與原對象完全相同的副本。

序列化的目的:

  1. 永久的保存對象數據(將對象數據保存在文件當中,或者是磁盤中)。
  2. 通過序列化操作將對象數據在網絡上進行傳輸(由於網絡傳輸是以字節流的方式對數據進行傳輸的.因此序列化的目的是將對象數據轉換成字節流的形式)。
  3. 將對象數據在進程之間進行傳遞(Activity之間傳遞對象數據時,需要在當前的Activity中對對象數據進行序列化操作.在另一個Activity中需要進行反序列化操作講數據取出)。
  4. Java平臺允許我們在內存中創建可複用的Java對象,但一般情況下,只有當JVM處於運行時,這些對象纔可能存在,即,這些對象的生命週期不會比JVM的生命週期更長(即每個對象都在JVM中)。但在現實應用中,就可能要停止JVM運行,但有要保存某些指定的對象,並在將來重新讀取被保存的對象。這時Java對象序列化就能夠實現該功能。(可選擇入數據庫、或文件的形式保存)
  5. 序列化對象的時候只是針對變量進行序列化,不針對方法進行序列化.
  6. 在Intent之間基本的數據類型直接進行相關傳遞即可,但是一旦數據類型比較複雜的時候,就需要進行序列化操作了.

在Android中一個對象實現序列化操作,該類就必須實現了Serializable接口或者Parcelable接口,其中Serializable接口是在java中的序列化抽象類,而Parcelable接口則是android中特有的序列化接口。

下面就分別介紹一下Serializable和Parcelable。

Serializable類

首先簡單介紹一下這個類:

public interface Serializable {
}

Serializable是一個標記接口,從上面的類中也可以看出來,他是一個空的接口。對於各種各樣的類,我們需要有一個類來對他們進行統一。但是Java中是單根繼承,而接口恰好彌補了這個地方。我們可以將實現的接口看做父類,這樣通過Serializable類就實現了多種類的統一,同時爲類打上了標籤,可以通過instanceof Serializable判斷是否能序列化。

使用Serializable實現類的序列化比較簡單,只要在類聲明中實現 Serializable 接口即可,同時強烈建議聲明序列化標識serialVersionUID 。

友情提示:我們在實現Serializable時,類會有一個序列化的值,這個值默認的情況下會隨我們Bean類中屬性成員變化而變化,如果對類成員屬性進行了修改,那麼再次讀取這個對象時,會因爲UID不同而拋出異常。所以我們在實現Serializable接口同時,最好在類中聲明UID。

serialVersionUID 是用來輔助序列化和反序列化過程的,原則上序列化後的對象中serialVersionUID只有和當前類的serialVersionUID相同才能夠正常被反序列化,也就是說序列化與反序列化的serialVersionUID必須相同才能夠使序列化操作成功。實際上我們不聲明serialVersionUID也是可以的,因爲在序列化過程中會自動生成一個serialVersionUID來標識序列化對象。

當一個類實現SerSerializable接口,沒有添加serialVersionUID的作用字段時,IDE會發出警告,這個字段可以手動指定一個值,比如1L,也可指定爲IED根據類的結構生成一個long值,它們的效果是一樣的。在序列化時會將這個值寫入存儲介質,反序列化時就校驗本地類的serialVersionUID和序列化介質中的是否一致,不一致將拋出異常 java.io.InvalidClassException。

異常信息如下:

java.io.InvalidClassException: StudentBean; local class incompatible: stream classdesc serialVersionUID = -8088515121892865631, local class serialVersionUID = 630907180238371889
	at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:687)
	at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1883)
	at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1749)
	at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2040)
	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1571)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
	at MyProject.reverseSerilizeData(MyProject.java:51)
	at MyProject.main(MyProject.java:15)
Exception in thread "main" java.lang.NullPointerException
	at MyProject.main(MyProject.java:16)

serialVersionUID補充:

(1)若不指定serialVersionUID:系統會根據類的結構計算出一個serialVersionUID,一旦類的結構發生改變這個值就會改變,將導致反序列化失敗;

(2)指定一個serialVersionUID值:當類的結構發生改變時,也可以不修改serialVersionUID的值,這種情況下能最大程度上通過反序列化回覆數據,若類的結構發生毀滅性的改變,例如字段數據類型改變了,也會導致反序列失敗。

友情提示: 類的結構發生改變指的是類的成員變量的改變,添加一個普通的方法是不會導致計算得到的serialVersionUID改變的。構造方法、toString()、getter/setter改變會引起serialVersionUID改變。

並且:

  • transient修飾的成員變量不參與序列化,反序列化時改成員爲該數據類型的默認值;
  • 靜態成員不參與序列化;
  • 反序列化得到的一個新對象的過程並沒有調用構造方法;

Serializable代碼示例

在IO流中有兩個類是用於讀寫對象的——ObjectInputStreamObjectOutputStream。ObjectOutputStream負責將內存中的對象寫入存儲中。ObjectInputStream負責從本地存儲中讀取存入的對象。而是用這兩個類,那麼傳入的這個類必須是序列化的。

接下來我們看一下代碼就知道了,寫一個Bean類,實現接口Serializable:

public class StudentBean implements Serializable{
        private static final long serialVersionUID = 630907180238371889L;
	private String name;	
	public StudentBean(String name) {
		super();
		this.name = name;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
}

接下來我們將序列化的Bean類通過類輸入流寫入文件:

StudentBean student = new StudentBean("Tom");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.txt"));
oos.writeObject(student);
oos.close();

最後我們將本地存儲對象的文件讀取出來:

ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.txt"));
StudentBean student = (StudentBean) ois.readObject();
ois.close();

可是如果我們沒有讓StudentBean類實現Serializable接口接回報出下面的異常:

java.io.NotSerializableException: StudentBean
	at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
	at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
	at MyProject.serilizeData(MyProject.java:30)
	at MyProject.main(MyProject.java:14)
java.io.WriteAbortedException: writing aborted; java.io.NotSerializableException: StudentBean
	at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1575)
	at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
	at MyProject.reverseSerilizeData(MyProject.java:51)
	at MyProject.main(MyProject.java:15)
Caused by: java.io.NotSerializableException: StudentBean
	at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
	at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
	at MyProject.serilizeData(MyProject.java:30)
	at MyProject.main(MyProject.java:14)
Exception in thread "main" java.lang.NullPointerException
	at MyProject.main(MyProject.java:16)

Serializable自定義序列化

序列化時可以重寫的方法:

  • writeReplace():序列化時,首先系統會先調用writeReplace方法,在這個階段,可以進行自己操作,將需要進行序列化的對象換成我們指定的對象, 一般很少重寫該方法。
  • writeObject():接着系統將調用writeObject方法,來將對象中的屬性一個個進行序列化,我們可以在這個方法中控制住哪些屬性需要序列化。
  • readObject():反序列化時,系統會調用readObject方法,將我們剛剛在writeObject方法序列化好的屬性,反序列化回來。然後通過readResolve方法,我們也可以指定系統返回給我們特定的對象可以不是writeReplace序列化時的對象,可以指定其他對象.
  • readResolve():通過readResolve方法,我們也可以指定系統返回給我們特定的對象可以不是writeReplace序列化時的對象,可以指定其他對象,一般很少重寫該方法。

直接代碼示例使用一下:

public class Person implements Serializable {
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }

    /**
     * 序列化時,
     * 首先系統會先調用writeReplace方法,在這個階段,
     * 可以進行自己操作,將需要進行序列化的對象換成我們指定的對象.
     * 一般很少重寫該方法
     */
    private Object writeReplace() throws ObjectStreamException {
        System.out.println("writeReplace invoked");
        return this;
    }

    /**
     * 接着系統將調用writeObject方法,來將對象中的屬性一個個進行序列化,
     * 我們可以在這個方法中控制住哪些屬性需要序列化. 這裏只序列化name屬性
     */
    private void writeObject(java.io.ObjectOutputStream out) throws IOException {
        System.out.println("writeObject invoked");
        out.writeObject(this.name == null ? "默認值" : this.name);
    }

    /**
     * 反序列化時,系統會調用readObject方法,將我們剛剛在writeObject方法序列化好的屬性,
     * 反序列化回來.然後通過readResolve方法,我們也可以指定系統返回給我們特定的對象
     * 可以不是writeReplace序列化時的對象,可以指定其他對象.
     */
    private void readObject(java.io.ObjectInputStream in) throws IOException,
            ClassNotFoundException {
        System.out.println("readObject invoked");
        this.name = (String) in.readObject();
        System.out.println("name:" + name);
    }

    /**
     * 通過readResolve方法,我們也可以指定系統返回給我們特定的對象
     * 可以不是writeReplace序列化時的對象,可以指定其他對象.一般很少重寫該方法
     */
    private Object readResolve() throws ObjectStreamException {
        System.out.println("readResolve invoked");
        return this;
    }
}

Parcelable類

鑑於Serializable在內存序列化上開銷比較大,而內存資源屬於android系統中的稀有資源(android系統分配給每個應用的內存開銷都是有限的),爲此android中提供了Parcelable接口來實現序列化操作,Parcelable的性能比Serializable好,在內存開銷方面較小,所以在內存間數據傳輸時推薦使用Parcelable,如通過Intent在activity間傳輸數據。

我們直接來看看Android中如何去使用Parcelable實現類的序列化操作流程吧:

  • writeToParcel 將對象數據序列化成一個Parcel對象(序列化之後成爲Parcel對象以便Parcel容器取出數據);
  • 重寫describeContents方法,默認值爲0; 
  • Public static final Parcelable.Creator<T>CREATOR (將Parcel容器中的數據轉換成對象數據) 同時需要實現兩個方法:  
    1. CreateFromParcel(從Parcel容器中取出數據並進行轉換);   
    2. newArray(int size)返回對象數據的大小;

Parcelable代碼示例

public class RectBean implements Parcelable {
  
    public RectBean() {
    }
    /** 返回當前對象的描述,0或1(大多數情況都返回0) */
    @Override
    public int describeContents() {
        return 0;
    } 
    /** 將當前對象寫入序列化結構,flag標識有0或1,1時標識當前對象作爲返回值返回(大多數情況返回0) */
    @Override
    public void writeToParcel(Parcel dest, int flags) {
    } 
    protected RectBean(Parcel in) {
    } 
    public static final Creator<RectBean> CREATOR = new Creator<RectBean>() {
        /** 從序列化後的對象中創建原始對象 */
        @Override
        public RectBean createFromParcel(Parcel source) {
            return new RectBean(source);
        }
 
        @Override
        public RectBean[] newArray(int size) {
            return new RectBean[size];
        }
    };
}

Parcel類

在這裏先要介紹一下里面出現很多的一個類——Parcel。這個類翻譯過來就是打包的意思,而我們序列化後的對象全都存儲在了這個裏面。其實Parcelable類只是一個外殼,而真正實現了序列化的,是Parcel這個類,它裏面有大量的方法,對各種數據進行序列化。

關於Parcel我們現在不用瞭解太多,我們只需要知道是它實現序列化和反序列化功能就夠了。我們在通過Parcel類進行序列化和反序列化操作時,對應的不同類型的屬性,需要調用不同的方法,在上面我們也看到了,Parcel衆多類型的方法,就是爲了對應不同類型的屬性。

下面介紹四種類型 : 除boolean之外的基本類型(int、float、char、)、boolean類型對象類型集合類型(List等等)。

1.除boolean外的基本類型

public class RectBean implements Parcelable {
    private float x,y;
    public RectBean() {
    }
    @Override
    public int describeContents() {
        return 0;
    } 
    /** 在writeToParcel中我們調用了Parcel的readFloat方法,將兩個屬性序列化,存儲到Parcel中*/
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeFloat(this.x);
        dest.writeFloat(this.y);
    }
    protected RectBean(Parcel in) {
        this.x = in.readFloat();
        this.y = in.readFloat();
    }
    public static final Creator<RectBean> CREATOR = new Creator<RectBean>() {
        /** 然後在createFromParcel中,又通過readFloat中,將序列化的對象還原成原對象 */
        @Override
        public RectBean createFromParcel(Parcel source) {
            return new RectBean(source);
        }
        @Override
        public RectBean[] newArray(int size) {
            return new RectBean[size];
        }
    };
}

2.boolean類型 : 由於沒有boolean類型存儲,我們可以用byte或者int類型來進行存儲。

public class RectBean implements Parcelable {
    private boolean isVisible;
    public RectBean() {
    }
    @Override
    public int describeContents() {
        return 0;
    }
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeByte(this.isVisible ? (byte) 1 : (byte) 0);
    }
 
    protected RectBean(Parcel in) {
        this.isVisible = in.readByte() != 0;
    }
    public static final Creator<RectBean> CREATOR = new Creator<RectBean>() {
        @Override
        public RectBean createFromParcel(Parcel source) {
            return new RectBean(source);
        }
 
        @Override
        public RectBean[] newArray(int size) {
            return new RectBean[size];
        }
    };
}

3.對象類型 

首先第一步,我們添加的對象屬性對應的類,一定也要被序列化。創建一個Point類,並讓它實現Parcelable接口:

public class Point implements Parcelable {
    protected Point(Parcel in) {
    }
    public static final Creator<Point> CREATOR = new Creator<Point>() {
        @Override
        public Point createFromParcel(Parcel in) {
            return new Point(in);
        }
        @Override
        public Point[] newArray(int size) {
            return new Point[size];
        }
    };
    @Override
    public int describeContents() {
        return 0;
    }
    @Override
    public void writeToParcel(Parcel dest, int flags) {
    }
}

然後在RectBean中添加Point類的屬性,這次調用的是Parcel的readParcelable方法和writeParcelable方法。但是在反序列化時候,需要用到屬性類的類加載器: this.point = in.readParcelable(Point.class.getClassLoader());

public class RectBean implements Parcelable {
    private Point point;
    public RectBean() {
    }
    @Override
    public int describeContents() {
        return 0;
    }
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeParcelable(this.point, flags);
    }
    protected RectBean(Parcel in) {
        this.point = in.readParcelable(Point.class.getClassLoader());
    }
    public static final Creator<RectBean> CREATOR = new Creator<RectBean>() {
        @Override
        public RectBean createFromParcel(Parcel source) {
            return new RectBean(source);
        }
        @Override
        public RectBean[] newArray(int size) {
            return new RectBean[size];
        }
    };
}

4.集合類型

友情提示:集合類型的屬性切記,一定要初始化!

public class RectBean implements Parcelable {
    private List<Point> points  = new ArrayList<>();
    public RectBean() {
    }
    @Override
    public int describeContents() {
        return 0;
    }
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeTypedList(this.points);
    }
 
    protected RectBean(Parcel in) {
        this.points = in.createTypedArrayList(Point.CREATOR);
    }
    public static final Creator<RectBean> CREATOR = new Creator<RectBean>() {
        @Override
        public RectBean createFromParcel(Parcel source) {
            return new RectBean(source);
        }
        @Override
        public RectBean[] newArray(int size) {
            return new RectBean[size];
        }
    };
}

友情提示如下:

1.我們如果說List存放的是String,那麼在序列化和反序列化對應的方法就是writeStringList和createStringArrayList:

dest.writeStringList(this.points);
.....
this.points = in.createStringArrayList();

2.如果是系統中自帶的類,那麼會調用writeList和readList方法,而在readList中也需要系統類的類加載器:

dest.writeList(this.points);
...
in.readList(this.points, Thread.class.getClassLoader());

3.而如果是自定義的類,那麼會調用writeTypedList和createTypedArrayList,在createTypedArrayList中將自定義類的CREATOR傳入(說明該自定義類也需要序列化)。

dest.writeTypedList(this.points);
...
this.points = in.createTypedArrayList(Point.CREATOR);

Parcelable與Serializable的比較

  1). 在內存的使用中,前者在性能方面要強於後者;

  2). 後者在序列化操作的時候會產生大量的臨時變量,(原因是使用了反射機制)從而導致GC的頻繁調用,因此在性能上會稍微遜色;

  3). Parcelable是以Ibinder作爲信息載體的.在內存上的開銷比較小,因此在內存之間進行數據傳遞的時候,Android推薦使用Parcelable,既然是內存方面比價有優勢,那麼自然就要優先選擇.

  4). 在讀寫數據的時候,Parcelable是在內存中直接進行讀寫,而Serializable是通過使用IO流的形式將數據讀寫入在硬盤上.

雖然Parcelable的性能要強於Serializable,但是仍然有特殊的情況需要使用Serializable,而不去使用Parcelable,因爲Parcelable無法將數據進行持久化,因此在將數據保存在磁盤的時候,仍然需要使用後者,因爲前者無法很好的將數據進行持久化(原因是在不同的Android版本當中,Parcelable可能會不同,因此數據的持久化方面仍然是使用Serializable)。

總結

Java應用程序中有Serializable來實現序列化操作,Android中有Parcelable來實現序列化操作,相關的性能也作出了比較,因此在Android中除了對數據持久化的時候需要使用到Serializable來實現序列化操作,其他的時候我們仍然需要使用Parcelable來實現序列化操作,因爲在Android中效率並不是最重要的,而是內存,通過比較Parcelable在效率和內存上都要優秀與Serializable,儘管Parcelable實現起來比較複雜,但是如果我們想要成爲一名優秀的Android軟件工程師,那麼我們就需要勤快一些去實現Parcelable,而不是偷懶與實現Serializable.

see you

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章