通过封装,我们假装没有透露对象的内部表示,我们只通过它们的公共接口与我们的组件交互;当我们想要更改组件中数据的内部表示而不破坏其用户的任何代码时,我们通常会利用该属性。
相反,序列化意味着通过将对象的状态转换为可以在以后存储和恢复的其他格式来暴露对象的内部状态。这意味着,一旦被序列化,一个对象的内部结构就不能在不冒这个复活过程成功的风险的情况下被改变。
序列化的问题不仅可能出现在开放系统的情况下,也可能出现在以某种方式依赖它的分布式系统中。例如,如果我们停止我们的应用程序服务器,它可能会选择序列化当前会话中的对象以便稍后在服务器重新启动时复活它们,但是如果我们使用新版本的可序列化对象重新部署我们的应用程序,它们是否仍然是当服务器尝试复活它们时兼容吗?在分布式系统中常见使用代码移动性,即,类集位于一个中央存储库中,可供客户端和服务器共享公共代码。在这种方法中,由于对象被序列化以在客户端和服务器之间共享,如果我们更新这个公共存储库中的可序列化类,我们是否冒着破坏任何东西的风险?
例如,假设我们有一个 Person 类,如下所示:
public class Person {
private String firstName;
private String lastName;
private boolean isMale;
private int age;
public boolean isMale() {
return this.isMale;
}
public int getAge() {
return this.age;
}
//more getters and setters
}
假设我们发布了我们的 API 的第一个版本,其中包含一个 Person 的抽象。但是,对于第二个版本,我们想引入两个更改:首先,我们发现如果我们可以存储一个人的出生日期而不是整数形式的年龄会更好,其次我们对当 Java 没有枚举时可能会出现类 Person,但现在我们想用它们来表示一个人的性别。
显然,由于字段被正确封装,我们可以在不影响公共接口的情况下更改类的内部工作。有点像这样:
public class Person {
private String firstName;
private String lastName;
private Gender gender;
private Date dateOfBirth;
public boolean isMale() {
return this.gender == Gender.MALE;
}
public int getAge() {
Calendar today = Calendar.getInstance();
Calendar birth = Calendar.getInstance();
birth.setTime(this.dateOfBirth);
return today.get(Calendar.YEAR) - birth.get(Calendar.YEAR);
}
//the rest of getters and setters
}
通过如上所示进行这些更改,我们可以确保预先存在的客户端不会中断,因为即使我们更改了对象状态的内部表示,我们也保持公共接口不变。
然而,考虑到 Person 类默认是可序列化的,如果我们的系统是一个开放系统,可能会有数千行代码依赖于它们能够基于原始类复活序列化对象这一事实,甚至可能是基于类的原始版本作为父类序列化扩展类的客户。这些对象中的一些可能已经被我们 API 的用户序列化为二进制形式或其他格式,他们现在希望进化到我们的第二版代码。
然后,如果我们想像在第二个示例中那样做一些更改,我们会立即破坏其中的一些;所有具有基于原始版本的类的序列化对象的对象,这些对象存储的对象包含一个名为 age 的 int 类型的字段,其中包含一个人的年龄,而名为 isMale 的 boolean 类型的字段包含有关性别的信息,很可能在这些对象的反序列化,因为新的类定义使用了新的字段和新的数据类型。
很明显,这里的问题是序列化暴露了我们对象的敏感信息,现在我们不能简单地改变任何东西,甚至不能改变我们认为封装的东西,因为通过序列化,所有东西都被公开了。
现在,考虑一个场景,其中 JDK API 中的每个类默认都是可序列化的。Java 的设计者根本无法在不冒险破坏许多应用程序的情况下发展 Java 的 API。他们将被迫假设有人可能拥有 JDK 中任何类的序列化版本。
有一些方法可以处理可序列化类的演变,但这里重要的一点是,当涉及到封装时,我们希望尽可能地包含可序列化类,对于那些我们确实需要序列化的类,那么我们可能需要考虑任何可能的场景的含义,在这些场景中,我们可能会尝试使用其类的进化版本来复活一个对象。
尽管如此,序列化也具有安全隐患,因为有关我们对象的重要敏感信息很容易暴露。
因此,标记可序列化的类可以让 API 的设计者更容易处理它们。