1. 序列化概述

序列化是将对象状态转换为字节流的过程;反序列化则执行相反的操作。简单来说,序列化就是把 Java 对象转换成静态的字节序列,我们可以将这些字节保存到数据库或通过网络传输。

2. 序列化与反序列化机制

序列化过程与实例无关——比如我们可以在一个平台上序列化对象,然后在另一个平台上反序列化。可序列化的类必须实现一个特殊的标记接口 Serializable

ObjectInputStreamObjectOutputStream 是分别继承自 java.io.InputStreamjava.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 获取。


原始标题:Introduction to Java Serialization