Java 序列化和反序列化(一)Serializable 使用場景


Java 序列化和反序列化(一)Serializable 使用場景

以下為 Java 序列化系列文章:

1. Java 序列化和反序列化(一)Serializable 使用場景
2. Java 序列化和反序列化(二)Serializable 源碼分析 - 1
3. Java 序列化和反序列化(三)Serializable 源碼分析 - 2

將 Java 對象序列化為二進制文件的 Java 序列化技術是 Java 系列技術中一個較為重要的技術點,在大部分情況下,開發人員只需要了解被序列化的類需要實現 Serializable 接口,使用 ObjectInputStream 和 ObjectOutputStream 進行對象的讀寫。

眾所周知,Java 原生的序列化方法可以分為兩種,下面介紹一下這兩個接口的用法:

  • 實現 Serializable 接口:可以自定義 writeObject、readObject、writeReplace、readResolve 方法,會通過反射調用。
  • 實現 Externalizable 接口:需要實現 writeExternal 和 readExternal 方法。

1. 最簡單的使用:Serializable 接口

情境: Java 的序列化使用起來很簡單,實現 Serializable 接口即可,實際由 ObjectOutputStream、ObjectInputStream 完成對這個標記接口的處理。

Code-1. Java 序列化 Serializable 接口

@Test
public void testSerializable() throws IOException, ClassNotFoundException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(baos);

    User user1 = new User();
    user1.setName("binarylei");
    oos.writeObject(user1);
    byte[] bytes = baos.toByteArray();
    System.out.println(bytes.length);               // length=89

    ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
    ObjectInputStream ois = new ObjectInputStream(bais);
    User user2 = (User) ois.readObject();
    Assert.assertEquals(user1.getName(), user2.getName());  // 反序列化后保存了 User.name 信息
    Assert.assertNotEquals(user1, user2);                   // 反序列化后不是同一個對象

    oos.close();
    ois.close();
}
class User implements Serializable {
    private String name;
    // getter setter
}

總結: 如果不實現 Serializable 接口會發生什么事情呢?如果 User 不實現 Serializable 接口而直接序列化會拋出 NotSerializableException 異常。

java.io.NotSerializableException: com.github.binarylei.io.User
  at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
  at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)

2. 序列化 ID 的問題

情境: 兩個客戶端 A 和 B 試圖通過網絡傳遞對象數據,A 端將對象 User 序列化為二進制數據再傳給 B,B 反序列化得到 User。

問題: 在 A 和 B 端都有這么一個類文件,功能代碼完全一致。也都實現了 Serializable 接口,但是反序列化時總是提示不成功。

解決: 虛擬機是否允許反序列化,不僅取決於類路徑和功能代碼是否一致,一個非常重要的一點是兩個類的序列化 ID 是否一致(就是 private static final long serialVersionUID = 1L)。Code-2 中,雖然兩個類的功能代碼完全一致,但是序列化 ID 不同,他們無法相互序列化和反序列化。

Code-2. 相同功能代碼不同序列化 ID 的類對比

public class User implements Serializable { 
    private static final long serialVersionUID = 1L; 
    private String name;
} 
 
public class User implements Serializable { 
    private static final long serialVersionUID = 2L;  
    private String name; 
}

3. 靜態字段不會序列化

序列化時不保存靜態變量,這是因為序列化保存的是對象的狀態,靜態變量屬於類的狀態,因此 序列化並不保存靜態變量。

Code-3. 靜態字段不會序列化

class User implements Serializable {
    public static String staticVar;  // 不會序列化
    private String name;
}

4. 屏蔽字段:transient

transient 關鍵字有兩個特性:

  1. 如果你不想讓對象中的某個成員被序列化可以在定義它的時候加上 transient 關鍵字進行修飾,這樣,在對象被序列化時其就不會被序列化。

  2. transient 修飾過的成員反序列化后將賦予默認值,即 0 或 null。下面的 User 在反序列化后 password=null。

Code-4. transient 修辭的字段不會序列化

class User implements Serializable {
    public static String staticVar;   // 不會序列化
    public String name;
    public transient String password; // 不會序列化
}

