1. Serializable 接口
还记得上一节的示例吗?我们想要序列化的 Java 类必须实现一个特殊的接口 — java.io.Serializable。这是所谓的标记接口:它不包含任何方法,只是“标记”该类可以被序列化。如果类实现了这个接口,JVM 就会允许用标准手段序列化它的对象。
不建议对所有东西都进行序列化,因为并不是所有对象都能或都需要被保存为字节。有些对象依赖于操作系统状态、已打开的文件或网络连接。因此,Java 要求显式地将类标记为可序列化。
标记接口就像盒子上的“允许打包”贴纸。如果没有这张贴纸——打包工(JVM)就会拒绝工作。
示例:声明一个可序列化的类
import java.io.Serializable;
public class User implements Serializable {
private String name;
private int age;
// 构造函数、getter 和 setter
public User(String name, int age) {
this.name = name;
this.age = age;
}
// 为了美观:toString() 方法
@Override
public String toString() {
return "User{name='" + name + "', age=" + age + "}";
}
}
请注意:
- 我们只是在类声明中添加了 implements Serializable。
- 不需要实现任何方法(该接口是空的)。
- 所有可以序列化的 Java 标准类(例如 ArrayList、HashMap、String)都已经实现了 Serializable。
2. 如何让自己的类可序列化
规则 №1:只需添加 implements Serializable
对于类本身,这就足够了。但还有一些细节!
重要:所有被引用的对象也必须是可序列化的。
如果你的类有指向其他对象的引用字段,它们也必须是可序列化的。例如:
public class Profile implements Serializable {
private User user; // User 必须是可序列化的!
private int level;
}
只要有一个字段不可序列化,在尝试序列化时就会抛出异常。
3. 序列化与反序列化示例
我们来看看如何把对象序列化到文件并再反序列化回来。为此可以使用类 ObjectOutputStream 和 ObjectInputStream。
示例:将 User 对象序列化到文件
import java.io.*;
public class SerializeDemo {
public static void main(String[] args) {
User user = new User("Alice", 30);
// 将对象保存到文件
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.ser"))) {
oos.writeObject(user);
System.out.println("对象已成功序列化到文件 user.ser");
} catch (IOException e) {
System.out.println("序列化错误: " + e.getMessage());
}
}
}
这里发生了什么?
- 创建一个 User 对象。
- 打开一个 ObjectOutputStream,它写入文件 "user.ser"。
- 调用 writeObject(user)。此时 JVM 将对象转换为字节流并保存到文件。
示例:从文件反序列化对象
import java.io.*;
public class DeserializeDemo {
public static void main(String[] args) {
// 从文件中读取对象
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"))) {
User user = (User) ois.readObject();
System.out.println("对象已成功恢复: " + user);
} catch (IOException | ClassNotFoundException e) {
System.out.println("反序列化错误: " + e.getMessage());
}
}
}
这里发生了什么?
- 打开 ObjectInputStream,从文件 "user.ser" 中读取。
- 调用 readObject()。JVM 从字节中恢复对象。
- 别忘了把结果强制转换为所需类型(User),因为 readObject() 返回的是 Object。
- 如果在反序列化时找不到 User 类,可能会抛出 ClassNotFoundException。
整合:序列化与反序列化
import java.io.*;
public class SerializationExample {
public static void main(String[] args) {
User user = new User("Bob", 22);
// 序列化
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.ser"))) {
oos.writeObject(user);
System.out.println("序列化完成!");
} catch (IOException e) {
System.out.println("序列化错误: " + e.getMessage());
}
// 反序列化
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"))) {
User loaded = (User) ois.readObject();
System.out.println("反序列化完成! " + loaded);
} catch (IOException | ClassNotFoundException e) {
System.out.println("反序列化错误: " + e.getMessage());
}
}
}
结果:
序列化完成!
反序列化完成! User{name='Bob', age=22}
4. 序列化时“底层”发生了什么
当你调用 writeObject 时,JVM 首先会检查该类是否实现了接口 Serializable。如果类未被标记为可序列化,就会抛出异常。随后 JVM 会遍历对象的所有普通字段(即不是 static、也不是 transient 的字段),并将它们的值写入字节流。如果这些字段中包含其他对象,则会对它们递归执行序列化,但前提是它们也实现了 Serializable。
在反序列化时,对象会在不调用常规构造函数的情况下被创建,其字段会用保存的值填充——就像“没有构造函数的构造函数”从字节流中让对象复活。
有些字段不会被序列化。静态字段(static)属于类本身,而不是某个对象,因此它们的值不会被保存。被标记为 transient 的字段也会被跳过——这对于临时数据、缓存或密码等敏感信息很有用。
序列化流程示意图
flowchart TB
A[内存中的 User 对象] -- writeObject --> B[ObjectOutputStream]
B -- 保存字节 --> C[文件 user.ser]
C -- readObject --> D[ObjectInputStream]
D -- 恢复 --> E[内存中的 User 对象]
5. 使用 Serializable 时的常见错误
错误 №1:指向不可序列化对象的字段。如果在类 User 中出现一个字段,其类型例如是 Thread 或 Socket,序列化将无法工作。并不是所有对象都可以序列化——务必牢记这一点!
错误 №2:不可序列化的嵌套类。如果类 User 包含一个非 static 的内部类,序列化可能会失败。最好使用 static 嵌套类或独立的类文件。
错误 №3:尝试序列化 static 字段。静态字段不会被序列化——它们属于类而非对象。反序列化后,static 字段的值将是类中定义的值,而不是已序列化对象中保存的值。
错误 №4:类版本不匹配。如果在序列化之后修改了类的结构(例如添加或删除字段),然后尝试反序列化旧对象,可能会抛出 InvalidClassException。为进行版本控制,使用一个名为 serialVersionUID 的特殊字段——我们将在下一节课中详细讨论它。
GO TO FULL VERSION