自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Serializable:明明就一個空接口!為什么還要實現(xiàn)它?

開發(fā) 后端
對于 Java 的序列化,我一直停留在最淺顯的認知上——把那個要序列化的類實現(xiàn) Serializbale 接口就可以了。我不愿意做更深入的研究,因為會用就行了嘛。

[[318236]]

對于 Java 的序列化,我一直停留在最淺顯的認知上——把那個要序列化的類實現(xiàn) Serializbale 接口就可以了。我不愿意做更深入的研究,因為會用就行了嘛。

但隨著時間的推移,見到 Serializbale 的次數(shù)越來越多,我便對它產(chǎn)生了濃厚的興趣。是時候花點時間研究研究了。

01、先來點理論

Java 序列化是 JDK 1.1 時引入的一組開創(chuàng)性的特性,用于將 Java 對象轉(zhuǎn)換為字節(jié)數(shù)組,便于存儲或傳輸。此后,仍然可以將字節(jié)數(shù)組轉(zhuǎn)換回 Java 對象原有的狀態(tài)。

序列化的思想是“凍結(jié)”對象狀態(tài),然后寫到磁盤或者在網(wǎng)絡中傳輸;反序列化的思想是“解凍”對象狀態(tài),重新獲得可用的 Java 對象。

再來看看序列化 Serializbale 接口的定義: 

  1. public interface Serializable {  

明明就一個空的接口嘛,竟然能夠保證實現(xiàn)了它的“類的對象”被序列化和反序列化?

02、再來點實戰(zhàn)

在回答上述問題之前,我們先來創(chuàng)建一個類(只有兩個字段,和對應的 getter/setter),用于序列化和反序列化。 

  1. class Wanger {  
  2.     private String name;  
  3.     private int age;  
  4.     public String getName() {  
  5.         return name;  
  6.     }  
  7.     public void setName(String name) {  
  8.         this.name = name;  
  9.     }  
  10.     public int getAge() {  
  11.         return age;  
  12.     }  
  13.     public void setAge(int age) {  
  14.         this.age = age;  
  15.     }  

再來創(chuàng)建一個測試類,通過 ObjectOutputStream 將“18 歲的王二”寫入到文件當中,實際上就是一種序列化的過程;再通過 ObjectInputStream 將“18 歲的王二”從文件中讀出來,實際上就是一種反序列化的過程。 

  1. public class Test {  
  2.     public static void main(String[] args) {  
  3.       // 初始化  
  4.         Wanger wanger = new Wanger();  
  5.         wanger.setName("王二");  
  6.         wanger.setAge(18);  
  7.         System.out.println(wanger);  
  8.         // 把對象寫到文件中  
  9.         try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("chenmo"));){  
  10.             oos.writeObject(wanger);  
  11.         } catch (IOException e) {  
  12.             e.printStackTrace();  
  13.         }  
  14.         // 從文件中讀出對象  
  15.         try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("chenmo")));){  
  16.             Wanger wanger1 = (Wanger) ois.readObject();  
  17.             System.out.println(wanger1);  
  18.         } catch (IOException | ClassNotFoundException e) {  
  19.             e.printStackTrace();  
  20.         }  
  21.     }  

不過,由于 Wanger 沒有實現(xiàn) Serializbale 接口,所以在運行測試類的時候會拋出異常,堆棧信息如下: 

  1. java.io.NotSerializableException: com.cmower.java_demo.xuliehua.Wanger  
  2.     at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)  
  3.     at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)  
  4.     at com.cmower.java_demo.xuliehua.Test.main(Test.java:21) 

