先定义两个简单的类:
package comm; import java.io.Serializable; import java.util.Date; import java.util.GregorianCalendar; public class Employee implements Serializable{ private static final long serialVersionUID = 8820461656542319555L; private String name; private double salay; private Date hireDay; public Employee(String name, double salay, int year, int month, int day) { super(); this.name = name; this.salay = salay; GregorianCalendar calender = new GregorianCalendar(year, (month - 1), day); this.hireDay = calender.getTime(); } public void raseSalay(double rSalay){ this.salay += rSalay; } @Override public String toString() { return "Employee [name=" + name + ", salay=" + salay + ", hireDay=" + hireDay + "]"; } public String getName() { return name; } public double getSalay() { return salay; } public Date getHireDay() { return hireDay; } public void setName(String name) { this.name = name; } public void setSalay(double salay) { this.salay = salay; } public void setHireDay(Date hireDay) { this.hireDay = hireDay; } }public class Employee Class
package comm; public class Manager extends Employee { private static final long serialVersionUID = 1L; public Manager(String name, double salay, int year, int month, int day) { super(name, salay, year, month, day); this.secretary = null; } private Employee secretary; public Employee getSecretary() { return secretary; } public void setSecretary(Employee secretary) { this.secretary = secretary; } @Override public String toString() { return "Manager [Name=" + getName() + ", Salay=" + getSalay() + ", HireDay=" + getHireDay() + ", secretary=" + secretary + "]"; } }public class Manager extends Employee
下面进入今天的正题:序列化和反序列化。
1、基本的用法
①、序列化(serialization)一个java对象,第一步就是构建一个ObjectOutputStream对象:
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("e://enum.dat"));
现在,就可以简单的调用ObjectOutputStream对象的writeObject()方法来序列化一个对象了,就像下面这样(后面会介绍到Employee要实现Serializable接口):
Employee harry = new Employee("Harry Hacker", 50000, 1989, 10, 1); out.writeObject(harry );
②、反序列化(deserialization)一个java对象,第一步则是要构建一个ObjectInputStream对象:
ObjectInputStream in = new ObjectInputStream(new FileInputStream("e://enum.dat"));
同样,有了ObjectInputStream对象以后就可以调用readObject()方法来反序列化一个对象了:
Employee emp = (Employee) in.readObject();
③、实现Serializable接口。任何想进行序列化和反序列化操作的对象,其类必须要实现Serializable接口:
class Employee implements Serializable{ ... }
实际上,Serializable这个接口并没有任何的方法和属性,和Cloneable接口一样。
好,到现在为止,基本用法讲完了。现在走一个看看:
package streamAndFile; import java.io.*; import comm.Employee; import comm.Manager; public class ObjectStreamTest { public static void main(String[] args) { Employee harry = new Employee("Harry Hacker", 50000, 1989, 10, 1); Manager carl = new Manager("Carl Cracker", 8000, 1987, 12, 15); Manager tony = new Manager("Tony Tester", 40000, 1990, 3, 15);
//两个Manager公用一个秘书(Employee)
carl.setSecretary(harry); tony.setSecretary(harry); Employee[] staff = new Employee[3]; staff[0] = harry; staff[1] = carl; staff[2] = tony; try { //将对象序列化到文件 ObjectOutputStream objOut = new ObjectOutputStream(new FileOutputStream("employee.dat")); objOut.writeObject(staff); objOut.close(); //将对象反序列化出来 ObjectInputStream objIn = new ObjectInputStream(new FileInputStream("employee.dat")); Employee[] newStaff = (Employee[]) objIn.readObject(); objIn.close(); //修改第一个对象的属性值,从结果中可以看出,反序列化以后的对象依然保持着原来的引用关系 newStaff[0].raseSalay(10); //打印 for(Employee e : newStaff){ System.out.println(e); } } catch (Exception e) { e.printStackTrace(); } } }
得到如下的【 结果1 】:
Employee [name=Harry Hacker, salay=50010.0, hireDay=Sun Oct 01 00:00:00 CST 1989] Manager [Name=Carl Cracker, Salay=8000.0, HireDay=Tue Dec 15 00:00:00 CST 1987, secretary=Employee [name=Harry Hacker, salay=50010.0, hireDay=Sun Oct 01 00:00:00 CST 1989]] Manager [Name=Tony Tester, Salay=40000.0, HireDay=Thu Mar 15 00:00:00 CST 1990, secretary=Employee [name=Harry Hacker, salay=50010.0, hireDay=Sun Oct 01 00:00:00 CST 1989]]
现在来解释一个问题:为什么我要在【 结果1 】中将 salay=50010.0 用红色字体标注?答:因为它值得用红色标注。
从上面我们可以看出,ObjectOutputStream能够检测到对象的所有属性并且保存(序列化,在这种上下文中 保存 和 序列化 是可以通用的, 读取 和 反序列化 同样通用)这些属性内容。比如,当序列化一个Employee对象的时候,name,hireDay和salary属性全部被保存。
然而,有一个非常重要的情形需要我们认真思考:当一个对象同时被多个对象引用的时候会发生什么?
就像 Manager类对象carl中有一个Employee类型 secretary属性一样,我们知道 carl. secretary 变量中实际上存放的是一个指向 Employee对象的地址(也就是通常说的引用,相当于于c/c++里面的指针) 。在上面的实例代码中就存在如下的引用关系:
要序列化这样一个具有网状引用关系的对象是一件具有挑战性的事情。当然,我们知道,对 secretary对象而言,我们不能够简单粗暴的保存一个内存地址(memory address)。序列化通常应用于网络对象传输,那么,对不同的机器而言,一个内存地址是没有意义的。
实际上,序列化的时候对于对象的引用(object reference)是按照下面的步骤进行处理的:
①、序列化过程中,对每一个遇到的 object reference 都分配一个序列号(serial number);
②、当一个对象引用是第一次遇见的时候,就保存其对象数据(object data);
③、如果某个对象引用不是第一次遇见,就将其标记为“和serial number x对象一样”;
就像下面这样:
从文件(或者是网络文件)中反序列化的时候按照下面的步骤处理:
①、当第一次读取到一个特定对象的时候,首先是构建一个对象,然后用流数据初始化它,同时记录下serial number和内存地址之间的联系;
②、当遇到标记为“ 和serial number x对象一样 ”的对象的时候,就用 serial number x 对应对象的内存地址初始化这个引用;
所以,从上面的过程可以看出,通过反序列化操作得到的对象和序列化之前的对象保持了一种同样的引用关系。所以,在上面实例代码中通过newStaff[0].raseSalay(10 )修改了其salary以后,后面两行也同样改变为 salay=50010.0 。
3、 变量修饰符 transient 对序列化的控制。被transient修饰的变量,在 使用默认序列化 的时候不维护其内容。
先定义下面两个类:
package comm; public class Person{ public String name; public int age; public Person(String name, int age) { super(); this.name = name; this.age = age; } public String toString() { return "Person [name=" + name + ", age=" + age + "]"; } }public class Person
import java.io.Serializable; public class Student implements Serializable{ private static final long serialVersionUID = 1L; public float score; //变量使用transient public transient String intrest; //对变量使用transient public transient Person person; public Student(Person person, float score, String intrest) { super(); this.person = person; this.score = score; this.intrest = intrest; } public String toString() { return "Student [person=" + person + ", score=" + score + ", intrest=" + intrest + "]"; } }public class Student implements Serializable
再看看使用 transient 的实际效果:
package streamAndFile; import java.io.*; import comm.Person; import comm.Student; public class TransientTest { public static void main(String[] args) { try { ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("transient.dat")); Student stu = new Student(new Person("李雷", 23), 89, "台球"); out.writeObject(stu); out.close(); ObjectInputStream in = new ObjectInputStream(new FileInputStream("transient.dat")); Student newStu = (Student) in.readObject(); in.close(); System.out.println("stu--> " + stu); System.out.println("newStu--> " + newStu); } catch (Exception e) { e.printStackTrace(); } } }
执行以后的结果:
stu--> Student [person=Person [name=李雷, age=23], score=89.0, intrest=台球] newStu--> Student [person=null, score=89.0, intrest=null]
可以看出,由transient修饰的变量在序列化和反序列化的时候被忽略了。所以,其值为null。同时, 还应该注意到Person类没有实现序列化接口 。由于在Strudent类中的person变量前使用了transient关键字,所以在序列化和反序列化的过程中被忽略,而没有抛出NotSerializableException异常。
4、修改默认序列化机制(modifying the default serialization mechanism),有两个层面:
层面一:在序列化类中添加 writeObject 和 readObject 方法,分别配合 defaultWriteObject() 和 defaultReadObject()方法使用
对象序列化机制提供了一种方式用于个性化的修改默认read和write的行为: 在序列化类中 用以下的方式来定义两个方法
private void writeObject(ObjectOutputStream out) throws IOException; private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException;
注意: 两个都是private void ,同时其参数分别是 ObjectOutputStream 和 ObjectInputStream 。这个和之前的方法有所不同。
现在假设我们遇到了这样的一种情况:像上面的Student和Person,其中Person类是写死了的,我们没有办法修改了(或许是我们没有源代码)。但是Student可以自行修改(掌握源码真好)。
现在有这样的一个需求,我序列化的时候是在有必要将Person一起进行,不然我的Student信息就不完整。
这时有人会说,那还不简单?!!在Student类中将person属性前面的transient关键字删掉就可以了。真的只是这么简单吗?难道你忘了Person类没有实现Serializable接口的事情了吗(没有源码真累,不然,直接实现一个接口还不是easy的事情)。
现在我们就用下面的代码介绍,没有源代码也能达到目的。
①、重写Student类(Person类不能动了):
package comm; import java.io.*; public class Student implements Serializable{ private static final long serialVersionUID = 1L; public float score; public String intrest; //对变量使用transient,防止其抛出NotSerializableException异常 public transient Person person; public Student(Person person, float score, String intrest) { super(); this.person = person; this.score = score; this.intrest = intrest; } private void writeObject(ObjectOutputStream out) throws IOException{ /* * defaultWriteObject()方法还比较特别:只能在序列化类的writeObject方法中调用 * 它能够完成默认的序列化操作:对那些没有使用transient的变量进行序列化操作 */ out.defaultWriteObject(); //手动的将person的属性写入到序列化流中 out.writeUTF(person.name); out.writeInt(person.age); }; private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException{ //完成默认的反序列化读取 in.defaultReadObject(); //手动的从反序列化流中读取person的属性 String na = in.readUTF(); int ag = in.readInt(); //新创建一个Person对象,并用手动读取的值进行初始化 this.person = new Person(na, ag); }; public String toString() { return "Student [person=" + person + ", score=" + score + ", intrest=" + intrest + "]"; } }
②、还是使用上面的 public class TransientTest 做测试,会得到同样的结果。所以,问题解决。总结到一点就是:在序列化类中重写两个方法,在方法中手动的写入和读取。从而,我们也可以看出,这两个类的灵活性还是挺大的。
层面二:一个类完全可以定义它自己的序列化机制:实现Externalizable接口,要求在类中定义以下两个方法:
public void readExternal(ObjectInputStream in) throws IOException, ClassNotFoundException; public void writeExternal(ObjectOutputStream out) throws IOException;
注意:它不同于上面说过的两个方法,它的范围是public。
为了说明问题,先定义两个用于测试的类:
package comm; import java.io.*; /** * 该类实现Externalizable接口,同时重载两个方法。对于实现了该接口的类而言, * 在序列化和反序列化的过程中,会用readExternal和writeExternal两个方法 * <b>完全负责</b>读取和保存对象的操作,<b>包括对其父类的数据</b>。 */ public class Car implements Externalizable{ public String brand; /* * 注意:在实现了Externalizable接口的序列化类中,变量关键字transient * 将会不起任何作用。关键字transient是和Serializable接口有关,在对实现 * 该接口的类进行默认序列化操作的时候,会自动忽略使用了transient关键字的变量。 */ public transient Double price; //必须要有一个无参的构造器,否则会报异常 public Car() {} public Car(String brand, Double price) { super(); this.brand = brand; this.price = price; } @Override public String toString() { return "Car [brand=" + brand + ", price=" + price + "]"; } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { //将类中的属性按顺序从流中读出,然后赋值给当前对象的属性 this.brand = in.readUTF(); this.price = in.readDouble(); } @Override public void writeExternal(ObjectOutput out) throws IOException { //将当前对象的属性值按顺序写入到流中 out.writeUTF(this.brand); out.writeDouble(this.price); } }public class Car implements Externalizable
package comm; import java.io.Serializable; public class Teacher implements Serializable { private static final long serialVersionUID = 1L; private String name; //包含一个实现了Externalizable接口的属性 private Car car; public Teacher(String name, Car car) { super(); this.name = name; this.car = car; } @Override public String toString() { return "Teacher [name=" + name + ", car=" + car + "]"; } }public class Teacher implements Serializable
Car类实现了Externalizable接口,同时实现了readExternal和writeExternal方法。为了,说明一个问题,还在Car类的price属性上面使用了关键字transient。但是,要注意,这里使用transient只是为了要说明一个问题, 实际上对于实现了Serializable接口的类而言,关键字transient不会起到任何作用。 transient 只是为了影响序列化的默认行为而设置的。 Serializable接口 完全抛弃了默认序列化机制,靠自定义实现。
Teacher类实现了Serializable接口,其中有一个Car类型的属性。
现在,对Teacher类型的对象做序列化和反序列化操作:
package streamAndFile; import java.io.*; import comm.Car; import comm.Teacher; public class ExternalizableTest { public static void main(String[] args) { try { ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("externalizable.dat")); Car car = new Car("奥迪", 500000.0); Teacher teach = new Teacher("李雷", car); out.writeObject(teach); out.close(); ObjectInputStream in = new ObjectInputStream(new FileInputStream("externalizable.dat")); Teacher newTeach = (Teacher) in.readObject(); in.close(); System.out.println(newTeach); } catch (Exception e) { e.printStackTrace(); } } }
打印结果:
Teacher [name=李雷, car=Car [brand=奥迪, price=500000.0]]
现在,分析结果:
①、Teacher对象中有car,说明Car对象仅仅只是实现了Externalizable接口也能进行序列化和反序列化操作;
②、Car中的price属性有值,则说明其transient关键字不会起到任何作用;
①、Externalizable和Serializable两个接口,实现其中任何一个都能够完成序列化操作。不一定非得实现Serializable接口才可以。
②、transient关键字是和Serializable默认序列化行为联系在一起的,同时也是和 ObjectOutputStream out.defaultWriteObject(),ObjectInputStream in.defaultReadObject() 这两个方法联系在一起的。在进行默认序列化操作,以及调用out.defaultWriteObject()和in.defaultReadObject()这两个方法进行序列化操作的时候,标注transient的变量会被序列化操作所忽略。除Serializable之外,transient关键字在其他地方不会起到任何作用。
③、实现了Externalizable接口的类,其序列化过程完全依靠readExternal和writeExternal这两个方法,包括其对父类数据的处理。也就是说,实现了Externalizable接口以后,其序列化过程完全不使用默认行为了。对所有的数据处理,都必须明明白白的写在readExternal和writeExternal这两个方法中。
④、我们还应该注意一点,Externalizable的优先级比Serializable的优先级要高。假如,某个类同时实现了两个接口,那么在序列化的时候只会考虑和Externalizable接口相关的性质,而不会考虑和Serializable相关的性质。
⑥、在《Core Java Volume II:Advanced Features》中说,序列化是一个比较慢的读写操作。但是,使用Externalizable接口会比使用Serializable接口要快35%到40%。
5、在序列化Singletons的时候需要注意的一个地方
先定义一个Singletons,便于说明问题:
class Orientation implements Serializable{ private static final long serialVersionUID = 1L; public static final Orientation HORIZONTAL = new Orientation(1); public static final Orientation VERTICAL = new Orientation(2); private int val; private Orientation(int val) { this.val = val; } public int getVal() { return val; } }
再定义一个enumeration:
enum Shrubbery{ GROUND, CRAWLING, HANGING }
我们知道,Singletons的本意就是:单体。Orientation类只有一个 private 属性的构造器,所以我们不能通过 new 来构造一个对象,只能通过Orientation.HORIZONTAL的方式获取对象。而且,任何地方获取到的对象都应该只同一个。包括:反序列化得到的对象。
从序列化和非序列化的角度上说,现在我们有两种方式来获得一个单体对象:①、Orientation.HORIZONTAL的方式;②、通过反序列化的方式。
那么,现在的意思就是说:即便你反序列化的数据来源于网络,来源于其它的机器;但是,只要你序列化之前的对象时Orientation.HORIZONTAL,那么反序列化以后得到的对象和我本地的Orientation.HORIZONTAL也必须是同一个(指向相同的内存空间)。
有人会说,本来就是这样,难道这里还有什么小九九吗?先看一段代码,从结果分析一下你就知道问题所在了:
package streamAndFile; import java.io.*; public class ObjectStreamEnum { public static void main(String[] args) { try { ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("e://enum.dat")); out.writeObject(Shrubbery.CRAWLING);
//序列化一个单体Orientation.HORIZONTAL out.writeObject(Orientation.HORIZONTAL); out.close(); ObjectInputStream in = new ObjectInputStream(new FileInputStream("e://enum.dat")); Shrubbery newShr = (Shrubbery) in.readObject();
//反序列化得到一个单体orient,那么,在本地而言,orient和Orientation.HORIZONTAL是同一个对象吗? Orientation orient = (Orientation) in.readObject(); in.close(); //使用enum关键字是可以工作的 System.out.println(newShr == Shrubbery.CRAWLING); /* * 如果没有在Orientation中定义readResolve()方法,则会返回false。 * 在反序列化的过程中,对于单体(singleton)而言,即便其构造器是私有的, * 在readObject()的时候还是会创造一个新的对象,然后返回。 * 而,readResolve()方法会在反序列化之后调用,所以,我们可以在这个函数中 * 做一下处理,把不一样的对象设法变成一样的。 */ System.out.println(orient == Orientation.HORIZONTAL); } catch (Exception e) { e.printStackTrace(); } } }
打印的结果:
true false
从第二个false可以看出,反序列化得到的单体和本地的单体并不是同一个对象。这个,太不能让人接受了!!! 但是,通过enum关键字得到的对象在其序列化之后还是同一个。
那么,怎样解决这个问题呢?答案是:在单体类中加入一个protected Object readResolve() throws ObjectStreamException方法,在这个方法中设法让不同的对象变得相同。
两个对象不一样的原因是:在 readObject() 方法中会构造一个新的对象,然后返回。即便,你单体只有一个private属性的构造器(谁叫java有个反射呢)。
解决方案的原理:如果实现了 Serializable 接口的类中具有一个readResolve()方法,那么这个方法会在反序列化完成之后调用。我么就可以在这个方法中做点儿手脚。
所以,修改以后的Orientation类就是下面这样:
package streamAndFile; class Orientation implements Serializable{ private static final long serialVersionUID = 1L; public static final Orientation HORIZONTAL = new Orientation(1); public static final Orientation VERTICAL = new Orientation(2); private int val; private Orientation(int val) { this.val = val; } public int getVal() { return val; } /** * if the <b>readResolve</b> method is defined, it is called after * the object is deserialized. It must return an object that then * becomes the return value of the <b>readObject</b> method. */ protected Object readResolve() throws ObjectStreamException {
//如果反序列化的结果是1,我就给你返回一个本地方法得到的对象Orientation.HORIZONTAL。这样肯定就一直了。原理其实简单。 if(val == 1) return Orientation.HORIZONTAL; if(val == 2) return Orientation.VERTICAL; return null; } }
再运行上面的 ObjectStreamEnum 就会得到两个true。
总结为一点:使用enum关键字就不会有这个问题,还是多使用enum。而少自己定义单体,省得麻烦。
6、序列化到底是个什么东西?!
(试图去追寻事物背后的真相是一件累和危险的事情。说错了,往往会招人嫌弃。)
java为序列化制定了一个特定的文件规范。序列化过程就是将对象转换成一串特定格式的数据,该数据可以保存在本地文件中,也可以通过网络共享和传递。
反序列化过程就是按照制定好的规范,将这一串数据转换为一个对象。
具体的文件格式规范可以自行参考相关的文献。