0%

Java专题:序列化

概述

类通过实现 java.io.Serializable 接口启用序列化功能。该接口只是一个标记接口。

由于使用接口标记该特性,所以,可序列化类的子类也是可序列化的。

相反,不可序列化类的可序列化子类有一些限制。

由于,超类不可序列化,所以除非子类负责保存和恢复超类的字段状态,否则它们将被忽略。

反序列化过程要求超类必须有一个子类可访问的无参构造器,否则将抛出 java.io.InvalidClassException

特殊方法

方法签名 描述
private void writeObject(java.io.ObjectOutputStream out) throws IOException 写入特定类的对象字段,以便 readObject() 方法恢复。注意调用 out.defaultWriteObject() 执行默认序列化,否则需要手动写入。
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException; 恢复对象字段。注意调用 in.defaultReadObject() 执行默认反序列化,否则需要手动读取。
private void readObjectNoData() throws ObjectStreamException; 为变更超类的序列化对象在反序列化时为新的超类属性赋值。
ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException; 替换序列化时写入的对象。
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException; 替换反序列化时读取的对象。

readResolve() 的一经典应用场景是在单例模式中,为了防止通过反序列化突破单例限制,通常会使用该方法返回单例实例。

版本

序列化使用 serialVersionUID 字段来验证发送者和接收者对应类的兼容性,如果不同则抛出 InvalidClassException

serialVersionUID 的完整定义如下:

1
ANY-ACCESS-MODIFIER static final long serialVersionUID = 1L;

若未显式声明 serialVersionUID,则序列化运行时将基于类的各个方面计算。

强烈建议显式声明 serialVersionUID。因为默认 serialVersionUID 的计算对类详细信息具有较高敏感性,不同编译器可能有差异, 从而导致反序列化时意外的 InvalidClassException

强烈建议将 serialVersionUID 声明为 private。因为该声明仅应用于直接声明类,继承没有用处。

数组类不能声明 serialVersionUID,它们总是计算值,但数组类没有匹配 serialVersionUID 值的要求。

附录

不可序列化类的继承限制

必须的无参构造器

假设定义如下一个没有无参构造器的不可序列化超类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class SuperClass
{
protected String name;

// 注意:没有定义无参构造器
public SuperClass(String name)
{
this.name = name;
}

public String getName()
{
return name;
}

public void setName(String name)
{
this.name = name;
}
}

可序列化子类定义如下:

1
2
3
4
5
6
7
8
9
public class SubClass extends SuperClass implements Serializable
{
private static final long serialVersionUID = 1L;

public SubClass(String name)
{
super(name);
}
}

下面测试用例先序列化子类后,再尝试反序列化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@FixMethodOrder(MethodSorters.JVM)
public class SerializableTester
{
@Test
public void write() throws Exception
{
SubClass subClass = new SubClass("Eric");
try (FileOutputStream fileOutputStream = new FileOutputStream("SubClass.jo");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);)
{
objectOutputStream.writeObject(subClass);
System.out.println(new File("SubClass.jo").getAbsolutePath());
}
}

@Test
public void read() throws Exception
{
try (FileInputStream fileInputStream = new FileInputStream("SubClass.jo");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);)
{
SubClass subClass = (SubClass) objectInputStream.readObject();
System.out.println(subClass.getName());
}
}
}

用例执行结果是,序列化方法 write() 成功,但反序列化方法 read() 失败了,因为超类中没有可访问的无参构造器。

超类字段值的丢失

假设我们为超类添加一个无参构造器,那么上述用例将成功执行,但是,反序列化得到的对象 name 字段为 null

这是由于 name 字段在超类声明,但是超类不可序列化,所以其字段值默认不会序列化保存。

但是,我们可以让子类来负责保存和恢复超类的字段,只需要添加如下两个方法即可:

1
2
3
4
5
6
7
8
9
10
11
private void writeObject(java.io.ObjectOutputStream out) throws IOException
{
out.defaultWriteObject();
out.writeUTF(getName());
}

private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException
{
in.defaultReadObject();
setName(in.readUTF());
}

超类的变更

Java API 中,对 readObjectNoData() 方法使用的说明相当地晦涩。其实,该方法主要用来处理一种不太常见的情况, 即我们序列化了某个类的对象时其父类是 A,但当反序列化时该类的父类被修改为 B 了,而我们又想为 B 中的属性指定值, 此时,就需要编写该方法了。

先定义一个父类 Animal:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Animal implements Serializable
{
private static final long serialVersionUID = 1L;

private String scientificName;

public String getScientificName()
{
return scientificName;
}

public void setScientificName(String scientificName)
{
this.scientificName = scientificName;
}
}

再为其定义一个子类 HumanBing:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class HumanBing extends Animal
{
private static final long serialVersionUID = 1L;

private String name;

public String getName()
{
return name;
}

public void setName(String name)
{
this.name = name;
}
}

现在编写一个测试方法,创建一个子类对象,然后序列化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void write() throws Exception
{
HumanBing humanBing = new HumanBing();
humanBing.setName("Eric");
humanBing.setScientificName("Human Bing");

try (FileOutputStream fileOutputStream = new FileOutputStream("HumanBing.jo");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);)
{
objectOutputStream.writeObject(humanBing);
System.out.println(new File("HumanBing.jo").getAbsolutePath());
}
}

现在,由于某些原因,我们重新定义一个类 HigherAnimal,并用其替换 HumanBing 的父类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class HigherAnimal implements Serializable
{
private static final long serialVersionUID = 1L;

private int age;

public int getAge()
{
return age;
}

public void setAge(int age)
{
this.age = age;
}
}

现编写一个测试方法从旧版的序列化文件中反序列化 HumanBing:

1
2
3
4
5
6
7
8
9
10
11
@Test
public void read() throws Exception
{
try (FileInputStream fileInputStream = new FileInputStream("HumanBing.jo");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);)
{
HumanBing humanBing = (HumanBing) objectInputStream.readObject();
System.out.println(humanBing.getName());
System.out.println(humanBing.getAge());
}
}

反序列化将成功,并读取回 name 属性。当然,scientificName 属性不会被读取回,即使在新父类或子类中添加该属性也不能。

但是,另一方面,我们又想为新的父类 HigherAnimal 中的 age 属性赋值。那么,需要在 HigherAnimal 中定义如下方法:

1
2
3
4
private void readObjectNoData() throws ObjectStreamException
{
age = 30;
}