Java编程的逻辑 (62) - 神奇的序列化
本系列文章經(jīng)補(bǔ)充和完善,已修訂整理成書(shū)《Java編程的邏輯》,由機(jī)械工業(yè)出版社華章分社出版,于2018年1月上市熱銷(xiāo),讀者好評(píng)如潮!各大網(wǎng)店和書(shū)店有售,歡迎購(gòu)買(mǎi),京東自營(yíng)鏈接:http://item.jd.com/12299018.html
在前面幾節(jié),我們?cè)趯?duì)象保存到文件時(shí),使用的是DataOutputStream,從文件讀入對(duì)象時(shí),使用的是DataInputStream, 使用它們,需要逐個(gè)處理對(duì)象中的每個(gè)字段,我們提到,這種方式比較啰嗦,Java中有一種更為簡(jiǎn)單的機(jī)制,那就是序列化。
簡(jiǎn)單來(lái)說(shuō),序列化就是將對(duì)象轉(zhuǎn)化為字節(jié)流,反序列化就是將字節(jié)流轉(zhuǎn)化為對(duì)象。在Java中,具體如何來(lái)使用呢?它是如何實(shí)現(xiàn)的?有什么優(yōu)缺點(diǎn)?本節(jié)就來(lái)探討這些問(wèn)題,我們先從它的基本用法談起。
基本用法
Serializable
要讓一個(gè)類(lèi)支持序列化,只需要讓這個(gè)類(lèi)實(shí)現(xiàn)接口java.io.Serializable,Serializable沒(méi)有定義任何方法,只是一個(gè)標(biāo)記接口。比如,對(duì)于57節(jié)提到的Student類(lèi),為支持序列化,可改為:
public class Student implements Serializable {String name;int age;double score;public Student(String name, int age, double score) {...}... }聲明實(shí)現(xiàn)了Serializable接口后,保存/讀取Student對(duì)象就可以使用另兩個(gè)流了ObjectOutputStream/ObjectInputStream。
ObjectOutputStream/ObjectInputStream
ObjectOutputStream是OutputStream的子類(lèi),但實(shí)現(xiàn)了ObjectOutput接口,ObjectOutput是DataOutput的子接口,增加了一個(gè)方法:
public void writeObject(Object obj) throws IOException這個(gè)方法能夠?qū)?duì)象obj轉(zhuǎn)化為字節(jié),寫(xiě)到流中。
ObjectInputStream是InputStream的子類(lèi),它實(shí)現(xiàn)了ObjectInput接口,ObjectInput是DataInput的子接口,增加了一個(gè)方法:
public Object readObject() throws ClassNotFoundException, IOException這個(gè)方法能夠從流中讀取字節(jié),轉(zhuǎn)化為一個(gè)對(duì)象。
使用這兩個(gè)流,57節(jié)介紹的保存學(xué)生列表的代碼就可以變?yōu)?#xff1a;
public static void writeStudents(List<Student> students) throws IOException {ObjectOutputStream out = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream("students.dat")));try {out.writeInt(students.size());for (Student s : students) {out.writeObject(s);}} finally {out.close();} }而從文件中讀入學(xué)生列表的代碼可以變?yōu)?#xff1a;
public static List<Student> readStudents() throws IOException,ClassNotFoundException {ObjectInputStream in = new ObjectInputStream(new BufferedInputStream(new FileInputStream("students.dat")));try {int size = in.readInt();List<Student> list = new ArrayList<>(size);for (int i = 0; i < size; i++) {list.add((Student) in.readObject());}return list;} finally {in.close();} }實(shí)際上,只要List對(duì)象也實(shí)現(xiàn)了Serializable (ArrayList/LinkedList都實(shí)現(xiàn)了),上面代碼還可以進(jìn)一步簡(jiǎn)化,讀寫(xiě)只需要一行代碼,如下所示:
public static void writeStudents(List<Student> students) throws IOException {ObjectOutputStream out = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream("students.dat")));try {out.writeObject(students);} finally {out.close();} }public static List<Student> readStudents() throws IOException,ClassNotFoundException {ObjectInputStream in = new ObjectInputStream(new BufferedInputStream(new FileInputStream("students.dat")));try {return (List<Student>) in.readObject();} finally {in.close();} }是不是很神奇?只要將類(lèi)聲明實(shí)現(xiàn)Serializable接口,然后就可以使用ObjectOutputStream/ObjectInputStream直接讀寫(xiě)對(duì)象了。我們之前介紹的各種類(lèi),如String, Date, Double, ArrayList, LinkedList, HashMap, TreeMap等,都實(shí)現(xiàn)了Serializable。
復(fù)雜對(duì)象
上面例子中的Student對(duì)象是非常簡(jiǎn)單的,如果對(duì)象比較復(fù)雜呢?比如:
- 如果a, b兩個(gè)對(duì)象都引用同一個(gè)對(duì)象c,序列化后c是保存兩份還是一份?在反序列化后還能讓a, b指向同一個(gè)對(duì)象嗎?
- 如果a, b兩個(gè)對(duì)象有循環(huán)引用呢?即a引用了b,而b也引用了a。
我們分別來(lái)看下。
引用同一個(gè)對(duì)象
我們看個(gè)簡(jiǎn)單的例子,類(lèi)A和類(lèi)B都引用了同一個(gè)類(lèi)Common,它們都實(shí)現(xiàn)了Serializable,這三個(gè)類(lèi)的定義如下:
class Common implements Serializable {String c;public Common(String c) {this.c = c;} }class A implements Serializable {String a;Common common;public A(String a, Common common) {this.a = a;this.common = common;}public Common getCommon() {return common;} }class B implements Serializable {String b;Common common;public B(String b, Common common) {this.b = b;this.common = common;}public Common getCommon() {return common;} }有三個(gè)對(duì)象, a, b, c,如下所示:
Common c = new Common("common"); A a = new A("a", c); B b = new B("b", c);a和b引用同一個(gè)對(duì)象c,如果序列化這兩個(gè)對(duì)象,反序列化后,它們還能指向同一個(gè)對(duì)象嗎?答案是肯定的,我們看個(gè)實(shí)驗(yàn)。
ByteArrayOutputStream bout = new ByteArrayOutputStream(); ObjectOutputStream out = new ObjectOutputStream(bout); out.writeObject(a); out.writeObject(b); out.close();ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bout.toByteArray())); A a2 = (A) in.readObject(); B b2 = (B) in.readObject();if (a2.getCommon() == b2.getCommon()) {System.out.println("reference the same object"); } else {System.out.println("reference different objects"); }輸出為:
reference the same object這也是Java序列化機(jī)制的神奇之處,它能自動(dòng)處理這種引用同一個(gè)對(duì)象的情況。更神奇的是,它還能自動(dòng)處理循環(huán)引用的情況,我們來(lái)看下。
循環(huán)引用
我們看個(gè)例子,有Parent和Child兩個(gè)類(lèi),它們相互引用,類(lèi)定義如下:
class Parent implements Serializable {String name;Child child;public Parent(String name) {this.name = name;}public Child getChild() {return child;}public void setChild(Child child) {this.child = child;} }class Child implements Serializable {String name;Parent parent;public Child(String name) {this.name = name;}public Parent getParent() {return parent;}public void setParent(Parent parent) {this.parent = parent;} }定義兩個(gè)對(duì)象:
Parent parent = new Parent("老馬"); Child child = new Child("小馬"); parent.setChild(child); child.setParent(parent);序列化parent, child兩個(gè)對(duì)象,Java能正確序列化嗎?反序列化后,還能保持原來(lái)的引用關(guān)系嗎?答案是肯定的,我們看代碼實(shí)驗(yàn):
ByteArrayOutputStream bout = new ByteArrayOutputStream(); ObjectOutputStream out = new ObjectOutputStream(bout); out.writeObject(parent); out.writeObject(child); out.close();ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bout.toByteArray())); parent = (Parent) in.readObject(); child = (Child) in.readObject();if (parent.getChild() == child && child.getParent() == parent&& parent.getChild().getParent() == parent&& child.getParent().getChild() == child) {System.out.println("reference OK"); } else {System.out.println("wrong reference"); }輸出為:
reference OK神奇吧?
定制序列化
默認(rèn)的序列化機(jī)制已經(jīng)很強(qiáng)大了,它可以自動(dòng)將對(duì)象中的所有字段自動(dòng)保存和恢復(fù),但這種默認(rèn)行為有時(shí)候不是我們想要的。
比如,對(duì)于有些字段,它的值可能與內(nèi)存位置有關(guān),比如默認(rèn)的hashCode()方法的返回值,當(dāng)恢復(fù)對(duì)象后,內(nèi)存位置肯定變了,基于原內(nèi)存位置的值也就沒(méi)有了意義。還有一些字段,可能與當(dāng)前時(shí)間有關(guān),比如表示對(duì)象創(chuàng)建時(shí)的時(shí)間,保存和恢復(fù)這個(gè)字段就是不正確的。
還有一些情況,如果類(lèi)中的字段表示的是類(lèi)的實(shí)現(xiàn)細(xì)節(jié),而非邏輯信息,那默認(rèn)序列化也是不適合的。為什么不適合呢?因?yàn)樾蛄谢袷奖硎疽环N契約,應(yīng)該描述類(lèi)的邏輯結(jié)構(gòu),而非與實(shí)現(xiàn)細(xì)節(jié)相綁定,綁定實(shí)現(xiàn)細(xì)節(jié)將使得難以修改,破壞封裝。
比如,我們?cè)谌萜黝?lèi)中介紹的LinkedList,它的默認(rèn)序列化就是不適合的,為什么呢?因?yàn)長(zhǎng)inkedList表示一個(gè)List,它的邏輯信息是列表的長(zhǎng)度,以及列表中的每個(gè)對(duì)象,但LinkedList類(lèi)中的字段表示的是鏈表的實(shí)現(xiàn)細(xì)節(jié),如頭尾節(jié)點(diǎn)指針,對(duì)每個(gè)節(jié)點(diǎn),還有前驅(qū)和后繼節(jié)點(diǎn)指針等。
那怎么辦呢?Java提供了多種定制序列化的機(jī)制,主要的有兩種,一種是transient關(guān)鍵字,另外一種是實(shí)現(xiàn)writeObject和readObject方法。
將字段聲明為transient,默認(rèn)序列化機(jī)制將忽略該字段,不會(huì)進(jìn)行保存和恢復(fù)。比如,類(lèi)LinkedList中,它的字段都聲明為了transient,如下所示:
transient int size = 0; transient Node<E> first; transient Node<E> last;聲明為了transient,不是說(shuō)就不保存該字段了,而是告訴Java默認(rèn)序列化機(jī)制,不要自動(dòng)保存該字段了,可以實(shí)現(xiàn)writeObject/readObject方法來(lái)自己保存該字段。
類(lèi)可以實(shí)現(xiàn)writeObject方法,以自定義該類(lèi)對(duì)象的序列化過(guò)程,其聲明必須為:
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException可以在這個(gè)方法中,調(diào)用ObjectOutputStream的方法向流中寫(xiě)入對(duì)象的數(shù)據(jù)。比如,LinkedList使用如下代碼序列化列表的邏輯數(shù)據(jù):
private void writeObject(java.io.ObjectOutputStream s)throws java.io.IOException {// Write out any hidden serialization magic s.defaultWriteObject();// Write out size s.writeInt(size);// Write out all elements in the proper order.for (Node<E> x = first; x != null; x = x.next)s.writeObject(x.item); }需要注意的是第一行代碼:
s.defaultWriteObject();這一行是必須的,它會(huì)調(diào)用默認(rèn)的序列化機(jī)制,默認(rèn)機(jī)制會(huì)保存所有沒(méi)聲明為transient的字段,即使類(lèi)中的所有字段都是transient,也應(yīng)該寫(xiě)這一行,因?yàn)镴ava的序列化機(jī)制不僅會(huì)保存純粹的數(shù)據(jù)信息,還會(huì)保存一些元數(shù)據(jù)描述等隱藏信息,這些隱藏的信息是序列化之所以能夠神奇的重要原因。
與writeObject對(duì)應(yīng)的是readObject方法,通過(guò)它自定義反序列化過(guò)程,其聲明必須為:
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException在這個(gè)方法中,調(diào)用ObjectInputStream的方法從流中讀入數(shù)據(jù),然后初始化類(lèi)中的成員變量。比如,LinkedList的反序列化代碼為:
private void readObject(java.io.ObjectInputStream s)throws java.io.IOException, ClassNotFoundException {// Read in any hidden serialization magic s.defaultReadObject();// Read in sizeint size = s.readInt();// Read in all elements in the proper order.for (int i = 0; i < size; i++)linkLast((E)s.readObject()); }注意第一行代碼:
s.defaultReadObject();這一行代碼也是必須的。
序列化的基本原理
稍微總結(jié)一下:
- 如果類(lèi)的字段表示的就是類(lèi)的邏輯信息,如上面的Student類(lèi),那就可以使用默認(rèn)序列化機(jī)制,只要聲明實(shí)現(xiàn)Serializable接口即可。
- 否則的話(huà),如LinkedList,那就可以使用transient關(guān)鍵字,實(shí)現(xiàn)writeObject和readObject來(lái)自定義序列化過(guò)程。
- Java的序列化機(jī)制可以自動(dòng)處理如引用同一個(gè)對(duì)象、循環(huán)引用等情況。
但,序列化到底是如何發(fā)生的呢?關(guān)鍵在ObjectOutputStream的writeObject和ObjectInputStream的readObject方法內(nèi)。它們的實(shí)現(xiàn)都非常復(fù)雜,正因?yàn)檫@些復(fù)雜的實(shí)現(xiàn)才使得序列化看上去很神奇,我們簡(jiǎn)單介紹下其基本邏輯。
writeObject的基本邏輯是:
- 如果對(duì)象沒(méi)有實(shí)現(xiàn)Serializable,拋出異常NotSerializableException。
- 每個(gè)對(duì)象都有一個(gè)編號(hào),如果之前已經(jīng)寫(xiě)過(guò)該對(duì)象了,則本次只會(huì)寫(xiě)該對(duì)象的引用,這可以解決對(duì)象引用和循環(huán)引用的問(wèn)題。
- 如果對(duì)象實(shí)現(xiàn)了writeObject方法,調(diào)用它的自定義方法。
- 默認(rèn)是利用反射機(jī)制(反射我們留待后續(xù)文章介紹),遍歷對(duì)象結(jié)構(gòu)圖,對(duì)每個(gè)沒(méi)有標(biāo)記為transient的字段,根據(jù)其類(lèi)型,分別進(jìn)行處理,寫(xiě)出到流,流中的信息包括字段的類(lèi)型即完整類(lèi)名、字段名、字段值等。
readObject的基本邏輯是:
- 不調(diào)用任何構(gòu)造方法。
- 它自己就相當(dāng)于是一個(gè)獨(dú)立的構(gòu)造方法,根據(jù)字節(jié)流初始化對(duì)象,利用的也是反射機(jī)制。
- 在解析字節(jié)流時(shí),對(duì)于引用到的類(lèi)型信息,會(huì)動(dòng)態(tài)加載,如果找不到類(lèi),會(huì)拋出ClassNotFoundException。
版本問(wèn)題
上面的介紹,我們忽略了一個(gè)問(wèn)題,那就是版本問(wèn)題。我們知道,代碼是在不斷演化的,而序列化的對(duì)象可能是持久保存在文件上的,如果類(lèi)的定義發(fā)生了變化,那持久化的對(duì)象還能反序列化嗎?
默認(rèn)情況下,Java會(huì)給類(lèi)定義一個(gè)版本號(hào),這個(gè)版本號(hào)是根據(jù)類(lèi)中一系列的信息自動(dòng)生成的。在反序列化時(shí),如果類(lèi)的定義發(fā)生了變化,版本號(hào)就會(huì)變化,與流中的版本號(hào)就會(huì)不匹配,反序列化就會(huì)拋出異常,類(lèi)型為java.io.InvalidClassException。
通常情況下,我們希望自定義這個(gè)版本號(hào),而非讓Java自動(dòng)生成,一方面是為了更好的控制,另一方面是為了性能,因?yàn)镴ava自動(dòng)生成的性能比較低,怎么自定義呢?在類(lèi)中定義如下變量:
private static final long serialVersionUID = 1L;在Java IDE如Eclipse中,如果聲明實(shí)現(xiàn)了Serializable而沒(méi)有定義該變量,IDE會(huì)提示自動(dòng)生成。這個(gè)變量的值可以是任意的,代表該類(lèi)的版本號(hào)。在序列化時(shí),會(huì)將該值寫(xiě)入流,在反序列化時(shí),會(huì)將流中的值與類(lèi)定義中的值進(jìn)行比較,如果不匹配,會(huì)拋出InvalidClassException。
那如果版本號(hào)一樣,但實(shí)際的字段不匹配呢?Java會(huì)分情況自動(dòng)進(jìn)行處理,以盡量保持兼容性,大概分為三種情況:
- 字段刪掉了:即流中有該字段,而類(lèi)定義中沒(méi)有,該字段會(huì)被忽略。
- 新增了字段:即類(lèi)定義中有,而流中沒(méi)有,該字段會(huì)被設(shè)為默認(rèn)值。
- 字段類(lèi)型變了:對(duì)于同名的字段,類(lèi)型變了,會(huì)拋出InvalidClassException。?
高級(jí)自定義
除了自定義writeObject/readObject方法,Java中還有如下自定義序列化過(guò)程的機(jī)制:
- Externalizable接口
- readResolve方法
- writeReplace方法?
這些機(jī)制實(shí)際用到的比較少,我們簡(jiǎn)要說(shuō)明下。
Externalizable是Serializable的子接口,定義了如下方法:
void writeExternal(ObjectOutput out) throws IOException void readExternal(ObjectInput in) throws IOException, ClassNotFoundException與writeObject/readObject的區(qū)別是,如果對(duì)象實(shí)現(xiàn)了Externalizable接口,則序列化過(guò)程會(huì)由這兩個(gè)方法控制,默認(rèn)序列化機(jī)制中的反射等將不再起作用,不再有類(lèi)似defaultWriteObject和defaultReadObject調(diào)用,另一個(gè)區(qū)別是,反序列化時(shí),會(huì)先調(diào)用類(lèi)的無(wú)參構(gòu)造方法創(chuàng)建對(duì)象,然后才調(diào)用readExternal。默認(rèn)的序列化機(jī)制由于需要分析對(duì)象結(jié)構(gòu),往往比較慢,通過(guò)實(shí)現(xiàn)Externalizable接口,可以提高性能。
readResolve方法返回一個(gè)對(duì)象,聲明為:
Object readResolve()如果定義了該方法,在反序列化之后,會(huì)額外調(diào)用該方法,該方法的返回值才會(huì)被當(dāng)做真正的反序列化的結(jié)果。這個(gè)方法通常用于反序列化單例對(duì)象的場(chǎng)景。
writeReplace也是返回一個(gè)對(duì)象,聲明為:
Object writeReplace()如果定義了該方法,在序列化時(shí),會(huì)先調(diào)用該方法,該方法的返回值才會(huì)被當(dāng)做真正的對(duì)象進(jìn)行序列化。
writeReplace和readResolve可以構(gòu)成一種所謂的序列化代理模式,這個(gè)模式描述在<Effective Java> 第二版78條中,Java容器類(lèi)中的EnumSet使用了該模式,我們一般用的比較少,就不詳細(xì)介紹了。
序列化特點(diǎn)分析
序列化的主要用途有兩個(gè),一個(gè)是對(duì)象持久化,另一個(gè)是跨網(wǎng)絡(luò)的數(shù)據(jù)交換、遠(yuǎn)程過(guò)程調(diào)用。
Java標(biāo)準(zhǔn)的序列化機(jī)制有很多優(yōu)點(diǎn),使用簡(jiǎn)單,可自動(dòng)處理對(duì)象引用和循環(huán)引用,也可以方便的進(jìn)行定制,處理版本問(wèn)題等,但它也有一些重要的局限性:
- Java序列化格式是一種私有格式,是一種Java語(yǔ)言特有的技術(shù),不能被其他語(yǔ)言識(shí)別,不能實(shí)現(xiàn)跨語(yǔ)言的數(shù)據(jù)交換。
- Java在序列化字節(jié)中保存了很多描述信息,使得序列化格式比較大。
- Java的默認(rèn)序列化使用反射分析遍歷對(duì)象結(jié)構(gòu),性能比較低。
- Java的序列化格式是二進(jìn)制的,不方便查看和修改。
由于這些局限性,實(shí)踐中往往會(huì)使用一些替代方案。在跨語(yǔ)言的數(shù)據(jù)交換格式中,XML/JSON是被廣泛采用的文本格式,各種語(yǔ)言都有對(duì)它們的支持,文件格式清晰易讀,有很多查看和編輯工具,它們的不足之處是性能和序列化大小,在性能和大小敏感的領(lǐng)域,往往會(huì)采用更為精簡(jiǎn)高效的二進(jìn)制方式如ProtoBuf, Thrift, MessagePack等。
小結(jié)
本節(jié)介紹了Java的標(biāo)準(zhǔn)序列化機(jī)制,我們介紹了它的用法和基本原理,最后分析了它的特點(diǎn),它是一種神奇的機(jī)制,通過(guò)簡(jiǎn)單的Serializable接口就能自動(dòng)處理很多復(fù)雜的事情,但它也有一些重要的限制,最重要的是不能跨語(yǔ)言。
在接來(lái)下的幾節(jié)中,我們來(lái)看一些替代方案,包括XML/JSON和MessagePack。
----------------
未完待續(xù),查看最新文章,敬請(qǐng)關(guān)注微信公眾號(hào)“老馬說(shuō)編程”(掃描下方二維碼),從入門(mén)到高級(jí),深入淺出,老馬和你一起探索Java編程及計(jì)算機(jī)技術(shù)的本質(zhì)。用心原創(chuàng),保留所有版權(quán)。
總結(jié)
以上是生活随笔為你收集整理的Java编程的逻辑 (62) - 神奇的序列化的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: python 调用 so 库 需要注意的
- 下一篇: 21天养成习惯?不一定