對象序列化的簡單介紹
所謂對象的序列化其實就是把JVM運(yùn)行過程中生成的對象通過特殊的處理手段轉(zhuǎn)換為字節(jié)形式的文件。轉(zhuǎn)換之后就可以將其永久保存到磁盤中程储,或者以字節(jié)流進(jìn)行網(wǎng)絡(luò)傳輸。
在Android中使用Intent傳遞數(shù)據(jù)時,基本數(shù)據(jù)類型可以直接傳遞,而比較復(fù)雜的引用類型的數(shù)據(jù)就需要先將對象序列化再進(jìn)行傳遞梆惯。
序列化的轉(zhuǎn)換只是將對象的屬性進(jìn)行序列化,不針對方法進(jìn)行序列化吗垮。
Android中有兩種實現(xiàn)序列化的方法垛吗,一種是實現(xiàn)Java提供的Serializable
接口,另一種是Android提供的Parcelable
接口烁登。
使用Serializable 序列化對象
Serializable是Java提供的接口(interface)怯屉,它里面沒有任何的屬性跟方法,純粹就是起到個標(biāo)識的作用饵沧。如果想讓某個類下的對象能夠序列化需要先實現(xiàn)Serializable接口锨络。
例如,我們想讓一個Person類的對象能夠序列化狼牺,這個類就需要被聲明為:
public class Person implements Serializable{
private String name;
private int age;
...
}
之后我們就可以將Person類的對象序列化寫入文件中永久保存了羡儿,這個環(huán)節(jié)你需要ObjectOutStream
的幫助:
// 構(gòu)造一個指定具體文件的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);
上面代碼就完成了寫入對象的操作是钥,要想讀回對象的話需要用到ObjectInputStream
:
ObjectInputStream in = new ObjectInputStream(Files.newInPutStream(path));
// 讀取 peter
Person p1 = (Person) in.readObject();
// 讀取mike
Person p2 = (Person) in.readObject();
注意掠归!讀取對象的順序與寫入對象的順序是一致的缅叠。
如果序列化對象的屬性是基本數(shù)據(jù)類型的則會以二進(jìn)制形式保存數(shù)據(jù),如果屬性也是一個對象那么它會被writeObject()
再次寫入虏冻,直到所有屬性都是基本數(shù)據(jù)類型為止痪署。
還有一點(diǎn),如果寫入的兩個對象里引用了同一個對象兄旬,當(dāng)讀取回這兩個對象時它們引用的對象還是同一個,而不會是兩個內(nèi)容相同卻是不同引用的對象余寥。這歸功于在讀寫對象時會為每個對象記錄一個唯一序列號领铐。
使用transient關(guān)鍵字忽略某些屬性
在實際中某些屬性是不需要被序列化的,例如數(shù)據(jù)庫連接對象就沒必要序列化宋舷,為了實現(xiàn)某些屬性不被序列化绪撵,我們可以給這些屬性加上一個transient
修飾標(biāo)記符,那么這些屬性在序列化時就會被自動忽略祝蝠。
public class Person implements Serializable{
private String name;
private int age;
// 不需要序列化的屬性
private transient Connection mConn;
...
}
關(guān)于序列化版本
有時候我們會將序列化的對象從一臺JVM傳到另一臺JVM上運(yùn)行音诈,為保證讀取的對象與寫入的對象一致,JVM在寫入對象的時候為類分配了一個serialVersionUID
屬性.
serialVersionUID
屬性用來標(biāo)識當(dāng)前序列化對象的類版本绎狭,如果我們沒有手動指定它细溅,JVM會根據(jù)類的信息自動生成一個UID。但如果是兩臺JVM互傳數(shù)據(jù)時為保證類的一致性儡嘶,我們最好自己手動聲明這個屬性:
public class Person implements Serializable{
// 序列化的版本喇聊,自己定義具體數(shù)據(jù)來實現(xiàn)每次的版本更新
private static final long serialVersionUID = 1L;
private String name;
private int age;
// 不需要序列化的屬性
private transient Connection mConn;
...
}
自定義序列化細(xì)節(jié)
到現(xiàn)在為止我們序列化對象的方法只是直接調(diào)用了Java的API,序列化的過程全部由Java幫我們默認(rèn)實現(xiàn)蹦狂。但是有些情況我們需要在序列化時進(jìn)行一些特殊處理誓篱,例如某些表示狀態(tài)的屬性序列化時不需要保存而反序列化成對象時希望能夠被賦值,顯然transient
關(guān)鍵字不能幫我們實現(xiàn)凯楔,這時候我們就需要自定義序列化的細(xì)節(jié)窜骄。
ObjectOutputStream
和ObjectInputStream
在序列化與反序列化時會檢查我們的類是否聲明了如下幾個方法:
-
void writeObject(ObjectOutputStream oos) throws IOException
序列化對象時調(diào)用的方法 -
void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException
反序列化對象時調(diào)用的方法 -
Object writeReplace() throws ObjectStreamException ObjectOutPutStream
序列化對象之前調(diào)用的方法,在這里可以替換真正被序列化的對象 -
Object readResolve() throws ObjectStreamException
在反序列化對象后調(diào)用的方法摆屯,在這里可以替換反序列化后得到的對象
以上方法如果你自己聲明了那么就執(zhí)行你自定義的方法邻遏,否則使用系統(tǒng)默認(rèn)的方法。至于自定義方法的權(quán)限修飾符private protected public
都無所謂鸥拧,因為使用ObjectXXXputStream
使用反射調(diào)用的党远。他們在序列化與反序列化的調(diào)用流程如下圖。
此四個方法你可以根據(jù)需要任意替換成自己的方法富弦,不過一般都是都是讀寫成對替換的沟娱,下面看我們?nèi)绾斡米远x方法實現(xiàn)序列化:
public class Person implements Serializable{
// 序列化的版本,自己定義具體數(shù)據(jù)來實現(xiàn)每次的版本更新
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 {
// 默認(rèn)的序列化對象方法
out.defaultWriteObject();
//我們自定義添加的東西
out.writeInt(100);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
// 默認(rèn)反序列化方法
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()
方法的用處有疑問腕柜,在下面序列化代理中會見識到它們的用處济似。
關(guān)于反序列化需要注意的
從字節(jié)流中讀取的數(shù)據(jù)后反序列化的對象并不是通過構(gòu)造器創(chuàng)建的矫废,那么很多依賴于構(gòu)造器保證的約束條件在對象反序列化時都無法保證。比如一個設(shè)計成單例的類如果能夠被序列化就可以分分鐘克隆出多個實例...
序列化代理
在知道了Java在反序列化時并不是通過構(gòu)造器創(chuàng)建的對象砰蠢,那么別人只需要解析你序列化后的字節(jié)碼就能夠輕而易舉的獲取你的內(nèi)容蓖扑,不僅如此,再利用同樣的序列化格式生成任意的字節(jié)碼送你你的程序分分鐘就攻破你的程序台舱。
為解決該隱患律杠,大神們推薦我們使用靜態(tài)內(nèi)部類作為代理來進(jìn)行類的序列化:
public class Person implements Serializable{
// 序列化的版本,自己定義具體數(shù)據(jù)來實現(xiàn)每次的版本更新
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()方法永遠(yuǎn)不會執(zhí)行
// 執(zhí)行的是PersonProxy 代理對象的readObject()方法
private void readObject(ObjectInputStream stream) throws InvalidObjectException {
// 如果該方法被執(zhí)行說明有流氓入侵柜去,直接拋異常
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設(shè)計 Parcelable 的目的是讓其支持進(jìn)程間通信的功能嗓奢,因此它不具備類似Serializable的版本功能,所以Parcelable 不適合永久存儲浑厚。
實現(xiàn)Parcelable 接口需要滿足兩個條件:
實現(xiàn)Parcelable 接口下的兩個方法
describeContents()
和writeToParcel(Parcel out股耽,int flags)
。聲明一個非空的靜態(tài)屬性
CREATOR
且類型為Parcelable.Creator <T>
钳幅。
例如我們想讓person類實現(xiàn)Parcelable 接口:
public class Person implements Parcelable {
private int age;
// 定義當(dāng)前傳送的 Parcelable實例包含的特殊對象的類別
public int describeContents() {
// 一般情況我們用不到物蝙,直接為0就行
return 0;
}
// 在該方法中將對象的屬性寫入字節(jié)流
public void writeToParcel(Parcel out, int flags) {
out.writeInt(age);
}
// 該靜態(tài)屬性會從Parcel 字節(jié)流中生成Parcelable類的實例
public static final Parcelable.Creator<MyParcelable> CREATOR
= new Parcelable.Creator<Person>() {
// 該方法接收Parcel解析成對應(yīng)的實例
public Person createFromParcel(Parcel in) {
return new Person(in);
}
// 根據(jù)size創(chuàng)建對應(yīng)數(shù)量的實例數(shù)組
public Person[] newArray(int size) {
return new Person[size];
}
};
private Person(Parcel in) {
age= in.readInt();
}
}
到此Person就具備了序列化的條件。至于讀和寫就看具體的需求了敢艰,最簡單的使用方法可以利用Intent
傳遞茬末。
讓我驚訝的是Parcelable 的使用并沒有那么簡單,它牽扯出了一大堆進(jìn)程間通信相關(guān)的問題盖矫,待學(xué)習(xí)到進(jìn)程間通信時需要再重新梳理一遍丽惭。