圖文詳解Java中的序列化機制
概述
java中的序列化可能大家像我一樣都停留在實現(xiàn)Serializable
接口上,對于它里面的一些核心機制沒有深入了解過。直到最近在項目中踩了一個坑,就是序列化對象添加一個字段以后,使用方系統(tǒng)報了反序列化失敗,原因是我們雙方的序列化對象沒有加上serialVersionUID
,那你們知道下面幾個問題嗎:
- 序列化對象中的
serialVersionUID
是干嘛用的? - 如何修改默認(rèn)的序列化機制?
- 如何使用序列化的方式克隆對象?
對象序列化和反序列化機制
序列化: 將對象轉(zhuǎn)成二進(jìn)制寫到輸出流的過程。
反序列化: 通過輸入流讀回二進(jìn)制轉(zhuǎn)成對象的過程。
通過對象的序列化和反序列化機制可以實現(xiàn)對象在網(wǎng)絡(luò)之間傳輸。
在Java中,如果一個對象要想實現(xiàn)序列化,必須要實現(xiàn)下面兩個接口之一:
- 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; }
結(jié)果:
readObject
反序列化栗子:
現(xiàn)在模擬另外一個系統(tǒng)需要反序列化user.dat
@Test public void testDeSerializable() throws FileNotFoundException { User user = null; // 寫到內(nèi)存中,當(dāng)然也可以寫到文件中 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類不實現(xiàn)Serializable接口, 那會怎么樣?
當(dāng)然是報錯了,如下圖:
小結(jié):
一個對象想要被序列化,那么它的類就要實現(xiàn)此接口或者它的子接口。
修改默認(rèn)的序列化機制
默認(rèn)的情況下,如果實現(xiàn)了Serializable接口的對象進(jìn)行序列化的時候,默認(rèn)會將全部的數(shù)據(jù)域,也就是成員變量進(jìn)行序列化輸出,那往往有時候并不需要這樣,有什么方法可以修改序列化機制呢?下面提供3中方式。
使用transient關(guān)鍵字
將成員變量標(biāo)記成transient,那么在序列化的過程中這些數(shù)據(jù)域會被跳過,如下圖所示:
這是一種最簡單的方式,但是不夠靈活。
自定義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
只要類中有這兩個簽名的方法,那么就不會調(diào)用默認(rèn)的序列化,取而代之調(diào)用這些方法。
本例我們舉個jdk中的例子,ArrayList就實現(xiàn)了這兩個方法,重寫了序列化機制。
主要原因ArrayList底層的數(shù)組通常會預(yù)留一些容量,等容量不足時再擴(kuò)充容量,那么有些空間可能就沒有實際存儲元素,采用自定義方式實現(xiàn)序列化時,就可以保證只序列化實際存儲的那些元素,而不是整個數(shù)組,從而節(jié)省空間和時間。
實現(xiàn)Externalizable接口
Externalizable接口想必大家很少用到,它是Serializable接口的子類,用戶要實現(xiàn)的writeExternal()和readExternal() 方法,用來決定如何序列化和反序列化。
因為序列化和反序列化方法需要自己實現(xiàn),因此可以指定序列化哪些屬性,而transient在這里無效。
對Externalizable對象反序列化時,會先調(diào)用類的無參構(gòu)造方法,這是有別于默認(rèn)反序列方式的。如果把類的不帶參數(shù)的構(gòu)造方法刪除,或者把該構(gòu)造方法的訪問權(quán)限設(shè)置為private、默認(rèn)或protected級別,會拋出java.io.InvalidException: no valid constructor異常,因此Externalizable對象必須有默認(rèn)構(gòu)造函數(shù),而且必需是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的作用
這就回到概述中提到的項目中遇到的問題,現(xiàn)在簡要描述下:
A系統(tǒng)中的序列化對象User用的最新版本如下:
B系統(tǒng)中反序列化的對象,還是老的User版本如下:
這時候A系統(tǒng)生成的序列化文件,交給B系統(tǒng)反序列化時,出錯了, 如下圖:
原因:
類定義發(fā)生了變化,比如添加、刪除、修改類中的數(shù)據(jù)域后,它的唯一標(biāo)記符或者稱為SHA指紋、或者理解為serialVersionUID
都會發(fā)生變化,這個值會保存在序列化二進(jìn)制中,如果反序列化過程發(fā)現(xiàn)對不上,就會報錯,如上圖所示。
那么如何處理呢?
這時候,我們?nèi)绻X得這個序列化對象是可以兼容的,那么可以自定義一個serialVersionUID
的靜態(tài)成員變量,它就不會自動生成,而是直接用這個值,如下圖:
使用序列化clone
clone大家都知道吧,在深拷貝的時候編碼還是很麻煩的,借用序列化機制可以實現(xiàn)深拷貝。做法很簡單,就是將對象序列化到輸出流中,然后讀回。
public class SerialCloneable implements Cloneable, Serializable { @Override public Object clone() throws CloneNotSupportedException { try { // 保存到字節(jié)數(shù)組流 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; } } }
注意一點,這種方式性能不高,通常比顯示構(gòu)建、復(fù)制數(shù)據(jù)要慢不少。
到此這篇關(guān)于圖文詳解Java中的序列化機制的文章就介紹到這了,更多相關(guān)Java序列化機制內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java使用jni清屏功能的實現(xiàn)(只針對cmd)
JNI是Java Native Interface的縮寫,它提供了若干的API實現(xiàn)了Java和其他語言的通信(主要是C&C++)。這篇文章主要介紹了Java使用jni清屏功能的實現(xiàn)(只針對cmd) ,感興趣的朋友跟隨腳本之家小編一起學(xué)習(xí)吧2018-05-05java求數(shù)組元素重復(fù)次數(shù)和java字符串比較大小示例
這篇文章主要介紹了java求數(shù)組元素重復(fù)次數(shù)和java字符串比較大小示例,需要的朋友可以參考下2014-04-04如何用Java將數(shù)據(jù)庫的數(shù)據(jù)生成pdf返回給前端用戶下載
本文詳細(xì)介紹了使用SpringBoot、iText庫、MyBatis等技術(shù)從數(shù)據(jù)庫中選取數(shù)據(jù)并生成PDF文件的后端處理流程,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-09-09Java并發(fā)編程之ConcurrentLinkedQueue隊列詳情
這篇文章主要介紹了Java并發(fā)編程之ConcurrentLinkedQueue隊列詳情,ConcurrentLinkedQueue?內(nèi)部的隊列使用單向鏈表方式實現(xiàn),下文更多相關(guān)內(nèi)容敘述需要的小伙伴可以參考一下2022-04-04Spring?Cloud實現(xiàn)灰度發(fā)布的示例代碼
這篇文章主要為大家詳細(xì)介紹了Spring?Cloud實現(xiàn)灰度發(fā)布的相關(guān)知識,文中的示例代碼講解詳細(xì),具有一定的學(xué)習(xí)價值,感興趣的小伙伴可以了解一下2023-09-09