Effective Java读书笔记八:序列化(74-78)

来源:互联网 发布:个人收支软件app 编辑:程序博客网 时间:2024/06/09 20:46

第74条:谨慎地实现Serializable接口

对象序列化API,它提供了一个框架,用来将对象编码成字节流,并从字节流编码中重新构建对象。将一个对象编码成一个字节流,称作将该对象序列化,相反的处理过程称作反序列化。一旦对象被序列化后,它的编码就可以从一台正在运行的虚拟机传递到另一台虚拟机上,或者被存储到磁盘上,供以后反序列化时用。序列化技术为远程通信提供了标准和线路级对象表示法,也为JavaBeans组件提供了标准和持久化数据格式。

要想使一个类的实例可被序列化,非常简单,只要在它的声明中加入”implements Serializable”字样即可。正因为太容易了,所以普遍存在这样一种误解,认为程序员只需要做极少量的工作就可以支持序列化了。实际的情形要复杂得多。虽然使一个类可被序列化的直接开销低到甚至可以忽略不计,但是为了序列化而付出的长期开销往往是实实在在的。

为实现Serializable而付出的最大代价是,一旦一个类被发布,就大大降低了”改变这个类的实现”的灵活性。如果一个类实现了Serializable,它的字节流编码(或者说序列化形式,serialized form)就变成了它的导出的API的一部分。一旦这个类被广泛使用,往往必须永远支持这种序列化形式,就好像你必须要支持导出的API的所有其他部分一样。如果你不努力设计一个自定义的序列化形式(custom serialized form),而仅仅接受了默认的序列化形式,这种序列化形式将被永远地束缚在该类最初的内部表示法上。换句话说,如果你接受了默认的序列化形式,这个类中私有的和包级私有的实例域将都变成导出的API的一部分,这不符合”最低限度地访问域”的实践准则,从而它就失去了作为信息隐藏工具的有效性。

如果你接受了默认的序列化形式,并且以后又要改变这个类的内部表示法,结果可能导致序列化形式的不兼容。客户端程序企图用这个类的旧版本来序列化一个类,然后用新版本进行反序列化,结果将导致程序失败。在改变内部表示法的同时仍然维持原来的序列化形式(使用ObjectOutputStream.putFields和ObjectInputStream.readFields),这也是可能的,但是做起来比较困难,并且会在源代码中留下一些可以明显的隐患。因此,你应该仔细地设计一种高质量的序列化形式,并且在很长时间内都愿意使用这种形式。这样做将会增加开发的初始成本,但这是值得的。设计良好的序列化形式也许会给类的演变带来限制;但是设计不好的序列化形式则可能会使类根本无法演变。

序列化会使类的演变受到限制,这种限制的一个例子与流的唯一标识符(stream unique identifier)有关,通常它也被称为序列版本UID(serial version UID)。每个可序列化的类都有一个唯一标识号与它相关联。如果你没有在一个名为serialVersionUID的私有静态final的long域中显式地指定该标识号,系统就会自动地将一个复杂的过程作用在这个类上,从而在运行时产生该标识号。这个自动产生的值会受到类名称、它所实现的接口的名称、以及所有公有的和受保护的成员的名称所影响。如果你通过任何方式改变了这些信息,比如,增加了一个不是很重要的工具方法,自动产生的序列版本UID也会发生变化。因此,如果你没有声明一个显式的序列版本UID,兼容性将会遭到破坏,在运行时导致InvalidClassException异常。

实现Serializable的第二个代价是,它增加了出现Bug和安全漏洞的可能性。通常情况下,对象是利用构造器来创建的;序列化机制是一种语言之外的对象创建机制(extralinguistic mechanism)。无论你是接受了默认的行为,还是覆盖了默认的行为,反序列化机制(deserialization)都是一个”隐藏的构造器”,具备与其他构造器相同的特点。因为反序列化机制中没有显式的构造器,所以你很容易忘记要确保:反序列化过程必须也要保证所有”由构造器建立起来的约束关系”,并且不允许攻击者访问正在构造过程中的对象的内部信息。依靠默认的反序列化机制,可以很容易地使对象的约束关系遭到破坏,以及遭受到非法访问。

