java流与文件——对象流和序列化
【0】README
0.1) 本文描述轉自 core java volume 2, 旨在理解 java流與文件——對象流和序列化 的相關知識;
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】對象流和序列化
1.1)problem + solution
- 1.1.1)problem: 當你需要存儲相同類型的數據時, 使用固定長度的記錄格式是一個不錯的選擇。在面向對象程序中創建的對象很少全部都具有相同的類型; (如父類指針指向子類對象)
- 1.1.2)solution : Java的 對象序列化(Object serialization)是非常通用的機制; 它可以將對象寫出到流中, 并在之后將其讀回;
1.2)保存數據對象 的步驟:
- step1)打開一個 ObjectOutputStream 對象:
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(“employee.dat”)); - step2)為保存對象,直接使用 ObjectOutputStream 的 writeObject方法;
- step3)為將對象讀回,需要獲得一個 ObjectInputStream 對象:
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(“employee.dat”)); - step4) 用readObject方法以這些對象被寫出時的順序獲取他們:
Employee e1 = (Employee)ois.readObject();
- 1.2.1)但是,對希望在對象流中存儲或恢復的所有類都應該進行一次修改, 這些類必須實現 Serializable 接口:
Attention)
- A1) Serializable 接口 沒有任何方法, 你不需要做任何改動, 這一點和 Cloneable 接口很相似;
- A2)你只有在寫出對象時 才能用 writeObject/readObject 方法; 對于基本類型,使用writeInt/readInt 或writeDouble/readDouble 這樣的方法(對象流類都實現了 DataInput/DataOutput 接口)
- A3)在后臺,是ObjectOutputStream 在瀏覽對象的所有域, 并存儲它們的內容;
1.3)有一種case 需要考慮: 當一個對象被多個對象共享時, 作為它們各自狀態的一部分,會發生什么呢?(如兩個經理共用一個秘書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: 每個對象都是用一個序列號保存的, 這就是這種機制稱為對象序列化的原因。 下面是其算法(Alg):
A1)對你遇到的每一個對象引用都關聯一個序列號;
- A2)對于每個對象, 當第一個遇到時,保存其對象數據到流中;
- A3)如果某個對象之前已經被保存過, 那么只寫出“與之前保存過的序列號為x的對象相同”, 在讀回對象時,過程是相反的;
- A4) 對于流中的對象,在第一次遇到其序列號時, 構建它, 并使用流中數據來初始化它, 然后記錄這個順序號和新對象之間的關系;
A5) 當遇到 “與之前保存過的序列號為x的對象相同”標記時, 獲取與這個順序號相關聯的對象引用;
Attention)序列化的另一種重要應用是: 通過網絡將對象集合傳送到另一臺計算機上。 正如文件中保存原生的內存地址毫無意義一樣, 這些地址對于在不同處理器之間的通信也毫無意義的。因為序列化用序列號代替了內存地址, 所以它允許將對象集合從一臺機器傳送到另一臺機器;(干貨——序列化應用于代替內存地址來表示對象集合 便于對象集合在網絡間的傳輸)
【2】理解對象序列化的文件格式
2.1)當存儲一個對象時,這個對象所屬的類也必須存儲。這個類的描述(Descriptions)包含:
- D1)類名;
- D2)序列化的版本唯一的ID, 它是數據域類型和方法簽名的指紋;
- D3)描述序列化方法的標志集;
- D4)對數據域的描述;
(干貨——序列化版本ID 就是 指紋, 而指紋是包含于對象描述內容中的)
2.2)指紋: 是通過對類、超類、接口、域類型和方法簽名按照規范方式排序的, 然后將安全散列算法(SHA) 應用于這些數據而獲得的;(干貨——指紋定義)
- 2.2.1)指紋比對: 在讀入一個對象時, 會拿其指紋與它所屬的類的當前指紋進行比對, 如果不匹配, 那么就說明這個類的定義在該對象被寫出之后發生過變化, 因此會產生一個異常;
(干貨——指紋比對) - 2.2.2)類標識符是如何存儲的? (干貨——類標識符是如何存儲的,了解而已)
- 2.2.3)每個數據域描述符的格式如下: (干貨——每個數據域描述符的格式)
Attention)你應該記住: (干貨——需要記住的對象流的key point)
- A1)對象流輸出中包含所有對象的類型和數據域;
- A2)每個對象都被賦予一個序列號;
- A3) 相同對象的重復出現將被存儲為對這個對象的序列號的引用;
【3】修改默認的序列化機制
3.1)problem+solution (干貨——某些數據域及其所屬類是不可以序列化的)
- 3.1.1)problem:某些數據域是不可以序列化的, 如, 只對本地方法有意義的存儲文件句柄或窗口句柄的整數值; 這種域的值如果不恰當,還會引起本地方法崩潰;
- 3.1.2)solution: java 將他們標記為 transient;如果這些域屬于不可序列化的類, 那么類也需要標記為 transient; (干貨——transient關鍵字的意義, transient==瞬變現象,過往旅客,候鳥;短暫的,路過的)
3.2)序列化機制為單個類提供了一種方式,去向默認的讀寫行為添加驗證或任何其他想要的行為, 可序列化的類可以定義具有下列簽名的方法:readObject 和 writeObject;
- 3.2.1)之后數據域就再也不會被自動序列化了, 取而代之的是調用這些方法;
- 3.2.2)除了讓序列化及直接來保存和恢復對象數據, 類還可以定義它自己的機制。為了做到這一點,必須要實現 Externalizeble 接口, 這需要它定義兩個方法:readExternal 和 writeExternal 方法;
Warning)
- W1) readObject 和 writeObject是私有的, 并且只能被序列化機制調用;
- W2)與此不同的是, readExternal 和 writeExternal 方法是公共的, 特別地, readExternal 還潛在地允許修改現有對象的狀態;
【4】序列化單例和類型安全的枚舉
4.1) 當類型安全的枚舉實現 Serializable 接口時, 你必須牢記存在一種重要變化, 此時,默認的序列化機制是不適用的。 假設我們寫出一個 Orientation 類型的值, 并將其再次讀回:
Orientation original = Orientation.HORIZONTAL; ObjectOutputStream oos = ..; oos.write(original); oos.close(); ObjectInputStream ois = ...; Orientation saved = (Orientation) ois.read();4.2)problem: 下面的測試 if(saved === Orientation.HORIZONTAL) 將失敗;
4.3)solution:為解決這個問題,必須定義一種稱為 readResolve 的特殊序列化方法。 如果定義了 readResolve 方法, 在對象被序列化后就會調用它。 它必須返回這個對象, 而該對象之后會成為 readObject 的返回值;
Attention)向遺留代碼中所有類型安全的枚舉以及向所有支持單例設計模式的類中添加readResolve 方法; (干貨——添加readResolve方法的條件)
【5】版本管理 (干貨——存儲在磁盤上version1的對象與內存中version2的對象的輸入輸出問題)
5.1)如果使用序列化來保存對象,就要考慮在程序演化時會有什么問題?
5.2)無論類的定義產生什么樣的變化,它的SHA指紋也會跟著變化, 而我們都知道對象流將拒絕讀入具有不同指紋的對象。但是類可以表明它對其早期版本保持兼容, 要想這樣做, 就必須先獲得這個類的早期版本的指紋。
- 5.2.1)我們可以使用JDK中的單機程序 serialver 來獲得這個數字, 如:serialver Employee 會打印出
- 5.2.2)如果一個類具有名為 serialVersionUID 的靜態數據成員, 它就不需要再人工地計算其指紋了,而只需要直接使用這個值;
- 5.2.3)一旦該靜態成員( serialVersionUID) 被設置在某個類的內部, 那么序列化系統就可以讀入這個類的對象的不同版本;
5.3)如果這個類只有方法產生了變化,那么在讀入新對象數據時是不會有任何問題的。但是, 如果數據域發生了變化, 那么就有可能有問題了;
5.4)對象流會將這個類當期版本的數據域和流中版本的數據域進行比較;
- 5.4.1)如果這兩部分數據域之間名字匹配而類型不匹配, 那么對象流不會嘗試將一種類型轉換為另一種類型, 因為這兩個對象不兼容;
- 5.4.2)如果流中的對象具有在當前版本中所沒有的數據域, 那么對象流會忽略這些額外的數據;
- 5.4.3)如果當期版本具有在流化對象中所沒有的數據域,那么這些新添加的域將被設置為它們的默認值(對象設置為null, 數組設置為0, 而boolean 設置為false)
5.5)看個荔枝:
- 5.5.1)假設我們已經用雇員類的1.0version 在磁盤上保存了大量的雇員記錄, 現在我們在 Employee 上添加了 department的數據域,從而將其演化到 version2.0;
- 5.5.2)將version2的磁盤對象讀入到version1的程序中(內存中),直接將department 數據域忽略掉;因為version1 中的類描述內容沒有關于department的描述;
5.5.3)以上的忽略和置空真的安全嗎? 視情況而定;
5.5.3.1)丟掉數據域看起來是無害的, 因為接收者仍然可以擁有它知道如何處理的所有數據, 但是將數據域設置為null, 卻可能并不那么安全了;
- 5.5.3.2) 這個問題取決于類的設計者是否能夠在 readObject方法中實現額外的代碼區訂正版本不兼容問題, 或者是否能夠確保所有的方法在處理null 數據時都足夠健壯;
【6】為克隆使用序列化
6.1)序列化是一種很有用 的用法: 提供了一種克隆對象的簡便途徑, 只要對應的類是可序列化的即可;
6.2)做法很簡單:直接將對象序列化到輸入流中, 然后將其讀回。
- 6.2.1)這樣產生的新對象是對現有對象 的一個深拷貝。
- 6.2.2)在這個過程中, 我們不必將對象寫出到文件中, 因為可以用 ByteArrayOutputStream 將數據保存到字節數組中;
6.3)看個荔枝: 要想得到 clone 方法, 只需要擴展 SerialCloneable 類就可以了; (干貨——為克隆使用序列化,需要實現SerialCloneable )
總結
以上是生活随笔為你收集整理的java流与文件——对象流和序列化的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 云服务器怎么设置域名(云服务器怎么设置域
- 下一篇: 二叉堆的操作总结(insert+dele