順著堆棧信息,我們來看一下 ObjectOutputStream 的 writeObject0() 方法。其部分源碼如下: 

  1. if (obj instanceof String) {  
  2.     writeString((String) obj, unshared);  
  3. } else if (cl.isArray()) {  
  4.     writeArray(obj, desc, unshared);  
  5. } else if (obj instanceof Enum) {  
  6.     writeEnum((Enum<?>) obj, desc, unshared);  
  7. } else if (obj instanceof Serializable) {  
  8.     writeOrdinaryObject(obj, desc, unshared);  
  9. } else {  
  10.     if (extendedDebugInfo) {  
  11.         throw new NotSerializableException(  
  12.             cl.getName() + "\n" + debugInfoStack.toString());  
  13.     } else {  
  14.         throw new NotSerializableException(cl.getName());  
  15.     }  

也就是說,ObjectOutputStream 在序列化的時候,會判斷被序列化的對象是哪一種類型,字符串?數(shù)組?枚舉?還是 Serializable,如果全都不是的話,拋出 NotSerializableException。

假如 Wanger 實現(xiàn)了 Serializable 接口,就可以序列化和反序列化了。 

  1. class Wanger implements Serializable{ 
  2.      private static final long serialVersionUID = -2095916884810199532L;  
  3.     private String name;  
  4.     private int age; 
  5.  

具體怎么序列化呢?

以 ObjectOutputStream 為例吧,它在序列化的時候會依次調(diào)用 writeObject()→writeObject0()→writeOrdinaryObject()→writeSerialData()→invokeWriteObject()→defaultWriteFields()。 

  1. private void defaultWriteFields(Object obj, ObjectStreamClass desc)  
  2.         throws IOException  
  3.     {  
  4.         Class<?> cl = desc.forClass();  
  5.         desc.checkDefaultSerialize();  
  6.         int primDataSize = desc.getPrimDataSize();  
  7.         desc.getPrimFieldValues(obj, primVals);  
  8.         bout.write(primVals, 0, primDataSize, false);  
  9.         ObjectStreamField[] fields = desc.getFields(false);  
  10.         Object[] objVals = new Object[desc.getNumObjFields()];  
  11.         int numPrimFields = fields.length - objVals.length;  
  12.         desc.getObjFieldValues(obj, objVals);  
  13.         for (int i = 0; i < objVals.length; i++) {  
  14.             try {  
  15.                 writeObject0(objVals[i],  
  16.                              fields[numPrimFields + i].isUnshared());  
  17.             }  
  18.         }  
  19.     } 

那怎么反序列化呢?

以 ObjectInputStream 為例,它在反序列化的時候會依次調(diào)用 readObject()→readObject0()→readOrdinaryObject()→readSerialData()→defaultReadFields()。 

  1. private void defaultWriteFields(Object obj, ObjectStreamClass desc)  
  2.         throws IOException  
  3.     {  
  4.         Class<?> cl = desc.forClass();  
  5.         desc.checkDefaultSerialize();  
  6.         int primDataSize = desc.getPrimDataSize();  
  7.         desc.getPrimFieldValues(obj, primVals);  
  8.         bout.write(primVals, 0, primDataSize, false);  
  9.         ObjectStreamField[] fields = desc.getFields(false);  
  10.         Object[] objVals = new Object[desc.getNumObjFields()];  
  11.         int numPrimFields = fields.length - objVals.length;  
  12.         desc.getObjFieldValues(obj, objVals);  
  13.         for (int i = 0; i < objVals.length; i++) {  
  14.             try {  
  15.                 writeObject0(objVals[i],  
  16.                              fields[numPrimFields + i].isUnshared());  
  17.             }  
  18.         }  
  19.     } 

我想看到這,你應該會恍然大悟的“哦”一聲了。Serializable 接口之所以定義為空,是因為它只起到了一個標識的作用,告訴程序?qū)崿F(xiàn)了它的對象是可以被序列化的,但真正序列化和反序列化的操作并不需要它來完成。

03、再來點注意事項

開門見山的說吧,static 和 transient 修飾的字段是不會被序列化的。

為什么呢?我們先來證明,再來解釋原因。

首先,在 Wanger 類中增加兩個字段。 

  1. class Wanger implements Serializable {  
  2.     private static final long serialVersionUID = -2095916884810199532L;  
  3.     private String name;  
  4.     private int age;  
  5.     public static String pre = "沉默" 
  6.     transient String meizi = "王三" 
  7.     @Override  
  8.     public String toString() {  
  9.         return "Wanger{" + "name=" + name + ",age=" + age + ",pre=" + pre + ",meizi=" + meizi + "}";  
  10.     }  

其次,在測試類中打印序列化前和反序列化后的對象,并在序列化后和反序列化前改變 static 字段的值。具體代碼如下: 

  1. class Wanger implements Serializable {  
  2.     private static final long serialVersionUID = -2095916884810199532L;  
  3.     private String name;  
  4.     private int age;  
  5.     public static String pre = "沉默" 
  6.     transient String meizi = "王三" 
  7.     @Override  
  8.     public String toString() {  
  9.         return "Wanger{" + "name=" + name + ",age=" + age + ",pre=" + pre + ",meizi=" + meizi + "}";  
  10.     }  

從結(jié)果的對比當中,我們可以發(fā)現(xiàn):

1)序列化前,pre 的值為“沉默”,序列化后,pre 的值修改為“不沉默”,反序列化后,pre 的值為“不沉默”,而不是序列化前的狀態(tài)“沉默”。

為什么呢?因為序列化保存的是對象的狀態(tài),而 static 修飾的字段屬于類的狀態(tài),因此可以證明序列化并不保存 static 修飾的字段。

2)序列化前,meizi 的值為“王三”,反序列化后,meizi 的值為 null,而不是序列化前的狀態(tài)“王三”。

為什么呢?transient 的中文字義為“臨時的”(論英語的重要性),它可以阻止字段被序列化到文件中,在被反序列化后,transient 字段的值被設為初始值,比如 int 型的初始值為 0,對象型的初始值為 null。

如果想要深究源碼的話,你可以在 ObjectStreamClass 中發(fā)現(xiàn)下面這樣的代碼: 

  1. private static ObjectStreamField[] getDefaultSerialFields(Class<?> cl) {  
  2.     Field[] clclFields = cl.getDeclaredFields();  
  3.     ArrayList<ObjectStreamField> list = new ArrayList<>();  
  4.     int mask = Modifier.STATIC | Modifier.TRANSIENT;  
  5.     int size = list.size();  
  6.     return (size == 0) ? NO_FIELDS :  
  7.         list.toArray(new ObjectStreamField[size]);  

看到 Modifier.STATIC | Modifier.TRANSIENT,是不是感覺更好了呢?

04、再來點干貨

除了 Serializable 之外,Java 還提供了一個序列化接口 Externalizable(念起來有點拗口)。

兩個接口有什么不一樣的嗎?試一試就知道了。

首先,把 Wanger 類實現(xiàn)的接口  Serializable 替換為 Externalizable。 

  1. class Wanger implements Externalizable {  
  2.     private String name;  
  3.     private int age;  
  4.     public Wanger() {  
  5.     }  
  6.     public String getName() {  
  7.         return name;  
  8.     }  
  9.     @Override  
  10.     public String toString() {  
  11.         return "Wanger{" + "name=" + name + ",age=" + age + "}";  
  12.     }  
  13.     @Override  
  14.     public void writeExternal(ObjectOutput out) throws IOException {  
  15.     }  
  16.     @Override  
  17.     public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {  
  18.     }  

實現(xiàn) Externalizable 接口的 Wanger 類和實現(xiàn) Serializable 接口的 Wanger 類有一些不同:

1)新增了一個無參的構(gòu)造方法。

使用 Externalizable 進行反序列化的時候,會調(diào)用被序列化類的無參構(gòu)造方法去創(chuàng)建一個新的對象,然后再將被保存對象的字段值復制過去。否則的話,會拋出以下異常: 

  1. java.io.InvalidClassException: com.cmower.java_demo.xuliehua1.Wanger; no valid constructor  
  2.     at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(ObjectStreamClass.java:150)  
  3.     at java.io.ObjectStreamClass.checkDeserialize(ObjectStreamClass.java:790)  
  4.     at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1782)  
  5.     at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1353)  
  6.     at java.io.ObjectInputStream.readObject(ObjectInputStream.java:373)  
  7.     at com.cmower.java_demo.xuliehua1.Test.main(Test.java:27) 