实现Serializable的第三个代价是,随着类发行新的版本,相关的测试负担也增加了。当一个可序列化的类被修订的时候,很重要的一点是,要检查是否可以”在新版本中序列化一个实例,然后在旧版本中反序列化”,反之亦然。因此,测试所需要的工作量与”可序列化的类的数量和发行版本号”的乘积成正比,这个乘积可能会非常大。这些测试不可能自动构建,因为除了二进制兼容性(binary compatibility)以外,你还必须测试语义兼容性(semantic compatibility)。换句话说,你必须既要确保”序列化-反序列化”过程成功,也要确保结果产生的对象真正是原始对象的复制品。可序列化类的变化越大,它就越需要测试。如果在最初编写一个类的时候,就精心设计了自定义的序列化形式,测试的要求就可以有所降低,但是也不能完全没有测试。

实现Serializable接口并不是一个很轻松就可以做出的决定。它提供了一些实在的益处:如果一个类将要加入到某个框架中,并且该框架依赖于序列化来实现对象传输或者持久化,对于这个类来说,实现Serializable接口就非常有必要。更进一步来看,如果这个类要成为另一个类的一个组件,并且后者必须实现Serializable接口,若前者也实现了Serializable接口,它就会更易于被后者使用。然而,有许多实际的开销都与实现Serializable接口有关。每当你实现一个类的时候,都需要权衡一下所付出的代价和带来的好处。根据经验,比如Date和BigInteger这样的值类应该实现Serializable,大多数的集合类也应该如此。代表活动实体的类,比如线程池(thread pool),一般不应该实现Serializable。

为了继承而设计的类应该很少实现Serializable,接口也应该很少会扩展它。如果违反了这条规则,扩展这个类或者实现这个接口的程序员就会背上沉重的负担。然而在有些情况下违反这条规则却是合适的。例如,如果一个类或者接口存在的目的主要是为了参与到某个框架中,该框架要求所有的参与者都必须实现Serializable,那么,对于这个类或者接口来说,实现或者扩展Serializable就是非常有意义的。

为了继承而设计的类中真正实现了Serializable的有Throwable、Component和HttpServlet。因为Throwable实现了Serializable,所以RMI的异常可以从服务器端传到客户端。Component实现了Serializable,因此GUI可以被发送、保存和恢复。HttpServlet实现了Serializable,因此会话状态可以被缓存。

如果一个专门为了继承而设计的类不是可序列化的,就不可能编写出可序列化的子类。特别是,如果超类没有提供可访问的无参构造器,子类也不可能做到可序列化。因此,对于为继承而设计的不可序列化的类,你应该考虑提供一个无参构造器。

内部类不应该实现Serializable。它们使用编译器产生的合成域来保存指向外围实例的引用,以及保存来自外围作用域的局部变量的值。因此,内部类的默认序列化形式是定义不清楚的。然而,静态成员类却是可以实现Serializable接口。

简而言之,千万不要认为实现Serializable接口会很容易。除非一个类在用了一段时间之后就会被抛弃,否则,实现Serializable接口就是个很严肃的承诺,必须认真对待。如果一个类是为了继承而设计的,则吏加需要加倍小心。对于这样的类而言,在“允许子类实现Serializable接口”或“禁止子类实现Serializable接口”两者之间的一个折衷设计方案是,提供一个可访问的无参构造器,这种设计方案允许(但不要求)子类实现Serializable接口。

第75条:考虑使用自定义的序列化形式

设计一个类的序列化形式和设计该类的API 同样重要,因此在没有认真考虑好默认的序列化形式是否合适之前,不要贸然使用默认的序列化行为。在作出决定之前,你需要从灵活性、性能和正确性多个角度对这种编码形式进行考察。一般来讲,只有当你自行设计的自定义序列化形式与默认的形式基本相同时,才能接受默认的序列化形式。

比如,当一个对象的物理表示法等同于它的逻辑内容,可能就适合于使用默认的序列化形式。

见如下代码示例:

public class Name implements Serializable {             private final String lastName;             private final String firstName;             private final String middleName;             ... ...         } 

从逻辑角度而言,该类的三个域字段精确的反应出它的逻辑内容。然而有的时候,即便默认的序列化形式是合适的,通常还必须提供一个readObject 方法以保证约束关系和安全性,如上例代码中,firstName 和lastName 不能为null 等。

使用默认序列化形式会有以下几个缺点:
(1)它使这个类的导出API 永远的束缚在该类的内部表示法上,即使今后找到更好的的实现方式,也无法摆脱原有的实现方式。
(2)它会消耗过多的空间。
(3)它会消耗过多的时间。
(4)它会引起栈溢出。

transient是Java语言的关键字,用来表示一个域不是该对象串行化的一部分。当一个对象被串行化的时候,transient型变量的值不包括在串行化的表示中,然而非transient型的变量是被包括进去的。

