Serializable:明明就一个空接口!为什么还要实现它?
作者:沉默王二
微信公眾號:Java極客技術(ID:Javageektech)
對于 Java 的序列化,我一直停留在最淺顯的認知上——把那個要序列化的類實現?Serializbale?接口就可以了。我不愿意做更深入的研究,因為會用就行了嘛。
但隨著時間的推移,見到?Serializbale?的次數越來越多,我便對它產生了濃厚的興趣。是時候花點時間研究研究了。
01、先來點理論
Java 序列化是 JDK 1.1 時引入的一組開創性的特性,用于將 Java 對象轉換為字節數組,便于存儲或傳輸。此后,仍然可以將字節數組轉換回 Java 對象原有的狀態。
序列化的思想是“凍結”對象狀態,然后寫到磁盤或者在網絡中傳輸;反序列化的思想是“解凍”對象狀態,重新獲得可用的 Java 對象。
再來看看序列化?Serializbale?接口的定義:
public?interface?Serializable?{ }明明就一個空的接口嘛,竟然能夠保證實現了它的“類的對象”被序列化和反序列化?
02、再來點實戰
在回答上述問題之前,我們先來創建一個類(只有兩個字段,和對應的?getter/setter),用于序列化和反序列化。
class?Wanger?{private?String?name;private?int?age;public?String?getName()?{return?name;}public?void?setName(String?name)?{this.name?=?name;}public?int?getAge()?{return?age;}public?void?setAge(int?age)?{this.age?=?age;} }再來創建一個測試類,通過?ObjectOutputStream?將“18 歲的王二”寫入到文件當中,實際上就是一種序列化的過程;再通過?ObjectInputStream?將“18 歲的王二”從文件中讀出來,實際上就是一種反序列化的過程。
public?class?Test?{public?static?void?main(String[]?args)?{//?初始化Wanger?wanger?=?new?Wanger();wanger.setName("王二");wanger.setAge(18);System.out.println(wanger);//?把對象寫到文件中try?(ObjectOutputStream?oos?=?new?ObjectOutputStream(new?FileOutputStream("chenmo"));){oos.writeObject(wanger);}?catch?(IOException?e)?{e.printStackTrace();}//?從文件中讀出對象try?(ObjectInputStream?ois?=?new?ObjectInputStream(new?FileInputStream(new?File("chenmo")));){Wanger?wanger1?=?(Wanger)?ois.readObject();System.out.println(wanger1);}?catch?(IOException?|?ClassNotFoundException?e)?{e.printStackTrace();}}}不過,由于?Wanger?沒有實現?Serializbale?接口,所以在運行測試類的時候會拋出異常,堆棧信息如下:
java.io.NotSerializableException:?com.cmower.java_demo.xuliehua.Wangerat?java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)at?java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)at?com.cmower.java_demo.xuliehua.Test.main(Test.java:21)順著堆棧信息,我們來看一下?ObjectOutputStream?的?writeObject0()?方法。其部分源碼如下:
if?(obj?instanceof?String)?{writeString((String)?obj,?unshared); }?else?if?(cl.isArray())?{writeArray(obj,?desc,?unshared); }?else?if?(obj?instanceof?Enum)?{writeEnum((Enum<?>)?obj,?desc,?unshared); }?else?if?(obj?instanceof?Serializable)?{writeOrdinaryObject(obj,?desc,?unshared); }?else?{if?(extendedDebugInfo)?{throw?new?NotSerializableException(cl.getName()?+?"\n"?+?debugInfoStack.toString());}?else?{throw?new?NotSerializableException(cl.getName());} }也就是說,ObjectOutputStream?在序列化的時候,會判斷被序列化的對象是哪一種類型,字符串?數組?枚舉?還是?Serializable,如果全都不是的話,拋出?NotSerializableException。
假如?Wanger?實現了?Serializable?接口,就可以序列化和反序列化了。
class?Wanger?implements?Serializable{private?static?final?long?serialVersionUID?=?-2095916884810199532L;private?String?name;private?int?age; }具體怎么序列化呢?
以?ObjectOutputStream?為例吧,它在序列化的時候會依次調用?writeObject()→writeObject0()→writeOrdinaryObject()→writeSerialData()→invokeWriteObject()→defaultWriteFields()。
private?void?defaultWriteFields(Object?obj,?ObjectStreamClass?desc)throws?IOException{Class<?>?cl?=?desc.forClass();desc.checkDefaultSerialize();int?primDataSize?=?desc.getPrimDataSize();desc.getPrimFieldValues(obj,?primVals);bout.write(primVals,?0,?primDataSize,?false);ObjectStreamField[]?fields?=?desc.getFields(false);Object[]?objVals?=?new?Object[desc.getNumObjFields()];int?numPrimFields?=?fields.length?-?objVals.length;desc.getObjFieldValues(obj,?objVals);for?(int?i?=?0;?i?<?objVals.length;?i++)?{try?{writeObject0(objVals[i],fields[numPrimFields?+?i].isUnshared());}}}那怎么反序列化呢?
以?ObjectInputStream?為例,它在反序列化的時候會依次調用?readObject()→readObject0()→readOrdinaryObject()→readSerialData()→defaultReadFields()。
private?void?defaultWriteFields(Object?obj,?ObjectStreamClass?desc)throws?IOException{Class<?>?cl?=?desc.forClass();desc.checkDefaultSerialize();int?primDataSize?=?desc.getPrimDataSize();desc.getPrimFieldValues(obj,?primVals);bout.write(primVals,?0,?primDataSize,?false);ObjectStreamField[]?fields?=?desc.getFields(false);Object[]?objVals?=?new?Object[desc.getNumObjFields()];int?numPrimFields?=?fields.length?-?objVals.length;desc.getObjFieldValues(obj,?objVals);for?(int?i?=?0;?i?<?objVals.length;?i++)?{try?{writeObject0(objVals[i],fields[numPrimFields?+?i].isUnshared());}}}我想看到這,你應該會恍然大悟的“哦”一聲了。Serializable?接口之所以定義為空,是因為它只起到了一個標識的作用,告訴程序實現了它的對象是可以被序列化的,但真正序列化和反序列化的操作并不需要它來完成。
03、再來點注意事項
開門見山的說吧,static?和?transient?修飾的字段是不會被序列化的。
為什么呢?我們先來證明,再來解釋原因。
首先,在?Wanger?類中增加兩個字段。
class?Wanger?implements?Serializable?{private?static?final?long?serialVersionUID?=?-2095916884810199532L;private?String?name;private?int?age;public?static?String?pre?=?"沉默";transient?String?meizi?=?"王三";@Overridepublic?String?toString()?{return?"Wanger{"?+?"name="?+?name?+?",age="?+?age?+?",pre="?+?pre?+?",meizi="?+?meizi?+?"}";} }其次,在測試類中打印序列化前和反序列化后的對象,并在序列化后和反序列化前改變?static?字段的值。具體代碼如下:
//?初始化 Wanger?wanger?=?new?Wanger(); wanger.setName("王二"); wanger.setAge(18); System.out.println(wanger);//?把對象寫到文件中 try?(ObjectOutputStream?oos?=?new?ObjectOutputStream(new?FileOutputStream("chenmo"));){oos.writeObject(wanger);}?catch?(IOException?e)?{e.printStackTrace();}//?改變?static?字段的值 Wanger.pre?="不沉默";//?從文件中讀出對象 try?(ObjectInputStream?ois?=?new?ObjectInputStream(new?FileInputStream(new?File("chenmo")));){Wanger?wanger1?=?(Wanger)?ois.readObject();System.out.println(wanger1); }?catch?(IOException?|?ClassNotFoundException?e)?{e.printStackTrace(); } //?Wanger{name=王二,age=18,pre=沉默,meizi=王三} //?Wanger{name=王二,age=18,pre=不沉默,meizi=null}從結果的對比當中,我們可以發現:
1)序列化前,pre?的值為“沉默”,序列化后,pre?的值修改為“不沉默”,反序列化后,pre?的值為“不沉默”,而不是序列化前的狀態“沉默”。
為什么呢?因為序列化保存的是對象的狀態,而?static?修飾的字段屬于類的狀態,因此可以證明序列化并不保存?static?修飾的字段。
2)序列化前,meizi?的值為“王三”,反序列化后,meizi?的值為?null,而不是序列化前的狀態“王三”。
為什么呢?transient?的中文字義為“臨時的”(論英語的重要性),它可以阻止字段被序列化到文件中,在被反序列化后,transient?字段的值被設為初始值,比如?int型的初始值為 0,對象型的初始值為?null。
如果想要深究源碼的話,你可以在?ObjectStreamClass?中發現下面這樣的代碼:
private?static?ObjectStreamField[]?getDefaultSerialFields(Class<?>?cl)?{Field[]?clFields?=?cl.getDeclaredFields();ArrayList<ObjectStreamField>?list?=?new?ArrayList<>();int?mask?=?Modifier.STATIC?|?Modifier.TRANSIENT;int?size?=?list.size();return?(size?==?0)???NO_FIELDS?:list.toArray(new?ObjectStreamField[size]); }看到?Modifier.STATIC | Modifier.TRANSIENT,是不是感覺更好了呢?
04、再來點干貨
除了?Serializable?之外,Java 還提供了一個序列化接口?Externalizable(念起來有點拗口)。
兩個接口有什么不一樣的嗎?試一試就知道了。
首先,把?Wanger?類實現的接口 ?Serializable?替換為?Externalizable。
class?Wanger?implements?Externalizable?{private?String?name;private?int?age;public?Wanger()?{}public?String?getName()?{return?name;}@Overridepublic?String?toString()?{return?"Wanger{"?+?"name="?+?name?+?",age="?+?age?+?"}";}@Overridepublic?void?writeExternal(ObjectOutput?out)?throws?IOException?{}@Overridepublic?void?readExternal(ObjectInput?in)?throws?IOException,?ClassNotFoundException?{}}實現?Externalizable?接口的?Wanger?類和實現?Serializable?接口的?Wanger?類有一些不同:
1)新增了一個無參的構造方法。
使用?Externalizable?進行反序列化的時候,會調用被序列化類的無參構造方法去創建一個新的對象,然后再將被保存對象的字段值復制過去。否則的話,會拋出以下異常:
java.io.InvalidClassException:?com.cmower.java_demo.xuliehua1.Wanger;?no?valid?constructorat?java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(ObjectStreamClass.java:150)at?java.io.ObjectStreamClass.checkDeserialize(ObjectStreamClass.java:790)at?java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1782)at?java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1353)at?java.io.ObjectInputStream.readObject(ObjectInputStream.java:373)at?com.cmower.java_demo.xuliehua1.Test.main(Test.java:27)2)新增了兩個方法?writeExternal()?和?readExternal(),實現?Externalizable?接口所必須的。
然后,我們再在測試類中打印序列化前和反序列化后的對象。
//?初始化 Wanger?wanger?=?new?Wanger(); wanger.setName("王二"); wanger.setAge(18); System.out.println(wanger);//?把對象寫到文件中 try?(ObjectOutputStream?oos?=?new?ObjectOutputStream(new?FileOutputStream("chenmo"));)?{oos.writeObject(wanger); }?catch?(IOException?e)?{e.printStackTrace(); }//?從文件中讀出對象 try?(ObjectInputStream?ois?=?new?ObjectInputStream(new?FileInputStream(new?File("chenmo")));)?{Wanger?wanger1?=?(Wanger)?ois.readObject();System.out.println(wanger1); }?catch?(IOException?|?ClassNotFoundException?e)?{e.printStackTrace(); } //?Wanger{name=王二,age=18} //?Wanger{name=null,age=0}從輸出的結果看,反序列化后得到的對象字段都變成了默認值,也就是說,序列化之前的對象狀態沒有被“凍結”下來。
為什么呢?因為我們沒有為?Wanger?類重寫具體的?writeExternal()?和?readExternal()?方法。那該怎么重寫呢?
@Override public?void?writeExternal(ObjectOutput?out)?throws?IOException?{out.writeObject(name);out.writeInt(age); }@Override public?void?readExternal(ObjectInput?in)?throws?IOException,?ClassNotFoundException?{name?=?(String)?in.readObject();age?=?in.readInt(); }1)調用?ObjectOutput?的?writeObject()?方法將字符串類型的?name?寫入到輸出流中;
2)調用?ObjectOutput?的?writeInt()?方法將整型的?age?寫入到輸出流中;
3)調用?ObjectInput?的?readObject()?方法將字符串類型的?name?讀入到輸入流中;
4)調用?ObjectInput?的?readInt()?方法將字符串類型的?age?讀入到輸入流中;
再運行一次測試了類,你會發現對象可以正常地序列化和反序列化了。
序列化前:Wanger{name=王二,age=18}
序列化后:Wanger{name=王二,age=18}
05、再來點甜點
讓我先問問你吧,你知道?private static final long serialVersionUID = -2095916884810199532L;?這段代碼的作用嗎?
嗯……
serialVersionUID?被稱為序列化 ID,它是決定 Java 對象能否反序列化成功的重要因子。在反序列化時,Java 虛擬機會把字節流中的?serialVersionUID?與被序列化類中的?serialVersionUID?進行比較,如果相同則可以進行反序列化,否則就會拋出序列化版本不一致的異常。
當一個類實現了?Serializable?接口后,IDE 就會提醒該類最好產生一個序列化 ID,就像下面這樣:
1)添加一個默認版本的序列化 ID:
private?static?final?long?serialVersionUID?=?1L。2)添加一個隨機生成的不重復的序列化 ID。
private?static?final?long?serialVersionUID?=?-2095916884810199532L;3)添加?@SuppressWarnings?注解。
@SuppressWarnings("serial")怎么選擇呢?
首先,我們采用第二種辦法,在被序列化類中添加一個隨機生成的序列化 ID。
class?Wanger?implements?Serializable?{private?static?final?long?serialVersionUID?=?-2095916884810199532L;private?String?name;private?int?age;//?其他代碼忽略 }然后,序列化一個?Wanger?對象到文件中。
//?初始化 Wanger?wanger?=?new?Wanger(); wanger.setName("王二"); wanger.setAge(18); System.out.println(wanger);//?把對象寫到文件中 try?(ObjectOutputStream?oos?=?new?ObjectOutputStream(new?FileOutputStream("chenmo"));)?{oos.writeObject(wanger); }?catch?(IOException?e)?{e.printStackTrace(); }這時候,我們悄悄地把?Wanger?類的序列化 ID 偷梁換柱一下,嘿嘿。
//?private?static?final?long?serialVersionUID?=?-2095916884810199532L; private?static?final?long?serialVersionUID?=?-2095916884810199533L;好了,準備反序列化吧。
try?(ObjectInputStream?ois?=?new?ObjectInputStream(new?FileInputStream(new?File("chenmo")));)?{Wanger?wanger?=?(Wanger)?ois.readObject();System.out.println(wanger); }?catch?(IOException?|?ClassNotFoundException?e)?{e.printStackTrace(); }哎呀,出錯了。
java.io.InvalidClassException:??local?class?incompatible:?stream?classdesc? serialVersionUID?=?-2095916884810199532, local?class?serialVersionUID?=?-2095916884810199533at?java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1521)at?com.cmower.java_demo.xuliehua1.Test.main(Test.java:27)異常堆棧信息里面告訴我們,從持久化文件里面讀取到的序列化 ID 和本地的序列化 ID 不一致,無法反序列化。
那假如我們采用第三種方法,為?Wanger?類添加個?@SuppressWarnings("serial")?注解呢?
@SuppressWarnings("serial") class?Wanger3?implements?Serializable?{ //?省略其他代碼 }好了,再來一次反序列化吧。可惜依然報錯。
java.io.InvalidClassException:??local?class?incompatible:?stream?classdesc? serialVersionUID?=?-2095916884810199532,? local?class?serialVersionUID?=?-3818877437117647968at?java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1521)at?com.cmower.java_demo.xuliehua1.Test.main(Test.java:27)異常堆棧信息里面告訴我們,本地的序列化 ID 為 -3818877437117647968,和持久化文件里面讀取到的序列化 ID 仍然不一致,無法反序列化。這說明什么呢?使用?@SuppressWarnings("serial")?注解時,該注解會為被序列化類自動生成一個隨機的序列化 ID。
由此可以證明,Java 虛擬機是否允許反序列化,不僅取決于類路徑和功能代碼是否一致,還有一個非常重要的因素就是序列化 ID 是否一致。
也就是說,如果沒有特殊需求,采用默認的序列化 ID(1L)就可以,這樣可以確保代碼一致時反序列化成功。
class?Wanger?implements?Serializable?{private?static?final?long?serialVersionUID?=?1L; //?省略其他代碼 }06、再來點總結
寫這篇文章之前,我真沒想到:“空空其身”的Serializable?竟然有這么多可以研究的內容!
寫完這篇文章之后,我不由得想起理科狀元曹林菁說說過的一句話:“在學習中再小的問題也不放過,每個知識點都要總結”——說得真真真真的對啊!
總結
以上是生活随笔為你收集整理的Serializable:明明就一个空接口!为什么还要实现它?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 谷歌和 Facebook 是如何给工程师
- 下一篇: e.printStackTrace()不