面試官:三年工作經(jīng)驗(yàn),你連序列化都說不明白?
什么是序列化、反序列化
序列化:把Java對(duì)象轉(zhuǎn)換為字節(jié)序列的過程。
反序列化:把字節(jié)序列恢復(fù)為Java對(duì)象的過程。
序列化的作用
1、可以把對(duì)象的字節(jié)序列永久地保存到硬盤上,通常存放在一個(gè)文件中;(持久化對(duì)象)
2、也可以在網(wǎng)絡(luò)上傳輸對(duì)象的字節(jié)序列;(網(wǎng)絡(luò)傳輸對(duì)象)
序列化在Java中的用法
在Java中序列化的實(shí)現(xiàn):將需要被序列化的類實(shí)現(xiàn)Serializable接口,該接口沒有需要實(shí)現(xiàn)的方法,實(shí)現(xiàn)該接口只是為了標(biāo)注該對(duì)象是可被序列化的,然后使用一個(gè)輸出流(如:FileOutputStream)來構(gòu)造一個(gè)ObjectOutputStream(對(duì)象輸出流)對(duì)象,接著,使用ObjectOutputStream對(duì)象的writeObject(Object obj)方法就可以將參數(shù)為obj的對(duì)象寫出(即保存其狀態(tài)),要恢復(fù)的話則用ObjectInputStream(對(duì)象輸入流)。
如下為序列化、反序列化簡單案例 Test01:
java.io.FileInputStream; java.io.FileOutputStream; java.io.IOException; java.io.ObjectInputStream; java.io.ObjectOutputStream; java.io.Serializable; { { serializable(); deserialization(); } { (ObjectOutputStream oos = ObjectOutputStream( FileOutputStream())) { Person person = Person(); person.setName(); person.setAge(); oos.writeObject(person); } (IOException e) { e.printStackTrace(); } } { (ObjectInputStream ois = ObjectInputStream( FileInputStream())) { Person person = (Person) ois.readObject(); System.out.println(person); } (IOException | ClassNotFoundException e) { e.printStackTrace(); } } } { serialVersionUID = -; String name; age; { name; } { .name = name; } { age; } { .age = age; } { + + name + + + age + ; } }
上面案例中只是簡單的進(jìn)行了對(duì)象序列化和反序列化,但是序列化和反序列化過程中有很多值得思考的細(xì)節(jié)問題,例如:
1、序列化版本號(hào)(serialVersionUID)問題
2、靜態(tài)變量序列化
3、父類的序列化與 transient 關(guān)鍵字
4、自定義序列化規(guī)則
5、序列化存儲(chǔ)規(guī)則
1、序列化版本號(hào)(serialVersionUID)問題
在寫Java程序中有時(shí)我們經(jīng)常會(huì)看到類中會(huì)有一個(gè)序列化版本號(hào):serialVersionUID。這個(gè)值有的類是1L或者是自動(dòng)生成的。
private static final long serialVersionUID = 1L;
或者
private static final long serialVersionUID = -2052381772192998351L;
當(dāng)在反序列化時(shí)JVM需要判斷需要轉(zhuǎn)化的兩個(gè)類是不是同一個(gè)類,于是就需要一個(gè)序列化版本號(hào)。如果在反序列化的時(shí)候兩個(gè)類的serialVersionUID不一樣則JVM會(huì)拋出java.io.InvalidClassException的異常;如果serialVersionUID一致則表明可以轉(zhuǎn)換。
如果可序列化類未顯式聲明 serialVersionUID,則序列化運(yùn)行時(shí)將基于該類的各個(gè)方面計(jì)算該類的默認(rèn) serialVersionUID 值。不過,強(qiáng)烈建議 所有可序列化類都顯式聲明 serialVersionUID 值,原因是計(jì)算默認(rèn)的 serialVersionUID 對(duì)類的詳細(xì)信息具有較高的敏感性,根據(jù)編譯器實(shí)現(xiàn)的不同可能千差萬別,這樣在反序列化過程中可能會(huì)導(dǎo)致意外的 InvalidClassException,所以這種方式不支持反序列化重構(gòu)。所謂重構(gòu)就是可以對(duì)類增加或者減少屬性字段,也就是說即使兩個(gè)類并不完全一致,他們也是可以轉(zhuǎn)換的,只不過如果找不到對(duì)應(yīng)的字段,它的值會(huì)被設(shè)為默認(rèn)值。
因此,為保證 serialVersionUID 值跨不同 java 編譯器實(shí)現(xiàn)的一致性或代碼重構(gòu)時(shí),序列化類必須聲明一個(gè)明確的 serialVersionUID 值。還強(qiáng)烈建議使用 private 修飾符顯示聲明 serialVersionUID(如果可能),原因是這種聲明僅應(yīng)用于直接聲明類 -- serialVersionUID 字段作為繼承成員沒有用處。數(shù)組類不能聲明一個(gè)明確的 serialVersionUID,因此它們總是具有默認(rèn)的計(jì)算值,但是數(shù)組類沒有匹配 serialVersionUID 值的要求。
還有一個(gè)常見的值是1L(或者其他固定值),如果所有類都這么寫那還怎么區(qū)分它們,這個(gè)字段還有什么意義嗎?有的!首先如果兩個(gè)類有了相同的反序列化版本號(hào),比如1L,那么表明這兩個(gè)類是支持在反序列化時(shí)重構(gòu)的。但是會(huì)有一個(gè)明顯的問題:如果兩個(gè)類是完全不同的,但是他們的序列化版本號(hào)都是1L,那么對(duì)于JVM來說他們也是可以進(jìn)行反序列化重構(gòu)的!這這顯然是不對(duì)的,但是回過頭來說這種明顯的,愚蠢的錯(cuò)誤在實(shí)際開發(fā)中是不太可能會(huì)犯的,如果不是那么嚴(yán)謹(jǐn)?shù)脑捰?L是個(gè)不錯(cuò)的選擇。
一般的情況下這個(gè)值是顯式地指定為一個(gè)64位的哈希字段,比如你寫了一個(gè)類實(shí)現(xiàn)了java.io.Serializable接口,在idea里會(huì)提示你加上這個(gè)序列化id。這樣做可以區(qū)分不同的類,也支持反序列化重構(gòu)。
總結(jié)如下:
serialVersionUID區(qū)分不同類支持相同類的重構(gòu)
不指定 | YEs | NO |
1L | NO | YES |
64位哈希值 | YES | YES |
簡單而言,從嚴(yán)謹(jǐn)性的角度來說,指定64位哈希值>默認(rèn)值1L>不指定serialVersionUID值,具體怎么使用就看你的需求了。
2、靜態(tài)變量序列化
java.io.FileInputStream; java.io.FileOutputStream; java.io.IOException; java.io.ObjectInputStream; java.io.ObjectOutputStream; java.io.Serializable; { { (ObjectOutputStream oos = ObjectOutputStream( FileOutputStream()); ObjectInputStream ois = ObjectInputStream( FileInputStream())) { Person person = Person(); person.setName(); person.setAge(); oos.writeObject(person); Person.avgAge =; Person person1 = (Person) ois.readObject(); System.out.println(person1.avgAge); } (IOException | ClassNotFoundException e) { e.printStackTrace(); } } } { serialVersionUID = -; String name; age; avgAge =; { name; } { .name = name; } { age; } { .age = age; } { + + name + + + age + ; } }
執(zhí)行結(jié)果顯示如下:
我們看到 Test02.java將對(duì)象序列化后,修改靜態(tài)變量的數(shù)值再將序列化對(duì)象讀取出來,然后通過讀取出來的對(duì)象獲得靜態(tài)變量的數(shù)值并打印出來,最后的輸出是 10,之所以打印 10 的原因在于序列化時(shí),并不保存靜態(tài)變量,這其實(shí)比較容易理解,序列化保存的是對(duì)象的狀態(tài),靜態(tài)變量屬于類的狀態(tài),因此 序列化并不保存靜態(tài)變量 。
3、父類的序列化與 transient關(guān)鍵字
情境 :一個(gè)子類實(shí)現(xiàn)了 Serializable 接口,它的父類都沒有實(shí)現(xiàn) Serializable 接口,序列化該子類對(duì)象,然后反序列化后輸出父類定義的某變量的數(shù)值,該變量數(shù)值與序列化時(shí)的數(shù)值不同。
解決 :要想將父類對(duì)象也序列化,就需要讓父類也實(shí)現(xiàn) Serializable 接口 。如果父類不實(shí)現(xiàn)的話的,就需要有默認(rèn)的無參的構(gòu)造函數(shù) 。在父類沒有實(shí)現(xiàn) Serializable 接口時(shí),虛擬機(jī)是不會(huì)序列化父對(duì)象的,而一個(gè) Java 對(duì)象的構(gòu)造必須先有父對(duì)象,才有子對(duì)象,反序列化也不例外。所以反序列化時(shí),為了構(gòu)造父對(duì)象,只能調(diào)用父類的無參構(gòu)造函數(shù)作為默認(rèn)的父對(duì)象。因此當(dāng)我們?nèi)「笇?duì)象的變量值時(shí),它的值是調(diào)用父類無參構(gòu)造函數(shù)后的值。如果你考慮到這種序列化的情況,在父類無參構(gòu)造函數(shù)中對(duì)變量進(jìn)行初始化,否則的話,父類變量值都是默認(rèn)聲明的值,如 int 型的默認(rèn)是 0,string 型的默認(rèn)是 null。
transient 關(guān)鍵字的作用是控制變量的序列化,在變量聲明前加上該關(guān)鍵字,可以阻止該變量被序列化到文件中,在被反序列化后,transient 變量的值被設(shè)為初始值,如 int 型的是 0,對(duì)象型的是 null。
3-1、特性使用案例:
我們熟悉使用 transient 關(guān)鍵字可以使得字段不被序列化,那么還有別的方法嗎?根據(jù)父類對(duì)象序列化的規(guī)則,我們可以將不需要被序列化的字段抽取出來放到父類中,子類實(shí)現(xiàn) Serializable 接口,父類不實(shí)現(xiàn),根據(jù)父類序列化規(guī)則,父類的字段數(shù)據(jù)將不被序列化,形成類圖如下圖所示。
上圖中可以看出,attr1、attr2、attr3、attr5 都不會(huì)被序列化,放在父類中的好處在于當(dāng)有另外一個(gè) Child 類時(shí),attr1、attr2、attr3 依然不會(huì)被序列化,不用重復(fù)書寫 transient 關(guān)鍵字,代碼簡潔。
4、自定義序列化規(guī)則
在序列化和反序列化過程中需要特殊處理的類必須使用下列準(zhǔn)確簽名來實(shí)現(xiàn)特殊方法:
; ; ;
writeObject 方法負(fù)責(zé)寫入特定類的對(duì)象的狀態(tài),以便相應(yīng)的 readObject 方法可以恢復(fù)它。通過調(diào)用 oos.defaultWriteObject 可以調(diào)用保存 Object 的字段的默認(rèn)機(jī)制。該方法本身不需要涉及屬于其超類或子類的狀態(tài)。通過使用 writeObject 方法或使用 DataOutput 支持的用于基本數(shù)據(jù)類型的方法將各個(gè)字段寫入 ObjectOutputStream,狀態(tài)可以被保存。
readObject 方法負(fù)責(zé)從流中讀取并恢復(fù)類字段。它可以調(diào)用 oin.defaultReadObject 來調(diào)用默認(rèn)機(jī)制,以恢復(fù)對(duì)象的非靜態(tài)和非瞬態(tài)(非 transient 修飾)字段。defaultReadObject方法使用流來分配保存在流中的對(duì)象的字段當(dāng)前對(duì)象中相應(yīng)命名的字段。這用于處理類演化后需要添加新字段的情形。該方法本身不需要涉及屬于其超類或子類的狀態(tài)。通過使用 writeObject 方法或使用 DataOutput 支持的用于基本數(shù)據(jù)類型的方法將各個(gè)字段寫入 ObjectOutputStream,狀態(tài)可以被保存。
在序列化流不列出給定類作為將被反序列化對(duì)象的超類的情況下,readObjectNoData 方法負(fù)責(zé)初始化特定類的對(duì)象狀態(tài)。這在接收方使用的反序列化實(shí)例類的版本不同于發(fā)送方,并且接收者版本擴(kuò)展的類不是發(fā)送者版本擴(kuò)展的類時(shí)發(fā)生。在序列化流已經(jīng)被篡改時(shí)也將發(fā)生;因此,不管源流是“敵意的”還是不完整的,readObjectNoData 方法都可以用來正確地初始化反序列化的對(duì)象。
readObjectNoData()應(yīng)用示例:
java.io.FileOutputStream; java.io.ObjectOutputStream; java.io.Serializable; { { (ObjectOutputStream oos = ObjectOutputStream( FileOutputStream())) { Person person = Person(); person.setAge(); oos.writeObject(person); } (Exception e) { e.printStackTrace(); } } } { age; { .age = age; } { .age; } }
java.io.FileInputStream; java.io.ObjectInputStream; java.io.Serializable; { { (ObjectInputStream ois = ObjectInputStream( FileInputStream())) { Person person = (Person) ois.readObject(); System.out.println(person.getName()); } (Exception e) { e.printStackTrace(); } } } { age; { .age = age; } { .age; } } { String name; { .name = name; } { .name; } { .name =; } }
將對(duì)象寫入流時(shí)需要指定要使用的替代對(duì)象的可序列化類,應(yīng)使用準(zhǔn)確的簽名來實(shí)現(xiàn)此特殊方法:
ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;
此 writeReplace 方法將由序列化調(diào)用,前提是如果此方法存在,而且它可以通過被序列化對(duì)象的類中定義的一個(gè)方法訪問。因此,該方法可以擁有私有 (private)、受保護(hù)的 (protected) 和包私有 (package-private) 訪問。子類對(duì)此方法的訪問遵循 java 訪問規(guī)則。
在從流中讀取類的一個(gè)實(shí)例時(shí)需要指定替代的類應(yīng)使用的準(zhǔn)確簽名來實(shí)現(xiàn)此特殊方法。
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
此 readResolve 方法遵循與 writeReplace 相同的調(diào)用規(guī)則和訪問規(guī)則。
TIP: readResolve常用來反序列單例類,保證單例類的唯一性
例如:
java.io.FileInputStream; java.io.FileOutputStream; java.io.IOException; java.io.ObjectInputStream; java.io.ObjectOutputStream; java.io.Serializable; { { (ObjectOutputStream oos = ObjectOutputStream( FileOutputStream())) { oos.writeObject(Brand.NIKE); } (ObjectInputStream ois = ObjectInputStream( FileInputStream())) { Brand b = (Brand) ois.readObject(); System.out.println(b == Brand.NIKE); } } } { val; { .val = val; } Brand NIKE = Brand(); Brand ADDIDAS = Brand(); }
答案很顯然是false,因?yàn)锽rand.NIKE是程序中創(chuàng)建的對(duì)象,而b是從磁盤中讀取并恢復(fù)過來的對(duì)象,兩者明顯來源不同,因此必然內(nèi)存空間是不同的,引用(地址)顯然也是不同的;
但這不是我們想看到的,因?yàn)槲覀儼袯rand設(shè)計(jì)成枚舉類型,不管是程序中創(chuàng)建的還是從哪里讀取的,其必須應(yīng)該和枚舉常量完全相等,這才是枚舉的意義?。?/p>
而此時(shí)readResolve就派上用場(chǎng)了,我們可以這樣實(shí)現(xiàn)readResolve:
java.io.FileInputStream; java.io.FileOutputStream; java.io.IOException; java.io.ObjectInputStream; java.io.ObjectOutputStream; java.io.ObjectStreamException; java.io.Serializable; { { (ObjectOutputStream oos = ObjectOutputStream( FileOutputStream())) { oos.writeObject(Brand.NIKE); } (ObjectInputStream ois = ObjectInputStream( FileInputStream())) { Brand b = (Brand) ois.readObject(); System.out.println(b == Brand.NIKE); } } } { val; { .val = val; } Brand NIKE = Brand(); Brand ADDIDAS = Brand(); { (val ==) { NIKE; } (val ==) { ADDIDAS; } ; } }
改造以后,不管來源如何,最終得到的都將是程序中Brand的枚舉值了!因?yàn)閞eadResolve的代碼在執(zhí)行時(shí)已經(jīng)進(jìn)入了程序內(nèi)存環(huán)境,因此其返回的NIKE和ADDIDAS都將是Brand的靜態(tài)成員對(duì)象;
因此保護(hù)性恢復(fù)的含義就在此:首先恢復(fù)的時(shí)候沒有改變其值(val的值沒有改變)同時(shí)恢復(fù)的時(shí)候又能正常實(shí)現(xiàn)枚舉值的對(duì)比(地址也完全相同);
4-1、對(duì)敏感字段加密
情境:服務(wù)器端給客戶端發(fā)送序列化對(duì)象數(shù)據(jù),對(duì)象中有一些數(shù)據(jù)是敏感的,比如密碼字符串等,希望對(duì)該密碼字段在序列化時(shí),進(jìn)行加密,而客戶端如果擁有解密的密鑰,只有在客戶端進(jìn)行反序列化時(shí),才可以對(duì)密碼進(jìn)行讀取,這樣可以一定程度保證序列化對(duì)象的數(shù)據(jù)安全。
解決:在序列化過程中,虛擬機(jī)會(huì)試圖調(diào)用對(duì)象類里的 writeObject 和 readObject 方法,進(jìn)行用戶自定義的序列化和反序列化,該方法必須要被聲明為private,如果沒有這樣的方法,則默認(rèn)調(diào)用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。用戶自定義的 writeObject 和 readObject 方法可以允許用戶控制序列化的過程,比如可以在序列化的過程中動(dòng)態(tài)改變序列化的數(shù)值。基于這個(gè)原理,可以在實(shí)際應(yīng)用中得到使用,用于敏感字段的加密工作,如下代碼展示了這個(gè)過程。
import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; { { (ObjectOutputStream oos = ObjectOutputStream( FileOutputStream()); ObjectInputStream ois = ObjectInputStream( FileInputStream())) { oos.writeObject( Account()); Account account = (Account) ois.readObject(); System..println( + account.getPassword()); } (IOException | ClassNotFoundException e) { e.printStackTrace(); } } } { String password =; { password; } { .password = password; } { { ObjectOutputStream.PutField putFields =.putFields(); System..println( + password); password =; putFields.put(, password); System..println( + password); .writeFields(); } (IOException e) { e.printStackTrace(); } } { { ObjectInputStream.GetField readFields =.readFields(); Object = readFields.(,); System..println( +.toString()); password =; } (IOException e) { e.printStackTrace(); } (ClassNotFoundException e) { e.printStackTrace(); } } }
上述代碼中的 writeObject 方法中,對(duì)密碼進(jìn)行了加密,在 readObject 中則對(duì) password 進(jìn)行解密,只有擁有密鑰的客戶端,才可以正確的解析出密碼,確保了數(shù)據(jù)的安全。
4-2、序列化SDK中不可序列化的類型
4-1、對(duì)敏感字段加密案例使用 writeObject 和 readObject 進(jìn)行了對(duì)象屬性值加解密操作,有時(shí)我們想將對(duì)象中的某一字段序列化,但它在SDK中的定義卻是不可序列化的類型,這樣的話我們也必須把他標(biāo)注為 transient 才能保證正常序列化,可是不能序列化又怎么恢復(fù)呢?這就用到了上面提到的 writeObject 和 readObject 方法,進(jìn)行自定義序列化操作了。
示例:java.awt.geom包中的Point2D.Double類就是不可序列化的,因?yàn)樵擃悰]有實(shí)現(xiàn)Serializable接口
java.awt.geom.Point2D; java.io.FileInputStream; java.io.FileOutputStream; java.io.IOException; java.io.ObjectInputStream; java.io.ObjectOutputStream; java.io.Serializable; { { LabeledPoint label = LabeledPoint(,,); { System.out.println(label); ObjectOutputStream out = ObjectOutputStream( FileOutputStream()); out.writeObject(label); out.close(); System.out.println(label); ObjectInputStream in = ObjectInputStream( FileInputStream()); LabeledPoint label1 = (LabeledPoint) in.readObject(); in.close(); System.out.println(label1); } (Exception e) { e.printStackTrace(); } } } { String label; Point2D.Double point; { label = str; point = Point2D.Double(x, y); } { oos.defaultWriteObject(); oos.writeDouble(point.getX()); oos.writeDouble(point.getY()); } { ois.defaultReadObject(); x = ois.readDouble() +; y = ois.readDouble() +; point = Point2D.Double(x, y); } { + + label + + + point + ; } }
在 4-1、序列化SDK中不可序列化的類型案例中,你會(huì)發(fā)現(xiàn)調(diào)用了defaultWriteObject()和defaultReadObject()。它們做的是默認(rèn)的序列化進(jìn)程,就像寫/讀所有的non-transient和 non-static字段(但他們不會(huì)去做serialVersionUID的檢查)。通常說來,所有我們想要自己處理的字段都應(yīng)該聲明為transient。這樣的話 defaultWriteObject/defaultReadObject 便可以專注于其余字段,而我們則可為這些特定的字段(指transient)定制序列化。使用那兩個(gè)默認(rèn)的方法并不是強(qiáng)制的,而是給予了處理復(fù)雜應(yīng)用時(shí)更多的靈活性。
5、序列化存儲(chǔ)規(guī)則
5-1、存儲(chǔ)兩次相同對(duì)象
java.io.File; java.io.FileInputStream; java.io.FileOutputStream; java.io.IOException; java.io.ObjectInputStream; java.io.ObjectOutputStream; java.io.Serializable; { { (ObjectOutputStream oos = ObjectOutputStream( FileOutputStream()); ObjectInputStream ois = ObjectInputStream( FileInputStream())) { Account account = Account(); account.setPassword(); oos.writeObject(account); oos.flush(); System.out.println( File().length()); oos.writeObject(account); System.out.println( File().length()); Account account1 = (Account) ois.readObject(); Account account2 = (Account) ois.readObject(); System.out.println(account1 == account2); } (IOException | ClassNotFoundException e) { e.printStackTrace(); } } } { String password; { password; } { .password = password; } }
上述代碼中對(duì)同一對(duì)象兩次寫入文件,打印出寫入一次對(duì)象后的存儲(chǔ)大小和寫入兩次后的存儲(chǔ)大小,然后從文件中反序列化出兩個(gè)對(duì)象,比較這兩個(gè)對(duì)象是否為同一對(duì)象。一般的思維是,兩次寫入對(duì)象,文件大小會(huì)變?yōu)閮杀兜拇笮?,反序列化時(shí),由于從文件讀取,生成了兩個(gè)對(duì)象,判斷相等時(shí)應(yīng)該是輸入 false 才對(duì),但
我們看到,第二次寫入對(duì)象時(shí)文件只增加了 5 字節(jié),并且兩個(gè)對(duì)象是相等的,因?yàn)镴ava 序列化機(jī)制為了節(jié)省磁盤空間,具有特定的存儲(chǔ)規(guī)則,當(dāng)寫入文件的為同一對(duì)象時(shí),并不會(huì)再將對(duì)象的內(nèi)容進(jìn)行存儲(chǔ),而只是再次存儲(chǔ)一份引用,上面增加的 5 字節(jié)的存儲(chǔ)空間就是新增引用和一些控制信息的空間。反序列化時(shí),恢復(fù)引用關(guān)系,使得上述代碼中的 account1 和 account2 指向唯一的對(duì)象,二者相等,輸出 true。該存儲(chǔ)規(guī)則極大的節(jié)省了存儲(chǔ)空間
5-2、存儲(chǔ)兩次相同對(duì)象,更改屬性值
java.io.FileInputStream; java.io.FileOutputStream; java.io.IOException; java.io.ObjectInputStream; java.io.ObjectOutputStream; java.io.Serializable; { { (ObjectOutputStream oos = ObjectOutputStream( FileOutputStream()); ObjectInputStream ois = ObjectInputStream( FileInputStream())) { Account account = Account(); account.setPassword(); oos.writeObject(account); oos.flush(); account.setPassword(); oos.writeObject(account); Account account1 = (Account) ois.readObject(); Account account2 = (Account) ois.readObject(); System.out.println(account1.getPassword()); System.out.println(account2.getPassword()); } (IOException | ClassNotFoundException e) { e.printStackTrace(); } } } { String password; { password; } { .password = password; } }
上述代碼的目的是希望將 account 對(duì)象兩次保存到 object.txt 文件中,寫入一次以后修改對(duì)象屬性值再次保存第二次,然后從 object.txt 中再依次讀出兩個(gè)對(duì)象,輸出這兩個(gè)對(duì)象的 password 屬性值。上述代碼的目的原本是希望一次性傳輸對(duì)象修改前后的狀態(tài)。
結(jié)果兩個(gè)輸出的都是 123456, 原因就是第一次寫入對(duì)象以后,第二次再試圖寫的時(shí)候,虛擬機(jī)根據(jù)引用關(guān)系知道已經(jīng)有一個(gè)相同對(duì)象已經(jīng)寫入文件,因此只保存第二次寫的引用,所以讀取時(shí),都是第一次保存的對(duì)象。這也驗(yàn)證了 5-1、存儲(chǔ)兩次相同對(duì)象案例的現(xiàn)象,相同對(duì)象存在只會(huì)存儲(chǔ)引用,不再進(jìn)行對(duì)象存儲(chǔ),所以第二次修改的屬性未變化。讀者在使用一個(gè)文件多次 writeObject 需要特別注意這個(gè)問題。
*博客內(nèi)容為網(wǎng)友個(gè)人發(fā)布,僅代表博主個(gè)人觀點(diǎn),如有侵權(quán)請(qǐng)聯(lián)系工作人員刪除。