- 概述
- 向对象流写入数据
- 从对象流读取数据
- 对象流作为容器
- 为类定义可序列化字段
- 为类记录可序列化字段和数据
- 访问类的可序列化字段
- ObjectOutput接口
- ObjectInput接口
- Serializable接口
- Externalizable接口
- 枚举常量的序列化
- 记录的序列化
- 循环引用
- 保护敏感信息
1.1 概述
存储和检索Java对象的能力对于构建除了最短暂的应用程序之外的所有应用程序都是必不可少的。以序列化形式存储和检索对象的关键在于表示对象的状态足以重建对象。要保存在流中的对象可以支持Serializable
或Externalizable
接口。对于Java对象,序列化形式必须能够识别和验证对象内容保存自哪个Java类,并将内容恢复到新实例。对于可序列化对象,流包含足够的信息以将流中的字段恢复到类的兼容版本。对于Externalizable对象,类完全负责其内容的外部格式。
要存储和检索的对象通常引用其他对象。这些其他对象必须同时存储和检索,以保持对象之间的关系。当存储对象时,从该对象可达的所有对象也会被存储。
序列化Java对象的目标是:
- 具有简单但可扩展的机制。
- 在序列化形式中保持Java对象类型和安全属性。
- 可扩展以支持远程对象所需的编组和解组。
- 可扩展以支持Java对象的简单持久性。
- 仅需要每个类实现以进行定制。
- 允许对象定义其外部格式。
1.2 向对象流写入数据
向流中写入对象和基本类型是一个简单的过程。例如:
// 将今天的日期序列化到文件中。
FileOutputStream f = new FileOutputStream("tmp");
ObjectOutput s = new ObjectOutputStream(f);
s.writeObject("Today");
s.writeObject(new Date());
s.flush();
首先需要一个OutputStream
,在这种情况下是一个FileOutputStream
,用于接收字节。然后创建一个写入FileOutputStream
的ObjectOutputStream
。接下来,将字符串"Today"和一个Date对象写入流中。一般来说,对象使用writeObject
方法写入,基本类型使用DataOutput
的方法写入流中。
writeObject
方法(参见第2.3节,“writeObject方法”)序列化指定的对象,并递归地遍历其引用的其他对象,以创建对象图的完整序列化表示。在流中,对任何对象的第一个引用导致该对象被序列化或外部化,并为该对象分配一个句柄。对该对象的后续引用被编码为该句柄。使用句柄保留了对象图中自然发生的对象共享,并允许对象之间存在循环引用(即图中的循环)。
数组、枚举常量和类型为Class
、ObjectStreamClass
和String
的对象需要特殊处理。其他对象必须实现Serializable
或Externalizable
接口才能保存或从流中恢复。
基本数据类型使用DataOutput
接口的方法写入流中,例如writeInt
、writeFloat
或writeUTF
。单个字节和字节数组使用OutputStream
的方法写入。除了可序列化字段外,基本数据以块数据记录的形式写入流中,每个记录前缀带有标记和记录中字节的数量指示。
ObjectOutputStream
可以扩展以自定义流中关于类的信息或替换要序列化的对象。有关详细信息,请参阅annotateClass
和replaceObject
方法的描述。
1.3 从对象流读取数据
从流中读取对象,与写入一样,是一个简单的过程:
// 从文件中反序列化字符串和日期。
FileInputStream in = new FileInputStream("tmp");
ObjectInputStream s = new ObjectInputStream(in);
String today = (String)s.readObject();
Date date = (Date)s.readObject();
首先需要一个InputStream
,在这种情况下是一个FileInputStream
,作为源流。然后创建一个从InputStream
读取的ObjectInputStream
。接下来,从流中读取字符串"Today"和一个Date对象。一般来说,对象使用readObject
方法读取,基本类型从流中读取使用DataInput
的方法。
readObject
方法反序列化流中的下一个对象,并递归地遍历其引用的其他对象,以创建序列化对象的完整图。
基本数据类型使用DataInput
接口的方法从流中读取,例如readInt
、readFloat
或readUTF
。单个字节和字节数组使用InputStream
的方法读取。除了可序列化字段外,基本数据从块数据记录中读取。
ObjectInputStream
可以扩展以利用流中关于类的自定义信息或替换已反序列化的对象。有关详细信息,请参阅resolveClass
和resolveObject
方法的描述。
1.4 对象流作为容器
对象序列化生成和消耗包含一个或多个基本类型和对象的字节流。写入流的对象反过来引用其他对象,这些对象也在流中表示。对象序列化生成仅一种流格式,编码和存储包含的对象。
每个充当容器的对象都实现一个接口,该接口允许将基本类型和对象存储在其中或从其中检索。这些接口是ObjectOutput
和ObjectInput
接口,它们:
- 提供要写入和读取的流
- 处理写入基本类型和对象到流的请求
- 处理从流中读取基本类型和对象的请求
要存储在对象流中的每个对象必须显式允许自己被存储,并且必须实现保存和恢复其状态所需的协议。对象序列化定义了两种这样的协议。这些协议允许容器要求对象写入和读取其状态。
要存储在对象流中,每个对象必须实现Serializable
或Externalizable
接口之一:
-
对于
Serializable
类,对象序列化可以自动保存和恢复对象的每个类的字段,并自动处理通过添加字段或超类型演变的类。可序列化类可以声明要保存或恢复的字段,以及写入和读取可选值和对象。 -
对于
Externalizable
类,对象序列化将类的外部格式以及如何保存和恢复超类型的状态完全委托给类。
1.5 为类定义可序列化字段
类的可序列化字段可以通过两种不同的方式定义。类的默认可序列化字段被定义为非瞬态和非静态字段。可以通过在Serializable
类中声明一个特殊字段serialPersistentFields
来覆盖此默认计算。此字段必须用ObjectStreamField
对象数组初始化,列出可序列化字段的名称和类型。字段的修饰符必须是private、static和final。如果字段的值为null或者不是ObjectStreamField[]
的实例,或者字段没有所需的修饰符,则行为就像根本没有声明该字段一样。
例如,以下声明复制了默认行为。
class List implements Serializable {
List next;
private static final ObjectStreamField[] serialPersistentFields
= {new ObjectStreamField("next", List.class)};
}
通过使用serialPersistentFields
为类定义可序列化字段,不再存在可序列化字段必须是Serializable
类当前定义内的字段的限制。Serializable
类的writeObject
和readObject
方法可以使用描述在第1.7节,“访问类的可序列化字段”中描述的接口,将类的当前实现映射到类的可序列化字段。因此,Serializable
类的字段可以在后续版本中更改,只要它保持回到其可序列化字段的映射,这些字段必须跨版本边界保持兼容。
注意:然而,使用此机制指定内部类的可序列化字段存在限制。内部类只能包含初始化为常量或从常量构建的表达式的最终静态字段。因此,不可能为内部类设置serialPersistentFields
(尽管可以为静态成员类设置)。有关序列化内部类实例的其他限制,请参见第1.10节,“Serializable接口”。
1.6 为类记录可序列化字段和数据
重要的是记录类的可序列化状态,以便与可替代的Serializable类实现进行互操作,并记录类的演变。记录可序列化字段为人提供了最后一次机会来审查该字段是否应该是可序列化的。序列化javadoc标签@serial
、@serialField
和@serialData
提供了一种在源代码中记录Serializable类的序列化形式的方式。
-
@serial
标签应放置在默认可序列化字段的javadoc注释中。语法如下:@serial
field-description 可选的field-description描述字段的含义及其可接受的值。field-description可以跨越多行。当在初始发布后添加字段时,@since标签指示添加字段的版本。对于@serial
的field-description提供了与序列化相关的文档,并附加到序列化形式文档中字段的javadoc注释。 -
@serialField
标签用于记录serialPersistentFields
数组的ObjectStreamField
组件。每个ObjectStreamField
组件应使用一个此类标签。语法如下:@serialField
field-name field-type field-description -
@serialData
标签描述写入或读取的数据序列和类型。该标签描述由writeObject
写入的可选数据的序列和类型,或由Externalizable.writeExternal
方法写入的所有数据。语法如下:@serialData
data-description
javadoc应用程序识别序列化javadoc标签,并为每个Serializable和Externalizable类生成规范。有关使用这些标签的示例,请参见第C.1节,“java.io.File的替代实现示例”。
当声明一个类为Serializable时,对象的可序列化状态由可序列化字段(按名称和类型)加上可选数据定义。可选数据只能由Serializable
类的writeObject
方法显式写入。可选数据可以由Serializable
类的readObject
方法读取,或者序列化将跳过未读取的可选数据。
当声明一个类为Externalizable时,由类本身写入流的数据定义了序列化状态。类必须指定写入流的每个数据的顺序、类型和含义。类必须处理自身的演变,以便可以继续读取先前版本写入的数据并写入可以被先前版本读取的数据。类必须在保存和恢复数据时与超类协调。必须指定超类数据在流中的位置。
Serializable类的设计者必须确保为类保存的信息适合持久性,并遵循序列化指定的互操作性和演变规则。类演变在第5章,“可序列化对象的版本控制”中有更详细的解释。
1.7 访问类的可序列化字段
序列化提供了两种访问流中可序列化字段的机制:
- 默认机制无需自定义
- Serializable Fields API允许类通过名称和类型显式访问/设置可序列化字段
当读取或写入实现Serializable
接口的对象且不进行进一步自定义时,默认机制会自动使用。可序列化字段映射到类的相应字段,并相应地将值写入流中或读取并分配值。如果类提供了writeObject
和readObject
方法,则可以通过调用defaultWriteObject
和defaultReadObject
来调用默认机制。当实现writeObject
和readObject
方法时,类有机会在写入或读取可序列化字段值之前或之后修改这些值。
当无法使用默认机制时,可序列化类可以使用ObjectOutputStream
的putFields
方法将可序列化字段的值放入流中。ObjectOutputStream
的writeFields
方法按正确顺序放置值,然后使用现有的序列化协议将其写入流。相应地,ObjectInputStream
的readFields
方法从流中读取值,并按名称以任何顺序提供给类。有关Serializable Fields API的详细描述,请参见第2.2节,“ObjectOutputStream.PutField类”和第3.2节,“ObjectInputStream.GetField类”。
1.8 ObjectOutput接口
ObjectOutput
接口提供了一个抽象的基于流的对象存储接口。它扩展了DataOutput接口,因此这些方法可用于写入原始数据类型。实现此接口的对象可用于存储基本类型和对象。
package java.io;
public interface ObjectOutput extends DataOutput
{
public void writeObject(Object obj) throws IOException;
public void write(int b) throws IOException;
public void write(byte b[]) throws IOException;
public void write(byte b[], int off, int len) throws IOException;
public void flush() throws IOException;
public void close() throws IOException;
}
writeObject
方法用于写入对象。抛出的异常反映了访问对象或其字段时出现的错误,或在写入存储时发生的异常。如果抛出任何异常,底层存储可能会损坏。如果发生这种情况,请参考实现此接口的对象以获取更多信息。
1.9 ObjectInput接口
ObjectInput
接口提供了一个抽象的基于流的对象检索接口。它扩展了DataInput
接口,因此在此接口中可以访问用于读取原始数据类型的方法。
package java.io;
public interface ObjectInput extends DataInput
{
public Object readObject()
throws ClassNotFoundException, IOException;
public int read() throws IOException;
public int read(byte b[]) throws IOException;
public int read(byte b[], int off, int len) throws IOException;
public long skip(long n) throws IOException;
public int available() throws IOException;
public void close() throws IOException;
}
readObject
方法用于读取并返回对象。抛出的异常反映了访问对象或其字段时出现的错误,或在从存储中读取时发生的异常。如果抛出任何异常,底层存储可能会损坏。如果发生这种情况,请参考实现此接口的对象以获取更多信息。
1.10 Serializable接口
对象序列化生成包含正在保存的对象的JavaTM类信息的流。对于可序列化对象,保留了足够的信息以便即使存在不同(但兼容)版本的类实现,也可以还原这些对象。Serializable
接口被定义为标识实现可序列化协议的类:
package java.io;
public interface Serializable {};
可序列化类必须执行以下操作:
-
实现
java.io.Serializable
接口 -
标识应该是可序列化的字段
(使用
serialPersistentFields
成员显式声明它们为可序列化,或使用transient关键字表示非可序列化字段。) -
能够访问其第一个非可序列化超类的无参构造函数
类可以选择定义以下方法:
-
一个
writeObject
方法来控制保存哪些信息或向流中追加附加信息 -
一个
readObject
方法,用于读取相应writeObject
方法写入的信息或在恢复后更新对象的状态 -
一个
writeReplace
方法,允许类指定要写入流的替换对象(有关更多信息,请参见第2.5节,“writeReplace方法”。)
-
一个
readResolve
方法,允许类为刚从流中读取的对象指定替换对象(有关更多信息,请参见第3.7节,“readResolve方法”。)
ObjectOutputStream
和ObjectInputStream
允许它们操作的可序列化类演变(允许对与先前版本兼容的类进行更改)。有关用于允许兼容更改的机制的信息,请参见第5.5节,“兼容的Java类型演变”。
注意:强烈建议不要序列化内部类(即,非静态成员内部类,包括局部类和匿名类)。因为在非静态上下文中声明的内部类包含对外部类实例的隐式非瞬态引用,因此序列化这样的内部类实例将导致其关联的外部类实例的序列化。由javac
(或其他JavaTM编译器)生成的用于实现内部类的合成字段是依赖于实现的,并且在编译器之间可能有所不同;这些字段的差异可能会破坏兼容性,也可能导致冲突的默认serialVersionUID
值。分配给局部和匿名内部类的名称也是依赖于实现的,并且可能在编译器之间有所不同。由于内部类不能声明除编译时常量字段之外的静态成员,因此它们无法使用serialPersistentFields
机制来指定可序列化字段。最后,由于与外部实例关联的内部类没有零参数构造函数(这样的内部类的构造函数隐式接受封闭实例作为前置参数),它们无法实现Externalizable
。然而,上述问题都不适用于静态成员类。
1.11 Externalizable接口
对于Externalizable对象,容器仅保存对象的类的标识;类必须保存和恢复内容。Externalizable
接口定义如下:
package java.io;
public interface Externalizable extends Serializable
{
public void writeExternal(ObjectOutput out)
throws IOException;
public void readExternal(ObjectInput in)
throws IOException, java.lang.ClassNotFoundException;
}
Externalizable对象的类必须执行以下操作:
-
实现
java.io.Externalizable
接口 -
实现
writeExternal
方法以保存对象的状态(它必须明确与其超类型协调以保存其状态。)
-
实现
readExternal
方法以从流中读取writeExternal
方法写入的数据并恢复对象的状态(它必须明确与超类型协调以保存其状态。)
-
让
writeExternal
和readExternal
方法负责格式,如果写入了外部定义的格式注意:
writeExternal
和readExternal
方法是公共的,并存在客户端可能能够通过方法和字段之外的方式写入或读取对象信息的风险。这些方法只能在对象持有的信息不敏感或暴露它不会带来安全风险时使用。 -
拥有一个公共无参数构造函数
注意:与封闭实例相关联的内部类不能有无参数构造函数,因为这些类的构造函数隐式地接受封闭实例作为前置参数。因此,无法将
Externalizable
接口机制用于内部类,它们应该实现Serializable
接口,如果必须进行序列化。对于可序列化的内部类也存在一些限制;有关完整枚举,请参见第1.10节,“Serializable接口”。
Externalizable类可以选择定义以下方法:
-
一个
writeReplace
方法,允许类指定要写入流的替换对象(有关更多信息,请参见第2.5节,“writeReplace方法”。)
-
一个
readResolve
方法,允许类为刚从流中读取的对象指定替换对象(有关更多信息,请参见第3.7节,“readResolve方法”。)
1.12 枚举常量的序列化
枚举常量的序列化方式与普通可序列化或可外部化对象不同。枚举常量的序列化形式仅包含其名称;常量的字段值不在形式中。为了序列化枚举常量,ObjectOutputStream
写入枚举常量的name
方法返回的值。为了反序列化枚举常量,ObjectInputStream
从流中读取常量名称;然后通过调用java.lang.Enum.valueOf
方法,传递常量的枚举类型和接收到的常量名称作为参数来获取反序列化的常量。与其他可序列化或可外部化对象一样,枚举常量可以作为后续出现在序列化流中的反向引用的目标。
枚举常量的序列化过程无法定制:枚举类型定义的任何特定于类的writeObject
、readObject
、readObjectNoData
、writeReplace
和readResolve
方法在序列化和反序列化过程中将被忽略。同样,任何serialPersistentFields
或serialVersionUID
字段声明也将被忽略--所有枚举类型都具有固定的serialVersionUID
为0L
。对于枚举类型的可序列化字段和数据进行文档化是不必要的,因为发送的数据类型没有变化。
1.13 记录的序列化
记录的序列化方式与普通可序列化或可外部化对象不同。记录对象的序列化形式是从记录组件派生的值序列。记录对象的流格式与流中的普通对象相同。在反序列化过程中,如果指定流类描述符的本地类等效于记录类,则首先读取流字段并重建以用作记录的组件值;其次,通过使用组件值作为参数(或者如果流中缺少组件值,则使用组件类型的默认值)调用记录的规范构造函数来创建记录对象。
与其他可序列化或可外部化对象一样,记录对象可以作为后续出现在序列化流中的反向引用的目标。然而,通过记录对象直接或间接地被其组件之一引用的图中的循环不会被保留。在调用记录构造函数之前,记录组件将被反序列化,因此这种限制存在(有关更多信息,请参见第1.14节,“循环引用”)。
记录对象的序列化或外部化过程无法定制;记录类定义的任何特定于类的writeObject
、readObject
、readObjectNoData
、writeExternal
和readExternal
方法在序列化和反序列化过程中将被忽略。但是,可以通过writeReplace
和readResolve
方法指定要序列化的替代对象或指定替换对象。任何serialPersistentFields
字段声明都将被忽略。记录类的文档化可序列化字段和数据是不必要的,因为序列化形式中没有数据类型的变化,除非使用了替代或替换对象。记录类的serialVersionUID
为0L
,除非显式声明。对于记录类,匹配的serialVersionUID
值的要求被豁免。
1.14 循环引用
如第1.2节,“写入对象流”中所述,使用句柄可以保留对象图中出现的循环引用。
一个仅用于说明目的的最小人为示例:
class Data implements Serializable {
private static final long serialVersionUID = ...
Object obj;
}
class Carrier implements Serializable {
private static final long serialVersionUID = ...
private final Data d;
public Carrier(Data d) { this.d = d; }
public Data d() { return d; }
}
// 创建Data和Carrier的实例,并在它们之间创建一个循环
Data d1 = new Data();
Carrier c1 = new Carrier(d1);
d1.obj = c1;
// 序列化
ObjectOutputStream oos = new ObjectOutputStream(...);
oos.writeObject(c1);
// 反序列化
ObjectInputStream ois = new ObjectInputStream(...);
Carrier c2 = (Carrier) ois.readObject();
在反序列化时,对象
对象
现在考虑如果
record Carrier(Data d) implements Serializable { }
在反序列化时,对象
记录对象
1.15 保护敏感信息
在开发提供对资源受控访问的类时,必须小心保护敏感信息和功能。在反序列化过程中,对象的私有状态将被恢复。例如,文件描述符包含一个句柄,提供对操作系统资源的访问。能够伪造文件描述符将允许某些形式的非法访问,因为从流中恢复状态。因此,序列化运行时必须采取保守的方法,不信任流仅包含对象的有效表示。为了避免损害类,对象的敏感状态不应从流中恢复,或者必须由类重新验证。有几种技术可用于保护类中的敏感数据。
最简单的技术是将包含敏感数据的字段标记为私有瞬态。瞬态字段不是持久的,不会被任何持久性机制保存。标记字段将防止状态出现在流中并在反序列化过程中被恢复。由于写入和读取(私有字段)不能在类外部被覆盖,类的瞬态字段是安全的。
特别敏感的类根本不应该被序列化。为了实现这一点,对象不应该实现Serializable
或Externalizable
接口。
一些类可能发现允许写入和读取但专门处理和重新验证状态在反序列化时是有益的。类应该实现writeObject
和readObject
方法,仅保存和恢复适当的状态。如果应拒绝访问,抛出NotSerializableException
将阻止进一步访问。