Java 序列化:readObject() 與 readResolve()
一、概述
在本教程中,我們將了解如何在 Java 反序列化 API 中使用readObject()
和readResolve()
方法。此外,我們將檢查這兩種方法之間的區別。
2.連載
Java 序列化更深入地介紹了序列化和反序列化的工作原理。在本文中,我們將重點介紹readResolve()
和readObject()
方法,它們在使用反序列化時經常會引發問題。
3. readObject()
的使用
Java 對像在序列化期間被轉換為字節流,以保存在文件中或通過 Internet 傳輸。使用ObjectInputStream
的readObject()
方法在反序列化期間將序列化的字節流轉換回原始對象,該方法在內部調用defaultReadObject()
進行默認反序列化。
如果我們的類中存在readObject()
方法,則ObjectInputStream
的readObject()
方法將使用我們類的readObject()
方法從流中讀取對象。
例如,在某些情況下,我們可以在我們的類中實現readObject()
以特定方式反序列化任何字段。
在展示我們的用例之前,讓我們檢查一下在我們的類中實現readObject()
方法的語法:
private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException;
現在,假設我們有一個包含兩個字段的User
類:
public class User implements Serializable {
private static final long serialVersionUID = 3659932210257138726L;
private String userName;
private String password;
// standard setters, getters, constructor(s) and toString()
}
再者,我們又不想把password
序列化為明文,怎麼辦呢?讓我們看看 Java 的readObject()
在這裡如何幫助我們。
3.1.添加writeObject()
用於序列化期間的自定義更改
首先,我們可以在序列化期間對對象的字段進行特定更改,例如在writeObject()
方法中對password
進行編碼。
因此,對於我們的User
類,讓我們實現the writeObject()
方法並在序列化期間向我們的密碼字段添加一個額外的字符串前綴:
private void writeObject(ObjectOutputStream oos) throws IOException {
this.password = "xyz" + password;
oos.defaultWriteObject();
}
3.2.在沒有readObject()
實現的情況下進行測試
現在,讓我們測試我們的User
類,但不實現readObject()
。在這種情況下,將調用ObjectInputStream
類的readObject()
:
@Test
public void testDeserializeObj_withDefaultReadObject() throws ClassNotFoundException, IOException {
// Serialization
FileOutputStream fos = new FileOutputStream("user.ser");
ObjectOutputStream oos = new ObjectOutputStream(fos);
User acutalObject = new User("Sachin", "Kumar");
oos.writeObject(acutalObject);
// Deserialization
User deserializedUser = null;
FileInputStream fis = new FileInputStream("user.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
deserializedUser = (User) ois.readObject();
assertNotEquals(deserializedUser.hashCode(), acutalObject.hashCode());
assertEquals(deserializedUser.getUserName(), "Sachin");
assertEquals(deserializedUser.getPassword(), "xyzKumar");
}
在這裡,我們可以看到密碼是xyzKumar
,因為我們的類中還沒有任何readObject()
可以檢索原始字段並進行自定義更改。
3.3.添加readObject()
用於反序列化期間的自定義更改
接下來,我們可以在readObject()
方法中對反序列化過程中對象的字段進行特定的更改,例如解碼password
。
讓我們在我們的User
類中實現the readObject()
方法,並刪除我們在序列化期間添加到密碼字段的額外字符串前綴:
private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException {
ois.defaultReadObject();
this.password = password.substring(3);
}
3.4.使用readObject()
實現進行測試
讓我們再次測試我們的User
類,只是這一次,我們有一個自定義的readObject()
方法,該方法將在反序列化期間調用:
@Test
public void testDeserializeObj_withOverriddenReadObject() throws ClassNotFoundException, IOException {
// Serialization
FileOutputStream fos = new FileOutputStream("user.ser");
ObjectOutputStream oos = new ObjectOutputStream(fos);
User acutalObject = new User("Sachin", "Kumar");
oos.writeObject(acutalObject);
// Deserialization
User deserializedUser = null;
FileInputStream fis = new FileInputStream("user.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
deserializedUser = (User) ois.readObject();
assertNotEquals(deserializedUser.hashCode(), acutalObject.hashCode());
assertEquals(deserializedUser.getUserName(), "Sachin");
assertEquals(deserializedUser.getPassword(), "Kumar");
}
在這裡,我們可以注意到一些事情。一是對像不一樣,二是調用了我們自定義的readObject()
,正確轉換了密碼字段。
4. readResolve()
的使用
在 Java 反序列化中, readResolve()
方法用於將反序列化期間創建的對象替換為不同的對象。這在我們需要確保在我們的應用程序中只存在特定類的單個實例或者當我們想要用內存中可能已經存在的不同實例替換對象時很有用。
讓我們回顧一下在我們的類中添加readResolve()
的語法:
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
在readObject()
示例中需要注意的一件事是對象hashCode
不同。這是因為,在反序列化過程中,新對像是從流式對象創建的。
我們可能想要使用readResolve()
的常見場景是在創建單例實例時。我們可以使用readResolve()
來確保反序列化的對象與單例實例的現有實例相同。
讓我們以創建單例對象為例:
public class Singleton implements Serializable {
private static final long serialVersionUID = 1L;
private static Singleton INSTANCE = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return INSTANCE;
}
}
4.1 沒有readResolve()
實現的測試
此時,我們還沒有添加任何readResolve()
方法.
讓我們測試一下我們的Singleton
類:
@Test
public void testSingletonObj_withNoReadResolve() throws ClassNotFoundException, IOException {
// Serialization
FileOutputStream fos = new FileOutputStream("singleton.ser");
ObjectOutputStream oos = new ObjectOutputStream(fos);
Singleton actualSingletonObject = Singleton.getInstance();
oos.writeObject(actualSingletonObject);
// Deserialization
Singleton deserializedSingletonObject = null;
FileInputStream fis = new FileInputStream("singleton.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
deserializedSingletonObject = (Singleton) ois.readObject();
assertNotEquals(actualSingletonObject.hashCode(), deserializedSingletonObject.hashCode());
}
在這裡,我們可以看到兩個對像是不同的,這違背了我們Singleton
類的目標。
4.2.使用readResolve()
實現進行測試
為了解決這個問題,讓我們在Singleton
類中添加readResolve()
方法:
private Object readResolve() throws ObjectStreamException {
return INSTANCE;
}
現在,讓我們再次使用Singleton
類中的readResolve()
方法進行測試:
@Test
public void testSingletonObj_withCustomReadResolve() throws ClassNotFoundException, IOException {
// Serialization
FileOutputStream fos = new FileOutputStream("singleton.ser");
ObjectOutputStream oos = new ObjectOutputStream(fos);
Singleton actualSingletonObject = Singleton.getInstance();
oos.writeObject(actualSingletonObject);
// Deserialization
Singleton deserializedSingletonObject = null;
FileInputStream fis = new FileInputStream("singleton.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
deserializedSingletonObject = (Singleton) ois.readObject();
assertEquals(actualSingletonObject.hashCode(), deserializedSingletonObject.hashCode());
}
在這裡,我們可以看到兩個對象具有相同的hashCode
。
5. readObject()
與readResolve()
讓我們快速總結一下這兩者之間的區別:
readResolve() | readObject() |
方法返回類型是Object | 方法返回類型為void |
無方法參數 | ObjectInputStream 作為參數 |
通常用於實現單例模式,反序列化後需要返回同一個對象。 | 用於設置對象的未序列化的非瞬態字段的值,例如從其他字段派生的字段或動態初始化的字段。 |
拋出ClassNotFoundException , ObjectStreamException | 拋出ClassNotFoundException , IOException |
比readObject() 更快,因為它不讀取整個對像圖。 | 比readResolve() 慢,因為它讀取整個對像圖。 |
六,結論
在本文中,我們了解了 Java 序列化 API 的readObject()
和readResolve()
方法。此外,我們已經看到了這兩者之間的區別。與往常一樣,本文的示例代碼可在 GitHub 上獲得。