5. 父類的序列化

情境: 一個子類實現了 Serializable 接口,它的父類都沒有實現 Serializable 接口,序列化該子類對象,然后反序列化后輸出父類定義的某變量的數值,該變量數值與序列化時的數值不同。

解決: 要想將父類對象也序列化,就需要讓父類也實現 Serializable 接口。如果父類不實現的話的,就 需要有默認的無參的構造函數。在父類沒有實現 Serializable 接口時,虛擬機是不會序列化父對象的,而一個 Java 對象的構造必須先有父對象,才有子對象,反序列化也不例外。所以反序列化時,為了構造父對象,只能調用父類的無參構造函數作為默認的父對象。因此當我們取父對象的變量值時,它的值是調用父類無參構造函數后的值。如果你考慮到這種序列化的情況,在父類無參構造函數中對變量進行初始化,否則的話,父類變量值都是默認聲明的值,如 int 型的默認是 0,string 型的默認是 null。

6. 自定義序列化:readObject 和 writeObject

情境: 服務器端給客戶端發送序列化對象數據,對象中有一些數據是敏感的,比如密碼字符串等,希望對該密碼字段在序列化時,進行加密,而客戶端如果擁有解密的密鑰,只有在客戶端進行反序列化時,才可以對密碼進行讀取,這樣可以一定程度保證序列化對象的數據安全。

解決:在序列化過程中,虛擬機會試圖調用對象類里的 writeObject 和 readObject 方法,進行用戶自定義的序列化和反序列化,如果沒有這樣的方法,則默認調用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。用戶自定義的 writeObject 和 readObject 方法可以允許用戶控制序列化的過程,比如可以在序列化的過程中動態改變序列化的數值。基於這個原理,可以在實際應用中得到使用,用於敏感字段的加密工作,Code-6 展示了這個過程。

Code-6. 使用 writeObject 和 readObject 對敏感字段加密代碼

@Test
public void testEncryptionPassword() throws Exception {
    ObjectOutputStream out = new ObjectOutputStream(
            new FileOutputStream("result.obj"));
    out.writeObject(new User("binarylei", "password"));
    out.close();

    ObjectInputStream oin = new ObjectInputStream(
            new FileInputStream("result.obj"));
    User t = (User) oin.readObject();
    Assert.assertEquals("password", t.getPassword());
    oin.close();
}

class User implements Serializable {
    private String name;
    private String password;

    private void writeObject(ObjectOutputStream out) throws Exception {
        ObjectOutputStream.PutField putFields = out.putFields();
        putFields.put("password", password + "-1");
        out.writeFields();
    }
    private void readObject(ObjectInputStream in) throws Exception {
        ObjectInputStream.GetField readFields = in.readFields();
        String encryptionPassword = (String) readFields.get("password", "");
        // 模擬解密
        password = encryptionPassword.substring(0, encryptionPassword.indexOf('-'));
    }
}

在 Code-6 的 writeObject 方法中,對密碼進行了加密,在 readObject 中則對 password 進行解密,只有擁有密鑰的客戶端,才可以正確的解析出密碼,確保了數據的安全。

7. 寫入時替換對象:writeReplace

Serializable 還有兩個標記接口方法可以實現序列化對象的替換,即 writeReplace 和 readResolve;

  1. writeObject 序列化時會調用 writeReplace 方法將當前對象替換成另一個對象並將其寫入流中,此時如果有自定義的 writeObject 也不會生效了;
  2. 反序列化時不是用 readResolve 恢復,readResolve 並不是用來恢復 writeReplace 的。

Code-7. 使用 writeReplace 替換對象

@Test
public void testWriteReplace() throws Exception {
    ObjectOutputStream out = new ObjectOutputStream(
            new FileOutputStream("result.obj"));
    User user = new User("binarylei", "password");
    out.writeObject(user);  // 序列化時直接調用 writeReplace,此時如果有自定義的 writeObject 也無效
    out.close();

    ObjectInputStream oin = new ObjectInputStream(
            new FileInputStream("result.obj"));
    List t = (List) oin.readObject();     // 反序列化的對象為 List
    Assert.assertEquals(user.getName(), t.get(0));
    Assert.assertEquals(user.getPassword(), t.get(1));
    oin.close();
}

