序列化是干什么的
簡(jiǎn)單說(shuō)就是為了保存在內(nèi)存中的各種對(duì)象的狀態(tài)(也就是實(shí)例變量,不是方法),并且可以把保存的對(duì)象狀態(tài)再讀出來(lái)。雖然你可以用你自己的各種各樣的方法來(lái)保 存object states,但是Java給你提供一種應(yīng)該比你自己好的保存對(duì)象狀態(tài)的機(jī)制,那就是序列化。
什么情況下需要序列化
當(dāng)你想把的內(nèi)存中的對(duì)象狀態(tài)保存到一個(gè)文件中或者數(shù)據(jù)庫(kù)中時(shí)候;
當(dāng)你想用套接字在網(wǎng)絡(luò)上傳送對(duì)象的時(shí)候;
當(dāng)你想通過(guò)RMI傳輸對(duì)象的時(shí)候;
序列化的幾種方式
在Java中socket傳輸數(shù)據(jù)時(shí),數(shù)據(jù)類(lèi)型往往比較難選擇??赡芤紤]帶寬、跨語(yǔ)言、版本的兼容等問(wèn)題。比較常見(jiàn)的做法有兩種:一是把對(duì)象包裝成JSON字符串傳輸,二是采用java對(duì)象的序列化和反序列化。隨著Google工具protoBuf的開(kāi)源,protobuf也是個(gè)不錯(cuò)的選擇。對(duì)JSON,Object Serialize,ProtoBuf 做個(gè)對(duì)比。
Object Serialize
Java的序列化機(jī)制是通過(guò)在運(yùn)行時(shí)判斷類(lèi)的serialVersionUID來(lái)驗(yàn)證版本一致性的。在進(jìn)行反序列化時(shí),JVM會(huì)把傳來(lái)的字節(jié)流中的serialVersionUID與本地相應(yīng)實(shí)體(類(lèi))的serialVersionUID進(jìn)行比較,如果相同就認(rèn)為是一致的,可以進(jìn)行反序列化,否則就會(huì)出現(xiàn)序列化版本不一致的異常。
serialVersionUID 用來(lái)表明類(lèi)的不同版本間的兼容性。有兩種生成方式:
一個(gè)是默認(rèn)的1L,比如:private static final long serialVersionUID = 1L;
一個(gè)是根據(jù)類(lèi)名、接口名、成員方法及屬性等來(lái)生成一個(gè)64位的哈希字段,比如: private static final long serialVersionUID = xxxxL;
下面來(lái)討論Java類(lèi)中為什么需要重載 serialVersionUID 屬性?
當(dāng)兩個(gè)進(jìn)程在進(jìn)行遠(yuǎn)程通信時(shí),彼此可以發(fā)送各種類(lèi)型的數(shù)據(jù)。無(wú)論是何種類(lèi)型的數(shù)據(jù),都會(huì)以二進(jìn)制序列的形式在網(wǎng)絡(luò)上傳送。發(fā)送方需要把這個(gè)Java對(duì)象轉(zhuǎn)換為字節(jié)序列,才能在網(wǎng)絡(luò)上傳送;接收方則需要把字節(jié)序列再恢復(fù)為Java對(duì)象。
把Java對(duì)象轉(zhuǎn)換為字節(jié)序列的過(guò)程稱(chēng)為對(duì)象的序列化。
把字節(jié)序列恢復(fù)為Java對(duì)象的過(guò)程稱(chēng)為對(duì)象的反序列化。
對(duì)象的序列化主要有兩種用途:(1)把對(duì)象的字節(jié)序列永久地保存到硬盤(pán)上,通常存放在一個(gè)文件中; (2)在網(wǎng)絡(luò)上傳送對(duì)象的字節(jié)序列;
java.io.ObjectOutputStream代表對(duì)象輸出流,它的writeObject(Object obj)方法可對(duì)參數(shù)指定的obj對(duì)象進(jìn)行序列化,把得到的字節(jié)序列寫(xiě)到一個(gè)目標(biāo)輸出流中。
java.io.ObjectInputStream代表對(duì)象輸入流,它的readObject()方法從一個(gè)源輸入流中讀取字節(jié)序列,再把它們反序列化為一個(gè)對(duì)象,并將其返回。
只有實(shí)現(xiàn)了Serializable和Externalizable接口的類(lèi)的對(duì)象才能被序列化。Externalizable接口繼承自Serializable接口,實(shí)現(xiàn)Externalizable接口的類(lèi)完全由自身來(lái)控制序列化的行為,而僅實(shí)現(xiàn)Serializable接口的類(lèi)可以采用默認(rèn)的序列化方式 。
凡是實(shí)現(xiàn)Serializable接口的類(lèi)都有一個(gè)表示序列化版本標(biāo)識(shí)符的靜態(tài)變量:private static final long serialVersionUID;
類(lèi)的serialVersionUID的默認(rèn)值完全依賴(lài)于Java編譯器的實(shí)現(xiàn),對(duì)于同一個(gè)類(lèi),用不同的Java編譯器編譯,有可能會(huì)導(dǎo)致不同的serialVersionUID,也有可能相同。為了提高serialVersionUID的獨(dú)立性和確定性,強(qiáng)烈建議在一個(gè)可序列化類(lèi)中顯示的定義serialVersionUID,為它賦予明確的值。顯式地定義serialVersionUID有兩種用途:
在某些場(chǎng)合,希望類(lèi)的不同版本對(duì)序列化兼容,因此需要確保類(lèi)的不同版本具有相同的serialVersionUID;在某些場(chǎng)合,不希望類(lèi)的不同版本對(duì)序列化兼容,因此需要確保類(lèi)的不同版本具有不同的serialVersionUID。
當(dāng)你序列化了一個(gè)類(lèi)實(shí)例后,希望更改一個(gè)字段或添加一個(gè)字段,不設(shè)置serialVersionUID,所做的任何更改都將導(dǎo)致無(wú)法反序化舊有實(shí)例,并在反序列化時(shí)拋出一個(gè)異常。如果你添加了serialVersionUID,在反序列舊有實(shí)例時(shí),新添加或更改的字段值將設(shè)為初始化值(對(duì)象為null,基本類(lèi)型為相應(yīng)的初始默認(rèn)值),字段被刪除將不設(shè)置。
相關(guān)注意事項(xiàng):
a)序列化時(shí),只對(duì)對(duì)象的狀態(tài)進(jìn)行保存,而不管對(duì)象的方法;
b)當(dāng)一個(gè)父類(lèi)實(shí)現(xiàn)序列化,子類(lèi)自動(dòng)實(shí)現(xiàn)序列化,不需要顯式實(shí)現(xiàn)Serializable接口;
c)當(dāng)一個(gè)對(duì)象的實(shí)例變量引用其他對(duì)象,序列化該對(duì)象時(shí)也把引用對(duì)象進(jìn)行序列化;
詳細(xì)描述:
序列化的過(guò)程就是對(duì)象寫(xiě)入字節(jié)流和從字節(jié)流中讀取對(duì)象。將對(duì)象狀態(tài)轉(zhuǎn)換成字節(jié)流之后,可以用java.io包中的各種字節(jié)流類(lèi)將其保存到文件中,管道到另一 線程中或通過(guò)網(wǎng)絡(luò)連接將對(duì)象數(shù)據(jù)發(fā)送到另一主機(jī)。對(duì)象序列化功能非常簡(jiǎn)單、強(qiáng)大,在RMI、Socket、JMS、EJB都有應(yīng)用。對(duì)象序列化問(wèn)題在網(wǎng)絡(luò) 編程中并不是最激動(dòng)人心的課題,但卻相當(dāng)重要,具有許多實(shí)用意義。
對(duì)象序列化可以實(shí)現(xiàn)分布式對(duì)象。主要應(yīng)用例如:RMI要利用對(duì)象序列化運(yùn)行遠(yuǎn)程主機(jī)上的服務(wù),就像在本地機(jī)上運(yùn)行對(duì)象時(shí)一樣。
java 對(duì)象序列化不僅保留一個(gè)對(duì)象的數(shù)據(jù),而且遞歸保存對(duì)象引用的每個(gè)對(duì)象的數(shù)據(jù)。可以將整個(gè)對(duì)象層次寫(xiě)入字節(jié)流中,可以保存在文件中或在網(wǎng)絡(luò)連接上傳遞。利用 對(duì)象序列化可以進(jìn)行對(duì)象的“深復(fù)制”,即復(fù)制對(duì)象本身及引用的對(duì)象本身。序列化一個(gè)對(duì)象可能得到整個(gè)對(duì)象序列。
從上面的敘述中,我們知道了對(duì)象序列化是java編程中的必備武器,那么讓我們從基礎(chǔ)開(kāi)始,好好學(xué)習(xí)一下它的機(jī)制和用法。
java序列化比較簡(jiǎn)單,通常不需要編寫(xiě)保存和恢復(fù)對(duì)象狀態(tài)的定制代碼。實(shí)現(xiàn)java.io.Serializable接口的類(lèi)對(duì)象可以轉(zhuǎn)換成字節(jié)流或從 字節(jié)流恢復(fù),不需要在類(lèi)中增加任何代碼。只有極少數(shù)情況下才需要定制代碼保存或恢復(fù)對(duì)象狀態(tài)。這里要注意:不是每個(gè)類(lèi)都可序列化,有些類(lèi)是不能序列化的, 例如涉及線程的類(lèi)與特定JVM有非常復(fù)雜的關(guān)系。
序列化機(jī)制:
序列化分為兩大部分:序列化和反序列化。序列化是這個(gè)過(guò)程的第一部分,將數(shù)據(jù)分解成字節(jié)流,以便存儲(chǔ)在文件中或在網(wǎng)絡(luò)上傳輸。反序列化就是打開(kāi)字節(jié)流并重構(gòu)對(duì)象。對(duì)象序列化不僅要將基本數(shù)據(jù)類(lèi)型轉(zhuǎn)換成字節(jié) 表示,有時(shí)還要恢復(fù)數(shù)據(jù)。恢復(fù)數(shù)據(jù)要求有恢復(fù)數(shù)據(jù)的對(duì)象實(shí)例。ObjectOutputStream中的序列化過(guò)程與字節(jié)流連接,包括對(duì)象類(lèi)型和版本信 息。反序列化時(shí),JVM用頭信息生成對(duì)象實(shí)例,然后將對(duì)象字節(jié)流中的數(shù)據(jù)復(fù)制到對(duì)象數(shù)據(jù)成員中。
處理對(duì)象流:序列化過(guò)程和反序列化過(guò)程
java.io包有兩個(gè)序列化對(duì)象的類(lèi)。ObjectOutputStream負(fù)責(zé)將對(duì)象寫(xiě)入字節(jié)流,ObjectInputStream從字節(jié)流重構(gòu)對(duì)象。
我們先了解ObjectOutputStream類(lèi)吧。ObjectOutputStream類(lèi)擴(kuò)展DataOutput接口。writeObject() 方法是最重要的方法,用于對(duì)象序列化。如果對(duì)象包含其他對(duì)象的引用,則writeObject()方法遞歸序列化這些對(duì)象。每個(gè) ObjectOutputStream維護(hù)序列化的對(duì)象引用表,防止發(fā)送同一對(duì)象的多個(gè)拷貝。(這點(diǎn)很重要)由于writeObject()可以序列化整 組交叉引用的對(duì)象,因此同一ObjectOutputStream實(shí)例可能不小心被請(qǐng)求序列化同一對(duì)象。這時(shí),進(jìn)行反引用序列化,而不是再次寫(xiě)入對(duì)象字節(jié)流。
// 序列化 today’s date 到一個(gè)文件中.
FileOutputStream f = new FileOutputStream(“tmp”); //創(chuàng)建一個(gè)包含恢復(fù)對(duì)象(即對(duì)象進(jìn)行反序列化信息)的”tmp”數(shù)據(jù)文件
ObjectOutputStream s = new ObjectOutputStream(f);
s.writeObject(“Today”); //寫(xiě)入字符串對(duì)象;
s.writeObject(new Date()); //寫(xiě)入瞬態(tài)對(duì)象;
s.flush();
現(xiàn)在,讓我們來(lái)了解ObjectInputStream這個(gè)類(lèi)。它與ObjectOutputStream相似。它擴(kuò)展DataInput接口。 ObjectInputStream中的方法鏡像DataInputStream中讀取Java基本數(shù)據(jù)類(lèi)型的公開(kāi)方法。readObject()方法從 字節(jié)流中反序列化對(duì)象。每次調(diào)用readObject()方法都返回流中下一個(gè)Object。對(duì)象字節(jié)流并不傳輸類(lèi)的字節(jié)碼,而是包括類(lèi)名及其簽名。 readObject()收到對(duì)象時(shí),JVM裝入頭中指定的類(lèi)。如果找不到這個(gè)類(lèi),則readObject()拋出 ClassNotFoundException,如果需要傳輸對(duì)象數(shù)據(jù)和字節(jié)碼,則可以用RMI框架。ObjectInputStream的其余方法用于定制反序列化過(guò)程。
//從文件中反序列化 string 對(duì)象和 date 對(duì)象
FileInputStream in = new FileInputStream(“tmp”);
ObjectInputStream s = new ObjectInputStream(in);
String today = (String)s.readObject(); //恢復(fù)對(duì)象;
Date date = (Date)s.readObject();
定制序列化過(guò)程:
序列化通??梢宰詣?dòng)完成,但有時(shí)可能要對(duì)這個(gè)過(guò)程進(jìn)行控制。java可以將類(lèi)聲明為serializable,但仍可手工控制聲明為static或transient的數(shù)據(jù)成員。
public class SimpleSerializableClass implements Serializable{
String sToday=”Today:”;
transient Date dtToday=new Date();
}
序 列化時(shí),類(lèi)的所有數(shù)據(jù)成員應(yīng)可序列化除了聲明為transient或static的成員。將變量聲明為transient告訴JVM我們會(huì)負(fù)責(zé)將變?cè)蛄?化。將數(shù)據(jù)成員聲明為transient后,序列化過(guò)程就無(wú)法將其加進(jìn)對(duì)象字節(jié)流中,沒(méi)有從transient數(shù)據(jù)成員發(fā)送的數(shù)據(jù)。后面數(shù)據(jù)反序列化時(shí), 要重建數(shù)據(jù)成員(因?yàn)樗穷?lèi)定義的一部分),但不包含任何數(shù)據(jù),因?yàn)檫@個(gè)數(shù)據(jù)成員不向流中寫(xiě)入任何數(shù)據(jù)。記住,對(duì)象流不序列化static或 transient。我們的類(lèi)要用writeObject()與readObject()方法以處理這些數(shù)據(jù)成員。使用writeObject()與 readObject()方法時(shí),還要注意按寫(xiě)入的順序讀取這些數(shù)據(jù)成員。
//重寫(xiě)writeObject()方法以便處理transient的成員。
public void writeObject(ObjectOutputStream outputStream) throws IOException{
outputStream.defaultWriteObject();//使定制的writeObject()方法可以利用自動(dòng)序列化中內(nèi)置的邏輯。
outputStream.writeObject(oSocket.getInetAddress());
outputStream.writeInt(oSocket.getPort());
}
//重寫(xiě)readObject()方法以便接收transient的成員。
private void readObject(ObjectInputStream inputStream) throws IOException,ClassNotFoundException{
inputStream.defaultReadObject();//defaultReadObject()補(bǔ)充自動(dòng)序列化
InetAddress oAddress=(InetAddress)inputStream.readObject();
int iPort =inputStream.readInt();
oSocket = new Socket(oAddress,iPort);
iID=getID();
dtToday =new Date();
}
完全定制序列化過(guò)程:
如果一個(gè)類(lèi)要完全負(fù)責(zé)自己的序列化,則實(shí)現(xiàn)Externalizable接口而不是Serializable接口。Externalizable接口定義包 括兩個(gè)方法writeExternal()與readExternal()。利用這些方法可以控制對(duì)象數(shù)據(jù)成員如何寫(xiě)入字節(jié)流.類(lèi)實(shí)現(xiàn) Externalizable時(shí),頭寫(xiě)入對(duì)象流中,然后類(lèi)完全負(fù)責(zé)序列化和恢復(fù)數(shù)據(jù)成員,除了頭以外,根本沒(méi)有自動(dòng)序列化。這里要注意了。聲明類(lèi)實(shí)現(xiàn) Externalizable接口會(huì)有重大的安全風(fēng)險(xiǎn)。writeExternal()與readExternal()方法聲明為public,惡意類(lèi)可 以用這些方法讀取和寫(xiě)入對(duì)象數(shù)據(jù)。如果對(duì)象包含敏感信息,則要格外小心。這包括使用安全套接或加密整個(gè)字節(jié)流。到此為至,我們學(xué)習(xí)了序列化的基礎(chǔ)部分知識(shí)。
以下來(lái)源于J2EE API:
對(duì)象的默認(rèn)序列化機(jī)制寫(xiě)入的內(nèi)容是:對(duì)象的類(lèi),類(lèi)簽名,以及非瞬態(tài)和非靜態(tài)字段的值。其他對(duì)象的引用(瞬態(tài)和靜態(tài)字段除外)也會(huì)導(dǎo)致寫(xiě)入那些對(duì)象??墒褂靡霉蚕頇C(jī)制對(duì)單個(gè)對(duì)象的多個(gè)引用進(jìn)行編碼,這樣即可將對(duì)象的圖形還原為最初寫(xiě)入它們時(shí)的形狀。
例如,要寫(xiě)入可通過(guò) ObjectInputStream 中的示例讀取的對(duì)象,請(qǐng)執(zhí)行以下操作:
FileOutputStream fos = new FileOutputStream(“t.tmp”);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeInt(12345);
oos.writeObject(“Today”);
oos.writeObject(new Date());
oos.close();
在序列化和反序列化過(guò)程中需要特殊處理的類(lèi)必須實(shí)現(xiàn)具有下列準(zhǔn)確簽名的特殊方法:
private void readObject(java.io.ObjectInputStream stream) throws IOException, ClassNotFoundException;
private void writeObject(java.io.ObjectOutputStream stream) throws IOException;
writeObject 方法負(fù)責(zé)寫(xiě)入特定類(lèi)的對(duì)象狀態(tài),以便相應(yīng)的 readObject 方法可以還原它。該方法本身不必與屬于對(duì)象的超類(lèi)或子類(lèi)的狀態(tài)有關(guān)。狀態(tài)是通過(guò)使用 writeObject 方法或使用 DataOutput 支持的用于基本數(shù)據(jù)類(lèi)型的方法將各個(gè)字段寫(xiě)入 ObjectOutputStream 來(lái)保存的。
序列化操作不寫(xiě)出沒(méi)有實(shí)現(xiàn) java.io.Serializable 接口的任何對(duì)象的字段。不可序列化的 Object 的子類(lèi)可以是可序列化的。在此情況下,不可序列化的類(lèi)必須有一個(gè)無(wú)參數(shù)構(gòu)造方法,以便允許初始化其字段。在此情況下,子類(lèi)負(fù)責(zé)保存和還原不可序列化的類(lèi)的 狀態(tài)。經(jīng)常出現(xiàn)的情況是,該類(lèi)的字段是可訪問(wèn)的(public、package 或 protected),或者存在可用來(lái)還原狀態(tài)的 get 和 set 方法。
實(shí)現(xiàn) writeObject 和 readObject 方法可以阻止對(duì)象的序列化,這時(shí)拋出 NotSerializableException。ObjectOutputStream 導(dǎo)致發(fā)生異常并中止序列化進(jìn)程。
實(shí) 現(xiàn) Externalizable 接口允許對(duì)象假定可以完全控制對(duì)象的序列化形式的內(nèi)容和格式。調(diào)用 Externalizable 接口的方法(writeExternal 和 readExternal)來(lái)保存和恢復(fù)對(duì)象的狀態(tài)。通過(guò)類(lèi)實(shí)現(xiàn)時(shí),它們可以使用 ObjectOutput 和 ObjectInput 的所有方法讀寫(xiě)它們自己的狀態(tài)。對(duì)象負(fù)責(zé)處理出現(xiàn)的任何版本控制。
Enum 常量的序列化不同于普通的 serializable 或 externalizable 對(duì)象。enum 常量的序列化形式只包含其名稱(chēng);常量的字段值不被傳送。為了序列化 enum 常量,ObjectOutputStream 需要寫(xiě)入由常量的名稱(chēng)方法返回的字符串。與其他 serializable 或 externalizable 對(duì)象一樣,enum 常量可以作為序列化流中后續(xù)出現(xiàn)的 back 引用的目標(biāo)。用于序列化 enum 常量的進(jìn)程不可定制;在序列化期間,由 enum 類(lèi)型定義的所有類(lèi)特定的 writeObject 和 writeReplace 方法都將被忽略。類(lèi)似地,任何 serialPersistentFields 或 serialVersionUID 字段聲明也將被忽略,所有 enum 類(lèi)型都有一個(gè) 0L 的固定的 serialVersionUID。
基本數(shù)據(jù)(不包括 serializable 字段和 externalizable 數(shù)據(jù))以塊數(shù)據(jù)記錄的形式寫(xiě)入 ObjectOutputStream 中。塊數(shù)據(jù)記錄由頭部和數(shù)據(jù)組成。塊數(shù)據(jù)部分包括標(biāo)記和跟在部分后面的字節(jié)數(shù)。連續(xù)的基本寫(xiě)入數(shù)據(jù)被合并在一個(gè)塊數(shù)據(jù)記錄中。塊數(shù)據(jù)記錄的分塊因子為 1024 字節(jié)。每個(gè)塊數(shù)據(jù)記錄都將填滿 1024 字節(jié),或者在終止塊數(shù)據(jù)模式時(shí)被寫(xiě)入。調(diào)用 ObjectOutputStream 方法 writeObject、defaultWriteObject 和 writeFields 最初只是終止所有現(xiàn)有塊數(shù)據(jù)記錄。
更多信息請(qǐng)查看IT技術(shù)專(zhuān)欄