在Java的世界里,建立好对象以后,只要须要,对象是能够长驻内存,可是在程序终止时,全部对象仍是会被销毁。这其实很合理,可是即便合理也不必定能知足全部场景,仍然存在着一些状况,须要可以在程序不运行的状况下保持对象,因此序列化机制应运而生。java
简单来讲序列化的做用就是将内存中的对象保存起来,在须要时能够重建该对象,而且重建后的对象拥有与保存以前的对象所拥有的信息相同。在实际应用中,对象序列化常会用在以下场景:数据库
也许你会以为,要达到这种持久化的效果,咱们直接将信息写入文件或数据库也能够实现啊,为何还要序列化?这是一个好问题,试想若是咱们采用前面所述的方法,在序列化对象和反序列化恢复对象时,咱们必须考虑如何完整的保存和恢复对象的信息,这里面会涉及到不少繁琐的细节,稍加不注意就可能致使信息的丢失。若是可以有一种机制,只要将一个对象声明为是“持久性”的,就可以为咱们处理掉全部细节,这样岂不是很方便,这就是序列化要作的事情。Java已经将序列化的概念加入到语言中,本文的关于序列化的全部例子都是基于Java的。数组
Java提供的原生序列化机制功能强大,有其本身的一些特色:网络
Java的对象序列化机制是将那些实现了Serializable接口的对象转换成一个字节序列,并可以在之后将这个字节序列彻底恢复为原来的对象。app
要序列化一个对象,首先要建立一个OutputStream对象,而后将其封装在一个ObjectOutputStream对象内,接着只需调用writeObject()方法便可将对象序列化,并将序列化后的字节序列发送给OutputStream。要将一个序列还原为一个对象,则须要将一个InputStream封装在ObjectInputStream内,而后调用readObject(),该方法会返回一个引用,它指向一个向上转型的Object,必须向下转型才能直接使用。框架
咱们来看一个例子,如何序列化和反序列化对象。dom
public class Worm implements Serializable{ private static Random rand = new Random(47); private Data[] d = { new Data(rand.nextInt(10)), new Data(rand.nextInt(10)), new Data(rand.nextInt(10)) }; private Worm next; private char c; public Worm(int i, char x){ System.out.println("Worm constructor: " + i); c = x; if(--i > 0){ next = new Worm(i,(char)(x + 1)); } } public Worm(){ System.out.println("Default constructor"); } public String toString(){ StringBuilder result = new StringBuilder(":"); result.append(c); result.append("("); for(Data dat : d){ result.append(dat); } result.append(")"); if(next != null){ result.append(next); } return result.toString(); } public static void main(String[] args) throws ClassNotFoundException, IOException{ Worm w = new Worm(6,'a'); System.out.println("w = " + w); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("worm.out")); out.writeObject("Worm storage\n"); out.writeObject(w); out.close(); ObjectInputStream in = new ObjectInputStream(new FileInputStream("worm.out")); String s = (String)in.readObject(); Worm w2 = (Worm)in.readObject(); System.out.println(s + "w2 = " + w2); ByteArrayOutputStream bout = new ByteArrayOutputStream(); ObjectOutputStream out2 = new ObjectOutputStream(bout); out2.writeObject("Worm storage\n"); out2.writeObject(w); out2.flush(); ObjectInputStream in2 = new ObjectInputStream(new ByteArrayInputStream(bout.toByteArray())); s = (String)in2.readObject(); Worm w3 = (Worm)in2.readObject(); System.out.println(s + "w3 = " + w3); } } class Data implements Serializable{ private int n; public Data(int n){this.n = n;} public String toString(){return Integer.toString(n);} }
输出结果以下:函数
Worm constructor: 6 Worm constructor: 5 Worm constructor: 4 Worm constructor: 3 Worm constructor: 2 Worm constructor: 1 w = :a(853):b(119):c(802):d(788):e(199):f(881) Worm storage w2 = :a(853):b(119):c(802):d(788):e(199):f(881) Worm storage w3 = :a(853):b(119):c(802):d(788):e(199):f(881)
这段代码经过对连接的对象生成一个worm(蠕虫)对序列化机制进行测试,每一个对象都与worm中的下一段连接,同时又与属于不一样类(Data)的对象引用数组连接。测试
对象序列化不只保存了对象的“全景图”,并且能追踪对象内所包含的全部引用,并保存那些对象;还能对对象内包含的每一个这样的引用进行追踪;依此类推。ui
并且从上面的输出结果还能够看出一个Serializable对象进行还原的过程当中,没有调用任何构造器,包括默认的构造器。整个对象都是经过从InputStream中取得数据恢复而来的。
前面咱们有说到序列化的目的之一是支持rpc框架的数据传输,好比咱们将一个对象序列化,并经过网络将其传给另外一台计算机,另外一台计算机经过反序列化来还原这个对象,那是否只须要该序列化文件就能还原对象呢?咱们用下面的代码来测试一下。
public class Serialize implements Serializable{} } public class FreezeSerialize { public static void main(String[] args) throws Exception{ ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("X.file")); Serialize serialize = new Serialize(); os.writeObject(alien); } } public class ThawSerialize { public static void main(String[] args) throws Exception{ ObjectInputStream in = new ObjectInputStream(new FileInputStream("X.file")); Object mystery = in.readObject(); System.out.println(mystery.getClass()); } }
FreezeSerialize类用来把对象序列化到文件中,ThawSerialize类用来反序列化对象,在测试电脑上若是同时执行这两个类是没有问题的,但若是咱们将Serialize类的位置更改一下(或者直接将FreezeSerialize和Serialize删掉),则执行ThawSerialize反序列化时会报错误--ClassNotFoundException,因此能够知道,反序列化时是须要原对象的Class对象的。
既然反序列化时须要对应的Class对象,那若是序列化时和反序列化时对应的Class版本不同会怎么样呢(这种状况是存在的)?为了模拟这种状况,咱们先执行FreezeSerialize类的main方法,再给Serialize类添加一个属性,这时再跑一下ThawSerialize类的main方法,能够发现报java.io.InvalidClassException异常,明明是能经过编译的可是却报错了,这种状况有没有什么办法解决呢?有的,咱们能够给实现了Serializable接口的类添加一个long类型的成员:serialVersionUID,修饰符为private static final,而且指定一个随机数便可。
这个serialVersionUID其实叫序列化版本号,若是不指定的话,编译器会在编译后的class文件中默认添加一个,其值是根据当前类结构生成。可是这样会带来一个问题,若是类的结构发生了改变,那编译以后对应的版本号也会发生改变,而虚拟机是否容许反序列化,不只取决于类路径和功能代码是否一致,还有一个很是重要的一点是两个类的序列化ID是否一致,若是不一致则不容许序列化而且会抛出InvalidClassException异常,这就是前面不添加序列号时更改类结构再反序列化时会报错的缘由。因此建议给实现了Serializable接口的类添加一个序列化版本号serialVersionUID,并指定值。
关于序列化版本号还有一个点须要主意,版本号一致的状况下,若待反序列化的对象与当前类现有结构不一致时,则采用兼容模式,即:该对象的属性现有类有的则还原,没有的则忽略。
上面咱们咱们使用的是Java提供的默认序列化机制,即将对象成员所有序列化。可是,若是有特殊的须要呢?好比,只但愿对象的某一部分被序列化。在这种特殊状况下,能够经过若干种方法来实现想要的效果,下面一一介绍。
经过实现Externalizable接口(代替实现Serializable),能够对序列化过程进行控制。这个Externalizable接口继承了Serializable接口,同时增长了两个方法:writeExternal()和readExternal()。这两个方法会分别在序列化和反序列化还原的过程当中被自动调用,这样就能够在这两个方法种指定执行一些特殊操做。下面来看一个简单例子:
public class Blip implements Externalizable{ private int i; private String s; public Blip(){ System.out.println("Blip Constructor"); } public Blip(String x, int a){ System.out.println("Blip(String x, int a)"); s = x; i = a; } public String toString(){ return s + i; } public void writeExternal(ObjectOutput out) throws IOException{ System.out.println("Blip.writeExternal"); out.writeObject(s); out.writeInt(i); } public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException{ System.out.println("Blip.readExternal"); s = (String)in.readObject(); i = in.readInt(); } public static void main(String[] args) throws IOException, ClassNotFoundException{ System.out.println("Constructing objects:"); Blip b = new Blip("A String ",47); System.out.println(b); // 序列化对象 ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("Blip.out")); System.out.println("Saving object:"); o.writeObject(b); o.close(); // 还原对象 ObjectInputStream in = new ObjectInputStream(new FileInputStream("Blip.out")); System.out.println("Recovering b:"); b = (Blip)in.readObject(); System.out.println(b); } }
在这个例子中,对象继承了Externalizable接口,成员s和i只在第二个构造器中初始化(而不是默认构造器),咱们在writeExternal()方法中对要序列化保存的成员执行写入操做,在readExternal()方法中将其恢复。输出结果以下:
Constructing objects: Blip(String x, int a) A String 47 Saving object: Blip.writeExternal Recovering b: Blip Constructor Blip.readExternal A String 47
这里须要注意的几个点:
在咱们对序列化进行控制时,可能会碰到某个特定子对象不想让Java的序列化机制自动保存与恢复。好比一些敏感信息(密码),即便对象中的这些成员是由private修饰,一经序列化处理,经过读取文件或者网络抓包的方式仍是能访问到它。
前面说的经过实现Externalizable接口能够解决这个问题,可是假如对象有不少的成员,而咱们只但愿其中少许成员不被序列化,那经过实现Externalizable接口的方式就不合适了(由于须要在writeExternal()方法中作大量工做),这种状况下,transient关键就能够大显身手了。在实现了Serializable接口的类中,被transient关键字修饰的成员是不会被序列化的。并且,因为Externalizable对象在默认状况下不会序列化对象的任何字段,transient关键字只能和Serializable对象一块儿使用。
除了上面两种方法,还有一种相对不那么“正规”的办法--咱们能够实现Serializable接口,并添加名为writeObject()和readObject()的方法。当对象被序列化或者被反序列化还原时,就会自动地分别调用这两个方法(只要咱们提供了这两个方法,就会使用它们而不是默认的序列化机制)。可是须要注意的是这两个方法必须有准确的方法特征签名:
private void writeObject(ObjectOutputStream stream) throws IOException; private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException;
在调用ObjectOutputStream.writeObject()时,会检查所传递的Serializable对象,看看是否实现了它本身的writeObject()。若是是,就跳过正常的序列化过程并调用对象本身的writeObject()方法。readObject()的状况是相似的。这就是这种方式的原理。
还有一个技巧,在咱们提供的writeObject()内部,能够调用defaultWriteObject()来选择执行默认的writeObject()。相似,在readObject()内部,咱们能够调用defaultReadObject()。下面看一个例子,如何对一个Serializable对象的序列化与恢复进行控制:
public class SerialCtl implements Serializable{ private String noTran; private transient String tran; public SerialCtl(String noTran, String tran){ this.noTran = "Not Transient: " + noTran; this.tran = "Transient: " + tran; } public String toString(){ return noTran + "\n" + tran; } private void writeObject(ObjectOutputStream stream) throws IOException { stream.defaultWriteObject(); stream.writeObject(tran); } private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException{ stream.defaultReadObject(); tran = (String)stream.readObject(); } public static void main(String[] args) throws IOException, ClassNotFoundException{ SerialCtl sc = new SerialCtl("papaya","mango"); System.out.println("Before:\n" + sc); ByteArrayOutputStream buf = new ByteArrayOutputStream(); ObjectOutputStream o = new ObjectOutputStream(buf); o.writeObject(sc); // 还原 ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(buf.toByteArray())); SerialCtl sc2 = (SerialCtl)in.readObject(); System.out.println("After:\n" + sc2); } }
输出结果:
Before:
Not Transient: papaya
Transient: mango
After:
Not Transient: papaya
Transient: mango
在这个例子中,有一个String字段是普通字段,而另外一个是transient字段,对比证实非transient字段由defaultWriteObject()方法保存,而transient字段必须在程序中明确保存和恢复。
在writeObject()内部第一行调用defaultWriteObject()方法是为了利用默认序列化机制序列化对象的非transient成员,一样,在readObject()内部第一行调用defaultReadObject()方法是为了利用默认机制恢复非transient成员。注意,必须是第一行调用。
使用序列化的一个主要目的是存储程序的一些状态,以便咱们后面能够容易地将程序恢复到当前状态。在这样作以前,咱们先考虑几种状况。若是咱们将两个对象(它们都包含有有指向第三个对象的引用成员)进行序列化,会发生什么状况?当咱们从它们的序列化文件中恢复这两个对象时,第三个对象会只出现一次吗?若是将这两个对象序列化成独立的文件,而后在代码的不一样部分对它们进行反序列化还原,又会怎样呢?先看例子:
public class MyWorld { public static void main(String[] args) throws IOException, ClassNotFoundException{ House house = new House(); List<Animal> animals = new ArrayList<Animal>(); animals.add(new Animal("Bosco the dog", house)); animals.add(new Animal("Ralph the hamster", house)); animals.add(new Animal("Molly the cat",house)); System.out.println("animals: " + animals); ByteArrayOutputStream buf1 = new ByteArrayOutputStream(); ObjectOutputStream o1 = new ObjectOutputStream(buf1); o1.writeObject(animals); o1.writeObject(animals); // 写入到另外一个流中: ByteArrayOutputStream buf2 = new ByteArrayOutputStream(); ObjectOutputStream o2 = new ObjectOutputStream(buf2); o2.writeObject(animals); // 反序列化: ObjectInputStream in1 = new ObjectInputStream(new ByteArrayInputStream(buf1.toByteArray())); ObjectInputStream in2 = new ObjectInputStream(new ByteArrayInputStream(buf2.toByteArray())); List animals1 = (List)in1.readObject(),animals2 = (List)in1.readObject(),animals3 = (List)in2.readObject(); System.out.println("animals1: " + animals1); System.out.println("animals2: " + animals2); System.out.println("animals3: " + animals3); } } class House implements Serializable{} class Animal implements Serializable{ private String name; private House preferredHouse; Animal(String nm, House h){ name = nm; preferredHouse = h; } public String toString(){ return name + "[" + super.toString() + "]. " + preferredHouse + "\n"; } }
输出结果:
animals: [Bosco the dog[testDemos.Animal@7852e922]. testDemos.House@4e25154f , Ralph the hamster[testDemos.Animal@70dea4e]. testDemos.House@4e25154f , Molly the cat[testDemos.Animal@5c647e05]. testDemos.House@4e25154f ] animals1: [Bosco the dog[testDemos.Animal@2d98a335]. testDemos.House@16b98e56 , Ralph the hamster[testDemos.Animal@7ef20235]. testDemos.House@16b98e56 , Molly the cat[testDemos.Animal@27d6c5e0]. testDemos.House@16b98e56 ] animals2: [Bosco the dog[testDemos.Animal@2d98a335]. testDemos.House@16b98e56 , Ralph the hamster[testDemos.Animal@7ef20235]. testDemos.House@16b98e56 , Molly the cat[testDemos.Animal@27d6c5e0]. testDemos.House@16b98e56 ] animals3: [Bosco the dog[testDemos.Animal@4f3f5b24]. testDemos.House@15aeb7ab , Ralph the hamster[testDemos.Animal@7b23ec81]. testDemos.House@15aeb7ab , Molly the cat[testDemos.Animal@6acbcfc0]. testDemos.House@15aeb7ab ]
这里咱们经过一个字节数组来使用对象序列化,这样能够实现对任何可Serializable对象的“深度复制”(deep copy)--深度复制意味着复制的是整个对象网,而不只仅是基本对象及其引用。
在这个例子中,咱们从打印的结果能够看出,只要将任何对象序列化到单一流中,就能够恢复出与咱们写入时同样的对象网,而且不会有任何意外重复复制出的对象,对比animals1和animals2中的House。
另外一方面,在恢复animals3时,输出的House与animals1和animals2是不一样的,这说明了若是将对象序列化到不一样的文件中,而后在代码的不一样部分对它们进行反序列化还原,这时会产生出两个对象。
序列化的出现给保存程序运行状态提供了一种新的途径,实际主要使用在RPC框架的数据传输以及对象状态的持久化保存等场景。