在序列化过程中,虚拟机会试图调用对象类里的writeObject() 和readObject(),进行用户自定义的序列化和反序列化,如果没有则调用ObjectOutputStream.defaultWriteObject() 和ObjectInputStream.defaultReadObject()。

同样,在ObjectOutputStream和ObjectInputStream中最重要的方法也是writeObject() 和 readObject(),递归地写出/读入byte。

所以用户可以通过writeObject()和 readObject()自定义序列化和反序列化逻辑。对一些敏感信息加密的逻辑也可以放在此。

对于默认序列化还需要进一步说明的是,当一个或多个域字段被标记为transient 时,如果要进行反序列化,这些域字段都将被初始化为其类型默认值,如对象引用域被置为null,数值基本域的默认值为0,boolean域的默认值为false。如果这些值不能被任何transient 域所接受,你就必须提供一个readObject方法。它首先调用defaultReadObject,然后再把这些transient 域恢复为可接受的值。

最后需要说明的是,无论你是否使用默认的序列化形式,如果在读取整个对象状态的任何其他方法上强制任何同步,则也必须在对象序列化上强制这种同步。

无论你选择了哪种序列化形式,都要为自己编写的每个可序列化的类声明一个显式的序列版本UID。这样可以避免序列版本UID成为潜在的不兼容根源,同时也会带来小小的性能好处,因为不需要去算序列版本UID。

第76条:保护性地编写readObject方法

对于非final 的可序列化类,在readObject 方法和构造器之间还有其他类似的地方,readObject方法不可以调用可被覆盖的方法,无论是直接调用还是间接调都不可以。如果违反了该规则,并且覆盖了该方法,被覆盖的方法将在子类的状态被反序列化之前先运行。程序很可能会失败。

总而言之,每当你编写readObject方法的时候,都要这样想:你正在编写一个公有的构造器,无论给它传递什么样的字节流,它都必须产生一个有效的实例。不要假设这个字节流一定代表着一个真正被序列化过的实例。虽然在本条目的例子中,类使用了默认的序列化形式,但是,所有讨论到的有可能发生的问题也同样适用于使用自定义序列化形式的类。下面以摘要的形式给出一些指导方针,有助于编写出更加健壮的readObject方法:

  • 对于对象引用域必须保持为私有的类,要保护性地拷贝这些域中的每个对象。不可变类的可变组件就属于这一类别。
  • 对于任何约束条件,如果检查失败,则抛出一个InvalidObjectException异常。这些检查动作应该跟在所有的保护性拷贝之后。
  • 如果整个对象图在被反序列化之后必须进行验证,就应该使用ObjectInputValidation接口[JavaSE6,Serialization]。
  • 无论是直接方式还是间接方式,都不要调用类中任何可被覆盖的方法。

第77条:对于实例控制,枚举类型优先于readResolve

public class Elvis {public static final Elvis INSTANCE = new Elvis();private Elvis() { ... }public void leaveTheBuilding() { ... }}

如果这个类的声明中加上了”implements Serializable”的字样,它就不再是一个Singleton。无论该类使用了默认的序列化形式,还是自定义的序列化形式,都没有关系;也跟它是否提供了显式的readObject方法无关。任何一个readObject方法,不管是显式的还是默认的,它都会返回一个新建的实例,这个新建的实例不同于该类初始化时创建的实例。

readResolve特性允许你用readObject创建的实例代替另一个实例[Serialization, 3.7]。对于一个正在被反序列化的对象,如果它的类定义了一个readResolve方法,并且具备正确的声明,那么在反序列化之后,新建对象上的readResolve方法就会被调用。然后,该方法返回的对象引用将被返回,取代新建的对象。在这个特性的绝大多数用法中,指向新建对象的引用不需要再被保留,因此立即成为垃圾回收的对象。

如果Elvis类要实现Serializable接口,下面的readResolve方法就足以保证它的Singleton属性:

// readResolve for instance control - you can do better!private Object readResolve() {// Return the one true Elvis and let the garbage collector// take care of the Elvis impersonator.return INSTANCE;}

该方法忽略了被反序列化的对象,只返回该类初始化时创建的那个特殊的Elvis实例。因此,Elvis实例的序列化形式并不需要包含任何实际的数据;所有的实例域都应该被声明为transient的。事实上,如果依赖readResolve进行实例控制,带有对象引用类型的所有实例域则都必须声明为transient的。

