詳解Java 對象序列化和反序列化
之前的文章中我們介紹過有關(guān)字節(jié)流字符流的使用,當(dāng)時(shí)我們對于將一個(gè)對象輸出到流中的操作,使用DataOutputStream流將該對象中的每個(gè)屬性值逐個(gè)輸出到流中,讀出時(shí)相反。在我們看來這種行為實(shí)在是繁瑣,尤其是在這個(gè)對象中屬性值很多的時(shí)候。基于此,Java中對象的序列化機(jī)制就可以很好的解決這種操作。本篇就簡單的介紹Java對象序列化,主要內(nèi)容如下:
- 簡潔的代碼實(shí)現(xiàn)
- 序列化實(shí)現(xiàn)的基本算法
- 兩種特殊的情況
- 自定義序列化機(jī)制
- 序列化的版本控制
一、簡潔的代碼實(shí)現(xiàn)
在介紹對象序列化的使用方法之前,先看看我們之前是怎么存儲(chǔ)一個(gè)對象類型的數(shù)據(jù)的。
//簡單定義一個(gè)Student類 public class Student { private String name; private int age; public Student(){} public Student(String name,int age){ this.name = name; this.age=age; } public void setName(String name){ this.name = name; } public void setAge(int age){ this.age = age; } public String getName(){ return this.name; } public int getAge(){ return this.age; } //重寫toString @Override public String toString(){ return ("my name is:"+this.name+" age is:"+this.age); } }
//main方法實(shí)現(xiàn)了將對象寫入文件并讀取出來 public static void main(String[] args) throws IOException{ DataOutputStream dot = new DataOutputStream(new FileOutputStream("hello.txt")); Student stuW = new Student("walker",21); //將此對象寫入到文件中 dot.writeUTF(stuW.getName()); dot.writeInt(stuW.getAge()); dot.close(); //將對象從文件中讀出 DataInputStream din = new DataInputStream(new FileInputStream("hello.txt")); Student stuR = new Student(); stuR.setName(din.readUTF()); stuR.setAge(din.readInt()); din.close(); System.out.println(stuR); }
輸出結(jié)果:my name is:walker age is:21
顯然這種代碼書寫是繁瑣的,接下來我們看看,如何使用序列化來完成保存對象的信息。
public static void main(String[] args) throws IOException, ClassNotFoundException { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("hello.txt")); Student stuW = new Student("walker",21); oos.writeObject(stuW); oos.close(); //從文件中讀取該對象返回 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("hello.txt")); Student stuR = (Student)ois.readObject(); System.out.println(stuR); }
寫入文件時(shí),只用了一條語句就是writeObject,讀取時(shí)也是只用了一條語句readObject。并且Student中的那些set,get方法都用不到了。是不是很簡潔呢?接下來介紹實(shí)現(xiàn)細(xì)節(jié)。
二、實(shí)現(xiàn)序列化的基本算法
在這種機(jī)制中,每個(gè)對象都是對應(yīng)著唯一的一個(gè)序列號,而每個(gè)對象在被保存的時(shí)候也是根據(jù)這個(gè)序列號來對應(yīng)著每個(gè)不同的對象,對象序列化就是指利用了每個(gè)對象的序列號進(jìn)行保存和讀取的。首先以寫對象到流中為例,對于每個(gè)對象,第一次遇到的時(shí)候會(huì)將這個(gè)對象的基本信息保存到流中,如果當(dāng)前遇到的對象已經(jīng)被保存過了,就不會(huì)再次保存這些信息,轉(zhuǎn)而記錄此對象的序列號(因?yàn)閿?shù)據(jù)沒必要重復(fù)保存)。對于讀的情況,從流中遇到的每個(gè)對象,如果第一次遇到,直接輸出,如果讀取到的是某個(gè)對象的序列號,就會(huì)找到相關(guān)聯(lián)的對象,輸出。
說明幾點(diǎn),一個(gè)對象要想是可序列化的,就必須實(shí)現(xiàn)接口 java.io.Serializable;,這是一個(gè)標(biāo)記接口,不用實(shí)現(xiàn)任何的方法。而我們的ObjectOutputStream流,就是一個(gè)可以將對象信息轉(zhuǎn)為字節(jié)的流,構(gòu)造函數(shù)如下:
public ObjectOutputStream(OutputStream out)
也就是所有字節(jié)流都可以作為參數(shù)傳入,兼容一切字節(jié)操作。在這個(gè)流中定義了writeObject和readObject方法,實(shí)現(xiàn)了序列化對象和反序列化對象。當(dāng)然,我們也是可以通過在類中實(shí)現(xiàn)這兩個(gè)方法來自定義序列化機(jī)制,具體的后文介紹。此處我們只需要了解整個(gè)序列化機(jī)制,所有的對象數(shù)據(jù)只會(huì)保存一份,至于相同的對象再次出現(xiàn),只保存對應(yīng)的序列號。下面,通過兩個(gè)特殊的情況直觀的感受下他的這個(gè)基本算法。
三、兩個(gè)特殊的實(shí)例
先看第一個(gè)實(shí)例:
public class Student implements Serializable { String name; int age; Teacher t; //另外一個(gè)對象類型 public Student(){} public Student(String name,int age,Teacher t){ this.name = name; this.age=age; this.t = t; } public void setName(String name){this.name = name;} public void setAge(int age){this.age = age;} public void setT(Teacher t){this.t = t;} public String getName(){return this.name;} public int getAge(){return this.age;} public Teacher getT(){return this.t;} } public class Teacher implements Serializable { String name; public Teacher(String name){ this.name = name; } } public static void main(String[] args) throws IOException, ClassNotFoundException { Teacher t = new Teacher("li"); Student stu1 = new Student("walker",21,t); Student stu2 = new Student("yam",22,t); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("hello.txt")); oos.writeObject(stu1); oos.writeObject(stu2); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("hello.txt")); Student stuR1 = (Student)ois.readObject(); Student stuR2 = (Student)ois.readObject(); if (stuR1.getT() == stuR2.getT()) System.out.println("相同對象"); }
結(jié)果是很顯而易見的,輸出了相同對象。我們在main函數(shù)中定義了兩個(gè)student類型對象,他們卻都引用的同一個(gè)teacher對象在內(nèi)部。完成序列化之后,反序列化出來兩個(gè)對象,通過比較他們內(nèi)部的teacher對象是否是同一個(gè)實(shí)例,可以看出來,在序列化第一個(gè)student對象的時(shí)候t是被寫入流中的,但是在遇到第二個(gè)student對象的teacher對象實(shí)例時(shí),發(fā)現(xiàn)前面已經(jīng)寫過了,于是不再寫入流中,只保存對應(yīng)的序列號作為引用。當(dāng)然在反序列化的時(shí)候,原理類似。這和我們上面介紹的基本算法是一樣的。
下面看第二個(gè)特殊實(shí)例:
public class Student implements Serializable { String name; Teacher t; } public class Teacher implements Serializable { String name; Student stu; } public static void main(String[] args) throws IOException, ClassNotFoundException { Teacher t = new Teacher(); Student s =new Student(); t.name = "walker"; t.stu = s; s.name = "yam"; s.t = t; ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("hello.txt")); oos.writeObject(t); oos.writeObject(s); oos.close(); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("hello.txt")); Teacher tR = (Teacher)ois.readObject(); Student sR = (Student)ois.readObject(); if(tR == sR.t && sR == tR.stu)System.out.println("ok"); }
輸出的結(jié)果是ok,這個(gè)例子可以叫做:循環(huán)引用。從結(jié)果我們可以看出來,序列化之前兩個(gè)對象存在的相互的引用關(guān)系,經(jīng)過序列化之后,兩者之間的這種引用關(guān)系是依然存在的。其實(shí)按照我們之前介紹的判斷算法來看,首先我們先序列化了teacher對象,因?yàn)樗麅?nèi)部引用了student的對象,兩者都是第一次遇到,所以將兩者序列化到流中,然后我們?nèi)バ蛄谢痵tudent對象,發(fā)現(xiàn)這個(gè)對象以及內(nèi)部的teacher對象都已經(jīng)被序列化了,于是只保存對應(yīng)的序列號。讀取的時(shí)候根據(jù)序列號恢復(fù)對象。
四、自定義序列化機(jī)制
綜上,我們已經(jīng)介紹完了基本的序列化與反序列化的知識(shí)。但是往往我們會(huì)有一些特殊的要求,這種默認(rèn)的序列化機(jī)制雖然已經(jīng)很完善了,但是有些時(shí)候還是不能滿足我們的需求。所以我們看看如何自定義序列化機(jī)制。自定義序列化機(jī)制中,我們會(huì)使用到一個(gè)關(guān)鍵字,它也是我們之前在看源碼的時(shí)候經(jīng)常遇到的,transient。將字段聲明transient,等于是告訴默認(rèn)的序列化機(jī)制,這個(gè)字段你不要給我寫到流中去,我會(huì)自己處理的。
public class Student implements Serializable { String name; transient int age; public String toString(){ return this.name + ":" + this.age; } } public static void main(String[] args) throws IOException, ClassNotFoundException { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("hello.txt")); Student stu = new Student(); stu.name = "walker";stu.age = 21; oos.writeObject(stu); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("hello.txt")); Student stuR = (Student)ois.readObject(); System.out.println(stuR); }
輸出結(jié)果:walker:0
我們不是給age字段賦初始值了么,怎么會(huì)是0呢?正如我們上文所說的一樣,被transient修飾的字段不會(huì)被寫入流中,自然讀取出來就沒有值,默認(rèn)是0。下面看看我們怎么自己來序列化這個(gè)age。
//改動(dòng)過的student類,main方法沒有改動(dòng),大家可以往上看 public class Student implements Serializable { String name; transient int age; private void writeObject(ObjectOutputStream oos) throws IOException { oos.defaultWriteObject(); oos.writeInt(25); } private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { ois.defaultReadObject(); age = ois.readInt(); } public String toString(){ return this.name + ":" + this.age; } }
輸出結(jié)果:walker:25
結(jié)果既不是我么初始化的21,也不是0,而是我們在writeObject方法中寫的25?,F(xiàn)在我們一點(diǎn)一點(diǎn)看看每個(gè)步驟的意義。首先,要想要實(shí)現(xiàn)自定義序列化,就需要在該對象定義的類中實(shí)現(xiàn)兩個(gè)方法,writeObject和readObject,而且格式必須和上面貼出來的一樣,筆者試過改動(dòng)方法修飾符,結(jié)果導(dǎo)致不能成功序列化。這是因?yàn)?,Java采用反射機(jī)制,檢查該對象所在的類中有沒有實(shí)現(xiàn)這兩個(gè)方法,沒有的話就使用默認(rèn)的ObjectOutputStream中的這個(gè)方法序列化所有字段,如果有的話就執(zhí)行你自己實(shí)現(xiàn)的這個(gè)方法。
接下來,看看這兩個(gè)方法實(shí)現(xiàn)的細(xì)節(jié),先看writeObject方法,參數(shù)是ObjectOutputStream 類型的,這個(gè)拿到的是我們在main方法中定義的ObjectOutputStream 對象,要不然它怎么知道該把對象寫到那個(gè)地方去呢?第一行我們調(diào)用的是oos.defaultWriteObject();這個(gè)方法實(shí)現(xiàn)的功能是,將當(dāng)前對象中所有沒有被transient修飾的字段寫入流中,第二條語句我們顯式的調(diào)用了writeInt方法將age的值寫入流中。讀取的方法類似,此處不再贅述。
五、版本控制
最后我們來看看,序列化過程的的版本控制問題。在我們將一個(gè)對象序列化到流中之后,該對象對應(yīng)的類的結(jié)構(gòu)改變了,如果此時(shí)我們再次從流中將之前保存的對象讀取出來,會(huì)發(fā)生什么?這要分情況來說,如果原類中的字段被刪除了,那從流中輸出的對應(yīng)的字段將會(huì)被忽略。如果原類中增加了某個(gè)字段,那新增的字段的值就是默認(rèn)值。如果字段的類型發(fā)生了改變,拋出異常。在Java中每個(gè)類都會(huì)有一個(gè)記錄版本號的變量:static final serivalVersionUID = 115616165165L,此處的值只用于演示并不對應(yīng)任意某個(gè)類。這個(gè)版本號是根據(jù)該類中的字段等一些屬性信息計(jì)算出來的,唯一性較高。每次讀出的時(shí)候都會(huì)去比較之前和現(xiàn)在的版本號確認(rèn)是否發(fā)生版本不一致情況,如果版本不一致,就會(huì)按照上述的情形分別做處理。
對象的序列化就寫完了,如果有什么內(nèi)容不妥的地方,希望大家指出!
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Java?報(bào)錯(cuò)?java.util.ConcurrentModificationException:?null?
這篇文章主要介紹了Java?報(bào)錯(cuò)?java.util.ConcurrentModificationException:?null?的原因和解決方案,這個(gè)異常通常在多線程環(huán)境下出現(xiàn),意味著在迭代過程中,集合或者映射的結(jié)構(gòu)發(fā)生了變化,本文分享完美解決方案,需要的朋友可以參考下2023-07-07java中多個(gè)@Scheduled定時(shí)器不執(zhí)行的解決方法
在應(yīng)用開發(fā)中經(jīng)常需要一些周期性的操作,比如每5分鐘執(zhí)行某一操作等,這篇文章主要給大家介紹了關(guān)于java中多個(gè)@Scheduled定時(shí)器不執(zhí)行的解決方法,需要的朋友可以參考下2023-04-04mybatis插件pageHelper實(shí)現(xiàn)分頁效果
這篇文章主要為大家詳細(xì)介紹了mybatis插件pageHelper實(shí)現(xiàn)分頁效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-12-12解決Jenkins集成SonarQube遇到的報(bào)錯(cuò)問題
本文給大家分享Jenkins集成SonarQube遇到的報(bào)錯(cuò)問題及解決方法,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2021-07-07Java實(shí)現(xiàn)視頻初步壓縮和解壓的代碼示例
從攝像頭讀取每一幀的圖片,用一些簡單的方法將多張圖片信息壓縮到一份文件中(自定義的視頻文件),自定義解碼器讀取視頻文件,并將每幀圖片展示成視頻,本文主要介紹了Java實(shí)現(xiàn)視頻初步壓縮和解壓,需要的朋友可以參考下2023-10-10使用JSONObject.toJSONString 過濾掉值為空的key
這篇文章主要介紹了使用JSONObject.toJSONString 過濾掉值為空的key,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03SpringBoot簡單使用SpringData的jdbc和durid
今天給大家?guī)淼氖顷P(guān)于Java的相關(guān)知識(shí),文章圍繞著SpringBoot簡單使用SpringData的jdbc和durid,文中有非常詳細(xì)的介紹及代碼示例,需要的朋友可以參考下2021-06-06