Java序列化,看这篇就够了!
一、序列化的含義、意義及使用場(chǎng)景
序列化:將對(duì)象寫入到IO流中
反序列化:從IO流中恢復(fù)對(duì)象
意義:序列化機(jī)制允許將實(shí)現(xiàn)序列化的Java對(duì)象轉(zhuǎn)換位字節(jié)序列,這些字節(jié)序列可以保存在磁盤上,或通過(guò)網(wǎng)絡(luò)傳輸,以達(dá)到以后恢復(fù)成原來(lái)的對(duì)象。序列化機(jī)制使得對(duì)象可以脫離程序的運(yùn)行而獨(dú)立存在。
使用場(chǎng)景:所有可在網(wǎng)絡(luò)上傳輸?shù)膶?duì)象都必須是可序列化的,比如RMI(remote method invoke,即遠(yuǎn)程方法調(diào)用),傳入的參數(shù)或返回的對(duì)象都是可序列化的,否則會(huì)出錯(cuò);所有需要保存到磁盤的java對(duì)象都必須是可序列化的。通常建議:程序創(chuàng)建的每個(gè)JavaBean類都實(shí)現(xiàn)Serializeable接口。
二、序列化實(shí)現(xiàn)的方式
如果需要將某個(gè)對(duì)象保存到磁盤上或者通過(guò)網(wǎng)絡(luò)傳輸,那么這個(gè)類應(yīng)該實(shí)現(xiàn)Serializable接口或者Externalizable接口之一。
1、Serializable
1.1 普通序列化
Serializable接口是一個(gè)標(biāo)記接口,不用實(shí)現(xiàn)任何方法。一旦實(shí)現(xiàn)了此接口,該類的對(duì)象就是可序列化的。
序列化步驟:
步驟一:創(chuàng)建一個(gè)ObjectOutputStream輸出流;
步驟二:調(diào)用ObjectOutputStream對(duì)象的writeObject輸出可序列化對(duì)象。
public class Person implements Serializable {
private String name;
private int age;
//我不提供無(wú)參構(gòu)造器
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + ''' +
", age=" + age +
'}';
}
}
pblic class WriteObject {
public static void main(String[] args) {
try (//創(chuàng)建一個(gè)ObjectOutputStream輸出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"))) {
//將對(duì)象序列化到文件s
Person person = new Person("9龍", 23);
oos.writeObject(person);
} catch (Exception e) {
e.printStackTrace();
}
}
}
反序列化步驟:
步驟一:創(chuàng)建一個(gè)ObjectInputStream輸入流;
步驟:調(diào)用ObjectInputStream對(duì)象的readObject()得到序列化的對(duì)象。
我們將上面序列化到person.txt的person對(duì)象反序列化回來(lái)
waht???? 輸出告訴我們,反序列化并不會(huì)調(diào)用構(gòu)造方法。反序列的對(duì)象是由JVM自己生成的對(duì)象,不通過(guò)構(gòu)造方法生成。
1.2 成員是引用的序列化
如果一個(gè)可序列化的類的成員不是基本類型,也不是String類型,那這個(gè)引用類型也必須是可序列化的;否則,會(huì)導(dǎo)致此類不能序列化。
看例子,我們新增一個(gè)Teacher類。將Person去掉實(shí)現(xiàn)Serializable接口代碼。
public?class?Person{//省略相關(guān)屬性與方法 } public?class?Teacher?implements?Serializable?{pivate?String?name;private?Person?person;pubic?Teacher(String?name,?Person?person)?{this.name?=?name;this.person?=?person;}pulic?static?void?main(String[]?args)?throws?Exception?{try?(ObjectOutputStream?oos?=?new?ObjectOutputStream(new?FileOutputStream("teacher.txt")))?{Person?person?=?new?Person("路飛",?20);Teacher?teacher?=?new?Teacher("雷利",?person);oos.writeObject(teacher);}} }我們看到程序直
錯(cuò),因?yàn)镻erson類的對(duì)象是不可序列化的,這導(dǎo)致了Teacher的對(duì)象不可序列化
1.3 同一對(duì)象序列化多次的機(jī)制
同一對(duì)象序列化多次,會(huì)將這個(gè)對(duì)象序列化多次嗎?答案是否定的。
public?class?WriteTeacher?{public?static?void?main(String[]?args)?throws?Exception?{try?(ObjectOutputStream?oos?=?new?ObjectOutputStream(new?FileOutputStream("teacher.txt")))?{Person?person?=?new?Person("路飛",?20);Teacher?t1?=?new?Teacher("雷利",?person);Teacher?t2?=?new?Teacher("紅發(fā)香克斯",?person);//依次將4個(gè)對(duì)象寫入輸入流oos.writeObject(t1);oos.writeObject(t2);oos.writeObject(person);oos.writeObject(t2);}} }依次將t1、t2、person、t2對(duì)象序列化到文件teacher.txt文件中。
注意:反序列化的順序與序列化時(shí)的順序一致。
public?class?ReadTeacher?{public?static?void?main(String[]?args)?{try?(ObjectInputStream?ois?=?new?ObjectInputStream(new?FileInputStream("teacher.txt")))?{Teacher?t1?=?(Teacher)?ois.readObject();Teacher?t2?=?(Teacher)?ois.readObject();Person?p?=?(Person)?ois.readObject();Teacher?t3?=?(Teacher)?ois.readObject();System.out.println(t1?==?t2);System.out.println(t1.getPerson()?==?p);System.out.println(t2.getPerson()?==?p);System.out.println(t2?==?t3);System.out.println(t1.getPerson()?==?t2.getPerson());}?catch?(Exception?e)?{e.printStackTrace();}} } //輸出結(jié)果 //false //true //true //true //true從輸出結(jié)果可以看出,Java序列化同一對(duì)象,并不會(huì)將此對(duì)象序列化多次得到多個(gè)對(duì)象。
Java序列化算法
所有保存到磁盤的對(duì)象都有一個(gè)序列化編碼號(hào)
當(dāng)程序試圖序列化一個(gè)象時(shí),會(huì)先檢查此對(duì)象是否已經(jīng)序列化過(guò),只有此對(duì)象從未(在此虛擬機(jī))被序列化過(guò),才會(huì)將此對(duì)象序列化為字節(jié)序列輸出。
如果此對(duì)象已經(jīng)序列化過(guò)則直接輸出編號(hào)即可。
圖示上述序列化過(guò)程。
1.4 java序列
法潛在的問(wèn)題
由于java序利化算法不會(huì)重復(fù)序列化同一個(gè)對(duì)象,只會(huì)記錄已序列化對(duì)象的編號(hào)。如果序列化一個(gè)可變對(duì)象(對(duì)象內(nèi)的內(nèi)容可更改)后,更改了對(duì)象內(nèi)容,再次序列化,并不會(huì)再次將此對(duì)象轉(zhuǎn)換為字節(jié)序列,而只是保存序列化編號(hào)。
public?class?WriteObject?{public?static?void?main(String[]?args)?{try?(ObjectOutputStream?oos?=?new?ObjectOutputStream(new?FileOutputStream("person.txt"));ObjectInputStream?ios?=?new?ObjectInputStream(new?FileInputStream("person.txt")))?{//第一次序列化personPerson?person?=?new?Person("9龍",?23);oos.writeObject(person);System.out.println(person);/修改nameperson.setName("海賊王");System.out.println(person);//第二次序列化personoos.writeObject(person);//次反序列化出p1、p2Person?p1?=?(Person)?ios.readObject();Person?p2?=?(Person)?ios.readObject();System.out.println(p1?==?p2);System.out.println(p1.getName().equals(p2.getName()));}?catch?(Exception?e)?{e.printStackTrace();}} } //輸出結(jié)果 //Person{name='9龍',?age=23} //Person{name='海賊王',?age=23} //true //true1.5 可選的自定義序列化
有些時(shí)候,我們有這樣的需求,某些屬性不需要序列化。使用transient關(guān)鍵字選擇不需要序列化的字段。
從輸出我們看到,使用transient修飾的屬性,java序列化時(shí),會(huì)忽略掉此字段,所以反序列化出的對(duì)象,被transient修飾的屬性是默認(rèn)值。對(duì)于引用類型,值是null;基本類型,值是0;boolean類型,值是false。
使用transient雖然簡(jiǎn)單,但將此屬性完全隔離在了序列化之外。java提供了可選的自定義序列化。可以進(jìn)行控制序列化的方式,或者對(duì)序列化數(shù)據(jù)進(jìn)行編碼加密等。
通過(guò)重寫writeObject與readObject方法,可以自己選擇哪些屬性需要序列化, 哪些屬性不需要。如果writeObject使用某種規(guī)則序列化,則相應(yīng)的readObject需要相反的規(guī)則反序列化,以便能正確反序列化出對(duì)象。這里展示對(duì)名字進(jìn)行反轉(zhuǎn)加密。
當(dāng)序列化流不完整時(shí),readObjectNoData()方法可以用來(lái)正確地初始化反序列化的對(duì)象。例如,使用不同類接收反序列化對(duì)象,或者序列化流被篡改時(shí),系統(tǒng)都會(huì)調(diào)用readObjectNoData()方法來(lái)初始化反序列化的對(duì)象。
更徹底的自定義序列化
ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
writeReplace:在序列化時(shí),會(huì)先調(diào)用此方法,再調(diào)用writeObject方法。此方法可將任意對(duì)象代替目標(biāo)序列化對(duì)象
readResolve:反序列化時(shí)替換反序列化出的對(duì)象,反序列化出來(lái)的對(duì)象被立即丟棄。此方法在readeObject后調(diào)用。
readResolve常用來(lái)反序列單例類,保證單例類的唯一性。
注意:readResolve與writeReplace的訪問(wèn)修飾符可以是private、protected、public,如果父類重寫了這兩個(gè)方法,子類都需要根據(jù)自身需求重寫,這顯然不是一個(gè)好的設(shè)計(jì)。通常建議對(duì)于final修飾的類重寫readResolve方法沒(méi)有問(wèn)題;否則,重寫readResolve使用private修飾。
2、Externalizable:強(qiáng)制自定義序列化
通過(guò)實(shí)現(xiàn)Externalizable接口,必須實(shí)現(xiàn)writeExternal、readExternal方法。
public?interface?Externalizable?extends?java.io.Serializable?{void?writeExternal(ObjectOutput?out)?throws?IOException;void?readExternal(ObjectInput?in)?throws?IOException,?ClassNotFoundException; } 復(fù)制代碼 public?class?ExPerson?implements?Externalizable?{private?String?ame;private?int?age;//注意,必須加上pulic?無(wú)參構(gòu)造器public?ExPerson()?{}public?ExPerson(Sring?name,?int?age)?{this.name?=?name;this.age?=?age;}@Overridepubic?void?writeExternal(ObjectOutput?out)?throws?IOException?{//將name反轉(zhuǎn)后寫入二進(jìn)制流StringBuffer?reverse?=?new?StringBuffer(name).reverse();System.out.println(reverse.toString());out.writeObject(reverse);out.writeInt(age);}@Overridepubli?void?readExternal(ObjectInput?in)?throws?IOException,?ClassNotFoundException?{//將讀取的字符串反轉(zhuǎn)后賦值給name實(shí)例變量this.name?=?((StringBuffer)?in.readObject()).reverse().toString();System.out.println(name);this.age?=?in.readInt();}public?static?void?ain(String[]?args)?throws?IOException,?ClassNotFoundException?{try?(ObjectOutputStream?oos?=?new?ObjectOutputStream(new?FileOutputStream("ExPerson.txt"));ObjectInputStream?ois?=?new?ObjectInputStream(new?FileInputStream("ExPerson.txt")))?{oos.writeObject(new?ExPerson("brady",?23));ExPerson?ep?=?(ExPerson)?ois.readObject();System.out.println(ep);}} } //輸出結(jié)果 //ydarb //brady //ExPerson{name='brady',?age=23}注意:Externalizable接口不同于Serializable接口,實(shí)現(xiàn)此接口必須實(shí)現(xiàn)接口中的兩個(gè)方法實(shí)現(xiàn)自定義序列化,這是強(qiáng)制性的;特別之處是必須提供pulic的無(wú)參構(gòu)造器,因?yàn)樵诜葱蛄谢臅r(shí)候需要反射創(chuàng)建對(duì)象。
3、兩種序列化對(duì)比
實(shí)現(xiàn)Serializable接口實(shí)現(xiàn)Externalizable接口系統(tǒng)自動(dòng)存儲(chǔ)必要的信息程序員決定存儲(chǔ)哪些信息Java內(nèi)建支持,易于實(shí)現(xiàn),只需要實(shí)現(xiàn)該接口即可,無(wú)需任何代碼支持必須實(shí)現(xiàn)接口內(nèi)的兩個(gè)方法性能略差性能略好
雖然Externalizable接口帶來(lái)了一定的性能提升,但變成復(fù)雜度也提高了,所以一般通過(guò)實(shí)現(xiàn)Serializable接口進(jìn)行序列化。
三、序列化版本號(hào)serialVersionUID
我們知道,反序列化必須擁有class文件,但隨著項(xiàng)目的升級(jí),class文件也會(huì)升級(jí),序列化怎么保證升級(jí)前后的兼容性呢?
java序列化提供了一個(gè)private static final long serialVersionUID 的序列化版本號(hào),只有版本號(hào)相同,即使更改了序列化屬性,對(duì)象也可以正確被反序列化回來(lái)。
public?class?Person?implements?Serializable?{//序列化版本號(hào)private?static?final?long?serialVersionUID?=?1111013L;private?String?name;private?int?age;//省略構(gòu)造方法及get,set }如果反序列化使用的class的版本號(hào)與序列化時(shí)使用的不一致,反序列化會(huì)報(bào)InvalidClassException異常。
序列化版本號(hào)可自由指定,如果不指定,JVM會(huì)根
息自己計(jì)算一個(gè)版本號(hào),這樣隨著class的升級(jí),就無(wú)法正確反序列化;不指定版本號(hào)另一個(gè)明顯隱患是,不利于jvm間的移植,可能class文件沒(méi)有更改,但不同jvm可能計(jì)算的規(guī)則不一樣,這樣也會(huì)導(dǎo)致無(wú)法反序列化。
什么情況下需要修改serialVersionUID呢?分三種情況。
如果只是修改了方法,反序列化不容影響,則無(wú)需修改版本號(hào);
如果只是修改了靜態(tài)變量,瞬態(tài)變量(transient修飾的變量),反序列化不受影響,無(wú)需修改版本號(hào);
如果修改了非瞬態(tài)變量,則可能導(dǎo)致反序列化失敗。如果新類中實(shí)例變量的類型與序列化時(shí)類的類型不一致,則會(huì)反序列化失敗,這時(shí)候需要更改serialVersionUID。如果只是新增了實(shí)例變量,則反序列化回來(lái)新增的是默認(rèn)值;如果減少了實(shí)例變量,反序列化時(shí)會(huì)忽略掉減少的實(shí)例變量。
四、總結(jié)
所有需要網(wǎng)絡(luò)傳輸?shù)膶?duì)象都需要實(shí)現(xiàn)序列化接口,通過(guò)建議所有的javaBean都實(shí)現(xiàn)Serializable接口。
對(duì)象的類名、實(shí)例變量(包括基本類型,數(shù)組,對(duì)其他對(duì)象的引用)都會(huì)被序列化;方法、類變量、transient實(shí)例變量都不會(huì)被序列化。
如果想讓某個(gè)變量不被序列化,使用transient修飾。
序列化對(duì)象的引用類型成員變量,也必須是可序列化的,否則,會(huì)報(bào)錯(cuò)。
反序列化時(shí)必須有序列化對(duì)象的class文件。
當(dāng)通過(guò)文件、網(wǎng)絡(luò)來(lái)讀取序列化后的對(duì)象時(shí),必須按照實(shí)際寫入的順序讀取。
單例類序列化,需要重寫readResolve()方法;否則會(huì)破壞單例原則。
同一對(duì)象序列化多次,只有第一次序列化為二進(jìn)制流,以后都只是保存序列化編號(hào),不會(huì)重復(fù)序列化。
建議所有可序列化的類加上serialVersionUID 版本號(hào),方便項(xiàng)目升級(jí)
更多資料展示如下,需要的小伙伴歡迎加群:Java架構(gòu)度寒冬,962992162
轉(zhuǎn)載于:https://blog.51cto.com/14378044/2406615
總結(jié)
以上是生活随笔為你收集整理的Java序列化,看这篇就够了!的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: HTTP 和 HTTP API 设计
- 下一篇: 个人作业