2)新增了兩個方法 writeExternal() 和 readExternal(),實現(xiàn) Externalizable 接口所必須的。

然后,我們再在測試類中打印序列化前和反序列化后的對象。 

  1. // 初始化  
  2. Wanger wanger = new Wanger();  
  3. wanger.setName("王二");  
  4. wanger.setAge(18);  
  5. System.out.println(wanger);  
  6. // 把對象寫到文件中  
  7. try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("chenmo"));) {  
  8.     oos.writeObject(wanger);  
  9. } catch (IOException e) {  
  10.     e.printStackTrace();  
  11.  
  12. // 從文件中讀出對象  
  13. try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("chenmo")));) {  
  14.     Wanger wanger1 = (Wanger) ois.readObject();  
  15.     System.out.println(wanger1);  
  16. } catch (IOException | ClassNotFoundException e) {  
  17.     e.printStackTrace();  
  18.  
  19. // Wanger{name=王二,age=18 
  20. // Wanger{name=null,age=0

從輸出的結(jié)果看,反序列化后得到的對象字段都變成了默認值,也就是說,序列化之前的對象狀態(tài)沒有被“凍結(jié)”下來。

為什么呢?因為我們沒有為 Wanger 類重寫具體的 writeExternal() 和 readExternal() 方法。那該怎么重寫呢? 

  1. @Override  
  2. public void writeExternal(ObjectOutput out) throws IOException {  
  3.     out.writeObject(name);  
  4.     out.writeInt(age);  
  5.  
  6. @Override  
  7. public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {  
  8.     name = (String) in.readObject();  
  9.     age = in.readInt();  

1)調(diào)用 ObjectOutput 的 writeObject() 方法將字符串類型的 name 寫入到輸出流中;

2)調(diào)用 ObjectOutput 的 writeInt() 方法將整型的 age 寫入到輸出流中;

3)調(diào)用 ObjectInput 的 readObject() 方法將字符串類型的 name 讀入到輸入流中;

