序列化是什么意思,能不能給我通俗的講一下?
序列化是指把一個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字段的數據都以明文二進制的形式出現在網絡的套接字上,這顯然是不安全的。
解決方案:
- 序列化Hook化(移位和復位)
- 序列數據加密和簽名
- 利用transient的特性解決
- 打包和解包代理