序列化和反序列化是将对象转换为字节流(序列化)和从字节流恢复为对象(反序列化)的过程。这在需要将对象持久化到文件、数据库,或通过网络传输对象时非常有用。让我们详细说明这两个过程,并通过一个简单的例子来说明。
一、序列化的过程
序列化将 Java 对象的状态转换为字节流,这样它可以被存储或传输。以下是序列化的具体步骤:
- 步骤 1:对象实现
Serializable接口。该接口是一个标记接口,没有任何方法,表示该对象可以被序列化。 - 步骤 2:使用
ObjectOutputStream将对象写入到输出流中。ObjectOutputStream会将对象转换为字节流,并将其写入文件或网络中。 - 步骤 3:在序列化过程中,Java 会记录对象的类信息(包括类的
serialVersionUID)和对象的数据(字段的值)。
import java.io.*;
class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}
public class SerializationExample {
public static void main(String[] args) {
Person person = new Person("Alice", 30);
// 序列化过程
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.ser"))) {
oos.writeObject(person); // 将对象写入到文件中
System.out.println("序列化完成: " + person);
} catch (IOException e) {
e.printStackTrace();
}
}
}
在这个例子中,Person类实现了Serializable接口。在 main() 方法中,我们创建了一个 Person 对象,并使用 ObjectOutputStream 将它写入到文件 person.ser 中。
二、反序列化的过程
反序列化是将字节流恢复为 Java 对象的过程。具体步骤如下:
- 步骤 1:使用
ObjectInputStream从输入流中读取字节流。 - 步骤 2:
ObjectInputStream读取字节流并将其转换为对象。在这个过程中,Java 会检查字节流中记录的类信息(包括类名和serialVersionUID)是否与当前类匹配。 - 步骤 3:如果类信息匹配,Java 会重新创建对象,并将字段值恢复到对象中。
import java.io.*;
public class DeserializationExample {
public static void main(String[] args) {
// 反序列化过程
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.ser"))) {
Person person = (Person) ois.readObject(); // 从文件中读取对象
System.out.println("反序列化完成: " + person);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
在这个例子中,ObjectInputStream 从文件 person.ser 中读取对象,并将其恢复为 Person 对象。恢复后的对象保留了原始对象的状态(name 和 age 字段的值)。
三、序列化与反序列化过程的详细描述
-
序列化过程:
- 类检查:Java 首先检查对象所属的类是否实现了
Serializable接口。如果没有实现,会抛出NotSerializableException。 - 对象写入流:
ObjectOutputStream将对象的类信息(类名、serialVersionUID)和对象的字段值(包括基本类型和引用类型)写入到输出流中。 - 写入
serialVersionUID:Java 会将类的serialVersionUID写入到字节流中,以便在反序列化时验证类的版本。 - 对象的嵌套序列化:如果对象包含引用其他对象(例如嵌套对象),这些嵌套对象也会被递归地序列化。
- 类检查:Java 首先检查对象所属的类是否实现了
-
反序列化过程:
- 读取类信息:
ObjectInputStream从输入流中读取字节流,包括类的完全限定名和serialVersionUID。 - 类匹配:Java 会检查字节流中的类信息与当前的类是否匹配。如果类名或
serialVersionUID不匹配,会抛出InvalidClassException。 - 恢复对象:如果类信息匹配,Java 会创建对象的实例,并将字段值从字节流中恢复到对象中。
- 嵌套对象恢复:如果对象包含引用其他对象(例如嵌套对象),这些对象也会被递归地反序列化。
- 读取类信息:
四、序列化和反序列化中的注意事项
serialVersionUID:serialVersionUID是序列化机制中用于验证类版本一致性的标识。如果类的结构发生变化且serialVersionUID不匹配,反序列化会失败。因此,在类可能发生变化的情况下,手动设置serialVersionUID是一种好习惯。- 对象的深度序列化:序列化会递归地序列化对象中所有引用的对象,因此所有被引用的对象也需要实现
Serializable接口。 transient关键字:如果某个字段不需要序列化,可以使用transient关键字标记,这样该字段在序列化过程中会被忽略。
五、总结
- 序列化:将对象的状态转换为字节流,用于存储或传输。
- 反序列化:将字节流恢复为对象,用于读取存储的数据或接收传输的对象。
serialVersionUID:用于验证类的版本是否一致,确保序列化和反序列化过程的兼容性。
六、题外话 - 防止反序列化破坏单例
下面是获取单例对象的一种方法
如果实现了序列化接口, 还要做什么来防止反序列化破坏单例
public final class Singleton implements Serializable {
private Singleton() {}
private static final Singleton INSTANCE = new Singleton();
public static Singleton getInstance() {
return INSTANCE;
}
public Object readResolve() {
return INSTANCE;
}
}
单例模式与序列化的挑战
单例模式旨在确保某个类只有一个实例,并提供全局访问点。问题在于,当单例对象被序列化和反序列化时,可能会出现以下情况:
- 序列化: 对单例对象进行序列化时,实际上是将单例对象的状态写入到一个字节流中。
- 反序列化: 当从字节流中反序列化对象时,Java 的默认行为是创建一个新的实例,而不是返回原来的单例实例。
这样,虽然初始的单例实现是正确的,但反序列化操作可能导致创建出新的对象实例,从而破坏了单例模式的唯一性。
readResolve() 方法的作用
为了解决这个问题,Java 提供了 readResolve() 方法。这是 Serializable 接口的一部分,允许类在反序列化时进行自定义的处理。
- 定义:
readResolve()是一个特殊的方法,它的返回值会代替反序列化过程中的新创建对象的引用。 - 作用: 当 Java 在反序列化过程中创建一个新的对象实例时,
readResolve()方法被调用,它返回单例类的唯一实例。这个返回的实例会替代新创建的对象,从而保证反序列化后的对象仍然是单例。
示例代码分析
在你的单例类中,readResolve() 方法的实现如下:
public Object readResolve() {
return INSTANCE;
}
工作原理:
- 反序列化过程: 当一个序列化的单例对象被读取(反序列化)时,Java 会首先创建一个新的对象实例。
- 调用
readResolve(): 在创建新对象之后,Java 会调用readResolve()方法。 - 返回单例实例:
readResolve()方法返回INSTANCE,这是单例类中唯一的实例。 - 替换新对象: 返回的
INSTANCE替代了新创建的对象实例,因此最终得到的对象引用仍然是单例的唯一实例。
结果: 即使通过反序列化创建了一个新对象,readResolve() 方法确保了在反序列化后返回的对象是唯一的单例实例,从而保持了单例模式的正确性。
关键点总结
- 序列化: 将对象的状态转换为字节流。
- 反序列化: 从字节流中恢复对象状态,可能导致新的实例创建。
readResolve()方法: 确保在反序列化过程中返回的是单例实例,防止创建新的对象实例。
例外情况和注意事项
- 反射攻击: 即使
readResolve()方法可以防止反序列化破坏单例,但如果不采取额外措施(如反射检查),仍然可能通过反射创建多个实例。 - 实现细节: 在某些实现中,还需要确保类的构造函数和
readResolve()方法能够有效地防止其他类型的攻击或不一致性。