概述
类通过实现 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; }
|