4)調(diào)用 ObjectInput 的 readInt() 方法將字符串類型的 age 讀入到輸入流中;

再運行一次測試了類,你會發(fā)現(xiàn)對象可以正常地序列化和反序列化了。

序列化前:Wanger{name=王二,age=18}

序列化后:Wanger{name=王二,age=18}

05、再來點甜點

讓我先問問你吧,你知道 private static final long serialVersionUID = -2095916884810199532L; 這段代碼的作用嗎?

嗯……

serialVersionUID 被稱為序列化 ID,它是決定 Java 對象能否反序列化成功的重要因子。在反序列化時,Java 虛擬機會把字節(jié)流中的 serialVersionUID 與被序列化類中的 serialVersionUID 進行比較,如果相同則可以進行反序列化,否則就會拋出序列化版本不一致的異常。

當一個類實現(xiàn)了 Serializable 接口后,IDE 就會提醒該類最好產(chǎn)生一個序列化 ID,就像下面這樣:

1)添加一個默認版本的序列化 ID: 

  1. private static final long serialVersionUID = 1L。 

2)添加一個隨機生成的不重復的序列化 ID。 

  1. private static final long serialVersionUID = -2095916884810199532L; 

3)添加 @SuppressWarnings 注解。 

  1. @SuppressWarnings("serial") 

怎么選擇呢?

