本文主要介绍经典的序列化的实现方式Serializable和序列化被transient修饰的属性。
这里说到两个概念,序列化和反序列化。
序列化:简单理解就是把程序里面生成的对象以文件的形式保存到本地硬盘中,序列化写入文件的IO是ObjectOutputStream流。
反序列化:就是把序列化的对象文件导入到程序中,并生成为一个对象,供程序使用。反序列化的读取对象文件的IO流是ObjectInputStream。
java中,经典的方式实现对象序列化,序列化的类需要实现Serializable接口和定义serialVersionUID常量。
Serializable接口:只是一个标记,JVM在序列化的时候,会去判断要序列化的对象是否有实现Serializable接口,如果没有实现就会报错,不允许系列化。
serialVersionUID常量:是指JVM在序列化对象的时候,会把这个常量表示序列化对象所属的类的类ID。在反序列化时,反序列化对象的serialVersionUID能匹配上程序里面的类的serialVersionUID时,就判断这个反序列化的对象就是这个类生成的,因此允许反序列化。
还有一点需要注意的,就是如果需要序列化对象本身的属性,那么该属性的类也要实现Serializable接口,否则不能序列化。
这里有一个简单的实例化的例子:
1、先定义Emp接口实现Serializable接口。
public interface Emp extends Serializable{
public void work();
}
2、Employee对象实现Emp接口,需要序列化这个对象的属性empNo、dept、 name。
注意,这里的属性时Stirng对象,查看源代码知道,String也是实现了Serializable接口,因此可以序列化。
public class Employee implements Emp{
/* 序列化编号 */
private static final long serialVersionUID = 3694902274397865665L;
private String empNo;
//透明化处理,不能持久化
private transient String dept;
private String name;
public Employee(String empNo, String dept, String name) {
super();
this.empNo = empNo;
this.dept = dept;
this.name = name;
}
public Employee() {
super();
}
public void work() {
System.out.println("name:" + this.name + ",empNo:" + this.empNo + ",dept:" + this.dept + " is working.");
}
@Override
public String toString() {
return "Employee [empNo=" + empNo + ", dept=" + dept + ", name=" + name
+ "]";
}
}
3、写一个简单的实例化实现:
public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
String file = "D:\\test\\IO\\serialize\\e1.dat";
Emp e1 = new Employee("001", "技术部", "张三");
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file));
out.writeObject(e1);
out.close();
System.out.println(e1 + "序列化成功");
ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
Emp e2 = (Emp)in.readObject();;
in.close();
System.out.println(e2 + "反序列化成功");
}
这里生成了一个e1.dat的Employee 对象文件。
看看控制台输出的日志:
Employee [empNo=001, dept=技术部, name=张三]序列化成功
Employee [empNo=001, dept=null, name=张三]反序列化成功
这里就发现一个比较严重的问题,就是生成的对象是: [empNo=001, dept=技术部, name=张三],但最终序列化的对象是: [empNo=001, dept=null, name=张三],dept属性不能被序列化。
为什么呢?
这里就是我们上面说到的,由于Employee类中定义的dept属性,使用了transient关键字修饰。使用了transient修饰的对象是不能被序列化的。
那怎么办呢?
总不能把transient关键字删除吧,因为这个是项目中已经投产了,不能随意更改属性信息。
其实SUN还是提供了一些后门给我们处理这种问题的,就是实现了Serializable接口的类。如果需要序列化被transient修饰的属性,可以通过定义:writeObject(java.io.ObjectOutputStream s)序列化对象和readObject(java.io.ObjectInputStream s)反序列化对象。这两个方法来实现序列化和反序列化transient的属性。
还是刚才那个例子,我们在Employee类中添加两个方法,实现序列化transient的属性。
public class Employee implements Emp{
/* 序列化编号 */
private static final long serialVersionUID = 3694902274397865665L;
private String empNo;
//透明化处理,不能持久化
private transient String dept;
private String name;
public Employee(String empNo, String dept, String name) {
super();
this.empNo = empNo;
this.dept = dept;
this.name = name;
}
public Employee() {
super();
}
public void work() {
System.out.println("name:" + this.name + ",empNo:" + this.empNo + ",dept:" + this.dept + " is working.");
}
@Override
public String toString() {
return "Employee [empNo=" + empNo + ", dept=" + dept + ", name=" + name
+ "]";
}
// 强行序列化对象属性的方法
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
s.defaultWriteObject();
s.writeObject(this.dept);
}
// 强行反序列化对象属性的方法
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
this.dept = (String)s.readObject();
}
}
再次执行刚才的main方法,得到的结果是,文件重新生成了:
控制台打印的信息:
Employee [empNo=001, dept=技术部, name=张三]序列化成功
Employee [empNo=001, dept=技术部, name=张三]反序列化成功
这次终于把transient修饰的属性进行序列化和反序列化的操作成功了。
因此在这里稍微总结下吧:
通过实现Serializable接口的,默认是不会序列化transient修饰的属性,除非重写writeObject和readObject两个方法。其实这两个方法就是JVM在执行Serializable接口序列化对象时执行的方法。
其实这里还有一点需要注意的,如果对象有多个属性都被transient修饰了,例如是empNo和dept,那么在重写writeObject和readObject这两个方法时,一定要注意这两个属性的写入顺序和读取顺序必须保持一直,否则会导致反序列化失败。我们的例子中由于empNo和dept都是String对象,因此写错顺序也不会报错,只是会出现属性颠倒。
还是代码展示下吧,例如:
// 把写入和读取的属性顺序错乱了,就会导致反序列化对象的属性也错乱
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
s.defaultWriteObject();
s.writeObject(this.dept);
s.writeObject(this.empNo);
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
this.empNo = (String)s.readObject();
this.dept = (String)s.readObject();
}
打印的日志,empNo和dept属性错乱了:
Employee [empNo=001, dept=技术部, name=张三]序列化成功
Employee [empNo=技术部, dept=001, name=张三]反序列化成功
好了,本文就先写到这里了,下次有机会在介绍下另外一种序列化和反序列化的接口:Externalizable,其实原理也是一样的,有兴趣的可以自己去尝试。
我这里先把一些序列化和反序列化的结论整理下:
/**
* 这里实现了两种序列化方式,一种是经典的Serializable,另外一种是Externalizable
* 1、Serializable是自动序列化,因此直接编写序列化编号即可,如果需要序列化transient属性,
* 就需要重写writeObject(ObjectOutputStream s)、readObject(ObjectInputStream s),原则与下面一致。
* 2、Externalizable不是自动序列化,需要重写writeExternal(ObjectOutput out)、readExternal(ObjectInput in)方法,
* 另外也是需要对象有空的构造函数。
* 1)writeExternal、readExternal可以指定具体的序列化的属性。写一个序列化一个属性。
* 2)序列化属性和反序列化,都是按照顺序的。
* 3、其实实现这两个接口是一样效果的,只是Serializable可以自动序列化非transient的变量,而Externalizable不会序列化
* 任何变量。两者需要序列化transient都需要重写一些特定方法。
* 而Serializable的方法是JVM默认的方法,Externalizable则是接口定义的方法。
* 4、序列化数组,反序列化也要使用集合对接,否则会报类转义异常。
* 5、更改了类名,无法反序列化。
*/