Java 序列化与反序列化
序列化机制允许将实现序列化的 Java 对象转换成字节序列,保存在磁盘上,或者通过网络传输,以备以后重新恢复成原来的对象。
序列化(Serialize):将一个 Java 对象写入 IO 流中;
反序列化(Deserialize):从 IO 流中恢复该 Java 对象。
在 Java 中,让某个对象支持序列化机制,它的类必须实现如下两个接口之一:
- Serializable
- Externalizable
两个对比,Externalizable 在编程上会复杂很多,所以大多数时候都是用 Serializable 进行序列化,这里只介绍 Serializable
使用 Serializable 进行序列化
使用 Serializable 来实现序列化非常简单,主要让目标类实现 Serializable 接口即可,无需实现任何方法。
下面通过具体代码说明:
可序列化的实体类
import java.io.Serializable;
/**
* 实现 Serializable接口,表明这个类可序列化
*/
public class Person implements Serializable {
private String name;
private int age;
public Person(String name, int age) {
System.out.println("这是构造器");
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
// 为了测试输出使用 toString()
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
客户端序列化测试
import java.io.*;
public class SerialTest {
public static void main(String[] args) {
try {
// 创建 ObjectOutputStream 输出流,用于序列化对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
//创建一个对象
Person person = new Person("诸葛亮", 32);
//将对象写入输出流
oos.writeObject(person);
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行客户端程序后,可以在 object.txt 中看到 person 对象的信息(当然,是乱码的形式)。
ObjectOutputStream 输出流和 ObjectInputStream 输入流是用于对象序列化的一个处理流,建立在一个文件节点流之上,具体可以去看 API 。
反序列化操作
import java.io.*;
public class SerialTest {
public static void main(String[] args) {
try {
// 创建 ObjectInputStream 输入流
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
// 从输入流读取一个 Java 对象,并强转为 Person 对象
Person person = (Person) ois.readObject();
System.out.println(person);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
在控制到输出
Person{name='诸葛亮', age=32}
这就完成了一个反序列化过程。这个过程有几个重点要注意的地方:
- 反序列化读取的仅仅的 Java 对象的数据,而不是 Java 类,所以反序列化时必须提供 Java 对象所属类的 class 文件,否则就会有 ClassNotFoundException 异常。
- 反序列化时,并没有看到控制台打印构造器里的输出语句,这表明反序列化机制无须用过构造器来初始化 Java 对象。
- 当一个可序列化类有多个父类时(直接父类或间接父类),这些父类要么有无参构造器,要么是可序列化的----否则反序列化时会抛异常。如果父类只是带无参构造器,并不是可序列化的,则该父类中定义的成员变量值不会序列化到二进制流中。
- 如果使用序列化机制向同一个文件中写入多个 Java 对象,使用反序列化时必须按对象实际写入的顺序读取。
对象引用的序列化
前面使用的成员变量分别是 String 和 int 类型,如果一个类的成员变量类型不是基本数据类型或 String 类型,而是另一个引用类型,那么这个引用类必须是可序列化的,否则拥有该类型成员变量的类也是不可序列化的。
public class Teacher implements Serializable {
private String name;
// 要求 Person 是可序列化的
private Person student;
public Teacher(String name, Person student) {
this.name = name;
this.student = student;
}
//以下省略 setter 和 getter 方法
...
}
【提示】
因为 Teacher 对象持有一个 Person 对象的引用,在序列化 Teacher 对象时程序会顺带将该 Person 对象也进行序列化,所以要求 Person 类必须是可序列化的,否则 Teacher 类序列化会失效。
序列化多个对象
import java.io.*;
public class SerialTest {
public static void main(String[] args) {
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
Person per = new Person("孙悟空", 500);
Teacher t1 = new Teacher("唐三藏", per);
Teacher t2 = new Teacher("菩提祖师", per);
//依次写入4个对象,其中 t1 写入两次
oos.writeObject(per);
oos.writeObject(t1);
oos.writeObject(t2);
oos.writeObject(t1);
} catch (Exception e) {
e.printStackTrace();
}
}
}
上面依次序列化了 4 个对象,其中 t1 重复写入,后面比较反序列化的对象是否为同一个。
反序列化
import java.io.*;
public class SerialTest {
public static void main(String[] args) {
try {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
//依次从输入流中取出 4个 对象
Person person = (Person) ois.readObject();
Teacher t1 = (Teacher) ois.readObject();
Teacher t2 = (Teacher) ois.readObject();
Teacher t3 = (Teacher) ois.readObject();
System.out.println(person); // 输出 Person{name='孙悟空', age=500}
System.out.println(t1.getStudent() == person); // 输出 true
System.out.println(t2.getStudent() == person); // 输出 true
System.out.println(t2 == t1); // 输出 false
System.out.println(t3 == t1); // 输出 true
} catch (Exception e) {
e.printStackTrace();
}
}
}
在上面代码中,t1,t2,t3 的类都是 Teacher,通过比较 t1 和 t2 不是同一对象,但 t1 和 t3 是同一个对象,这是为什么呢?
首先我们要知道这几个对象在 JVM 上的内存分配是怎么样的,如图所示:
在序列化对象的时候,Java 采用了一种特殊的序列化算法,大致如下。
- 所有保存到磁盘中的对象都有一个序列化编号。
- 当程序视图序列化一个对象时,程序将先检查该对象是否已经被序列化过,只有该对象(在本次虚拟机中)从未被序列化过,系统才会将该对象转换成字节序列并输出。
- 如果某个对象已经序列化过,程序将会直接输出一个序列化编号,而不是再次重新序列化该对象。
所以在上面代码中,实际上只序列化了三个对象。t1 对象已经被序列化过,再次执行序列化时直接输出它的序列化编号,所以在磁盘当中其实只存在一个,所以在判断 t3 == t1 时输出为 true
.
但这样就引起了一个潜在的问题 ---- 当序列化一个可变对象时,在序列化一次对象之后,去改变这个对象的值再序列化一次,程序只是输出前面的序列化编号,后面改变的值并没有被序列化保存。
import java.io.*;
public class SerialTest {
public static void main(String[] args) {
try {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
//创建对象,并序列化
Person person = new Person("诸葛亮", 35);
oos.writeObject(person);
//更改这个对象的值,再次序列化
person.setName("娜可露露");
oos.writeObject(person);
Person p1 = (Person) ois.readObject();
Person p2 = (Person) ois.readObject();
// 输出 true,即反序列化后 p1 等于 p2
System.out.println(p1 == p2);
// 输出 “诸葛亮”,即更改后的实例变量并没有被序列化
System.out.println(p2.getName());
} catch (Exception e) {
e.printStackTrace();
}
}
}
自定义序列化
在一些特殊的场景,如果一个类中的某些变量是敏感信息,例如银行账号信息等,这时不希望系统将该实例变量值序列化;或者某个变量的引用类型是不可序列化的,又不想在序列化时抛异常。
可以在实例变量前使用 transient 关键字修饰,可以指定 Java 序列化时无须理会该实例变量。
例如还是一样的 Teacher 类:
public class Teacher implements Serializable {
private String name;
// 在实例化 Teacher 实例时,忽略这个变量;所以即使 Person 不可序列化也没关系
private transient Person student;
public Teacher(String name, Person student) {
this.name = name;
this.student = student;
}
//以下省略 setter 和 getter 方法
...
}
【提示】
transient 关键字只能用于修饰实例变量,不可修饰 Java 程序中的其他成分。
.
版本号
前面说过,反序列化 Java 对象时必须提供该对象的 class 文件,现在的问题是,随着项目的升级,系统的 class 文件也会升级,Java 如何保证两个 class 文件的兼容性?
没错,就是版本号!
Java 序列化机制中,提供有一个 private static final 的 serialVersionUID 变量值, 该变量值用于标识该Java 类的序列化版本号。也就是说,如果一个类升级后,只要它的 serialVersionUID 变量值保持不变,序列化机制就会把它们当成同一个序列化版本。
使用很简单:
public class Person implements Serializable {
// 为该类指定一个版本号
private static final long serialVersionUID = 125L;
...
}
如果不显式定义 serialVersionUID 类变量的值,该类变量的值也将由 JVM 根据类的相关信息计算得出,但修改后的类每次自动分配的结果往往不同,从而造成对象反序列化因为类版本不兼容而失败。
还有一个缺点就是,不利于不同 JVM 之间的移植,因为不同的 JVM 计算的策略可能不同。
总的来说,就是每次我们要使用序列化时,都显示定义版本号就行了。
.
如果类的修改确实会导致反序列化失败,则应该为该类的 serialVersionUID 类变量重新分配值。
那对类的哪些修改可能会导致反序列化失败呢?分几种情况讨论。
- 如果修改类时仅仅修改了方法,反序列化不受影响。
- 如果修改类时仅仅修改了静态变量或瞬态实例变量(瞬态变量就是被 transient 修饰的变量),反序列化也不受影响。
- 如果修改类时修改了非瞬态的实例变量,则可能导致序列化版本不兼容,这时应该更新 serialVersionUID 类变量的值。
- 如果只是新增或者删除类中的实例变量,序列化版本可以兼容,这时可以不更新 serialVersionUID 类变量的值;但是反序列化得到的新增的实例变量值都是 null 或 0 。