首先,我們采用第二種辦法,在被序列化類中添加一個隨機生成的序列化 ID。 

  1. class Wanger implements Serializable {  
  2.     private static final long serialVersionUID = -2095916884810199532L;  
  3.     private String name;  
  4.     private int age;  
  5.     // 其他代碼忽略  

然后,序列化一個 Wanger 對象到文件中。 

  1. // 初始化  
  2. Wanger wanger = new Wanger();  
  3. wanger.setName("王二");  
  4. wanger.setAge(18);  
  5. System.out.println(wanger);  
  6. // 把對象寫到文件中  
  7. try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("chenmo"));) {  
  8.     oos.writeObject(wanger);  
  9. } catch (IOException e) {  
  10.     e.printStackTrace();  

這時候,我們悄悄地把 Wanger 類的序列化 ID 偷梁換柱一下,嘿嘿。 

  1. // private static final long serialVersionUID = -2095916884810199532L;  
  2. private static final long serialVersionUID = -2095916884810199533L; 

好了,準備反序列化吧。 

  1. try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("chenmo")));) {  
  2.     Wanger wanger = (Wanger) ois.readObject();  
  3.     System.out.println(wanger);  
  4. } catch (IOException | ClassNotFoundException e) {  
  5.     e.printStackTrace();  

哎呀,出錯了。 

  1. java.io.InvalidClassException:  local class incompatible: stream classdesc   
  2. serialVersionUID = -2095916884810199532,  
  3. local class serialVersionUID = -2095916884810199533  
  4.     at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1521)  
  5.     at com.cmower.java_demo.xuliehua1.Test.main(Test.java:27) 

異常堆棧信息里面告訴我們,從持久化文件里面讀取到的序列化 ID 和本地的序列化 ID 不一致,無法反序列化。

那假如我們采用第三種方法,為 Wanger 類添加個 @SuppressWarnings("serial") 注解呢? 

  1. @SuppressWarnings("serial")  
  2. class Wanger3 implements Serializable {  
  3. // 省略其他代碼  

好了,再來一次反序列化吧??上б廊粓箦e。 

  1. java.io.InvalidClassException:  local class incompatible: stream classdesc   
  2. serialVersionUID = -2095916884810199532,   
  3. local class serialVersionUID = -3818877437117647968  
  4.     at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1521)  
  5.     at com.cmower.java_demo.xuliehua1.Test.main(Test.java:27) 

異常堆棧信息里面告訴我們,本地的序列化 ID 為 -3818877437117647968,和持久化文件里面讀取到的序列化 ID 仍然不一致,無法反序列化。這說明什么呢?使用 @SuppressWarnings("serial") 注解時,該注解會為被序列化類自動生成一個隨機的序列化 ID。

由此可以證明,Java 虛擬機是否允許反序列化,不僅取決于類路徑和功能代碼是否一致,還有一個非常重要的因素就是序列化 ID 是否一致。

也就是說,如果沒有特殊需求,采用默認的序列化 ID(1L)就可以,這樣可以確保代碼一致時反序列化成功。 

  1. class Wanger implements Serializable {  
  2.     private static final long serialVersionUID = 1L 
  3. // 省略其他代碼  

06、再來點總結(jié)

寫這篇文章之前,我真沒想到:“空空其身”的Serializable 竟然有這么多可以研究的內(nèi)容!

寫完這篇文章之后,我不由得想起理科狀元曹林菁說說過的一句話:“在學習中再小的問題也不放過,每個知識點都要總結(jié)”——說得真真真真的對?。?nbsp;

責任編輯:龐桂玉 來源: Java后端技術(shù)
相關(guān)推薦

2021-07-19 09:00:24

微軟Windows 11Windows

2024-10-12 15:10:23

2011-06-01 15:18:43

Serializabl

2019-05-14 09:05:16

SerializablJava對象

2021-11-17 08:26:22

空類EBO技術(shù)

2014-08-21 10:05:14

ZMapTCPIP

2022-04-29 08:00:06

Linux目錄網(wǎng)絡

2020-10-29 09:19:11

索引查詢存儲

2021-03-02 22:10:10

Java互聯(lián)網(wǎng)語言

2021-04-16 23:28:11

Java語言IT

2022-06-07 08:39:35

RPCHTTP

2016-10-27 11:11:12

頭條

2022-06-10 13:03:44

接口重試while

2023-03-26 00:04:14

2021-12-24 10:04:57

漏洞阿里云Log4Shell

2022-08-04 08:22:49

MySQL索引

2012-09-03 09:52:39

虛擬化

2023-11-07 08:00:00

Kubernetes

2018-03-22 14:47:13

容器開發(fā)人員筆記本

2020-04-29 08:04:11

NoSQLMySQLSQL
點贊
收藏

51CTO技術(shù)棧公眾號