java流与文件——对象流和序列化
【0】README
0.1) 本文描述轉(zhuǎn)自 core java volume 2, 旨在理解 java流與文件——對(duì)象流和序列化 的相關(guān)知識(shí);
0.2) for source code , please visit https://github.com/pacosonTang/core-java-volume/blob/master/coreJavaAdvanced/chapter1/ObjectStreamTest.java and https://github.com/pacosonTang/core-java-volume/blob/master/coreJavaAdvanced/chapter1/SerialCloneTest.java
【1】對(duì)象流和序列化
1.1)problem + solution
- 1.1.1)problem: 當(dāng)你需要存儲(chǔ)相同類(lèi)型的數(shù)據(jù)時(shí), 使用固定長(zhǎng)度的記錄格式是一個(gè)不錯(cuò)的選擇。在面向?qū)ο蟪绦蛑袆?chuàng)建的對(duì)象很少全部都具有相同的類(lèi)型; (如父類(lèi)指針指向子類(lèi)對(duì)象)
- 1.1.2)solution : Java的 對(duì)象序列化(Object serialization)是非常通用的機(jī)制; 它可以將對(duì)象寫(xiě)出到流中, 并在之后將其讀回;
1.2)保存數(shù)據(jù)對(duì)象 的步驟:
- step1)打開(kāi)一個(gè) ObjectOutputStream 對(duì)象:
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(“employee.dat”)); - step2)為保存對(duì)象,直接使用 ObjectOutputStream 的 writeObject方法;
- step3)為將對(duì)象讀回,需要獲得一個(gè) ObjectInputStream 對(duì)象:
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(“employee.dat”)); - step4) 用readObject方法以這些對(duì)象被寫(xiě)出時(shí)的順序獲取他們:
Employee e1 = (Employee)ois.readObject();
- 1.2.1)但是,對(duì)希望在對(duì)象流中存儲(chǔ)或恢復(fù)的所有類(lèi)都應(yīng)該進(jìn)行一次修改, 這些類(lèi)必須實(shí)現(xiàn) Serializable 接口:
Attention)
- A1) Serializable 接口 沒(méi)有任何方法, 你不需要做任何改動(dòng), 這一點(diǎn)和 Cloneable 接口很相似;
- A2)你只有在寫(xiě)出對(duì)象時(shí) 才能用 writeObject/readObject 方法; 對(duì)于基本類(lèi)型,使用writeInt/readInt 或writeDouble/readDouble 這樣的方法(對(duì)象流類(lèi)都實(shí)現(xiàn)了 DataInput/DataOutput 接口)
- A3)在后臺(tái),是ObjectOutputStream 在瀏覽對(duì)象的所有域, 并存儲(chǔ)它們的內(nèi)容;
1.3)有一種case 需要考慮: 當(dāng)一個(gè)對(duì)象被多個(gè)對(duì)象共享時(shí), 作為它們各自狀態(tài)的一部分,會(huì)發(fā)生什么呢?(如兩個(gè)經(jīng)理共用一個(gè)秘書(shū)harry, 上面的源代碼就是這種 case )
Employee harry = new Employee("Harry Hacker", 50000, 1989, 10, 1);Manager carl = new Manager("Carl Cracker", 80000, 1987, 12, 15);carl.setSecretary(harry);Manager tony = new Manager("Tony Tester", 40000, 1990, 3, 15);tony.setSecretary(harry);1.3.1)solution: 每個(gè)對(duì)象都是用一個(gè)序列號(hào)保存的, 這就是這種機(jī)制稱(chēng)為對(duì)象序列化的原因。 下面是其算法(Alg):
A1)對(duì)你遇到的每一個(gè)對(duì)象引用都關(guān)聯(lián)一個(gè)序列號(hào);
- A2)對(duì)于每個(gè)對(duì)象, 當(dāng)?shù)谝粋€(gè)遇到時(shí),保存其對(duì)象數(shù)據(jù)到流中;
- A3)如果某個(gè)對(duì)象之前已經(jīng)被保存過(guò), 那么只寫(xiě)出“與之前保存過(guò)的序列號(hào)為x的對(duì)象相同”, 在讀回對(duì)象時(shí),過(guò)程是相反的;
- A4) 對(duì)于流中的對(duì)象,在第一次遇到其序列號(hào)時(shí), 構(gòu)建它, 并使用流中數(shù)據(jù)來(lái)初始化它, 然后記錄這個(gè)順序號(hào)和新對(duì)象之間的關(guān)系;
A5) 當(dāng)遇到 “與之前保存過(guò)的序列號(hào)為x的對(duì)象相同”標(biāo)記時(shí), 獲取與這個(gè)順序號(hào)相關(guān)聯(lián)的對(duì)象引用;
Attention)序列化的另一種重要應(yīng)用是: 通過(guò)網(wǎng)絡(luò)將對(duì)象集合傳送到另一臺(tái)計(jì)算機(jī)上。 正如文件中保存原生的內(nèi)存地址毫無(wú)意義一樣, 這些地址對(duì)于在不同處理器之間的通信也毫無(wú)意義的。因?yàn)樾蛄谢眯蛄刑?hào)代替了內(nèi)存地址, 所以它允許將對(duì)象集合從一臺(tái)機(jī)器傳送到另一臺(tái)機(jī)器;(干貨——序列化應(yīng)用于代替內(nèi)存地址來(lái)表示對(duì)象集合 便于對(duì)象集合在網(wǎng)絡(luò)間的傳輸)
【2】理解對(duì)象序列化的文件格式
2.1)當(dāng)存儲(chǔ)一個(gè)對(duì)象時(shí),這個(gè)對(duì)象所屬的類(lèi)也必須存儲(chǔ)。這個(gè)類(lèi)的描述(Descriptions)包含:
- D1)類(lèi)名;
- D2)序列化的版本唯一的ID, 它是數(shù)據(jù)域類(lèi)型和方法簽名的指紋;
- D3)描述序列化方法的標(biāo)志集;
- D4)對(duì)數(shù)據(jù)域的描述;
(干貨——序列化版本ID 就是 指紋, 而指紋是包含于對(duì)象描述內(nèi)容中的)
2.2)指紋: 是通過(guò)對(duì)類(lèi)、超類(lèi)、接口、域類(lèi)型和方法簽名按照規(guī)范方式排序的, 然后將安全散列算法(SHA) 應(yīng)用于這些數(shù)據(jù)而獲得的;(干貨——指紋定義)
- 2.2.1)指紋比對(duì): 在讀入一個(gè)對(duì)象時(shí), 會(huì)拿其指紋與它所屬的類(lèi)的當(dāng)前指紋進(jìn)行比對(duì), 如果不匹配, 那么就說(shuō)明這個(gè)類(lèi)的定義在該對(duì)象被寫(xiě)出之后發(fā)生過(guò)變化, 因此會(huì)產(chǎn)生一個(gè)異常;
(干貨——指紋比對(duì)) - 2.2.2)類(lèi)標(biāo)識(shí)符是如何存儲(chǔ)的? (干貨——類(lèi)標(biāo)識(shí)符是如何存儲(chǔ)的,了解而已)
- 2.2.3)每個(gè)數(shù)據(jù)域描述符的格式如下: (干貨——每個(gè)數(shù)據(jù)域描述符的格式)
Attention)你應(yīng)該記住: (干貨——需要記住的對(duì)象流的key point)
- A1)對(duì)象流輸出中包含所有對(duì)象的類(lèi)型和數(shù)據(jù)域;
- A2)每個(gè)對(duì)象都被賦予一個(gè)序列號(hào);
- A3) 相同對(duì)象的重復(fù)出現(xiàn)將被存儲(chǔ)為對(duì)這個(gè)對(duì)象的序列號(hào)的引用;
【3】修改默認(rèn)的序列化機(jī)制
3.1)problem+solution (干貨——某些數(shù)據(jù)域及其所屬類(lèi)是不可以序列化的)
- 3.1.1)problem:某些數(shù)據(jù)域是不可以序列化的, 如, 只對(duì)本地方法有意義的存儲(chǔ)文件句柄或窗口句柄的整數(shù)值; 這種域的值如果不恰當(dāng),還會(huì)引起本地方法崩潰;
- 3.1.2)solution: java 將他們標(biāo)記為 transient;如果這些域?qū)儆诓豢尚蛄谢念?lèi), 那么類(lèi)也需要標(biāo)記為 transient; (干貨——transient關(guān)鍵字的意義, transient==瞬變現(xiàn)象,過(guò)往旅客,候鳥(niǎo);短暫的,路過(guò)的)
3.2)序列化機(jī)制為單個(gè)類(lèi)提供了一種方式,去向默認(rèn)的讀寫(xiě)行為添加驗(yàn)證或任何其他想要的行為, 可序列化的類(lèi)可以定義具有下列簽名的方法:readObject 和 writeObject;
- 3.2.1)之后數(shù)據(jù)域就再也不會(huì)被自動(dòng)序列化了, 取而代之的是調(diào)用這些方法;
- 3.2.2)除了讓序列化及直接來(lái)保存和恢復(fù)對(duì)象數(shù)據(jù), 類(lèi)還可以定義它自己的機(jī)制。為了做到這一點(diǎn),必須要實(shí)現(xiàn) Externalizeble 接口, 這需要它定義兩個(gè)方法:readExternal 和 writeExternal 方法;
Warning)
- W1) readObject 和 writeObject是私有的, 并且只能被序列化機(jī)制調(diào)用;
- W2)與此不同的是, readExternal 和 writeExternal 方法是公共的, 特別地, readExternal 還潛在地允許修改現(xiàn)有對(duì)象的狀態(tài);
【4】序列化單例和類(lèi)型安全的枚舉
4.1) 當(dāng)類(lèi)型安全的枚舉實(shí)現(xiàn) Serializable 接口時(shí), 你必須牢記存在一種重要變化, 此時(shí),默認(rèn)的序列化機(jī)制是不適用的。 假設(shè)我們寫(xiě)出一個(gè) Orientation 類(lèi)型的值, 并將其再次讀回:
Orientation original = Orientation.HORIZONTAL; ObjectOutputStream oos = ..; oos.write(original); oos.close(); ObjectInputStream ois = ...; Orientation saved = (Orientation) ois.read();4.2)problem: 下面的測(cè)試 if(saved === Orientation.HORIZONTAL) 將失敗;
4.3)solution:為解決這個(gè)問(wèn)題,必須定義一種稱(chēng)為 readResolve 的特殊序列化方法。 如果定義了 readResolve 方法, 在對(duì)象被序列化后就會(huì)調(diào)用它。 它必須返回這個(gè)對(duì)象, 而該對(duì)象之后會(huì)成為 readObject 的返回值;
Attention)向遺留代碼中所有類(lèi)型安全的枚舉以及向所有支持單例設(shè)計(jì)模式的類(lèi)中添加readResolve 方法; (干貨——添加readResolve方法的條件)
【5】版本管理 (干貨——存儲(chǔ)在磁盤(pán)上version1的對(duì)象與內(nèi)存中version2的對(duì)象的輸入輸出問(wèn)題)
5.1)如果使用序列化來(lái)保存對(duì)象,就要考慮在程序演化時(shí)會(huì)有什么問(wèn)題?
5.2)無(wú)論類(lèi)的定義產(chǎn)生什么樣的變化,它的SHA指紋也會(huì)跟著變化, 而我們都知道對(duì)象流將拒絕讀入具有不同指紋的對(duì)象。但是類(lèi)可以表明它對(duì)其早期版本保持兼容, 要想這樣做, 就必須先獲得這個(gè)類(lèi)的早期版本的指紋。
- 5.2.1)我們可以使用JDK中的單機(jī)程序 serialver 來(lái)獲得這個(gè)數(shù)字, 如:serialver Employee 會(huì)打印出
- 5.2.2)如果一個(gè)類(lèi)具有名為 serialVersionUID 的靜態(tài)數(shù)據(jù)成員, 它就不需要再人工地計(jì)算其指紋了,而只需要直接使用這個(gè)值;
- 5.2.3)一旦該靜態(tài)成員( serialVersionUID) 被設(shè)置在某個(gè)類(lèi)的內(nèi)部, 那么序列化系統(tǒng)就可以讀入這個(gè)類(lèi)的對(duì)象的不同版本;
5.3)如果這個(gè)類(lèi)只有方法產(chǎn)生了變化,那么在讀入新對(duì)象數(shù)據(jù)時(shí)是不會(huì)有任何問(wèn)題的。但是, 如果數(shù)據(jù)域發(fā)生了變化, 那么就有可能有問(wèn)題了;
5.4)對(duì)象流會(huì)將這個(gè)類(lèi)當(dāng)期版本的數(shù)據(jù)域和流中版本的數(shù)據(jù)域進(jìn)行比較;
- 5.4.1)如果這兩部分?jǐn)?shù)據(jù)域之間名字匹配而類(lèi)型不匹配, 那么對(duì)象流不會(huì)嘗試將一種類(lèi)型轉(zhuǎn)換為另一種類(lèi)型, 因?yàn)檫@兩個(gè)對(duì)象不兼容;
- 5.4.2)如果流中的對(duì)象具有在當(dāng)前版本中所沒(méi)有的數(shù)據(jù)域, 那么對(duì)象流會(huì)忽略這些額外的數(shù)據(jù);
- 5.4.3)如果當(dāng)期版本具有在流化對(duì)象中所沒(méi)有的數(shù)據(jù)域,那么這些新添加的域?qū)⒈辉O(shè)置為它們的默認(rèn)值(對(duì)象設(shè)置為null, 數(shù)組設(shè)置為0, 而boolean 設(shè)置為false)
5.5)看個(gè)荔枝:
- 5.5.1)假設(shè)我們已經(jīng)用雇員類(lèi)的1.0version 在磁盤(pán)上保存了大量的雇員記錄, 現(xiàn)在我們?cè)?Employee 上添加了 department的數(shù)據(jù)域,從而將其演化到 version2.0;
- 5.5.2)將version2的磁盤(pán)對(duì)象讀入到version1的程序中(內(nèi)存中),直接將department 數(shù)據(jù)域忽略掉;因?yàn)関ersion1 中的類(lèi)描述內(nèi)容沒(méi)有關(guān)于department的描述;
5.5.3)以上的忽略和置空真的安全嗎? 視情況而定;
5.5.3.1)丟掉數(shù)據(jù)域看起來(lái)是無(wú)害的, 因?yàn)榻邮照呷匀豢梢該碛兴廊绾翁幚淼乃袛?shù)據(jù), 但是將數(shù)據(jù)域設(shè)置為null, 卻可能并不那么安全了;
- 5.5.3.2) 這個(gè)問(wèn)題取決于類(lèi)的設(shè)計(jì)者是否能夠在 readObject方法中實(shí)現(xiàn)額外的代碼區(qū)訂正版本不兼容問(wèn)題, 或者是否能夠確保所有的方法在處理null 數(shù)據(jù)時(shí)都足夠健壯;
【6】為克隆使用序列化
6.1)序列化是一種很有用 的用法: 提供了一種克隆對(duì)象的簡(jiǎn)便途徑, 只要對(duì)應(yīng)的類(lèi)是可序列化的即可;
6.2)做法很簡(jiǎn)單:直接將對(duì)象序列化到輸入流中, 然后將其讀回。
- 6.2.1)這樣產(chǎn)生的新對(duì)象是對(duì)現(xiàn)有對(duì)象 的一個(gè)深拷貝。
- 6.2.2)在這個(gè)過(guò)程中, 我們不必將對(duì)象寫(xiě)出到文件中, 因?yàn)榭梢杂?ByteArrayOutputStream 將數(shù)據(jù)保存到字節(jié)數(shù)組中;
6.3)看個(gè)荔枝: 要想得到 clone 方法, 只需要擴(kuò)展 SerialCloneable 類(lèi)就可以了; (干貨——為克隆使用序列化,需要實(shí)現(xiàn)SerialCloneable )
總結(jié)
以上是生活随笔為你收集整理的java流与文件——对象流和序列化的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 云服务器怎么设置域名(云服务器怎么设置域
- 下一篇: 二叉堆的操作总结(insert+dele