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 關鍵字有兩個特性:
-
如果你不想讓對象中的某個成員被序列化可以在定義它的時候加上 transient 關鍵字進行修飾,這樣,在對象被序列化時其就不會被序列化。
-
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;
- writeObject 序列化時會調用 writeReplace 方法將當前對象替換成另一個對象並將其寫入流中,此時如果有自定義的 writeObject 也不會生效了;
- 反序列化時不是用 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。該存儲規則極大的節省了存儲空間。
參考:
- 本文轉載至 https://www.ibm.com/developerworks/cn/java/j-lo-serial/
- 《Java對象的序列化與反序列化》:http://www.importnew.com/17964.html
- 自定義序列化的方法(transient、writeReplace、readResolve、Externalizable)
每天用心記錄一點點。內容也許不重要,但習慣很重要!
