概念
Java中的序列化是一種將對象持久化(比如存儲在磁盤)的手段鼻种。一般情況下椎木,程序運行(即JVM運行)時镰绎,Java對象(短暫)存儲在內(nèi)存中缘揪。但JVM停止運行后耍群,對象的狀態(tài)信息就不能保存在內(nèi)存了。我們需要將對象持久化保存找筝,這就是序列化的用途蹈垢。
簡單說,序列化就是呻征,將對象的狀態(tài)信息轉(zhuǎn)換為字節(jié)序列的過程耘婚。通過序列化,可以將對象的狀態(tài)信息轉(zhuǎn)換為字節(jié)序列陆赋,然后通過IO流保存到磁盤或者網(wǎng)絡(luò)傳輸。然后從磁盤或網(wǎng)絡(luò)請求獲取到字節(jié)流嚷闭,通過反序列化攒岛,重新創(chuàng)建該對象。
Serializable
Serializable是Java提供的序列化接口胞锰,實現(xiàn)Serializable接口的類灾锯,極為可序列化的類⌒衢牛可以直接通過ObjectOutputStream序列化顺饮,也可以通過ObjectInputStream反序列化吵聪。
以下便是Java序列化的典型用法
public class SerializableTest implements Serializable {
private final static long serialVersionUID = 1L;
private String name;
private String job;
private UnSerializable unSerializable;
public SerializableTest(String name, String job,UnSerializable unSerializable) {
this.name = name;
this.job = job;
this.unSerializable = unSerializable;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getJob() {
return job;
}
public void setJob(String job) {
this.job = job;
}
public UnSerializable getUnSerializable() {
return unSerializable;
}
public void setUnSerializable(UnSerializable unSerializable) {
this.unSerializable = unSerializable;
}
}
serialVersionUID
serialVersionUID是類的序列化ID,可以理解為這個類的版本號兼雄。serialVersionUID可以自己手動聲明吟逝,也可以由系統(tǒng)默認。系統(tǒng)默認的serialVersionUID是根據(jù)類的定義計算出一個serialVersionUID赦肋。如果類的定義發(fā)生了改變块攒,默認的serialVersionUID也會隨之變化,就像類的版本發(fā)生了改變一樣佃乘。
serialVersionUID的作用在于囱井,虛擬機是否允許反序列化,不僅僅是取決于類的路徑和功能代碼是否一致趣避。還有一個非常重要的判斷依據(jù)是庞呕,兩個類的serialVersionUID是否一致。
具體的場景就是:客戶端A和B通過網(wǎng)絡(luò)傳遞對象數(shù)據(jù)程帕,客戶端A將c對象序列化為字節(jié)流住练,通過網(wǎng)絡(luò)傳輸給客戶端B,然后B反序列化得到c骆捧。此時如果A和B端的app版本不同枪汪,而導致C類的serialVersionUID不同,就會導致B端反序列化失敗撒穷。
當然恋沃,有時候,我們也可以通過這種方法枫攀,強制app的低版本升級
Java官方是建議我們手動聲明serialVersionUID括饶。一般情況下沒有特殊需求,serialVersionUID就直接聲明為1L来涨。
成員變量序列化
一個對象的成員變量是不可序列化的對象的引用图焰,會導致對象序列化失敗,拋出NotSerializableException異常蹦掐。序列化要求技羔,對象的成員變量也是可序列化的。
靜態(tài)變量序列化
序列化保存的是對象的狀態(tài)信息卧抗,靜態(tài)變量屬于類的狀態(tài)藤滥,因此,序列化不保存靜態(tài)變量社裆。
父類的序列化
一個子類實現(xiàn)了Serializable接口拙绊,它的父類沒有實現(xiàn)Serializable接口。序列化該子類對象,然后反序列化輸出其父類定義的變量的值标沪,該變量的值和序列化之前不同榄攀。
如果父類沒有實現(xiàn)Serializable接口,虛擬機是不會序列化父類對象的金句。此時就要求檩赢,父類必須有無參構(gòu)造函數(shù)。而一個Java對象的構(gòu)造必須先有父類對象趴梢,再創(chuàng)建子類對象漠畜,反序列化時也不例外。所以反序列化時坞靶,虛擬機只能調(diào)用父類的無參構(gòu)造函數(shù)來創(chuàng)建父類對象憔狞。這樣,父類定義的成員變量的值彰阴,就是調(diào)用無參構(gòu)造函數(shù)后的值瘾敢。如果你考慮到了這種序列化的情況,就可以在父類的無參構(gòu)造函數(shù)對成員變量賦值尿这。
Transient
Transient關(guān)鍵字的作用就是控制變量的序列化簇抵,在變量聲明前聲明Transient,表示該變量不會被序列化射众。在反序列化時碟摆,該變量值就會是該類型的初始值,如int型為0叨橱,String型為null典蜕。
自定義readObject和WriteObject
將對象序列化后,進行網(wǎng)絡(luò)傳輸罗洗,就要考慮到數(shù)據(jù)的安全性問題愉舔。這樣的場景,可以考慮封裝自定義的writeObject和readObject方法伙菜。這樣可以序列化時轩缤,對數(shù)據(jù)進行加密,反序列化時解密贩绕。
public class SerializableTest implements Serializable {
private final static long serialVersionUID = 1L;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
private void writeObject(ObjectOutputStream out) {
try {
ObjectOutputStream.PutField putFields = out.putFields();
System.out.println("原name:" + name);
name = "encryption-"+name;//模擬加密
putFields.put("name", name);
System.out.println("加密后的name" + name);
out.writeFields();
} catch (IOException e) {
e.printStackTrace();
}
}
private void readObject(ObjectInputStream in) {
try {
ObjectInputStream.GetField readFields = in.readFields();
Object object = readFields.get("name", "");
System.out.println("要解密的字符串:" + object.toString());
name = object.toString().split("-")[1];//模擬解密
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
序列化存儲規(guī)則
Java序列化機制為了節(jié)省磁盤空間火的,具有特定的存儲規(guī)則,當多次序列化寫入文件的是同一對象時淑倾,并不會再將對象的內(nèi)容寫入文件卫玖,而只是再存儲一個引用,此時寫入文件的就是新增的引用和一些控制信息踊淳。反序列化時,恢復引用關(guān)系,使得所有的引用指向的是同一個對象迂尝。這樣的特點脱茉,會帶來另一個值得注意的問題:
File file = new File("H:/sourceCode/workspace4java/test.txt");
SerializableTest testObject = new SerializableTest("Smith","engineer",32,new UnSerializable("1024"));
FileOutputStream fos = new FileOutputStream(file);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(testObject);
oos.flush();
testObject.setName("James");
oos.writeObject(testObject);
oos.close();
FileInputStream fis = new FileInputStream(file);
ObjectInputStream ois = new ObjectInputStream(fis);
SerializableTest result1 = (SerializableTest)ois.readObject();
SerializableTest result2 = (SerializableTest)ois.readObject();
System.out.println(result1.getName());
System.out.println(result2.getName());
輸出結(jié)果是
Smith
Smith
我們希望看到的是第二次反序列化的對象name為James,但是可以看到垄开,兩次反序列化的對象的name一樣琴许。這是因為第二次序列化寫入時,虛擬機根據(jù)引用關(guān)系溉躲,已經(jīng)判斷出寫入的是同一對象榜田,因此只保存了第二次的引用。所以反序列化讀取時锻梳,都是第一次保存的對象箭券。
Parcelable
Parcelable是Android提供的序列化接口。它相比于Serializable的優(yōu)點是疑枯,序列化的效率要高一些辩块。 實現(xiàn)了 Parcelable 接口的類在序列化和反序列化時會被轉(zhuǎn)換為 Parcel 類型的數(shù)據(jù) 。 Parcel 是一個載體荆永,它可以包含數(shù)據(jù)或者對象引用废亭,然后通過 IBinder 在進程間傳遞。
Parcelable和Serializable的對比:
- 一般在保存數(shù)據(jù)到 SD 卡或者網(wǎng)絡(luò)傳輸時建議使用 Serializable 即可具钥,雖然效率差一些豆村,好在使用方便。
- 而在運行時數(shù)據(jù)傳遞時建議使用 Parcelable骂删,比如 Intent掌动,Bundle 等,Android 底層做了優(yōu)化處理桃漾,效率很高坏匪。
本文參考:
[https://www.ibm.com/developerworks/cn/java/j-lo-serial/index.html
https://www.hollischuang.com/archives/1140
https://blog.csdn.net/u011240877/article/details/72455715