如果反过来,你将一个可序列化的实例受控的类编写成枚举,就可以绝对保证除了所声明的常量之外,不会有别的实例。JVM对此提供了保障,这一点你可以确信无疑。

用readResolve进行实例控制并不过时。如果必须编写可序列化的实例受控的类,它的实例在编译时还不知道,你就无法将类表示成一个枚举类型。

readResolve的可访问性(accessibility)很重要。如果把readResolve方法放在一个final类上,它就应该是私有的。如果把readResolver方法放在一个非final的类上,就必须认真考虑它的可访问性。如果它是私有的,就不适用于任何子类。如果它是包级私有的,就只适用于同一个包中的子类。如果它是受保护的或者公有的,就适用于所有没有覆盖它的子类。如果readResolve方法是受保护的或者公有的,并且子类没有覆盖它,对序列化过的子类实例进行反序列化,就会产生一个超类实例,这样有可能导致ClassCastException异常。

总而言之,你应该尽可能地使用枚举类型来实施实例控制的约束条件。如果做不到,同时又需要一个既可序列化又是实例受控的类,就必须提供一个readResolver方法,并确保该类的所有实例域都为基本类型,或者是瞬时的。

第78条:考虑用序列化代理代替序列化实例

序列化代理模式相当简单。首先,为可序列化的类设计一个私有的静态嵌套类,精确地表示外围类的实例的逻辑状态。这个嵌套类被称作序列化代理(serialization proxy),它应该有一个单独的构造器,其参数类型就是那个外围类。这个构造器只从它的参数中复制数据:它不需要进行任何一致性检查或者保护性拷贝。按设计,序列代理的默认序列化形式是外围类最好的序列化形式。外围类及其序列代理都必须声明实现Serializable接口。

具体demo可以参考jdk中EnumSet代码,以下是该类代码的简化版:

public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E>    implements Cloneable, java.io.Serializable{    /**     * The class of all the elements of this set.     */    final Class<E> elementType;    /**     * All of the values comprising T.  (Cached for performance.)     */    final Enum[] universe;    private static Enum[] ZERO_LENGTH_ENUM_ARRAY = new Enum[0];    EnumSet(Class<E>elementType, Enum[] universe) {        this.elementType = elementType;        this.universe    = universe;    }   //N多方法代码已省略    /**     * This class is used to serialize all EnumSet instances, regardless of     * implementation type.  It captures their "logical contents" and they     * are reconstructed using public static factories.  This is necessary     * to ensure that the existence of a particular implementation type is     * an implementation detail.     *     * @serial include     */    private static class SerializationProxy <E extends Enum<E>>        implements java.io.Serializable    {        /**         * The element type of this enum set.         *         * @serial         */        private final Class<E> elementType;        /**         * The elements contained in this enum set.         *         * @serial         */        private final Enum[] elements;        SerializationProxy(EnumSet<E> set) {            elementType = set.elementType;            elements = set.toArray(ZERO_LENGTH_ENUM_ARRAY);        }        private Object readResolve() {            EnumSet<E> result = EnumSet.noneOf(elementType);            for (Enum e : elements)                result.add((E)e);            return result;        }        private static final long serialVersionUID = 362491234563181265L;    }    Object writeReplace() {        return new SerializationProxy<>(this);    }    // readObject method for the serialization proxy pattern    // See Effective Java, Second Ed., Item 78.    private void readObject(java.io.ObjectInputStream stream)        throws java.io.InvalidObjectException {        throw new java.io.InvalidObjectException("Proxy required");    }}

序列化代理模式有两个局限性:

它不能与可以被客户端扩展的类兼容。它也不能与对象图中包含循环的某些类兼容:如果你企图从一个对象的序列化代理的readResolve方法内部调用这个对象中的方法,就会得到一个ClassCastException异常,因为你还没有这个对象,只有它的序列化代理。

最后,序列化代理模式所增强的功能和安全性并不是没有代价的。在我的机器上,通过序列化代理来序列化和反序列化Period实例的开销,比用保护性拷贝进行的开销增加了14%。

总而言之,每当你发现自己必须在一个不能被客户端扩展的类上编写readObject或者writeObject方法的时候,就应该考虑使用序列化代理模式。要想稳健地将带有重要约束条件的对象序列化时,这种模式可能是最容易的方法。

《Effective Java中文版 第2版》PDF版下载:
http://download.csdn.net/detail/xunzaosiyecao/9745699

作者:jiankunking 出处:http://blog.csdn.net/jiankunking

0 0
原创粉丝点击