Java中的序列化


序列化是什么意思,能不能給我通俗的講一下?

序列化是指把一個Java對象變成二進制內容,本質上就是一個byte[]數組。 為什么要把Java對象序列化呢?因為序列化后可以把byte[]保存到文件中,或者把byte[]通過網絡傳輸到遠程,這樣,就相當於把Java對象存儲到文件或者通過網絡傳輸出去了。
有序列化,就有反序列化,即把一個二進制內容(也就是byte[]數組)變回Java對象。有了反序列化,保存到文件中的byte[]數組又可以“變回”Java對象,或者從網絡上讀取byte[]並把它“變回”Java對象。

為什么需要要序列化?

  • 一般Java對象的生命周期比Java虛擬機短,而實際的開發中,我們需要在Jvm停止后能夠繼續持有對象,這個時候就需要用到序列化技術將對象持久到磁盤或數據庫。
  • 在多個項目進行RPC調用的,需要在網絡上傳輸JavaBean對象。我們知道數據只能以二進制的形式才能在網絡上進行傳輸。所以也需要用到序列化技術。

Java序列化的實現

在Java中,要把一個對象序列化,必須要實現下面兩個接口之一:

  • Serializble
  • Externalizable

Serializble接口

一個對象想要被序列化,那么它的類就要實現此接口或者它的子接口。
這個對象的所有屬性(包括private屬性、包括其引用的對象)都可以被序列化和反序列化來保存、傳輸。不想序列化的字段可以使用transient修飾。

Externalizable接口

它是Serializable接口的子類,用戶要實現的writeExternal()和readExternal() 方法,用來決定如何序列化和反序列化。

因為序列化和反序列化方法需要自己實現,因此可以指定序列化哪些屬性,而transient在這里無效。

對Externalizable對象反序列化時,會先調用類的無參構造方法,這是有別於默認反序列方式的。如果把類的不帶參數的構造方法刪除,或者把該構造方法的訪問權限設置為private、默認或protected級別,會拋出java.io.InvalidException: no valid constructor異常,因此Externalizable對象必須有默認構造函數,而且必需是public的。

兩者之間的對比

使用時,你只想隱藏一個屬性,比如用戶對象user的密碼pwd,如果使用Externalizable,並除了pwd之外的每個屬性都寫在writeExternal()方法里,這樣顯得麻煩,可以使用Serializable接口,並在要隱藏的屬性pwd前面加transient就可以實現了。如果要定義很多的特殊處理,就可以使用Externalizable。

當然這里我們有一些疑惑,Serializable 中的writeObject()方法與readObject()方法科可以實現自定義序列化,而Externalizable 中的writeExternal()和readExternal() 方法也可以,他們有什么異同呢?

  • readExternal(),writeExternal()兩個方法,這兩個方法除了方法簽名和readObject(),writeObject()兩個方法的方法簽名不同之外,其方法體完全一樣。

  • 需要指出的是,當使用Externalizable機制反序列化該對象時,程序會使用public的無參構造器創建實例,然后才執行readExternal()方法進行反序列化,因此實現Externalizable的序列化類必須提供public的無參構造。

  • 雖然實現Externalizable接口能帶來一定的性能提升,但由於實現ExternaLizable接口導致了編程復雜度的增加,所以大部分時候都是采用實現Serializable接口方式來實現序列化。

序列化版本號

在Java序列化中,可以控制序列化的版本,該字段為被序列化對象中的serialVersionUID字段。

private static final long serialVersionUID = 1L;

一個對象數據,在反序列化過程中,如果序列化串中的serialVersionUID與當前對象值不同,則反序列化失敗,否則成功。

如果serialVersionUID沒有顯式生成,系統就會自動生成一個。生成的輸入有:類名、類及其屬性修飾符、接口及接口順序、屬性、靜態初始化、構造器。任何一項的改變都會導致serialVersionUID變化。

為了避免這種問題, 一般系統都會要求實現serialiable接口的類顯式的生明一個serialVersionUID。

顯式定義serialVersionUID的兩種用途:

  • 希望類的不同版本對序列化兼容時,需要確保類的不同版本具有相同的serialVersionUID;

  • 不希望類的不同版本對序列化兼容時,需要確保類的不同版本具有不同的serialVersionUID。如果我們保持了serialVersionUID的一致,則在反序列化時,對於新增的字段會填入默認值null(int的默認值0),對於減少的字段則直接忽略。

序列化代碼實現

public class User implements Serializable {

  private static final long serialVersionUID = 1L;

  private String username;
  private String gender;
  private int age;
  private Date birth;
  private Pet pet;

  public User() {
  }

  public User(String username, String gender, int age, Date birth, Pet pet) {
    this.username = username;
    this.gender = gender;
    this.age = age;
    this.birth = birth;
    this.pet = pet;
  }

  //序列化時默認調用
  private void writeObject(ObjectOutputStream out) throws IOException {
    Base64.Encoder encoder = Base64.getEncoder();
    byte[] bytes = encoder.encode(this.username.getBytes());
    this.username = new String(bytes);
    out.defaultWriteObject();
  }

  //反序列化時默認調用
  private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    in.defaultReadObject();
    Base64.Decoder decoder = Base64.getDecoder();
    byte[] bytes = decoder.decode(this.username);
    this.username = new String(bytes);
  }
  
  //=======================測試一下==========================
  public static void main(String[] args) {
    User user = new User("jay", "male",
            18, new Date(), new Pet("wangcai", "male"));

    File file = new File("user.dat");
    if (!file.exists()){
      try {
        if(!file.createNewFile()){
          System.out.println("文件創建失敗!");
          return;
        }

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file, false));
        oos.writeObject(user);
        oos.close();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }

  }

  @Test
  public void test(){
    File file = new File("user.dat");
    if (!file.exists()) return;
    try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) {
      User user = (User) ois.readObject();
      System.out.println(user);
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

在網絡中傳輸使用socket模擬即可,不寫了

序列化和單例模式

所謂單例:就是單例模式就是在整個全局中(無論是單線程還是多線程),該對象只存在一個實例,而且只應該存在一個實例,沒有副本。
序列化對單例有破壞:

  • 1、通過對某個對象的序列化與反序列化得到的對象是一個新的對象,這就破壞了單例模式的單例性。
  • 2、我們知道readObject()的時候,底層運用了反射的技術,序列化會通過反射調用無參數的構造方法創建一個新的對象。這破壞了對象的單例性。
  • 3、解決方案:在需要的單例的對象類中添加如下代碼
private Object readResolve(){
	return instance;
}

為什么說序列化並不安全

因為序列化的對象數據轉換為二進制,並且完全可逆。但是在RMI調用時所有private字段的數據都以明文二進制的形式出現在網絡的套接字上,這顯然是不安全的。

解決方案:

  1. 序列化Hook化(移位和復位)
  2. 序列數據加密和簽名
  3. 利用transient的特性解決
  4. 打包和解包代理


免責聲明!

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



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