private static class User implements Serializable {
    private String name;
    private String password;

    private Object writeReplace() throws ObjectStreamException {
        List<Object> list = new ArrayList<>();
        list.add(name);
        list.add(password);
        return list;
    }
}

8. 保護性恢復對象:readResolve

readResolve 會在 readObject 調用之后自動調用,它最主要的目的就是對反序列化的對象進行修改后返回。

Code-8. 使用 readResolve 實現單例

@Test
public void testWriteReplace() throws Exception {
    ObjectOutputStream out = new ObjectOutputStream(
            new FileOutputStream("result.obj"));
    out.writeObject(Brand.NIKE);
    out.close();

    ObjectInputStream oin = new ObjectInputStream(
            new FileInputStream("result.obj"));
    Assert.assertEquals(Brand.NIKE, oin.readObject());
    oin.close();
}

private static class Brand implements Serializable {
    private int val;

    // 兩個枚舉值
    public static final Brand NIKE = new Brand(0);
    public static final Brand ADDIDAS = new Brand(1);

    private Object readResolve() throws ObjectStreamException {
        return val == 0 ? NIKE : ADDIDAS;
    }
}

9. 自定義序列化接口:Externalizable

不像 Serializable 接口只是一個標記接口,里面的接口方法都是可選的(可實現可不實現,如果不實現則啟用其自動序列化功能),而 Externalizable 接口不是一個標記接口,它強制你自己動手實現串行化和反串行化算法。

public interface Externalizable extends java.io.Serializable {
    void writeExternal(ObjectOutput out) throws IOException; // 串行化
    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException; // 反串行化
}

10. 序列化存儲規則

情境: 問題代碼如 Code-10 所示。

Code-10. 存儲規則問題代碼

@Test
public void test() throws Exception {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(baos);

    User user1 = new User();
    user1.setName("binarylei");
    oos.writeObject(user1);
    int length1 = baos.toByteArray().length;
    oos.writeObject(user1);
    int length2 = baos.toByteArray().length;
    Assert.assertEquals(5, length2 - length1);  // 同一個對象寫兩次,長度只增加了 5
    oos.close();

    ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
    ObjectInputStream ois = new ObjectInputStream(bais);
    User user2 = (User) ois.readObject();
    User user3 = (User) ois.readObject();
    Assert.assertEquals(user1.getName(), user2.getName());
    Assert.assertEquals(user2, user3);  // user2和user3是一個對象
    ois.close();
}

Code-10 中對同一對象兩次寫入文件,打印出寫入一次對象后的存儲大小和寫入兩次后的存儲大小,然后從文件中反序列化出兩個對象,比較這兩個對象是否為同一對象。一般的思維是,兩次寫入對象,文件大小會變為兩倍的大小,反序列化時,由於從文件讀取,生成了兩個對象,判斷相等時應該是輸入 false 才對,但是最后結果為 true。

我們看到,第二次寫入對象時文件只增加了 5 字節,並且兩個對象是相等的,這是為什么呢?

解答: Java 序列化機制為了節省磁盤空間,具有特定的存儲規則,當寫入文件的為同一對象時,並不會再將對象的內容進行存儲,而只是再次存儲一份引用,上面增加的 5 字節的存儲空間就是新增引用和一些控制信息的空間。反序列化時,恢復引用關系,使得清單 3 中的 t1 和 t2 指向唯一的對象,二者相等,輸出 true。該存儲規則極大的節省了存儲空間。

參考:

  1. 本文轉載至 https://www.ibm.com/developerworks/cn/java/j-lo-serial/
  2. 《Java對象的序列化與反序列化》:http://www.importnew.com/17964.html
  3. 自定義序列化的方法(transient、writeReplace、readResolve、Externalizable)

每天用心記錄一點點。內容也許不重要,但習慣很重要!


免責聲明!

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



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