1. 序列化概述
序列化是将对象状态转换为字节流的过程;反序列化则执行相反的操作。简单来说,序列化就是把 Java 对象转换成静态的字节序列,我们可以将这些字节保存到数据库或通过网络传输。
2. 序列化与反序列化机制
序列化过程与实例无关——比如我们可以在一个平台上序列化对象,然后在另一个平台上反序列化。可序列化的类必须实现一个特殊的标记接口 Serializable
。
ObjectInputStream
和 ObjectOutputStream
是分别继承自 java.io.InputStream
和 java.io.OutputStream
的高级类。ObjectOutputStream
能将基本类型和对象图以字节流形式写入 OutputStream
,而 ObjectInputStream
则负责读取这些流。
ObjectOutputStream
中最核心的方法是:
public final void writeObject(Object o) throws IOException;
这个方法接收一个可序列化对象,并将其转换为字节序列。相应地,ObjectInputStream
的核心方法是:
public final Object readObject()
throws IOException, ClassNotFoundException;
此方法能读取字节流并将其转换回 Java 对象,之后可以强制转型为原始对象类型。
我们用 Person
类来演示序列化。注意:静态字段属于类而非对象,不会被序列化。此外,可以用 transient
关键字跳过序列化特定字段:
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
static String country = "ITALY";
private int age;
private String name;
transient int height;
// getters and setters
}
下面的测试展示了将 Person
对象保存到本地文件再读取回来的过程:
@Test
public void whenSerializingAndDeserializing_ThenObjectIsTheSame() ()
throws IOException, ClassNotFoundException {
Person person = new Person();
person.setAge(20);
person.setName("Joe");
FileOutputStream fileOutputStream
= new FileOutputStream("yourfile.txt");
ObjectOutputStream objectOutputStream
= new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(person);
objectOutputStream.flush();
objectOutputStream.close();
FileInputStream fileInputStream
= new FileInputStream("yourfile.txt");
ObjectInputStream objectInputStream
= new ObjectInputStream(fileInputStream);
Person p2 = (Person) objectInputStream.readObject();
objectInputStream.close();
assertTrue(p2.getAge() == person.getAge());
assertTrue(p2.getName().equals(person.getName()));
}
我们使用 ObjectOutputStream
通过 FileOutputStream
将对象状态保存到文件。文件 "yourfile.txt"
会在项目目录下创建。接着用 FileInputStream
加载该文件,ObjectInputStream
读取字节流并重建名为 p2
的新对象。
最后验证加载对象的状态是否与原始对象一致。
⚠️ 注意:必须显式将加载对象强制转型为 Person
类型。
3. Java 序列化注意事项
序列化有几个需要留意的坑:
3.1. 继承与组合问题
- ✅ 当类实现
java.io.Serializable
接口时,其所有子类也自动可序列化 - ❌ 当对象引用其他对象时,被引用对象**必须单独实现
Serializable
**,否则会抛出NotSerializableException
public class Person implements Serializable {
private int age;
private String name;
private Address country; // 也必须可序列化
}
如果可序列化对象的某个字段是对象数组,那么数组中的所有对象都必须可序列化,否则同样会抛出 NotSerializableException
。
3.2. 序列版本 UID
JVM 会为每个可序列化类关联一个版本号(long 类型)。这个 serialVersionUID
用于验证序列化和反序列化的对象属性是否一致,确保兼容性。
- 大多数 IDE 能自动生成该值,基于类名、属性及访问修饰符计算
- 类的任何变更都会导致版本号变化,可能引发
InvalidClassException
- 如果可序列化类未声明
serialVersionUID
,JVM 会在运行时自动生成 - ✅ 强烈建议显式声明
serialVersionUID
,因为自动生成的值依赖编译器,可能导致意外的InvalidClassException
3.3. 自定义序列化
Java 虽然提供了默认序列化方式,但允许类覆盖此行为。当需要序列化包含不可序列化属性的对象时,自定义序列化特别有用。通过在类中定义以下两个方法实现:
private void writeObject(ObjectOutputStream out) throws IOException;
和
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException;
通过这些方法,我们可以将不可序列化属性转换为可序列化的形式:
public class Employee implements Serializable {
private static final long serialVersionUID = 1L;
private transient Address address; // 标记为 transient
private Person person;
// setters and getters
private void writeObject(ObjectOutputStream oos)
throws IOException {
oos.defaultWriteObject(); // 默认序列化
oos.writeObject(address.getHouseNumber()); // 手动处理 Address
}
private void readObject(ObjectInputStream ois)
throws ClassNotFoundException, IOException {
ois.defaultReadObject(); // 默认反序列化
Integer houseNumber = (Integer) ois.readObject();
Address a = new Address();
a.setHouseNumber(houseNumber);
this.setAddress(a);
}
}
public class Address {
private int houseNumber;
// setters and getters
}
运行以下单元测试验证自定义序列化:
@Test
public void whenCustomSerializingAndDeserializing_ThenObjectIsTheSame()
throws IOException, ClassNotFoundException {
Person p = new Person();
p.setAge(20);
p.setName("Joe");
Address a = new Address();
a.setHouseNumber(1);
Employee e = new Employee();
e.setPerson(p);
e.setAddress(a);
FileOutputStream fileOutputStream
= new FileOutputStream("yourfile2.txt");
ObjectOutputStream objectOutputStream
= new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(e);
objectOutputStream.flush();
objectOutputStream.close();
FileInputStream fileInputStream
= new FileInputStream("yourfile2.txt");
ObjectInputStream objectInputStream
= new ObjectInputStream(fileInputStream);
Employee e2 = (Employee) objectInputStream.readObject();
objectInputStream.close();
assertTrue(
e2.getPerson().getAge() == e.getPerson().getAge());
assertTrue(
e2.getAddress().getHouseNumber() == e.getAddress().getHouseNumber());
}
这段代码展示了如何通过自定义序列化处理不可序列化的 Address
对象。注意:必须将不可序列化属性标记为 transient
以避免 NotSerializableException
。
4. 总结
本文简要介绍了 Java 序列化机制,讨论了常见注意事项,并演示了如何实现自定义序列化。
本文示例代码可在 GitHub 获取。