Java 序列化 之 Serializable


概念

序列化:就是把對象轉化成字節。
反序列化:把字節數據轉換成對象。

對象序列化場景:

1、對象網絡傳輸
例如:在微服務系統中或給第三方提供接口調用時,使用rpc進行調用,一般會把對象轉化成字節序列,才能在網絡上傳輸;接收方則需要把字節序列再轉化為java對象。

2、對象保存至文件中
例如:hibernate中的二級緩存:把從數據庫中查詢出的對象,序列化轉存到硬盤中,下次讀取的時候,首先從內存中找是否有該對象,如果沒有在去二級緩存(硬盤)中去查找。減少數據庫的查詢次數,提升性能。

3、tomcat的鈍化和活化

  • tomcat 的session 鈍化和活化之 StandarManager :
    當Tomcat服務器關閉或者重啟時tomcat服務器會將當前內存中的session對象鈍化到服務器文件系統中;
    另一種情況是web應用程序被重新加載時(其實原理也是重啟tomcat),內存中的session對象也會被鈍化到服務器的文件系統中
    當系統啟動時,會把序列化到硬盤上session重新加載到內存中來。這樣用戶還保持這登錄狀態,提供系統的可用性。
    這樣,tomcat重啟,如果用戶在tomcat重啟之前登錄過,然后在tomcat重啟后可以不需要登錄(前提是session沒過期前,默認是30分鍾過期)。

  • tomcat 的session 鈍化和活化之 Persistentmanager:
    當網站有大量用戶訪問的時候,服務器會創建大量的session,會占用大量的服務器內存資源,當用戶開着瀏覽器一分鍾不操作頁面的話建議將session鈍化,將session生成文件放在tomcat工作目錄下。

可參考 : Tomcat 之 Session的活化和鈍化 源碼分析

1. java 序列化 Serializable

java 中只要對象實現了 java.io.Serializable 就可以進行序列化。

public class User implements Serializable { private String userName; private String password; private String addr; public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getAddr() { return addr; } public void setAddr(String addr) { this.addr = addr; } @Override public String toString() { return "User [userName=" + userName + ", password=" + password + ", addr=" + addr + "]"; } } 

該 User 類實現了 Serializable 接口,那么該類應該怎么序列化和發序列化呢?

2. ObjectInputStream 和 ObjectOutputStream

Java IO 包中為我們提供了 ObjectInputStream 和 ObjectOutputStream 兩個類。
java.io.ObjectOutputStream 類實現類的序列化功能。
java.io.ObjectInputStream 類實現了反序列化功能。

示例如下:

public class Test{ public static void main(String[] args) throws Exception { File file = new File("d:\\a.user"); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file)); User user1 = new User(); user1.setUserName("zhangsan"); user1.setPassword("123456"); user1.setAddr("北京中關村"); oos.writeObject(user1); ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file)); User user2 = (User)ois.readObject(); System.out.println(user2); } } 

輸出結果:
User [userName=zhangsan, password=123456, addr=北京中關村]

  1. 使用 ObjectOutputStream 把 user1 實例序列化到 d:\user 文件中。
  2. 使用 ObjectInputStream 把 d:\user 文件中的數據反序列化成 user2 實,並打印。

如果考慮安全問題,我們不想把密碼序列化進行保存,那么該怎么做呢?

3. transient關鍵字

當某個字段被聲明為transient后,默認序列化機制就會忽略該字段。此處將User類中的password字段聲明為transient,如下所示,

public class User implements Serializable { private String userName; private transient String password; private String addr; ... ... 

然后在執行Test類的 main 方法,執行結果如下:

輸出結果:
User [userName=zhangsan, password=null, addr=北京中關村]


當我們把 User 對象序列化保存到文件中,這時 User 類結構添加了一個新字段,那么它能成功反序列化嗎?

serialVersionUID的作用

 

 

User 類中添加一個新屬性 email 字段,如下圖:
 
 

 

 

然后再執行反序列化
 
 

執行結果如下:

Exception in thread "main" java.io.InvalidClassException: cn.com.infcn.serial.User; local class incompatible: stream classdesc serialVersionUID = 1318824539146791009, local class serialVersionUID = 7884536922902331245

執行反序列化報 java.io.InvalidClassException 異常。這是由於 User 類修改了,
也就是修改過后的class,不兼容了,處於安全機制考慮,程序拋出了錯誤,並且拒絕載入。從異常信息中可以看出,它是根據 serialVersionUID 值進行判斷類是否修改過。

如果在添加新字段 email 后,還可以繼續加載之前的字段怎么辦呢?

 

我們可以在類中添加 serialVersionUID 屬性字段。
 
 

serialVersionUID 的值和報錯中的 "stream classdesc serialVersionUID" 的值一樣就可以反序列化了。

如果類中沒有顯示的聲明 serialVersionUID 屬性,那么java編譯器會自動為我們生成一個 serialVersionUID (應該是根據 屬性和方法進行摘要算出來的,方法里面內容變動 serialVersionUID 的值不會改變。)

如果 User 對象升級版本,修改了結構,而且不想兼容之前的版本,那么只需要修改下 serialVersionUID 的值就可以了。

建議,每個需要序列化的對象,都要添加一個 serialVersionUID 字段。

如果需要把設置的 transient 的字段也需要序列化和發序列化,我們應該怎么辦?我們需要對密碼加密序列化,反序列化后解密處理,又應該怎么做?

readObject 和 writeObject

 
 
crypto 方法,實現加解密功能。
    /** * 簡單加密加密解密字符串 加密解密思路:先將字符串變成byte數組,再將數組每位與key做位運算,得到新的數組就是加密或解密后的byte數組. * 知識:^ 是java位運算,可以百度了解下,a = b ^ skey 反之也成立,即b = a ^ skey * * @param str 解密/加密 字符串 * @return * @throws Exception */ static String crypto(String str) { try { byte skey = (byte) 88; //密鑰 byte[] bytes = str.getBytes("GBK"); for (int i = 0; i < bytes.length; i++) { bytes[i] = (byte) (bytes[i] ^ skey); } return new String(bytes, "GBK"); } catch (Exception ex) { ex.printStackTrace(); } return null; } 

我們只需要在當前 User 類中添加 readObject() 和 writeObject() 方法,在 writeObject 方法中實現對 password 的字段加密,在 readObject 方法中實現對 password 字段解密,並賦值給 User 對象即可。

readObject() 和 writeObject() 可以實現對 transient 和 非transient字段進行序列化。

ArrayList 序列化源碼分析

我們知道,ArrayList 是通過數組進行存儲數據的,當數組中元素達到數組的最大容量時,會自動生成一個更大的數組,並復制到更大的數組中。

 
 

打開ArrayList 源碼,我們可以知道,數據是存儲在 Object[] elementData 數組中。
該屬性是 transient 關鍵字修飾的,通過上面代碼可以知道,用 transient 關鍵字修飾的字段,默認是不能被序列化的。ArrayList 如果要實現序列化,那么就必須通過 readObject() 和 writeObject() 方法去實現序列化,那么他這是多此一舉嗎?

writeObject() 方法

 
 

通過源碼,我們可以看到,ArrayList 序列化數組元素時做了優化。
因為 ArrayList 的 elementData 數組大小,不是ArrayList 的實際容量,這里只把實際存儲在 elementData中的數據,進行序列化。這樣減少了序列化的流大小。

 
 
 
 
25人點贊
 
 


作者:jijs
鏈接:https://www.jianshu.com/p/af2f0a4b03b5
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。


免責聲明!

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



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