你確定懂了Java中的序列化機制嗎
theme: cyanosis
持續創作,加速成長!這是我參與「掘金日新計劃 · 10 月更文挑戰」的第4天,點選檢視活動詳情
概述
java中的序列化可能大家像我一樣都停留在實現Serializable
介面上,對於它裡面的一些核心機制沒有深入瞭解過。直到最近在專案中踩了一個坑,就是序列化物件新增一個欄位以後,使用方系統報了反序列化失敗,原因是我們雙方的序列化物件沒有加上serialVersionUID
,那你們知道下面幾個問題嗎:
- 序列化物件中的
serialVersionUID
是幹嘛用的? - 如何修改預設的序列化機制?
- 如何使用序列化的方式克隆物件?
物件序列化和反序列化機制
序列化: 將物件轉成二進位制寫到輸出流的過程。
反序列化: 通過輸入流讀回二進位制轉成物件的過程。
通過物件的序列化和反序列化機制可以實現物件在網路之間傳輸。
在Java中,如果一個物件要想實現序列化,必須要實現下面兩個介面之一:
- Serializable 介面
- Externalizable 介面
這裡我們先講解常用的Serializable 介面。
writeObject
序列化過程栗子:
``` @Test public void testSerializable() throws FileNotFoundException { User user = new User("alvin", 19); // 檔案輸出流 FileOutputStream bout = new FileOutputStream("user.dat"); try (ObjectOutputStream out = new ObjectOutputStream(bout)) { // 序列化 out.writeObject(user); } catch (IOException e) { e.printStackTrace(); } }
@Data @NoArgsConstructor @AllArgsConstructor public class User implements Serializable {
private String username;
private Integer age;
} ```
結果:
readObject
反序列化栗子:
現在模擬另外一個系統需要反序列化user.dat
``` @Test public void testDeSerializable() throws FileNotFoundException { User user = null; // 寫到記憶體中,當然也可以寫到檔案中 FileInputStream fis = new FileInputStream("user.dat"); try (ObjectInputStream in = new ObjectInputStream(fis)) { // 反序列化 readObject user = (User) in.readObject(); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); }
Assert.assertEquals("alvin", user.getUsername());
} ```
如果User類不實現Serializable介面, 那會怎麼樣?
當然是報錯了,如下圖:
小結:
一個物件想要被序列化,那麼它的類就要實現此介面或者它的子介面。
修改預設的序列化機制
預設的情況下,如果實現了Serializable介面的物件進行序列化的時候,預設會將全部的資料域,也就是成員變數進行序列化輸出,那往往有時候並不需要這樣,有什麼方法可以修改序列化機制呢?下面提供3中方式。
使用transient關鍵字
將成員變數標記成transient,那麼在序列化的過程中這些資料域會被跳過,如下圖所示:
這是一種最簡單的方式,但是不夠靈活。
自定義readObject、writeObject方法
序列化類中可以通過定義下面簽名的方法:
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException
只要類中有這兩個簽名的方法,那麼就不會呼叫預設的序列化,取而代之呼叫這些方法。
本例我們舉個jdk中的例子,ArrayList就實現了這兩個方法,重寫了序列化機制。
主要原因ArrayList底層的陣列通常會預留一些容量,等容量不足時再擴充容量,那麼有些空間可能就沒有實際儲存元素,採用自定義方式實現序列化時,就可以保證只序列化實際儲存的那些元素,而不是整個陣列,從而節省空間和時間。
實現Externalizable介面
Externalizable介面想必大家很少用到,它是Serializable介面的子類,使用者要實現的writeExternal()和readExternal() 方法,用來決定如何序列化和反序列化。
因為序列化和反序列化方法需要自己實現,因此可以指定序列化哪些屬性,而transient在這裡無效。
對Externalizable物件反序列化時,會先呼叫類的無參構造方法,這是有別於預設反序列方式的。如果把類的不帶引數的構造方法刪除,或者把該構造方法的訪問許可權設定為private、預設或protected級別,會丟擲java.io.InvalidException: no valid constructor異常,因此Externalizable物件必須有預設建構函式,而且必需是public的。
舉例說明:
``` public class User2 implements Externalizable {
private String username;
private Integer age;
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(username);
out.writeInt(age);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
username = in.readUTF();
age = in.readInt();
}
} ```
serialVersionUID的作用
這就回到概述中提到的專案中遇到的問題,現在簡要描述下:
A系統中的序列化物件User用的最新版本如下:
B系統中反序列化的物件,還是老的User版本如下:
這時候A系統生成的序列化檔案,交給B系統反序列化時,出錯了, 如下圖:
原因:
類定義發生了變化,比如新增、刪除、修改類中的資料域後,它的唯一標記符或者稱為SHA指紋、或者理解為serialVersionUID
都會發生變化,這個值會儲存在序列化二進位制中,如果反序列化過程發現對不上,就會報錯,如上圖所示。
那麼如何處理呢?
這時候,我們如果覺得這個序列化物件是可以相容的,那麼可以自定義一個serialVersionUID
的靜態成員變數,它就不會自動生成,而是直接用這個值,如下圖:
使用序列化clone
clone大家都知道吧,在深拷貝的時候編碼還是很麻煩的,借用序列化機制可以實現深拷貝。做法很簡單,就是將物件序列化到輸出流中,然後讀回。
``` public class SerialCloneable implements Cloneable, Serializable {
@Override
public Object clone() throws CloneNotSupportedException {
try {
// 儲存到位元組陣列流
ByteArrayOutputStream bout = new ByteArrayOutputStream();
try(ObjectOutputStream out = new ObjectOutputStream(bout)) {
out.writeObject(this);
}
// 讀取
try(InputStream bin = new ByteArrayInputStream(bout.toByteArray())) {
ObjectInputStream in = new ObjectInputStream(bin);
return in.readObject();
}
} catch (IOException | ClassNotFoundException e) {
CloneNotSupportedException e2 = new CloneNotSupportedException();
e2.initCause(e);
throw e2;
}
}
} ```
注意一點,這種方式效能不高,通常比顯示構建、複製資料要慢不少。
總結
本文講解了序列化的一些核心機制,不再簡簡單單的停留在序列化就是實現Serializable介面了,希望能幫助到大家。
參考
- Java7到Java17, Switch語句進化史
- 樂觀鎖思想在JAVA中的實現——CAS
- 一步步帶你設計MySQL索引資料結構
- 我總結了寫出高質量程式碼的12條建議
- 工作這麼多年,我總結的資料傳輸物件 (DTO) 的最佳實踐
- Spring專案中用了這種解耦模式,經理對我刮目相看
- 大資料HDFS憑啥能存下百億資料?
- 5個介面效能提升的通用技巧
- 你的哪些SQL慢?看看MySQL慢查詢日誌吧
- 90%的Java開發人員都會犯的5個錯誤
- 喪心病狂,竟有Thread.sleep(0)這種寫法?
- 為什麼更推薦使用組合而非繼承關係?
- 一個30歲程式設計師的覺醒和進擊
- 推薦8個提高工作效率的IntelliJ外掛
- 公司的這種打包啟動方式,我簡直驚呆了
- 告別醜陋判空,一個Optional類搞定
- 你不知道的Map家族中的那些冷門容器
- SpringBoot 2.x整合Log4j2日誌
- SpringBoot應用自定義logback日誌
- 你確定懂了Java中的序列化機制嗎