什么是序列化?

序列化是将对象存储为二进制格式。

在序列化的过程中,对象和它的元数据(比如对象的类名和它的属性名称)存储为二进制格式。并且在需要的时候可以恢复对象(反序列化)。

对象序列化的两个使用场景:

对象持久化:将对象的状态持久化,比如存储到数据库中。

对象远程传输:将对象从一台计算机发送到另外一台计算机。

实现序列化

如果希望对象能够序列化需要实现一个接口,即序列化接口(Java.io.Serializable )。

import java.io.Serializable;
public class Person implements Serializable {
}

通过调用ObjectOutputStream.writeObject(Object o) 来实现将对象持久化

之后通过ObjectInputStream.readObject() 实现反序列化。

父类序列化与Transient关键字

情景:一个子类实现了Serializable接口,它的父类没有实现Serializable接口序列化该对子类对象,然后反序列化输出父类某域的数值,该变量与序列化时的数值不同(为0或为null)。

解决:要想父类对象也序列化,就需要让父类也实现Serializable接口。如果父类不实现的话就需要有默认的无参构造函数,在父类没有实现Serializable接口时,虚拟机是不会序列化父对象的,而一个Java对象的实例化过程必先伴随着父类对象的实例化,实例化也不例外。只能调用父类对象的无参构造函数作为父类实例化。

Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。

我们熟悉使用 Transient 关键字可以使得字段不被序列化,那么还有别的方法吗?根据父类对象序列化的规则,我们可以将不需要被序列化的字段抽取出来放到父类中,子类实现 Serializable 接口,父类不实现,根据父类序列化规则,父类的字段数据将不被序列化。

通过以上我们大致可以分析得出序列化和反序列化的实现是区别与构造函数的,在反序列化的过程中并不会去显式调用已经序列化对象的构造函数,而是当其父类没有实现序列化接口时候再去调用无参构造函数,通过这样的机制可以包装那些不需要序列化的域,所有语言的设计都是有其深层含义的 。这和Java语言设计的品味有关。

当然当一个父类对象实现序列化之后其子类默认实现序列化。

序列化的存储规则

ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("result.obj"));
Test test = new Test();
test.i = 1;
out.writeObject(test);
out.flush();
test.i = 2;
out.writeObject(test);
out.close();
ObjectInputStream oin = new ObjectInputStream(new FileInputStream(
"result.obj"));
Test t1 = (Test) oin.readObject();
Test t2 = (Test) oin.readObject();
System.out.println(t1.i);
System.out.println(t2.i);

按照我们目前的理解输出结果应该分别为1和2,但事实上输出的结果都是1,Java 序列化机制为了节省磁盘空间,具有特定的存储规则,当写入文件的为同一对象时,并不会再将对象的内容进行存储,而只是再次存储一份引用,而当反序列化时,恢复引用关系,使得代码中的 t1 和 t2 指向唯一的对象,第一次写入对象以后,第二次再试图写的时候,虚拟机根据引用关系知道已经有一个相同对象已经写入文件,因此只保存第二次写的引用,所以读取时,都是第一次保存的对象。因此写入相同对象时候需要注意这个问题。

序列化处理敏感字段

在序列化过程中,虚拟机会试图调用对象类里的 writeObject 和 readObject 方法,进行用户自定义的序列化和反序列化,如果没有这样的方法,则默认调用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。用户自定义的 writeObject 和 readObject 方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。基于这个原理,可以在实际应用中得到使用,用于敏感字段的加密工作。

public class Person
implements java.io.Serializable
{
public Person(String fn, String ln, int a)
{
this.firstName = fn; this.lastName = ln; this.age = a;
}
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public int getAge() { return age; }
public Person getSpouse() { return spouse; }
public void setFirstName(String value) { firstName = value; }
public void setLastName(String value) { lastName = value; }
public void setAge(int value) { age = value; }
public void setSpouse(Person value) { spouse = value; }
private void writeObject(java.io.ObjectOutputStream stream)
throws java.io.IOException
{
//简单加密数据
age = age << 2;
stream.defaultWriteObject();
}
private void readObject(java.io.ObjectInputStream stream)
throws java.io.IOException, ClassNotFoundException
{
stream.defaultReadObject();
// 数据的解码
age = age << 2;
}
public String toString()
{
return "[Person: firstName=" + firstName +
" lastName=" + lastName +
" age=" + age +
" spouse=" + (spouse!=null ? spouse.getFirstName() : "[null]") +
"]";
}
private String firstName;
private String lastName;
private int age;
private Person spouse;
}

序列化 ID 问题

虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致。即使虽然两个类的功能代码完全一致,但是序列化 ID 不同,他们无法相互序列化和反序列化。序列化 ID 在 Eclipse 下提供了两种生成策略,一个是固定的 1L,一个是随机生成一个不重复的 long 类型数据(实际上是使用 JDK 工具生成),在这里有一个建议,如果没有特殊需求,就是用默认的 1L 就可以,这样可以确保代码一致时反序列化成功。那么随机生成的序列化 ID 有什么作用呢,有些时候,通过改变序列化 ID 可以用来限制某些用户的使用。

private static final long serialVersionUID = 1L;