前一段時間寫的關於集合類源碼分析的博客中其實一直沒有提到兩個方法,那就是writeObject和readObject方法。這兩個方法涉及到序列化的內容,這篇博文總結遇到過的和序列化相關的內容。
什么是序列化?
序列化是將對象的狀態信息轉化為可以存儲或傳輸的形式的過程。在序列化期間,對象將其當前狀態寫入到臨時或持久性存儲區。以后可以通過存儲區中讀取或反序列化對象的狀態重新創建對象。
為什么要序列化?
有兩個最重要的原因促使對序列化的使用:一個原因是將對象的狀態保持在存儲媒體中,以便可以在以后重新創建精確的副本;另一個原因是通過值將對象從一個應用程序域發送到另一個應用程序域中。例如,在網絡中傳輸的數據都必須要序列化。
Java中的序列化
Java中的序列化機制能夠將一個實例對象的狀態信息寫入到一個字節流中,使其可以通過socket進行傳輸或者持久化存儲到數據庫或文件系統中,然后再需要的時候可以讀取字節流中的信息重構一個相同的對象。序列化在Java中有着廣泛的應用,RMI、Hessian等技術都是以此為基礎的。
下面是一些序列化涉及到的內容的例子。
UserInfo類是下面序列化例子中都要用到的一個保存基本信息的類。
1 public class UserInfo implements Serializable { 2 3 private static final long serialVersionUID = 1L; 4 public static String defaultPostcode = "310000"; 5 private int age; 6 private String name; 7 8 public int getAge() { 9 return age; 10 } 11 12 public void setAge(int age) { 13 this.age = age; 14 } 15 16 public String getName() { 17 return name; 18 } 19 20 public void setName(String name) { 21 this.name = name; 22 } 23 24 public static String getDefaultPostcode() { 25 return defaultPostcode; 26 } 27 28 public static void setDefaultPostcode(String defaultPostcode) { 29 UserInfo.defaultPostcode = defaultPostcode; 30 } 31 32 public void desSelf() { 33 System.out.println("Default Postcode: " + getDefaultPostcode()); 34 System.out.println("Age: " + getAge()); 35 System.out.println("Name: " + getName()); 36 } 37 }
結合UserInfo的內容,先看下面這個main方法。
1 public static void main(String[] args) throws IOException, ClassNotFoundException { 2 FileOutputStream fos = new FileOutputStream("temp.out"); 3 ObjectOutputStream oos = new ObjectOutputStream(fos); 4 UserInfo user = new UserInfo(); 5 user.setAge(25); 6 user.setName("Tom"); 7 System.out.println("Before Serialize"); 8 user.desSelf(); 9 // 保存對象后修改了DefaultPostcode 10 UserInfo.setDefaultPostcode("110"); 11 oos.writeObject(user); 12 oos.flush(); 13 FileInputStream fis = new FileInputStream("temp.out"); 14 ObjectInputStream ois = new ObjectInputStream(fis); 15 user = (UserInfo)ois.readObject(); 16 System.out.println("After Deserialize"); 17 user.desSelf(); 18 }
在5、6兩行設置了age為25,name為Tom,然后輸出了UserInfo自己的描述:
Before Serialize
Default Postcode: 310000
Age: 25
Name: Tom
可以看到Age和Name分別是設置的值。defaultPostCode是UserInfo的一個靜態變量,它的值是類中指定的310000。然后將這個對象進行了序列化,保存在temp.out中。
在第10行修改了defaultPostcode的值為110,然后反序列化並輸出user的描述信息,結果如下:
After Deserialize
Default Postcode: 110
Age: 25
Name: Tom
為什么反序列化后defaultPostcode不是310000而是修改的110呢?因為序列化保存的是對象的狀態,而靜態變量屬於類的狀態,在序列化的時候不會被保存。
注意,需要被序列化的對象必須實現Serializable接口,否則在序列化的時候會拋出java.io.NotSerializableException異常。
實現Serializable接口總是會要求添加一個serialVersionUID屬性,有以下兩種形式:
private static final long serialVersionUID = 5511561554099546149L;
private static final long serialVersionUID = 1L;
它們有什么區別呢?一個是固定的 1L,一個是隨機生成一個不重復的 long 類型數據(實際上是使用 JDK 工具生成),在這里有一個建議,如果沒有特殊需求,就是用默認的 1L 就可以,這樣可以確保代碼一致時反序列化成功。那么隨機生成的序列化 ID 有什么作用呢,有些時候,通過改變序列化 ID 可以用來限制某些用戶的使用。
下面看一個和序列化相關的關鍵字Transient(在《ArrayList源碼分析》中提到過)。
Java的serialization提供了一種持久化對象實例的機制。當持久化對象時,可能有一個特殊的對象數據成員,我們不想用serialization機制來保存它。為了在一個特定對象的一個域上關閉serialization,可以在這個域前加上關鍵字transient。
transient是Java語言的關鍵字,用來表示一個域不是該對象串行化的一部分。當一個對象被串行化的時候,transient型變量的值不包括在串行化的表示中,然而非transient型的變量是被包括進去的。
有點抽象,看個例子應該能明白。
1 public class UserInfo implements Serializable { 2 private static final long serialVersionUID = 996890129747019948L; 3 private String name; 4 private transient String psw; 5 6 public UserInfo(String name, String psw) { 7 this.name = name; 8 this.psw = psw; 9 } 10 11 public String toString() { 12 return "name=" + name + ", psw=" + psw; 13 } 14 } 15 16 public class TestTransient { 17 public static void main(String[] args) { 18 UserInfo userInfo = new UserInfo("張三", "123456"); 19 System.out.println(userInfo); 20 try { 21 // 序列化,被設置為transient的屬性沒有被序列化 22 ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream( 23 "UserInfo.out")); 24 o.writeObject(userInfo); 25 o.close(); 26 } catch (Exception e) { 27 // TODO: handle exception 28 e.printStackTrace(); 29 } 30 try { 31 // 重新讀取內容 32 ObjectInputStream in = new ObjectInputStream(new FileInputStream( 33 "UserInfo.out")); 34 UserInfo readUserInfo = (UserInfo) in.readObject(); 35 //讀取后psw的內容為null 36 System.out.println(readUserInfo.toString()); 37 } catch (Exception e) { 38 // TODO: handle exception 39 e.printStackTrace(); 40 } 41 } 42 }
下面說一下上文提到的ArrayList中的writeObject和readObject。先看這兩個方法在ArrayList中的具體內容。
1 private void writeObject(java.io.ObjectOutputStream s) 2 throws java.io.IOException{ 3 int expectedModCount = modCount; 4 s.defaultWriteObject(); 5 s.writeInt(elementData.length); 6 for (int i=0; i<size; i++) 7 s.writeObject(elementData[i]); 8 9 if (modCount != expectedModCount) { 10 throw new ConcurrentModificationException(); 11 } 12 } 13 14 private void readObject(java.io.ObjectInputStream s) 15 throws java.io.IOException, ClassNotFoundException { 16 s.defaultReadObject(); 17 int arrayLength = s.readInt(); 18 Object[] a = elementData = new Object[arrayLength]; 19 for (int i=0; i<size; i++) 20 a[i] = s.readObject(); 21 }
這兩個方法都是private的且沒在ArrayList中被調用過,那為什么需要這兩個方法呢?
writeObject和readObject並不是在每個類和接口中都會定義,而只是定義在哪些在序列化和反序列化過程中需要特殊處理的類中。
stackoverflow上的解答:http://stackoverflow.com/questions/7467313/why-are-readobject-and-writeobject-private-and-why-would-i-write-transient-vari
也就是說通過這兩個方法可以自己去控制序列化和反序列化的過程。下面是這兩個方法的一個例子。
1 private static final long serialVersionUID = 1L; 2 3 private String password = "pass"; 4 5 public String getPassword() { 6 return password; 7 } 8 9 public void setPassword(String password) { 10 this.password = password; 11 } 12 13 private void writeObject(ObjectOutputStream out) { 14 try { 15 PutField putFields = out.putFields(); 16 System.out.println("原密碼:" + password); 17 password = "encryption";//模擬加密 18 putFields.put("password", password); 19 System.out.println("加密后的密碼" + password); 20 out.writeFields(); 21 } catch (IOException e) { 22 e.printStackTrace(); 23 } 24 } 25 26 private void readObject(ObjectInputStream in) { 27 try { 28 GetField readFields = in.readFields(); 29 Object object = readFields.get("password", ""); 30 System.out.println("要解密的字符串:" + object.toString()); 31 password = "pass";//模擬解密,需要獲得本地的密鑰 32 } catch (IOException e) { 33 e.printStackTrace(); 34 } catch (ClassNotFoundException e) { 35 e.printStackTrace(); 36 } 37 38 } 39 40 public static void main(String[] args) { 41 try { 42 ObjectOutputStream out = new ObjectOutputStream( 43 new FileOutputStream("result.obj")); 44 out.writeObject(new Test()); 45 out.close(); 46 47 ObjectInputStream oin = new ObjectInputStream(new FileInputStream( 48 "result.obj")); 49 Test t = (Test) oin.readObject(); 50 System.out.println("解密后的字符串:" + t.getPassword()); 51 oin.close(); 52 } catch (FileNotFoundException e) { 53 e.printStackTrace(); 54 } catch (IOException e) { 55 e.printStackTrace(); 56 } catch (ClassNotFoundException e) { 57 e.printStackTrace(); 58 } 59 }
writeObject 方法中,對密碼進行了加密,在 readObject 中則對 password 進行解密,只有擁有密鑰的客戶端,才可以正確的解析出密碼,確保了數據的安全。
最后說一下序列化的存儲規則。
1 public class Test { 2 public static void main(String[] args) throws IOException, 3 ClassNotFoundException { 4 FileOutputStream fos = new FileOutputStream("temp.out"); 5 ObjectOutputStream oos = new ObjectOutputStream(fos); 6 UserInfo user = new UserInfo(); 7 user.setAge(25); 8 user.setName("Tom"); 9 oos.writeObject(user); 10 oos.flush(); 11 System.out.println(new File("temp.out").length()); 12 oos.writeObject(user); 13 oos.flush(); 14 oos.close(); 15 System.out.println(new File("temp.out").length()); 16 17 FileInputStream fis = new FileInputStream("temp.out"); 18 ObjectInputStream ois = new ObjectInputStream(fis); 19 UserInfo user1 = (UserInfo) ois.readObject(); 20 UserInfo user2 = (UserInfo) ois.readObject(); 21 ois.close(); 22 System.out.println(user1 == user2); 23 } 24 }
為什么同一對象寫入兩次,文件的大小不是寫入一次的文件大小的兩倍呢?而第三次寫入和第二次寫入增加的長度是一樣的呢?為什么反序列化后的兩個對象比較結果是true呢?
Java 序列化機制為了節省磁盤空間,具有特定的存儲規則,當寫入文件的為同一對象時,並不會再將對象的內容進行存儲,而只是再次存儲一份引用,上面增加的 5 字節的存儲空間就是新增引用和一些控制信息的空間。反序列化時,恢復引用關系,使得user1和user2二者相等,輸出 true。該存儲規則極大的節省了存儲空間。
肯定還有我不知道的內容,望多交流討論。