Java 序列化使用指南

1. 简介

序列化是将对象的状态转换为字节流;反序列化的作用恰恰相反。换句话说,序列化是将 Java 对象转换为静态字节流(序列),然后可以将其保存到数据库或通过网络传输。

2. 序列化和反序列化

序列化过程与实例无关;例如,可以在一个平台上序列化对象,在另一个平台上反序列化它们。符合序列化条件的类需要实现特殊的标记接口 Serializable。

ObjectInputStream 和 ObjectOutputStream 都是分别扩展 java.io.InputStream 和 java.io.OutputStream 的高级类。 ObjectOutputStream 可以将对象的字节流写入 OutputStream。然后,可以使用 ObjectInputStream 读取这些流。

ObjectOutputStream 中最重要的方法是:

代码语言:javascript代码运行次数:0运行复制
public final void writeObject(Object o) throws IOException;

同样,ObjectInputStream 中最重要的方法是:

代码语言:javascript代码运行次数:0运行复制
public final Object readObject() 
  throws IOException, ClassNotFoundException;

此方法可以读取字节流并将其转换回Java对象。然后可以将其转换回原始对象。

让用Person类来演示序列化。注意,静态字段属于类(与对象相反),并且没有序列化。另外,注意可以使用关键字transient在序列化过程中忽略类字段:

代码语言:javascript代码运行次数:0运行复制
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 类型的对象保存到本地文件,然后读回值的示例:

代码语言:javascript代码运行次数:0运行复制
@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 序列化注意事项

有一些关于Java序列化的警告。

3.1. 继承和组合

当一个类实现java.io.Serializable接口时,它的所有子类也是可序列化的。相反,当一个对象引用另一个对象时,这些对象必须单独实现 Serializable 接口,否则将引发 NotSerializableException

代码语言:javascript代码运行次数:0运行复制
public class Person implements Serializable {
    private int age;
    private String name;
    private Address country; // must be serializable too
}

如果可序列化对象中的一个字段由对象数组组成,则所有这些对象也必须是可序列化的,否则将引发 NotSerializableException

3.2. 串行版本 UID

JVM 将版本(long类型)编号与每个可序列化类相关联。使用它来验证保存和加载的对象是否具有相同的属性,因此在序列化时是否兼容。

大多数 IDE 都可以自动生成此编号,它基于类名、属性和关联的访问修饰符。任何更改都会导致不同的数字,并可能导致 InvalidClassException

如果可序列化类没有声明 serialVersionUID,JVM 将在运行时自动生成一个。但是,强烈建议每个类声明其 serialVersionUID,因为生成的类依赖于编译器,因此可能会导致意外的 InvalidClassExceptions

3.3. Java 中的自定义序列化

Java 指定序列化对象的默认方法,但 Java 类可以覆盖此默认行为。尝试序列化具有某些不可序列化属性的对象时,自定义序列化可能特别有用。可以通过在类中提供两个要序列化的方法来实现这一点:

代码语言:javascript代码运行次数:0运行复制
private void writeObject(ObjectOutputStream out) throws IOException;

代码语言:javascript代码运行次数:0运行复制
private void readObject(ObjectInputStream in) 
  throws IOException, ClassNotFoundException;

使用这些方法,可以将不可序列化的属性序列化为可以序列化的其他形式:

代码语言:javascript代码运行次数:0运行复制
public class Employee implements Serializable {
    private static final long serialVersionUID = 1L;
    private transient Address address;
    private Person person;

    // setters and getters

    private void writeObject(ObjectOutputStream oos) 
      throws IOException {
        oos.defaultWriteObject();
        oos.writeObject(address.getHouseNumber());
    }

    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);
    }
}
代码语言:javascript代码运行次数:0运行复制
public class Address {
    private int houseNumber;

    // setters and getters
}

可以运行以下单元测试来测试此自定义序列化:

代码语言:javascript代码运行次数:0运行复制
@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());
}

在此代码中,可以看到如何通过使用自定义序列化序列化地址来保存一些不可序列化的属性。请注意,必须将不可序列化的属性标记为瞬态,以避免 NotSerializableException。

4. 结论

在这篇简短的文章中,回顾了 Java 序列化,讨论了注意事项,并学习了如何进行自定义序列化。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。 原始发表:2023-04-07,如有侵权请联系 cloudcommunity@tencent 删除序列化java测试对象接口