一篇文章帶你了解Java 中序列化與反序列化
一、 序列化和反序列化概念
Serialization
(序列化)是一種將對(duì)象以一連串的字節(jié)描述的過程;反序列化deserialization是一種將這些字節(jié)重建成一個(gè)對(duì)象的過程。將程序中的對(duì)象,放入文件中保存就是序列化,將文件中的字節(jié)碼重新轉(zhuǎn)成對(duì)象就是反序列化。
二、 序列化和反序列化的必要性
當(dāng)兩個(gè)進(jìn)程進(jìn)行遠(yuǎn)程通信時(shí),可以相互發(fā)送各種類型的數(shù)據(jù),包括文本、圖片、音頻、視頻等, 而這些數(shù)據(jù)都會(huì)以二進(jìn)制序列的形式在網(wǎng)絡(luò)上傳送。
而java是面向?qū)ο蟮拈_發(fā)方式,一切都是java對(duì)象,想要實(shí)現(xiàn)java對(duì)象的網(wǎng)絡(luò)傳輸,就可以使用序列化和反序列化來實(shí)現(xiàn)。發(fā)送方將需要發(fā)送的Java對(duì)象序列化轉(zhuǎn)換為字節(jié)序列,然后在網(wǎng)絡(luò)上傳送;接收方接收到字符序列后,使用反序列化從字節(jié)序列中恢復(fù)出Java對(duì)象。
當(dāng)我們了解了為什么需要Java序列化和反序列化后,我們很自然地會(huì)想Java序列化的好處。
一是實(shí)現(xiàn)了數(shù)據(jù)的持久化,通過序列化可以把數(shù)據(jù)永久地保存到硬盤上(通常存放在文件里);
二是利用序列化實(shí)現(xiàn)遠(yuǎn)程通信,即在網(wǎng)絡(luò)上傳送對(duì)象的字節(jié)序列。
總結(jié),在網(wǎng)絡(luò)中數(shù)據(jù)的傳輸必須是序列化形式來進(jìn)行的。其他序列化的方式可以是json傳輸,xml形式傳輸。
三、 序列化和反序列化的實(shí)現(xiàn)
1. JDK類庫(kù)提供的序列化API
java.io.ObjectOutputStream:表示對(duì)象輸出流它的writeObject(Object obj)方法可以對(duì)參數(shù)指定的obj對(duì)象進(jìn)行序列化,把得到的字節(jié)序列寫到一個(gè)目標(biāo)輸出流中。
java.io.ObjectInputStream:表示對(duì)象輸入流它的readObject()方法從源輸入流中讀取字節(jié)序列,再把它們反序列化成為一個(gè)對(duì)象,并將其返回。
2. 實(shí)現(xiàn)序列化的要求
只有實(shí)現(xiàn)了Serializable或Externalizable接口的類的對(duì)象才能被序列化,否則拋出異常。
3. 實(shí)現(xiàn)Java對(duì)象序列化與反序列化的方法
假定一個(gè)Student類,它的對(duì)象需要序列化,可以有如下三種方法:
方法一:
若Student類僅僅實(shí)現(xiàn)了Serializable接口,則可以按照以下方式進(jìn)行序列化和反序列化。 ObjectOutputStream
采用默認(rèn)的序列化方式,對(duì)Student對(duì)象的非transient的實(shí)例變量進(jìn)行序列化。 ObjcetInputStream
采用默認(rèn)的反序列化方式,對(duì)對(duì)Student對(duì)象的非transient的實(shí)例變量進(jìn)行反序列化。
方法二:
若Student類僅僅實(shí)現(xiàn)了Serializable接口,并且還定義了readObject(ObjectInputStream in)
和writeObject(ObjectOutputSteam out)
,則采用以下方式進(jìn)行序列化與反序列化。 ObjectOutputStream
調(diào)用Student對(duì)象的writeObject(ObjectOutputStream out)
的方法進(jìn)行序列化。 ObjectInputStream
會(huì)調(diào)用Student對(duì)象的readObject(ObjectInputStream in)
的方法進(jìn)行反序列化。
方法三:
若Student類實(shí)現(xiàn)了Externalnalizable
接口,且Student類必須實(shí)現(xiàn)readExternal(ObjectInput in)
和writeExternal(ObjectOutput out)
方法,則按照以下方式進(jìn)行序列化與反序列化。 ObjectOutputStream
調(diào)用Student對(duì)象的writeExternal(ObjectOutput out))
的方法進(jìn)行序列化。 ObjectInputStream
會(huì)調(diào)用Student對(duì)象的readExternal(ObjectInput in)
的方法進(jìn)行反序列化。
4. JDK類庫(kù)中序列化的步驟
步驟一:創(chuàng)建一個(gè)對(duì)象輸出流,它可以包裝一個(gè)其它類型的目標(biāo)輸出流,如文件輸出流:
ObjectOutputStream out = new ObjectOutputStream(new fileOutputStream(“D:\\objectfile.obj”));
步驟二:通過對(duì)象輸出流的writeObject()方法寫對(duì)象:
out.writeObject(“Hello”); out.writeObject(new Date());
5. JDK類庫(kù)中反序列化的步驟
步驟一:創(chuàng)建一個(gè)對(duì)象輸入流,它可以包裝一個(gè)其它類型輸入流,如文件輸入流:
ObjectInputStream in = new ObjectInputStream(new fileInputStream(“D:\\objectfile.obj”));
步驟二:通過對(duì)象輸出流的readObject()方法讀取對(duì)象:
String obj1 = (String)in.readObject(); Date obj2 = (Date)in.readObject();
說明:為了正確讀取數(shù)據(jù),完成反序列化,必須保證向?qū)ο筝敵隽鲗憣?duì)象的順序與從對(duì)象輸入流中讀對(duì)象的順序一致。為了更好地理解Java序列化與反序列化,選擇方法一編碼實(shí)現(xiàn)。
Student類定義如下:
/** * 實(shí)現(xiàn)了序列化接口的學(xué)生類 */ public class Student implements Serializable { private String name; private char sex; private int year; private double gpa; public Student() { } public Student(String name,char sex,int year,double gpa) { this.name = name; this.sex = sex; this.year = year; this.gpa = gpa; } public void setName(String name) { this.name = name; } public void setSex(char sex) { this.sex = sex; } public void setYear(int year) { this.year = year; } public void setGpa(double gpa) { this.gpa = gpa; } public String getName() { return this.name; } public char getSex() { return this.sex; } public int getYear() { return this.year; } public double getGpa() { return this.gpa; } }
把Student類的對(duì)象序列化到文件/Users/sschen/Documents/student.txt
,并從該文件中反序列化,向console顯示結(jié)果。代碼如下:
public class UserStudent { public static void main(String[] args) { Student st = new Student("Tom",'M',20,3.6); File file = new File("/Users/sschen/Documents/student.txt"); try { file.createNewFile(); } catch(IOException e) { e.printStackTrace(); } try { //Student對(duì)象序列化過程 FileOutputStream fos = new FileOutputStream(file); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(st); oos.flush(); oos.close(); fos.close(); //Student對(duì)象反序列化過程 FileInputStream fis = new FileInputStream(file); ObjectInputStream ois = new ObjectInputStream(fis); Student st1 = (Student) ois.readObject(); System.out.println("name = " + st1.getName()); System.out.println("sex = " + st1.getSex()); System.out.println("year = " + st1.getYear()); System.out.println("gpa = " + st1.getGpa()); ois.close(); fis.close(); } catch(ClassNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }
而查看文件/Users/sschen/Documents/student.txt
,其內(nèi)保存的內(nèi)容并不是可以容易閱讀的內(nèi)容:
aced 0005 7372 001f 636f 6d2e 7373 6368 656e 2e53 6572 6961 6c69 7a61 626c 652e 5374 7564 656e 74f1 5dbd a4a0 3472 4d02 0004 4400 0367 7061 4300 0373 6578 4900 0479 6561 724c 0004 6e61 6d65 7400 124c 6a61 7661 2f6c 616e 672f 5374 7269 6e67 3b78 7040 0ccc cccc cccc cd00 4d00 0000 1474 0003 546f 6d
四、序列化的必要條件
1、必須是同包,同名。
2、serialVersionUID必須一致。有時(shí)候兩個(gè)類的屬性稍微不一致的時(shí)候,可以通過將此屬性寫死值,實(shí)現(xiàn)序列化和反序列化。
五、序列化高級(jí),使用情境分析
1. 序列化ID問題
情境:兩個(gè)客戶端 A 和 B 試圖通過網(wǎng)絡(luò)傳遞對(duì)象數(shù)據(jù),A 端將對(duì)象 C 序列化為二進(jìn)制數(shù)據(jù)再傳給 B,B 反序列化得到 C。
問題:C 對(duì)象的全類路徑假設(shè)為 com.inout.Test,在 A 和 B 端都有這么一個(gè)類文件,功能代碼完全一致。也都實(shí)現(xiàn)了 Serializable 接口,但是反序列化時(shí)總是提示不成功。
解決:虛擬機(jī)是否允許反序列化,不僅取決于類路徑和功能代碼是否一致,一個(gè)非常重要的一點(diǎn)是兩個(gè)類的序列化 ID 是否一致(就是 private static final long serialVersionUID = 1L)。下面的代碼中,雖然兩個(gè)類的功能代碼完全一致,但是序列化 ID 不同,他們無法相互序列化和反序列化。
簡(jiǎn)單來說,Java的序列化機(jī)制是通過在運(yùn)行時(shí)判斷類的serialVersionUID來驗(yàn)證版本一致性的。在進(jìn)行反序列化時(shí),JVM會(huì)把傳來的字節(jié)流中的serialVersionUID與本地相應(yīng)實(shí)體(類)的serialVersionUID進(jìn)行比較,如果相同就認(rèn)為是一致的,可以進(jìn)行反序列化,否則就會(huì)出現(xiàn)序列化版本不一致的異常。
當(dāng)實(shí)現(xiàn)java.io.Serializable接口的實(shí)體(類)沒有顯式地定義一個(gè)名為serialVersionUID,類型為long的變量時(shí),Java序列化機(jī)制會(huì)根據(jù)編譯的class自動(dòng)生成一個(gè)serialVersionUID作序列化版本比較用,這種情況下,只有同一次編譯生成的class才會(huì)生成相同的serialVersionUID 。
如果我們不希望通過編譯來強(qiáng)制劃分軟件版本,即實(shí)現(xiàn)序列化接口的實(shí)體能夠兼容先前版本,未作更改的類,就需要顯式地定義一個(gè)名為serialVersionUID,類型為long的變量,不修改這個(gè)變量值的序列化實(shí)體都可以相互進(jìn)行串行化和反串行化。相同功能代碼不同序列化 ID 的類對(duì)比,代碼如下:
public class SerialVersionIDA implements Serializable { private static final long serialVersionUID=1L; private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } public SerialVersionIDA() { } public SerialVersionIDA(String name) { this.name = name; } } public class SerialVersionIDA implements Serializable { private static final long serialVersionUID=2L; private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } public SerialVersionIDA() { } public SerialVersionIDA(String name) { this.name = name; } }
使用serialVersionUID為1L的類進(jìn)行序列化,而使用serialVersionUID為2L的類進(jìn)行反序列化,會(huì)提示異常,異常內(nèi)容為:
java.io.InvalidClassException: com.sschen.Serializable.SerialVersionIDA; local class incompatible: stream classdesc serialVersionUID = 2, local class serialVersionUID = 1 at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:616) at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1630) at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1521) at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1781) at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1353) at java.io.ObjectInputStream.readObject(ObjectInputStream.java:373) at com.sschen.Serializable.SerialVersionTest.main(SerialVersionTest.java:30) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
序列化 ID 在 Eclipse 下提供了兩種生成策略,一個(gè)是固定的 1L,一個(gè)是隨機(jī)生成一個(gè)不重復(fù)的 long 類型數(shù)據(jù)(實(shí)際上是使用 JDK 工具生成),在這里有一個(gè)建議,如果沒有特殊需求,就是用默認(rèn)的 1L 就可以,這樣可以確保代碼一致時(shí)反序列化成功。那么隨機(jī)生成的序列化 ID 有什么作用呢,有些時(shí)候,通過改變序列化 ID 可以用來限制某些用戶的使用。
特性使用案例
讀者應(yīng)該聽過 Façade 模式,它是為應(yīng)用程序提供統(tǒng)一的訪問接口,案例程序中的 Client 客戶端使用了該模式,案例程序結(jié)構(gòu)圖下圖所示。
Client 端通過 Façade Object 才可以與業(yè)務(wù)邏輯對(duì)象進(jìn)行交互。而客戶端的 Façade Object 不能直接由 Client 生成,而是需要 Server 端生成,然后序列化后通過網(wǎng)絡(luò)將二進(jìn)制對(duì)象數(shù)據(jù)傳給 Client,Client 負(fù)責(zé)反序列化得到 Façade 對(duì)象。該模式可以使得 Client 端程序的使用需要服務(wù)器端的許可,同時(shí) Client 端和服務(wù)器端的 Façade Object 類需要保持一致。當(dāng)服務(wù)器端想要進(jìn)行版本更新時(shí),只要將服務(wù)器端的 Façade Object 類的序列化 ID 再次生成,當(dāng) Client 端反序列化 Façade Object 就會(huì)失敗,也就是強(qiáng)制 Client 端從服務(wù)器端獲取最新程序。
2. 靜態(tài)變量序列化
public class SerialStaticTest implements Serializable { private static final long serialVersionUID = 1L; public static int staticVar = 5; public static void main(String[] args) { try { File file = new File("/Users/sschen/Documents/student.txt"); try { file.createNewFile(); } catch(IOException e) { e.printStackTrace(); } //初始時(shí)staticVar為5 ObjectOutputStream out = new ObjectOutputStream( new FileOutputStream(file)); out.writeObject(new SerialStaticTest()); out.close(); //序列化后修改為10 SerialStaticTest.staticVar = 10; ObjectInputStream oin = new ObjectInputStream(new FileInputStream(file)); SerialStaticTest t = (SerialStaticTest) oin.readObject(); oin.close(); //再讀取,通過t.staticVar打印新的值 System.out.println(t.staticVar); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }
上面代碼中的 main 方法,將對(duì)象序列化保存到文件后,修改靜態(tài)變量的數(shù)值,再將序列化對(duì)象讀取出來,然后通過讀取出來的對(duì)象獲得靜態(tài)變量的數(shù)值并打印出來。依照代碼,這個(gè) System.out.println(t.staticVar) 語(yǔ)句輸出的是 10 還是 5 呢?最后的輸出是 10,對(duì)于無法理解的讀者認(rèn)為,打印的 staticVar 是從讀取的對(duì)象里獲得的,應(yīng)該是保存時(shí)的狀態(tài)才對(duì)。之所以打印 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。
特性使用案例
我們熟悉使用 Transient 關(guān)鍵字可以使得字段不被序列化,那么還有別的方法嗎?根據(jù)父類對(duì)象序列化的規(guī)則,我們可以將不需要被序列化的字段抽取出來放到父類中,子類實(shí)現(xiàn) Serializable 接口,父類不實(shí)現(xiàn),根據(jù)父類序列化規(guī)則,父類的字段數(shù)據(jù)將不被序列化,形成類圖如圖 2 所示。
上圖中可以看出,attr1、attr2、attr3、attr5 都不會(huì)被序列化,放在父類中的好處在于當(dāng)有另外一個(gè) Child 類時(shí),attr1、attr2、attr3 依然不會(huì)被序列化,不用重復(fù)抒寫 transient,代碼簡(jiǎn)潔。
4. 對(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)行用戶自定義的序列化和反序列化,如果沒有這樣的方法,則默認(rèn)調(diào)用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。用戶自定義的 writeObject 和 readObject 方法可以允許用戶控制序列化的過程,比如可以在序列化的過程中動(dòng)態(tài)改變序列化的數(shù)值?;谶@個(gè)原理,可以在實(shí)際應(yīng)用中得到使用,用于敏感字段的加密工作,下面的代碼展示了這個(gè)過程。
public class SerialPwdTest implements Serializable { private static final long serialVersionUID = 1L; private String password = "pass"; public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } private void writeObject(ObjectOutputStream out) { try { ObjectOutputStream.PutField putFields = out.putFields(); System.out.println("原密碼:" + password); password = "encryption";//模擬加密 putFields.put("password", password); System.out.println("加密后的密碼" + password); out.writeFields(); } catch (IOException e) { e.printStackTrace(); } } private void readObject(ObjectInputStream in) { try { ObjectInputStream.GetField readFields = in.readFields(); Object object = readFields.get("password", ""); System.out.println("要解密的字符串:" + object.toString()); password = "pass";//模擬解密,需要獲得本地的密鑰 } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static void main(String[] args) { File file = new File("/Users/sschen/Documents/student.txt"); try { file.createNewFile(); } catch(IOException e) { e.printStackTrace(); } try { ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file)); out.writeObject(new SerialPwdTest()); out.close(); ObjectInputStream oin = new ObjectInputStream(new FileInputStream(file)); SerialPwdTest t = (SerialPwdTest) oin.readObject(); System.out.println("解密后的字符串:" + t.getPassword()); oin.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }
SerialPwdTest的 writeObject 方法中,對(duì)密碼進(jìn)行了加密,在加密后進(jìn)行序列化保存到文件中,在 readObject 中則在讀取到密碼后,對(duì) password 進(jìn)行解密,只有擁有密鑰的客戶端,才可以正確的解析出密碼,確保了數(shù)據(jù)的安全。上面代碼的執(zhí)行結(jié)果為:
原密碼:pass 加密后的密碼encryption 要解密的字符串:encryption 解密后的字符串:pass
特性使用案例
RMI 技術(shù)是完全基于 Java 序列化技術(shù)的,服務(wù)器端接口調(diào)用所需要的參數(shù)對(duì)象來至于客戶端,它們通過網(wǎng)絡(luò)相互傳輸。這就涉及 RMI 的安全傳輸?shù)膯栴}。一些敏感的字段,如用戶名密碼(用戶登錄時(shí)需要對(duì)密碼進(jìn)行傳輸),我們希望對(duì)其進(jìn)行加密,這時(shí),就可以采用本節(jié)介紹的方法在客戶端對(duì)密碼進(jìn)行加密,服務(wù)器端進(jìn)行解密,確保數(shù)據(jù)傳輸?shù)陌踩浴?/p>
5. 序列化存儲(chǔ)規(guī)則
情境:?jiǎn)栴}代碼如清單 4 所示。
清單 4. 存儲(chǔ)規(guī)則問題代碼
public class SerialSaveTest implements Serializable { public static void main(String[] args) { File file = new File("/Users/sschen/Documents/student.txt"); try { file.createNewFile(); } catch(IOException e) { e.printStackTrace(); } try { ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file)); SerialSaveTest test = new SerialSaveTest(); //試圖將對(duì)象兩次寫入文件 out.writeObject(test); out.flush(); System.out.println(file.length()); out.writeObject(test); out.close(); System.out.println(file.length()); ObjectInputStream oin = new ObjectInputStream(new FileInputStream(file)); //從文件依次讀出兩個(gè)文件 SerialSaveTest t1 = (SerialSaveTest) oin.readObject(); SerialSaveTest t2 = (SerialSaveTest) oin.readObject(); oin.close(); //判斷兩個(gè)引用是否指向同一個(gè)對(duì)象 System.out.println(t1 == t2); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }
清單4中對(duì)同一對(duì)象兩次寫入文件,打印出寫入一次對(duì)象后的存儲(chǔ)大小和寫入兩次后的存儲(chǔ)大小,然后從文件中反序列化出兩個(gè)對(duì)象,比較這兩個(gè)對(duì)象是否為同一對(duì)象。一般的思維是,兩次寫入對(duì)象,文件大小會(huì)變?yōu)閮杀兜拇笮?,反序列化時(shí),由于從文件讀取,生成了兩個(gè)對(duì)象,判斷相等時(shí)應(yīng)該是輸入 false 才對(duì),但是最后結(jié)果輸出如下:
59 64 true
我們看到,第二次寫入對(duì)象時(shí)文件只增加了 5 字節(jié),并且兩個(gè)對(duì)象是相等的,這是為什么呢?
解答:Java 序列化機(jī)制為了節(jié)省磁盤空間,具有特定的存儲(chǔ)規(guī)則,當(dāng)寫入文件的為同一對(duì)象時(shí),并不會(huì)再將對(duì)象的內(nèi)容進(jìn)行存儲(chǔ),而只是再次存儲(chǔ)一份引用,上面增加的 5 字節(jié)的存儲(chǔ)空間就是新增引用和一些控制信息的空間。反序列化時(shí),恢復(fù)引用關(guān)系,使得清單 3 中的 t1 和 t2 指向唯一的對(duì)象,二者相等,輸出 true。該存儲(chǔ)規(guī)則極大的節(jié)省了存儲(chǔ)空間。
特性案例分析
查看清單 5 的代碼。
清單5. 案例代碼
public class SerialSaveTest implements Serializable { private int id; public int getId() { return id; } public void setId(int id) { this.id = id; } public static void main(String[] args) { File file = new File("/Users/sschen/Documents/student.txt"); try { file.createNewFile(); } catch(IOException e) { e.printStackTrace(); } try { ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file)); SerialSaveTest test = new SerialSaveTest(); test.setId(1); //試圖將對(duì)象兩次寫入文件 out.writeObject(test); out.flush(); System.out.println(file.length()); test.setId(5); out.writeObject(test); out.close(); System.out.println(file.length()); ObjectInputStream oin = new ObjectInputStream(new FileInputStream(file)); //從文件依次讀出兩個(gè)文件 SerialSaveTest t1 = (SerialSaveTest) oin.readObject(); SerialSaveTest t2 = (SerialSaveTest) oin.readObject(); oin.close(); //判斷兩個(gè)引用是否指向同一個(gè)對(duì)象 System.out.println(t1 == t2); System.out.println(t1.getId()); System.out.println(t2.getId()); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }
清單 4 的目的是希望將 test 對(duì)象兩次保存到/Users/sschen/Documents/student.txt
文件中,寫入一次以后修改對(duì)象屬性值再次保存第二次,然后從/Users/sschen/Documents/student.txt
中再依次讀出兩個(gè)對(duì)象,輸出這兩個(gè)對(duì)象的 i 屬性值。案例代碼的目的原本是希望一次性傳輸對(duì)象修改前后的狀態(tài)。
結(jié)果兩個(gè)輸出的都是 1, 原因就是第一次寫入對(duì)象以后,第二次再試圖寫的時(shí)候,虛擬機(jī)根據(jù)引用關(guān)系知道已經(jīng)有一個(gè)相同對(duì)象已經(jīng)寫入文件,因此只保存第二次寫的引用,所以讀取時(shí),都是第一次保存的對(duì)象。讀者在使用一個(gè)文件多次 writeObject 需要特別注意這個(gè)問題。
總結(jié)
本篇文章就到這里了,希望能給你帶來幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
SpringBoot實(shí)現(xiàn)滑塊驗(yàn)證碼驗(yàn)證登陸校驗(yàn)功能詳解
驗(yàn)證碼作為一種自然人的機(jī)器人的判別工具,被廣泛的用于各種防止程序做自動(dòng)化的場(chǎng)景中。傳統(tǒng)的字符型驗(yàn)證安全性已經(jīng)名存實(shí)亡的情況下,各種新型的驗(yàn)證碼如雨后春筍般涌現(xiàn),今天給大家分享一篇SpringBoot實(shí)現(xiàn)滑塊驗(yàn)證碼2022-09-09JPA如何使用nativequery多表關(guān)聯(lián)查詢返回自定義實(shí)體類
這篇文章主要介紹了JPA如何使用nativequery多表關(guān)聯(lián)查詢返回自定義實(shí)體類,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11java自動(dòng)生成編號(hào)的實(shí)現(xiàn)(格式:yyMM+四位流水號(hào))
這篇文章主要介紹了java自動(dòng)生成編號(hào)的實(shí)現(xiàn)(格式:yyMM+四位流水號(hào)),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-10-10SpringBoot Actuator埋點(diǎn)和監(jiān)控及簡(jiǎn)單使用
最近做的項(xiàng)目涉及到埋點(diǎn)監(jiān)控、報(bào)表、日志分析的相關(guān)知識(shí),于是搗鼓的一番,下面把涉及的知識(shí)點(diǎn)及SpringBoot Actuator埋點(diǎn)和監(jiān)控的簡(jiǎn)單用法,給大家分享下,感興趣的朋友一起看看吧2021-11-11IDEA修改生成jar包名字的兩種方法實(shí)現(xiàn)
本文主要介紹了IDEA修改生成jar包名字的兩種方法實(shí)現(xiàn),通過簡(jiǎn)單的步驟,您可以修改項(xiàng)目名稱并在打包時(shí)使用新的名稱,具有一定的參考價(jià)值,感興趣的可以了解下2023-08-08JAVA反射機(jī)制中g(shù)etClass和class對(duì)比分析
這篇文章主要介紹了JAVA反射機(jī)制中g(shù)etClass和class對(duì)比分析,具有一定參考價(jià)值,需要的朋友可以了解下。2017-11-11springboot~nexus項(xiàng)目打包要注意的地方示例代碼詳解
這篇文章主要介紹了springboot~nexus項(xiàng)目打包要注意的地方,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-07-07