文件I/O:文件流→序列化
★文件流
文件操作是最簡(jiǎn)單最直接也是最容易想到的一種方式,我們說的文件操作不僅僅是通過FileInputStream/FileOutputStream這么“裸”的方式直接把數(shù)據(jù)寫入到本地文件(像我以前寫的一個(gè)掃雷的小游戲JavaMine就是這樣保存一局的狀態(tài)的),這樣就比較“底層”了。
主要類與方法和描述
FileInputStream.read() //從本地文件讀取二進(jìn)制格式的數(shù)據(jù)
FileReader.read() //從本地文件讀取字符(文本)數(shù)據(jù)
FileOutputStream.write() //保存二進(jìn)制數(shù)據(jù)到本地文件
FileWriter.write() //保存字符數(shù)據(jù)到本地文件
★XML
和上面的單純的I/O方式相比,XML就顯得“高檔”得多,以至于成為一種數(shù)據(jù)交換的標(biāo)準(zhǔn)。以DOM方式為例,它關(guān)心的是首先在內(nèi)存中構(gòu)造文檔樹,數(shù)據(jù)保存在某個(gè)結(jié)點(diǎn)上(可以是葉子結(jié)點(diǎn),也可以是標(biāo)簽結(jié)點(diǎn)的屬性),構(gòu)造好了以后一次性的寫入到外部文件,但我們只需要知道文件的位置,并不知道I/O是怎么操作的,XML操作方式可能多數(shù)人也實(shí)踐過,所以這里也只列出相關(guān)的方法,供初學(xué)者預(yù)先了解一下。主要的包是javax.xml.parsers,org.w3c.dom,javax.xml.transform。
主要類與方法和描述
DocumentBuilderFactory.newDocumentBuilder().parse() //解析一個(gè)外部的XML文件,得到一個(gè)Document對(duì)象的DOM樹
DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument() //初始化一棵DOM樹
Document.getDocumentElement().appendChild() //為一個(gè)標(biāo)簽結(jié)點(diǎn)添加一個(gè)子結(jié)點(diǎn)
Document.createTextNode() //生成一個(gè)字符串結(jié)點(diǎn)
Node.getChildNodes() //取得某個(gè)結(jié)點(diǎn)的所有下一層子結(jié)點(diǎn)
Node.removeChild() //刪除某個(gè)結(jié)點(diǎn)的子結(jié)點(diǎn)
Document.getElementsByTagName() 查找所有指定名稱的標(biāo)簽結(jié)點(diǎn)
Document.getElementById() //查找指定名稱的一個(gè)標(biāo)簽結(jié)點(diǎn),如果有多個(gè)符合,則返回某一個(gè),通常是第一個(gè)
Element.getAttribute() //取得一個(gè)標(biāo)簽的某個(gè)屬性的的值
Element.setAttribute() //設(shè)置一個(gè)標(biāo)簽的某個(gè)屬性的的值
Element.removeAttribute() //刪除一個(gè)標(biāo)簽的某個(gè)屬性
TransformerFactory.newInstance().newTransformer().transform() //將一棵DOM樹寫入到外部XML文件
★序列化
使用基本的文件讀寫方式存取數(shù)據(jù),如果我們僅僅保存相同類型的數(shù)據(jù),則可以用同一種格式保存,譬如在我的JavaMine中保存一個(gè)盤局時(shí),需要保存每一個(gè)方格的坐標(biāo)、是否有地雷,是否被翻開等,這些信息組合成一個(gè)“復(fù)合類型”;相反,如果有多種不同類型的數(shù)據(jù),那我們要么把它分解成若干部分,以相同類型(譬如String)保存,要么我們需要在程序中添加解析不同類型數(shù)據(jù)格式的邏輯,這就很不方便。于是我們期望用一種比較“高”的層次上處理數(shù)據(jù),程序員應(yīng)該花盡可能少的時(shí)間和代碼對(duì)數(shù)據(jù)進(jìn)行解析,事實(shí)上,序列化操作為我們提供了這樣一條途徑。
序列化(Serialization)大家可能都有所接觸,它可以把對(duì)象以某種特定的編碼格式寫入或從外部字節(jié)流(即ObjectInputStream/ObjectOutputStream)中讀取。序列化一個(gè)對(duì)象非常之簡(jiǎn)單,僅僅實(shí)現(xiàn)一下Serializable接口即可,甚至都不用為它專門添加任何方法:
public class MySerial implements java.io.Serializable
{
//...
}
但有一個(gè)條件:即你要序列化的類當(dāng)中,它的每個(gè)屬性都必須是是“可序列化”的。這句話說起來有點(diǎn)拗口,其實(shí)所有基本類型(就是int,char,boolean之類的)都是“可序列化”的,而你可以看看JDK文檔,會(huì)發(fā)現(xiàn)很多類其實(shí)已經(jīng)實(shí)現(xiàn)了Serializable(即已經(jīng)是“可序列化”的了),于是這些類的對(duì)象以及基本數(shù)據(jù)類型都可以直接作為你需要序列化的那個(gè)類的內(nèi)部屬性。如果碰到了不是“可序列化”的屬性怎么辦?對(duì)不起,那這個(gè)屬性的類還需要事先實(shí)現(xiàn)Serializable接口,如此遞歸,直到所有屬性都是“可序列化”的。
主要類與方法和描述
ObjectOutputStream.writeObject() //將一個(gè)對(duì)象序列化到外部字節(jié)流
ObjectInputStream.readObject() //從外部字節(jié)流讀取并重新構(gòu)造對(duì)象
從實(shí)際應(yīng)用上看來,“Serializable”這個(gè)接口并沒有定義任何方法,仿佛它只是一個(gè)標(biāo)記(或者說像是Java的關(guān)鍵字)而已,一旦虛擬機(jī)看到這個(gè)“標(biāo)記”,就會(huì)嘗試調(diào)用自身預(yù)定義的序列化機(jī)制,除非你在實(shí)現(xiàn)Serializable接口的同時(shí)還定義了私有的readObject()或writeObject()方法。這一點(diǎn)很奇怪。不過你要是不愿意讓系統(tǒng)使用缺省的方式進(jìn)行序列化,那就必須定義上面提到的兩個(gè)方法:
public class MySerial implements java.io.Serializable
{
private void writeObject(java.io.ObjectOutputStream out) throws IOException
{
//...
}
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException
{
//...
}
//...
}
譬如你可以在上面的writeObject()里調(diào)用默認(rèn)的序列化方法ObjectOutputStream.defaultWriteObject();譬如你不愿意將某些敏感的屬性和信息序列化,你也可以調(diào)用ObjectOutputStream.writeObject()方法明確指定需要序列化那些屬性。關(guān)于用戶可定制的序列化方法,我們將在后面提到。
★Bean
上面的序列化只是一種基本應(yīng)用,你把一個(gè)對(duì)象序列化到外部文件以后,用notepad打開那個(gè)文件,只能從為數(shù)不多的一些可讀字符中猜到這是有關(guān)這個(gè)類的信息文件,這需要你熟悉序列化文件的字節(jié)編碼方式,那將是比較痛苦的(在《Core Java 2》第一卷里提到了相關(guān)編碼方式,有興趣的話可以查看參考資料),某些情況下我們可能需要被序列化的文件具有更好的可讀性。另一方面,作為Java組件的核心概念“JavaBeans”,從JDK 1.4開始,其規(guī)范里也要求支持文本方式的“長(zhǎng)期的持久化”(long-term persistence)。
打開JDK文檔,java.beans包里的有一個(gè)名為“Encoder”的類,這就是一個(gè)可以序列化bean的實(shí)用類。和它相關(guān)的兩個(gè)主要類有XMLEcoder和XMLDecoder,顯然,這是以XML文件的格式保存和讀取bean的工具。他們的用法也很簡(jiǎn)單,和上面ObjectOutputStream/ObjectInputStream比較類似。
主要類與方法和描述
XMLEncoder.writeObject() //將一個(gè)對(duì)象序列化到外部字節(jié)流
XMLDecoder.readObject() //從外部字節(jié)流讀取并重新構(gòu)造對(duì)象
如果一個(gè)bean是如下格式:
public class MyBean
{
int i;
char[] c;
String s;
//...(get和set操作省略)...
}
那么通過XMLEcoder序列化出來的XML文件具有這樣的形式:
<?xml version="1.0" encoding="UTF-8"?>
<java version="1.4.0" class="java.beans.XMLDecoder">
<object class="MyBean">
<void property="i">
<int>1</int>
</void>
<void property="c">
<array class="char" length="3">
<void index="0">
<int>a</int>
</void>
<void index="1">
<int>b</int>
</void>
<void index="2">
<int>c</int>
</void>
</array>
</void>
<void property="s">
<string>fox jump!</string>
</void>
</object>
</java>
像AWT和Swing中很多可視化組件都是bean,當(dāng)然也是可以用這種方式序列化的,下面就是從JDK文檔中摘錄的一個(gè)JFrame序列化以后的XML文件:
<?xml version="1.0" encoding="UTF-8"?>
<java version="1.0" class="java.beans.XMLDecoder">
<object class="javax.swing.JFrame">
<void property="name">
<string>frame1</string>
</void>
<void property="bounds">
<object class="java.awt.Rectangle">
<int>0</int>
<int>0</int>
<int>200</int>
<int>200</int>
</object>
</void>
<void property="contentPane">
<void method="add">
<object class="javax.swing.JButton">
<void property="label">
<string>Hello</string>
</void>
</object>
</void>
</void>
<void property="visible">
<boolean>true</boolean>
</void>
</object>
</java>
因此但你想要保存的數(shù)據(jù)是一些不是太復(fù)雜的類型的話,把它做成bean再序列化也不失為一種方便的選擇。
★Properties
在以前我總結(jié)的一篇關(guān)于集合框架的小文章里提到過,Properties是歷史集合類的一個(gè)典型的例子,這里主要不是介紹它的集合特性。大家可能都經(jīng)常接觸一些配置文件,如Windows的ini文件,Apache的conf文件,還有Java里的properties文件等,這些文件當(dāng)中的數(shù)據(jù)以“關(guān)鍵字-值”對(duì)的方式保存。“環(huán)境變量”這個(gè)概念都知道吧,它也是一種“key-value”對(duì),以前也常常看到版上問“如何取得系統(tǒng)某某信息”之類的問題,其實(shí)很多都保存在環(huán)境變量里,只要用一條
System.getProperties().list(System.out);
就能獲得全部環(huán)境變量的列表:
-- listing properties --
java.runtime.name=Java(TM) 2 Runtime Environment, Stand...
sun.boot.library.path=C:\Program Files\Java\j2re1.4.2_05\bin
java.vm.version=1.4.2_05-b04
java.vm.vendor=Sun Microsystems Inc.
java.vendor.url=http://java.sun.com/
path.separator=;
java.vm.name=Java HotSpot(TM) Client VM
file.encoding.pkg=sun.io
user.country=CN
sun.os.patch.level=Service Pack 1
java.vm.specification.name=Java Virtual Machine Specification
user.dir=d:\my documents\項(xiàng)目\eclipse\SWTDemo
java.runtime.version=1.4.2_05-b04
java.awt.graphicsenv=sun.awt.Win32GraphicsEnvironment
java.endorsed.dirs=C:\Program Files\Java\j2re1.4.2_05\li...
os.arch=x86
java.io.tmpdir=C:\DOCUME~1\cn2lx0q0\LOCALS~1\Temp\
line.separator=
java.vm.specification.vendor=Sun Microsystems Inc.
user.variant=
os.name=Windows XP
sun.java2d.fontpath=
java.library.path=C:\Program Files\Java\j2re1.4.2_05\bi...
java.specification.name=Java Platform API Specification
java.class.version=48.0
java.util.prefs.PreferencesFactory=java.util.prefs.WindowsPreferencesFac...
os.version=5.1
user.home=D:\Users\cn2lx0q0
user.timezone=
java.awt.printerjob=sun.awt.windows.WPrinterJob
file.encoding=GBK
java.specification.version=1.4
user.name=cn2lx0q0
java.class.path=d:\my documents\項(xiàng)目\eclipse\SWTDemo\bi...
java.vm.specification.version=1.0
sun.arch.data.model=32
java.home=C:\Program Files\Java\j2re1.4.2_05
java.specification.vendor=Sun Microsystems Inc.
user.language=zh
awt.toolkit=sun.awt.windows.WToolkit
java.vm.info=mixed mode
java.version=1.4.2_05
java.ext.dirs=C:\Program Files\Java\j2re1.4.2_05\li...
sun.boot.class.path=C:\Program Files\Java\j2re1.4.2_05\li...
java.vendor=Sun Microsystems Inc.
file.separator=\
java.vendor.url.bug=http://java.sun.com/cgi-bin/bugreport...
sun.cpu.endian=little
sun.io.unicode.encoding=UnicodeLittle
sun.cpu.isalist=pentium i486 i386
主要類與方法和描述
load() //從一個(gè)外部流讀取屬性
store() //將屬性保存到外部流(特別是文件)
getProperty() //取得一個(gè)指定的屬性
setProperty() //設(shè)置一個(gè)指定的屬性
list() //列出這個(gè)Properties對(duì)象包含的全部“key-value”對(duì)
System.getProperties() //取得系統(tǒng)當(dāng)前的環(huán)境變量
你可以這樣保存一個(gè)properties文件:
Properties prop = new Properties();
prop.setProperty("key1", "value1");
...
FileOutputStream out = new FileOutputStream("config.properties");
prop.store(out, "--這里是文件頭,可以加入注釋--");
★Preferences
如果我說Java里面可以不使用JNI的手段操作Windows的注冊(cè)表你信不信?很多軟件的菜單里都有“Setting”或“Preferences”這樣的選項(xiàng)用來設(shè)定或修改軟件的配置,這些配置信息可以保存到一個(gè)像上面所述的配置文件當(dāng)中,如果是Windows平臺(tái)下,也可能會(huì)保存到系統(tǒng)注冊(cè)表中。從JDK 1.4開始,Java在java.util下加入了一個(gè)專門處理用戶和系統(tǒng)配置信息的java.util.prefs包,其中一個(gè)類Preferences是一種比較“高級(jí)”的玩意。從本質(zhì)上講,Preferences本身是一個(gè)與平臺(tái)無關(guān)的東西,但不同的OS對(duì)它的SPI(Service Provider Interface)的實(shí)現(xiàn)卻是與平臺(tái)相關(guān)的,因此,在不同的系統(tǒng)中你可能看到首選項(xiàng)保存為本地文件、LDAP目錄項(xiàng)、數(shù)據(jù)庫(kù)條目等,像在Windows平臺(tái)下,它就保存到了系統(tǒng)注冊(cè)表中。不僅如此,你還可以把首選項(xiàng)導(dǎo)出為XML文件或從XML文件導(dǎo)入。
主要類與方法和描述
systemNodeForPackage() //根據(jù)指定的Class對(duì)象得到一個(gè)Preferences對(duì)象,這個(gè)對(duì)象的注冊(cè)表路徑是從“HKEY_LOCAL_MACHINE\”開始的
systemRoot() //得到以注冊(cè)表路徑HKEY_LOCAL_MACHINE\SOFTWARE\Javasoft\Prefs 為根結(jié)點(diǎn)的Preferences對(duì)象
userNodeForPackage() //根據(jù)指定的Class對(duì)象得到一個(gè)Preferences對(duì)象,這個(gè)對(duì)象的注冊(cè)表路徑是從“HKEY_CURRENT_USER\”開始的
userRoot() //得到以注冊(cè)表路徑HKEY_CURRENT_USER\SOFTWARE\Javasoft\Prefs 為根結(jié)點(diǎn)的Preferences對(duì)象
putXXX() //設(shè)置一個(gè)屬性的值,這里XXX可以為基本數(shù)值型類型,如int、long等,但首字母大寫,表示參數(shù)為相應(yīng)的類型,也可以不寫而直接用put,參數(shù)則為字符串
getXXX() //得到一個(gè)屬性的值
exportNode() //將全部首選項(xiàng)導(dǎo)出為一個(gè)XML文件
exportSubtree() //將部分首選項(xiàng)導(dǎo)出為一個(gè)XML文件
importPreferences() //從XML文件導(dǎo)入首選項(xiàng)
你可以按如下步驟保存數(shù)據(jù):
Preferences myPrefs1 = Preferences.userNodeForPackage(this);// 這種方法是在“HKEY_CURRENT_USER\”下按當(dāng)前類的路徑建立一個(gè)注冊(cè)表項(xiàng)
Preferences myPrefs2 = Preferences.systemNodeForPackage(this);// 這種方法是在“HKEY_LOCAL_MACHINE\”下按當(dāng)前類的路徑建立一個(gè)注冊(cè)表項(xiàng)
Preferences myPrefs3 = Preferences.userRoot().node("com.jungleford.demo");// 這種方法是在“HKEY_CURRENT_USER\SOFTWARE\Javasoft\Prefs\”下按“com\jungleford\demo”的路徑建立一個(gè)注冊(cè)表項(xiàng)
Preferences myPrefs4 = Preferences.systemRoot().node("com.jungleford.demo");// 這種方法是在“HKEY_LOCAL_MACHINE\SOFTWARE\Javasoft\Prefs\”下按“com\jungleford\demo”的路徑建立一個(gè)注冊(cè)表項(xiàng)
myPrefs1.putInt("key1", 10);
myPrefs1.putDouble("key2", -7.15);
myPrefs1.put("key3", "value3");
FileOutputStream out = new FileOutputStream("prefs.xml");
myPrefs1.exportNode(out);
網(wǎng)絡(luò)I/O:Socket→RMI
★Socket
Socket編程可能大家都很熟,所以就不多討論了,只是說通過socket把數(shù)據(jù)保存到遠(yuǎn)端服務(wù)器或從網(wǎng)絡(luò)socket讀取數(shù)據(jù)也不失為一種值得考慮的方式。
★RMI
RMI機(jī)制其實(shí)就是RPC(遠(yuǎn)程過程調(diào)用)的Java版本,它使用socket作為基本傳輸手段,同時(shí)也是序列化最重要的一個(gè)應(yīng)用。現(xiàn)在網(wǎng)絡(luò)傳輸從編程的角度來看基本上都是以流的方式操作,socket就是一個(gè)例子,將對(duì)象轉(zhuǎn)換成字節(jié)流的一個(gè)重要目標(biāo)就是為了方便網(wǎng)絡(luò)傳輸。
想象一下傳統(tǒng)的單機(jī)環(huán)境下的程序設(shè)計(jì),對(duì)于Java語言的函數(shù)(方法)調(diào)用(注意與C語言函數(shù)調(diào)用的區(qū)別)的參數(shù)傳遞,會(huì)有兩種情況:如果是基本數(shù)據(jù)類型,這種情況下和C語言是一樣的,采用值傳遞方式;如果是對(duì)象,則傳遞的是對(duì)象的引用,包括返回值也是引用,而不是一個(gè)完整的對(duì)象拷貝!試想一下在不同的虛擬機(jī)之間進(jìn)行方法調(diào)用,即使是兩個(gè)完全同名同類型的對(duì)象他們也很可能是不同的引用!此外對(duì)于方法調(diào)用過程,由于被調(diào)用過程的壓棧,內(nèi)存“現(xiàn)場(chǎng)”完全被被調(diào)用者占有,當(dāng)被調(diào)用方法返回時(shí),才將調(diào)用者的地址寫回到程序計(jì)數(shù)器(PC),恢復(fù)調(diào)用者的狀態(tài),如果是兩個(gè)虛擬機(jī),根本不可能用簡(jiǎn)單壓棧的方式來保存調(diào)用者的狀態(tài)。因?yàn)榉N種原因,我們才需要建立RMI通信實(shí)體之間的“代理”對(duì)象,譬如“存根”就相當(dāng)于遠(yuǎn)程服務(wù)器對(duì)象在客戶機(jī)上的代理,stub就是這么來的,當(dāng)然這是后話了。
本地對(duì)象與遠(yuǎn)程對(duì)象(未必是物理位置上的不同機(jī)器,只要不是在同一個(gè)虛擬機(jī)內(nèi)皆為“遠(yuǎn)程”)之間傳遞參數(shù)和返回值,可能有這么幾種情形:
值傳遞:這又包括兩種子情形:如果是基本數(shù)據(jù)類型,那么都是“可序列化”的,統(tǒng)統(tǒng)序列化成可傳輸?shù)淖止?jié)流;如果是對(duì)象,而且不是“遠(yuǎn)程對(duì)象”(所謂“遠(yuǎn)程對(duì)象”是實(shí)現(xiàn)了java.rmi.Remote接口的對(duì)象),本來對(duì)象傳遞的應(yīng)該是引用,但由于上述原因,引用是不足以證明對(duì)象身份的,所以傳遞的仍然是一個(gè)序列化的拷貝(當(dāng)然這個(gè)對(duì)象也必須滿足上述“可序列化”的條件)。
引用傳遞:可以引用傳遞的只能是“遠(yuǎn)程對(duì)象”。這里所謂的“引用”不要理解成了真的只是一個(gè)符號(hào),它其實(shí)是一個(gè)留在(客戶機(jī))本地stub中的,和遠(yuǎn)端服務(wù)器上那個(gè)真實(shí)的對(duì)象張得一模一樣的鏡像而已!只是因?yàn)樗悬c(diǎn)“特權(quán)”(不需要經(jīng)過序列化),在本地內(nèi)存里已經(jīng)有了一個(gè)實(shí)例,真正引用的其實(shí)是這個(gè)“孿生子”。
由此可見,序列化在RMI當(dāng)中占有多么重要的地位。
數(shù)據(jù)庫(kù)I/O:CMP、Hibernate
★什么是“Persistence”
用過VMWare的朋友大概都知道當(dāng)一個(gè)guest OS正在運(yùn)行的時(shí)候點(diǎn)擊“Suspend”將虛擬OS掛起,它會(huì)把整個(gè)虛擬內(nèi)存的內(nèi)容保存到磁盤上,譬如你為虛擬OS分配了128M的運(yùn)行內(nèi)存,那掛起以后你會(huì)在虛擬OS所在的目錄下找到一個(gè)同樣是128M的文件,這就是虛擬OS內(nèi)存的完整鏡像!這種內(nèi)存的鏡像手段其實(shí)就是“Persistence”(持久化)概念的由來。
★CMP和Hibernate
因?yàn)槲覍?duì)J2EE的東西不是太熟悉,隨便找了點(diǎn)材料看看,所以擔(dān)心說的不到位,這次就不作具體總結(jié)了,人要學(xué)習(xí)……真是一件痛苦的事情
序列化再探討
從以上技術(shù)的討論中我們不難體會(huì)到,序列化是Java之所以能夠出色地實(shí)現(xiàn)其鼓吹的兩大賣點(diǎn)??分布式(distributed)和跨平臺(tái)(OS independent)的一個(gè)重要基礎(chǔ)。TIJ(即“Thinking in Java”)談到I/O系統(tǒng)時(shí),把序列化稱為“l(fā)ightweight persistence”??“輕量級(jí)的持久化”,這確實(shí)很有意思。
★為什么叫做“序列”化?
開場(chǎng)白里我說更習(xí)慣于把“Serialization”稱為“序列化”而不是“串行化”,這是有原因的。介紹這個(gè)原因之前先回顧一些計(jì)算機(jī)基本的知識(shí),我們知道現(xiàn)代計(jì)算機(jī)的內(nèi)存空間都是線性編址的(什么是“線性”知道吧,就是一個(gè)元素只有一個(gè)唯一的“前驅(qū)”和唯一的“后繼”,當(dāng)然頭尾元素是個(gè)例外;對(duì)于地址來說,它的下一個(gè)地址當(dāng)然不可能有兩個(gè),否則就亂套了),“地址”這個(gè)概念推廣到數(shù)據(jù)結(jié)構(gòu),就相當(dāng)于“指針”,這個(gè)在本科低年級(jí)大概就知道了。注意了,既然是線性的,那“地址”就可以看作是內(nèi)存空間的“序號(hào)”,說明它的組織是有順序的,“序號(hào)”或者說“序列號(hào)”正是“Serialization”機(jī)制的一種體現(xiàn)。為什么這么說呢?譬如我們有兩個(gè)對(duì)象a和b,分別是類A和B的實(shí)例,它們都是可序列化的,而A和B都有一個(gè)類型為C的屬性,根據(jù)前面我們說過的原則,C當(dāng)然也必須是可序列化的。
import java.io.*;
...
class A implements Serializable
{
C c;
...
}
class B implements Serializable
{
C c;
...
}
class C implements Serializable
{
...
}
A a;
B b;
C c1;
...
注意,這里我們?cè)趯?shí)例化a和b的時(shí)候,有意讓他們的c屬性使用同一個(gè)C類型對(duì)象的引用,譬如c1,那么請(qǐng)?jiān)囅胍幌拢覀冃蛄谢痑和b的時(shí)候,它們的c屬性在外部字節(jié)流(當(dāng)然可以不僅僅是文件)里保存的是一份拷貝還是兩份拷貝呢?序列化在這里使用的是一種類似于“指針”的方案:它為每個(gè)被序列化的對(duì)象標(biāo)上一個(gè)“序列號(hào)”(serial number),但序列化一個(gè)對(duì)象的時(shí)候,如果其某個(gè)屬性對(duì)象是已經(jīng)被序列化的,那么這里只向輸出流寫入該屬性的序列號(hào);從字節(jié)流恢復(fù)被序列化的對(duì)象時(shí),也根據(jù)序列號(hào)找到對(duì)應(yīng)的流來恢復(fù)。這就是“序列化”名稱的由來!這里我們看到“序列化”和“指針”是極相似的,只不過“指針”是內(nèi)存空間的地址鏈,而序列化用的是外部流中的“序列號(hào)鏈”。
使用“序列號(hào)”而不是內(nèi)存地址來標(biāo)識(shí)一個(gè)被序列化的對(duì)象,是因?yàn)閺牧髦谢謴?fù)對(duì)象到內(nèi)存,其地址可能就未必是原來的地址了??我們需要的只是這些對(duì)象之間的引用關(guān)系,而不是死板的原始位置,這在RMI中就更是必要,在兩臺(tái)不同的機(jī)器之間傳遞對(duì)象(流),根本就不可能指望它們?cè)趦膳_(tái)機(jī)器上都具有相同的內(nèi)存地址。
★更靈活的“序列化”:transient屬性和Externalizable
Serializable確實(shí)很方便,方便到你幾乎不需要做任何額外的工作就可以輕松將內(nèi)存中的對(duì)象保存到外部。但有兩個(gè)問題使得Serializable的威力收到束縛:
一個(gè)是效率問題,《Core Java 2》中指出,Serializable使用系統(tǒng)默認(rèn)的序列化機(jī)制會(huì)影響軟件的運(yùn)行速度,因?yàn)樾枰獮槊總€(gè)屬性的引用編號(hào)和查號(hào),再加上I/O操作的時(shí)間(I/O和內(nèi)存讀寫差的可是一個(gè)數(shù)量級(jí)的大小),其代價(jià)當(dāng)然是可觀的。
另一個(gè)困擾是“裸”的Serializable不可定制,傻乎乎地什么都給你序列化了,不管你是不是想這么做。其實(shí)你可以有至少三種定制序列化的選擇。其中一種前面已經(jīng)提到了,就是在implements Serializable的類里面添加私有的writeObject()和readObject()方法(這種Serializable就不裸了,),在這兩個(gè)方法里,該序列化什么,不該序列化什么,那就由你說了算了,你當(dāng)然可以在這兩個(gè)方法體里面分別調(diào)用ObjectOutputStream.defaultWriteObject()和ObjectInputStream.defaultReadObject()仍然執(zhí)行默認(rèn)的序列化動(dòng)作(那你在代碼上不就做無用功了?呵呵),也可以用ObjectOutputStream.writeObject()和ObjectInputStream.readObject()方法對(duì)你中意的屬性進(jìn)行序列化。但虛擬機(jī)一看到你定義了這兩個(gè)方法,它就不再用默認(rèn)的機(jī)制了。
如果僅僅為了跳過某些屬性不讓它序列化,上面的動(dòng)作似乎顯得麻煩,更簡(jiǎn)單的方法是對(duì)不想序列化的屬性加上transient關(guān)鍵字,說明它是個(gè)“暫態(tài)變量”,默認(rèn)序列化的時(shí)候就不會(huì)把這些屬性也塞到外部流里了。當(dāng)然,你如果定義writeObject()和readObject()方法的化,仍然可以把暫態(tài)變量進(jìn)行序列化。題外話,像transient、violate、finally這樣的關(guān)鍵字初學(xué)者可能會(huì)不太重視,而現(xiàn)在有的公司招聘就偏偏喜歡問這樣的問題 :(
再一個(gè)方案就是不實(shí)現(xiàn)Serializable而改成實(shí)現(xiàn)Externalizable接口。我們研究一下這兩個(gè)接口的源代碼,發(fā)現(xiàn)它們很類似,甚至容易混淆。我們要記住的是:Externalizable默認(rèn)并不保存任何對(duì)象相關(guān)信息!任何保存和恢復(fù)對(duì)象的動(dòng)作都是你自己定義的。Externalizable包含兩個(gè)public的方法:
public void writeExternal(ObjectOutput out) throws IOException;
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
乍一看這和上面的writeObject()和readObject()幾乎差不多,但Serializable和Externalizable走的是兩個(gè)不同的流程:Serializable在對(duì)象不存在的情況下,就可以僅憑外部的字節(jié)序列把整個(gè)對(duì)象重建出來;但Externalizable在重建對(duì)象時(shí),先是調(diào)用該類的默認(rèn)構(gòu)造函數(shù)(即不含參數(shù)的那個(gè)構(gòu)造函數(shù))使得內(nèi)存中先有這么一個(gè)實(shí)例,然后再調(diào)用readExternal方法對(duì)實(shí)例中的屬性進(jìn)行恢復(fù),因此,如果默認(rèn)構(gòu)造函數(shù)中和readExternal方法中都沒有賦值的那些屬性,特別他們是非基本類型的話,將會(huì)是空(null)。在這里需要注意的是,transient只能用在對(duì)Serializable而不是Externalizable的實(shí)現(xiàn)里面。
★序列化與克隆
從“可序列化”的遞歸定義來看,一個(gè)序列化的對(duì)象貌似對(duì)象內(nèi)存映象的外部克隆,如果沒有共享引用的屬性的化,那么應(yīng)該是一個(gè)深度克隆。關(guān)于克隆的話題有可以談很多,這里就不細(xì)說了,有興趣的話可以參考IBM developerWorks上的一篇文章:JAVA中的指針,引用及對(duì)象的clone
一點(diǎn)啟示
作為一個(gè)實(shí)際的應(yīng)用,我在寫那個(gè)簡(jiǎn)易的郵件客戶端JExp的時(shí)候曾經(jīng)對(duì)比過好幾種保存Message對(duì)象(主要是幾個(gè)關(guān)鍵屬性和郵件的內(nèi)容)到本地的方法,譬如XML、Properties等,最后還是選擇了用序列化的方式,因?yàn)檫@種方法最簡(jiǎn)單, 大約可算是“學(xué)以致用”罷。這里“存取程序狀態(tài)”其實(shí)只是一個(gè)引子話題罷了,我想說的是??就如同前面我們討論的關(guān)于logging的話題一樣??在Java面前對(duì)同一個(gè)問題你可以有很多種solution:熟悉文件操作的,你可能會(huì)覺得Properties、XML或Bean比較方便,然后又發(fā)現(xiàn)了還有Preferences這么一個(gè)東東,大概又會(huì)感慨“天外有天”了,等到你接觸了很多種新方法以后,結(jié)果又會(huì)“殊途同歸”,重新反省Serialization機(jī)制本身。這不僅是Java,科學(xué)也是同樣的道理。
參考資料
Core Java 2. by Cay S. Horstmann, Gary Cornell
J2SE進(jìn)階. by JavaResearch.org
Thinking in Java. by Bruce Eckel
J2SE 1.4.2 Documentation. by java.sun.com
Java Network Programming. by Elliotte R. Harold
Java分布式對(duì)象:RMI和CORBA. by IBM developerWorks
posted on 2005-11-17 10:37
安德爾斯 閱讀(300)
評(píng)論(0) 編輯 收藏