Java對象表示方式1:序列化、反序列化和transient關鍵字的作用


平時我們在Java內存中的對象,是無 法進行IO操作或者網絡通信的,因為在進行IO操作或者網絡通信的時候,人家根本不知道內存中的對象是個什么東西,因此必須將對象以某種方式表示出來,即 存儲對象中的狀態。一個Java對象的表示有各種各樣的方式,Java本身也提供給了用戶一種表示對象的方式,那就是序列化。換句話說,序列化只是表示對 象的一種方式而已。OK,有了序列化,那么必然有反序列化,我們先看一下序列化、反序列化是什么意思。

序列化:將一個對象轉換成一串二進制表示的字節數組,通過保存或轉移這些字節數據來達到持久化的目的。

反序列化:將字節數組重新構造成對象。

 

默認序列化

序列化只需要實現java.io.Serializable接口就可以了。序列化的時候有一個serialVersionUID參數,Java序列化機制是通過在運行時判斷類的serialVersionUID來驗證版本一致性的。 在進行反序列化,Java虛擬機會把傳過來的字節流中的serialVersionUID和本地相應實體類的serialVersionUID進行比較, 如果相同就認為是一致的實體類,可以進行反序列化,否則Java虛擬機會拒絕對這個實體類進行反序列化並拋出異常。serialVersionUID有兩 種生成方式:

1、默認的1L

2、根據類名、接口名、成員方法以及屬性等來生成一個64位的Hash字段

如果實現 java.io.Serializable接口的實體類沒有顯式定義一個名為serialVersionUID、類型為long的變量時,Java序列化 機制會根據編譯的.class文件自動生成一個serialVersionUID,如果.class文件沒有變化,那么就算編譯再多 次,serialVersionUID也不會變化。換言之,Java為用戶定義了默認的序列化、反序列化方法,其實就是ObjectOutputStream的defaultWriteObject方法和ObjectInputStream的defaultReadObject方法。看一個例子:

復制代碼
 1 public class SerializableObject implements Serializable  2 {  3 private static final long serialVersionUID = 1L;  4  5 private String str0;  6 private transient String str1;  7 private static String str2 = "abc";  8  9 public SerializableObject(String str0, String str1) 10  { 11 this.str0 = str0; 12 this.str1 = str1; 13  } 14 15 public String getStr0() 16  { 17 return str0; 18  } 19 20 public String getStr1() 21  { 22 return str1; 23  } 24 }
復制代碼
復制代碼
 1 public static void main(String[] args) throws Exception  2 {  3 File file = new File("D:" + File.separator + "s.txt");  4 OutputStream os = new FileOutputStream(file);  5 ObjectOutputStream oos = new ObjectOutputStream(os);  6 oos.writeObject(new SerializableObject("str0", "str1"));  7  oos.close();  8  9 InputStream is = new FileInputStream(file); 10 ObjectInputStream ois = new ObjectInputStream(is); 11 SerializableObject so = (SerializableObject)ois.readObject(); 12 System.out.println("str0 = " + so.getStr0()); 13 System.out.println("str1 = " + so.getStr1()); 14  ois.close(); 15 }
復制代碼

先不運行,用一個二進制查看器查看一下s.txt這個文件,並詳細解釋一下每一部分的內容。

第1部分是序列化文件頭

◇AC ED:STREAM_MAGIC序列化協議

◇00 05:STREAM_VERSION序列化協議版本

◇73:TC_OBJECT聲明這是一個新的對象

第2部分是要序列化的類的描述,在這里是SerializableObject類

◇72:TC_CLASSDESC聲明這里開始一個新的class

◇00 1F:十進制的31,表示class名字的長度是31個字節

◇63 6F 6D ... 65 63 74:表示的是“com.xrq.test.SerializableObject”這一串字符,可以數一下確實是31個字節

◇00 00 00 00 00 00 00 01:SerialVersion,序列化ID,1

◇02:標記號,聲明該對象支持序列化

◇00 01:該類所包含的域的個數為1個

第3部分是對象中各個屬性項的描述

◇4C:字符"L",表示該屬性是一個對象類型而不是一個基本類型

◇00 04:十進制的4,表示屬性名的長度

73 74 72 30:字符串“str0”,屬性名

◇74:TC_STRING,代表一個new String,用String來引用對象

第4部分是該對象父類的信息,如果沒有父類就沒有這部分。有父類和第2部分差不多

◇00 12:十進制的18,表示父類的長度

◇4C 6A 61 ... 6E 67 3B:“L/java/lang/String;”表示的是父類屬性

◇78:TC_ENDBLOCKDATA,對象塊結束的標志

◇70:TC_NULL,說明沒有其他超類的標志

第5部分輸出對象的屬性項的實際值,如果屬性項是一個對象,這里還將序列化這個對象,規則和第2部分一樣

◇00 04:十進制的4,屬性的長度

