Java序列化简介

2023/07/02

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序列化注意事项

有一些注意事项与Java中的序列化有关。

3.1 继承与组合

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

public class Person implements Serializable {
    private int age;
    private String name;
    private Address country; // must be serializable too
}

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

3.2 SerialVersionUID

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

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

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

3.3 Java中的自定义序列化

Java指定了序列化对象的默认方式,但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;
    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);
    }
}
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上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章