Java readobject()和writeObject()的用法(附带实例)
在极少数情况下,您可能需要调整序列化机制。可序列化类可以通过定义具有以下签名的方向,向默认的读取和写入行为添加任何需要的操作:
注意 @Serial 注解。对序列化进行调整的方法不属于接口。因此,你将不能使用 @Override 注解让编译器检查方法声明。@Serial 注解的含义是为序列化方法启用相同的检查。到 Java 17 为止,javac 编译器都不进行这种检查,但并不保证未来是否会进行注解检查。一些 IDE 会执行该检查。
java.awt.geom 包中的许多类,例如,Point2D.Double 就是不可序列化的。现在,假设你想序列化一个类 LabeledPoint,该类存储了 String 和 Point2D.Double。
首先,需要将 Point2D.Double 字段标记为 transient,以避免抛出 NotSerializableException 异常:
在 writeObject() 方法中,首先通过调用 defaultWriteObject() 方法,来完成对象描述符和 String 字段 label 的写入。该方法是 ObjectOutputStream 类中的一个特殊方法,只能在可序列化类的 writeObject() 方法中调用。这样我们就可以使用标准 DataOutput 调用来写入点的坐标了。
在 readObject() 方法中,我们反过来执行上述过程:
另一个例子是 HashSet 类,这个类提供自己的 readObject() 和 writeObject() 方法:
readObject() 和 writeObject() 方法只需要保存和加载数据。它们不关心超类数据或任何其他类的信息。
Date 类使用这样一种方法,它的 writeObject() 方法保存了从“纪元”(1970 年 1 月 1 日)以来的毫秒数,但是缓存日历数据的数据结构并未被存储。
就像构造器一样,readObject() 方法是一种针对初始化对象的操作。如果你在 readObject() 中调用了一个在子类中被覆盖的非 final() 方法,那么它可能会访问未初始化的数据。
注意,如果一个可序列化类定义了以下字段,那么序列化将使用这些字段描述符来代替非瞬态、非静态字段:
@Serial private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException @Serial private void writeObject(ObjectOutputStream out) throws IOException这样,对象的信息头就会继续像往常一样被写入,但是实例变量字段不再自动序列化。相反,会调用以上方法。
注意 @Serial 注解。对序列化进行调整的方法不属于接口。因此,你将不能使用 @Override 注解让编译器检查方法声明。@Serial 注解的含义是为序列化方法启用相同的检查。到 Java 17 为止,javac 编译器都不进行这种检查,但并不保证未来是否会进行注解检查。一些 IDE 会执行该检查。
java.awt.geom 包中的许多类,例如,Point2D.Double 就是不可序列化的。现在,假设你想序列化一个类 LabeledPoint,该类存储了 String 和 Point2D.Double。
首先,需要将 Point2D.Double 字段标记为 transient,以避免抛出 NotSerializableException 异常:
public class LabeledPoint implements Serializable { private String label; private transient Point2D.Double point; ... }
在 writeObject() 方法中,首先通过调用 defaultWriteObject() 方法,来完成对象描述符和 String 字段 label 的写入。该方法是 ObjectOutputStream 类中的一个特殊方法,只能在可序列化类的 writeObject() 方法中调用。这样我们就可以使用标准 DataOutput 调用来写入点的坐标了。
@Serial before private void writeObject(ObjectOutputStream out) throws IOException { out.defaultWriteObject(); out.writeDouble(point.getX()); out.writeDouble(point.getY()); }
在 readObject() 方法中,我们反过来执行上述过程:
@Serial before private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); double x = in.readDouble(); double y = in.readDouble(); point = new Point2D.Double(x, y); }
另一个例子是 HashSet 类,这个类提供自己的 readObject() 和 writeObject() 方法:
- writeObject() 方法直接存储容量、负载因子、大小和元素,并没有存储哈希表的内部结构;
- readObject() 方法则读回哈希表的容量和负载因子,并构建新的哈希表,最后插入元素。
readObject() 和 writeObject() 方法只需要保存和加载数据。它们不关心超类数据或任何其他类的信息。
Date 类使用这样一种方法,它的 writeObject() 方法保存了从“纪元”(1970 年 1 月 1 日)以来的毫秒数,但是缓存日历数据的数据结构并未被存储。
就像构造器一样,readObject() 方法是一种针对初始化对象的操作。如果你在 readObject() 中调用了一个在子类中被覆盖的非 final() 方法,那么它可能会访问未初始化的数据。
注意,如果一个可序列化类定义了以下字段,那么序列化将使用这些字段描述符来代替非瞬态、非静态字段:
@Serial private static final ObjectStreamField[] serialPersistentFields此外,还有一个专门的 API 可以在序列化之前设置字段的值,或在反序列化之后读取它们的值。这对于在类演化后保留旧版布局非常有用。例如,BigDecimal 类就使用此机制以不再反映实例字段的格式来序列化其实例。