73 74 72 30:字符串“str0”,str0的屬性值

從以上對於序列化后的二進制文件的解析,我們可以得出以下幾個關鍵的結論:

1、序列化之后保存的是類的信息

2、被聲明為transient的屬性不會被序列化,這就是transient關鍵字的作用

3、被聲明為static的屬性不會被序列化,這個問題可以這么理解,序列化保存的是對象的狀態,但是static修飾的變量是屬於類的而不是屬於變量的,因此序列化的時候不會序列化它

接下來運行一下上面的代碼看一下

str0 = str0
str1 = null

因為str1是一個transient類型的變量,沒有被序列化,因此反序列化出來也是沒有任何內容的,顯示的null,符合我們的結論。

 

手動指定序列化過程

Java並不強求用戶非要使用默認的序列化方式,用戶也可以按照自己的喜好自己指定自己想要的序列化方式----只要你自己能保證序列化前后能得到想要的數據就好了。手動指定序列化方式的規則是:

進 行序列化、反序列化時,虛擬機會首先試圖調用對象里的writeObject和readObject方法,進行用戶自定義的序列化和反序列化。如果沒有這 樣的方法,那么默認調用的是ObjectOutputStream的defaultWriteObject以及ObjectInputStream的 defaultReadObject方法。換言之,利用自定義的writeObject方法和readObject方法,用戶可以自己控制序列化和反序列 化的過程。

這是非常有用的。比如:

1、有些 場景下,某些字段我們並不想要使用Java提供給我們的序列化方式,而是想要以自定義的方式去序列化它,比如ArrayList的 elementData、HashMap的table(至於為什么在之后寫這兩個類的時候會解釋原因),就可以通過將這些字段聲明為transient, 然后在writeObject和readObject中去使用自己想要的方式去序列化它們

2、因為 序列化並不安全,因此有些場景下我們需要對一些敏感字段進行加密再序列化,然后再反序列化的時候按照同樣的方式進行解密,就在一定程度上保證了安全性了。 要這么做,就必須自己寫writeObject和readObject,writeObject方法在序列化前對字段加密,readObject方法在序 列化之后對字段解密

上面的例子SerializObject這個類修改一下,主函數不需要修改:

復制代碼
 1 public class SerializableObject implements Serializable  2 {  3 private static final long serialVersionUID = 1L;  4  5 private String str0;  6 private transient String str1;  7 private static String str2 = "abc";  8  9 public SerializableObject(String str0, String str1) 10  { 11 this.str0 = str0; 12 this.str1 = str1; 13  } 14 15 public String getStr0() 16  { 17 return str0; 18  } 19 20 public String getStr1() 21  { 22 return str1; 23  } 24 25 private void writeObject(java.io.ObjectOutputStream s) throws Exception 26  { 27 System.out.println("我想自己控制序列化的過程"); 28  s.defaultWriteObject(); 29  s.writeInt(str1.length()); 30 for (int i = 0; i < str1.length(); i++) 31  s.writeChar(str1.charAt(i)); 32  } 33 34 private void readObject(java.io.ObjectInputStream s) throws Exception 35  { 36 System.out.println("我想自己控制反序列化的過程"); 37  s.defaultReadObject(); 38 int length = s.readInt(); 39 char[] cs = new char[length]; 40 for (int i = 0; i < length; i++) 41 cs[i] = s.readChar(); 42 str1 = new String(cs, 0, length); 43  } 44 }
復制代碼

直接看一下運行結果

我想自己控制序列化的過程
我想自己控制反序列化的過程
str0 = str0 str1 = str1

看到,程序走到了我們自己寫的writeObject和readObject中,而且被transient修飾的str1也成功序列化、反序列化出來了----因為手動將str1寫入了文件和從文件中讀了出來。不妨再看一下s.txt文件的二進制:

看到橘黃色的部分就是writeObject方法追加的str1的內容。至此,總結一下writeObject和readObject的通常用法:

先通過defaultWriteObject和defaultReadObject方法序列化、反序列化對象,然后在文件結尾追加需要額外序列化的內容/從文件的結尾讀取額外需要讀取的內容。 

 

復雜序列化情況總結

雖然Java的序列化能夠保證對象狀態的持久保存,但是遇到一些對象結構復雜的情況還是比較難處理的,最后對一些復雜的對象情況作一個總結:

1、當父類繼承Serializable接口時,所有子類都可以被序列化

2、子類實現了Serializable接口,父類沒有,父類中的屬性不能序列化(不報錯,數據丟失),但是在子類中屬性仍能正確序列化

3、如果序列化的屬性是對象,則這個對象也必須實現Serializable接口,否則會報錯

4、反序列化時,如果對象的屬性有修改或刪減,則修改的部分屬性會丟失,但不會報錯

5、反序列化時,如果serialVersionUID被修改,則反序列化時會失敗


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM