高效Java(第三版) Effective Java
1. 考虑使用静态工厂方法替代构造方法 2. 当构造方法参数过多时使用builder模式 3. 使用私有构造方法或枚类实现Singleton属性 4. 使用私有构造方法执行非实例化 5. 使用依赖注入取代硬连接资源 6. 避免创建不必要的对象 7. 消除过期的对象引用 8. 避免使用Finalizer和Cleaner机制 9. 使用try-with-resources语句替代try-finally语句 10. 重写equals方法时遵守通用约定 11. 重写equals方法时同时也要重写hashcode方法 12. 始终重写 toString 方法 13. 谨慎地重写 clone 方法 14. 考虑实现Comparable接口 15. 使类和成员的可访问性最小化 16. 在公共类中使用访问方法而不是公共属性 17. 最小化可变性 18. 组合优于继承 19. 如果使用继承则设计,并文档说明,否则不该使用 20. 接口优于抽象类 21. 为后代设计接口 22. 接口仅用来定义类型 23. 优先使用类层次而不是标签类 24. 优先考虑静态成员类 25. 将源文件限制为单个顶级类 26. 不要使用原始类型 27. 消除非检查警告 28. 列表优于数组 29. 优先考虑泛型 30. 优先使用泛型方法 31. 使用限定通配符来增加API的灵活性 32. 合理地结合泛型和可变参数 33. 优先考虑类型安全的异构容器 34. 使用枚举类型替代整型常量 35. 使用实例属性替代序数 36. 使用EnumSet替代位属性 37. 使用EnumMap替代序数索引 38. 使用接口模拟可扩展的枚举 39. 注解优于命名模式 40. 始终使用Override注解 41. 使用标记接口定义类型 42. lambda表达式优于匿名类 43. 方法引用优于lambda表达式 44. 优先使用标准的函数式接口 45. 明智审慎地使用Stream 46. 优先考虑流中无副作用的函数 47. 优先使用Collection而不是Stream来作为方法的返回类型 48. 谨慎使用流并行 49. 检查参数有效性 50. 必要时进行防御性拷贝 51. 仔细设计方法签名 52. 明智而审慎地使用重载 53. 明智而审慎地使用可变参数 54. 返回空的数组或集合不要返回null 55. 明智而审慎地返回Optional 56. 为所有已公开的API元素编写文档注释 57. 最小化局部变量的作用域 58. for-each循环优于传统for循环 59. 熟悉并使用Java类库 60. 需要精确的结果时避免使用float和double类型 61. 基本类型优于装箱的基本类型 62. 当有其他更合适的类型时就不用字符串 63. 注意字符串连接的性能 64. 通过对象的接口引用对象 65. 接口优于反射 66. 明智谨慎地使用本地方法 67. 明智谨慎地进行优化 68. 遵守普遍接受的命名约定 69. 仅在发生异常的条件下使用异常 70. 对可恢复条件使用检查异常,对编程错误使用运行时异常 71. 避免不必要地使用检查异常 72. 赞成使用标准异常 73. 抛出合乎于抽象的异常 74. 文档化每个方法抛出的所有异常 75. 在详细信息中包含失败捕获信息 76. 争取保持失败原子性 77. 同步访问共享的可变数据 78. 避免过度同步 79. EXECUTORS, TASKS, STREAMS 优于线程 80. 优先使用并发实用程序替代wait和notify 81. 线程安全文档化 82. 明智谨慎地使用延迟初始化 83. 不要依赖线程调度器 84. 其他替代方式优于Java本身序列化 85. 非常谨慎地实现SERIALIZABLE接口 86. 考虑使用自定义序列化形式 87. 防御性地编写READOBJECT方法 88. 对于实例控制,枚举类型优于READRESOLVE 89. 考虑序列化代理替代序列化实例

考虑使用自定义序列化形式

Tips
书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code
注意,书中的有些代码里方法是基于Java 9 API中的,所以JDK 最好下载 JDK 9以上的版本。

87. 考虑使用自定义序列化形式

当在时间紧迫的情况下编写类时,通常应该将精力集中在设计最佳API上。有时这意味着发布一个“一次性使用(throwaway)”实现,将在将来的版本中替换它。通常这不是一个问题,但是如果类实现Serializable并使用默认的序列化形式,将永远无法完全“摆脱一次性使用”的实现了。它永远决定序列化的形式。这不仅仅是一个理论问题。这种情况发生在Java类库中的几个类上,包括BigInteger。

如果没有考虑是否合适,请不要接受默认的序列化形式。 接受默认的序列化形式应该有意识地决定,从灵活性,性能和正确性的角度来看这种编码是合理的。 一般来说,只有在与设计自定义序列化形式时所选择的编码大致相同的情况下,才应接受默认的序列化形式。

对象的默认序列化形式是对象图(object graph)的物理表示形式的一种相当有效的编码,该表示形式以对象为根。换句话说,它描述了对象中包含的数据以及从该对象可以访问的每个对象中的数据。它还描述了所有这些对象相互关联的拓扑结构。理想的对象序列化形式只包含对象所表示的逻辑数据。它独立于物理表示。

如果对象的物理表示与其逻辑内容相同,则默认的序列化形式可能是合适的。例如,默认的序列化形式对于下面的类来说是合理的,它简单地表示一个人的名字:

// Good candidate for default serialized form
public class Name implements Serializable {
    /**
     * Last name. Must be non-null.
     * @serial
     */
    private final String lastName;

    /**
     * First name. Must be non-null.
     * @serial
     */
    private final String firstName;

    /**
     * Middle name, or null if there is none.
     * @serial
     */
    private final String middleName;

    ... // Remainder omitted
}

从逻辑上讲,名称由三个字符串组成,分别表示姓、名和中间名。名称中的实例属性精确地反映了这个逻辑内容。

即使你确定默认的序列化形式是合适的,通常也必须提供readObject方法以确保不变性和安全性。 对于Name类,readObject方法必须确保属性lastName和firstName为非null。 条目 88和90详细讨论了这个问题。

注意,虽然lastName、firstName和middleName属性是私有的,但是它们都有文档注释。这是因为这些私有属性定义了一个公共API,它是类的序列化形式,并且必须对这个公共API进行文档化。@serial标签的存在告诉Javadoc将此文档放在一个特殊的页面上,该页面记录序列化的形式。

与Name类的另一极端,考虑下面的类,它表示一个字符串列表(暂时忽略使用标准List实现可能更好的建议):

// Awful candidate for default serialized form
public final class StringList implements Serializable {
    private int size = 0;
    private Entry head = null;

    private static class Entry implements Serializable {
        String data;
        Entry  next;
        Entry  previous;
    }

    ... // Remainder omitted
}

从逻辑上讲,这个类表示字符串序列。在物理上,它将序列表示为双链表。如果接受默认的序列化形式,则序列化形式将煞费苦心地镜像链表中的每个entry,以及每一个entry之间的所有双向链接。

当对象的物理表示与其逻辑数据内容有很大差异时,使用默认的序列化形式有四个缺点:

  • 它将导出的API永久绑定到当前类的内部表示。 在上面的示例中,私有StringList.Entry类成为公共API的一部分。 如果在将来的版本中更改了表示,则StringList类仍需要接受输入上的链表表示,并在输出时生成它。 该类永远不会消除处理链表entry的所有代码,即使不再使用它们。

  • 它会消耗过多的空间。 在上面的示例中,序列化形式不必要地表示链接列表中的每个entry和所有链接。 这些entry和链接仅仅是实现细节,不值得包含在序列化形式中。 由于序列化形式过大,将其写入磁盘或通过网络发送将会非常慢。

  • 它会消耗过多的时间。 序列化逻辑不了解对象图的拓扑结构,因此必须经历昂贵的图遍历。 在上面的例子中,仅仅遵循下一个引用就足够了。

  • 它会导致堆栈溢出。 默认的序列化过程执行对象图的递归遍历,即使对于中等大小的对象图,也可能导致堆栈溢出。 使用1,000-1,800个元素序列化StringList实例,就会在我的机器上生成StackOverflowError异常。 令人惊讶的是,序列化导致堆栈溢出的最小列表大小因运行而异(在我的机器上)。 显示此问题的最小列表大小可能取决于平台实现和命令行标记; 某些实现可能根本没有这个问题。

StringList的合理序列化形式,就是列表中的字符串数量,然后紧跟着字符串本身。这构成了由StringList表示的逻辑数据,去掉了其物理表示的细节。下面是修改后的StringList版本,包含实现此序列化形式的writeObject和readObject方法。提醒一下,transient修饰符表示要从类的默认序列化形式中省略一个实例属性:

// StringList with a reasonable custom serialized form
public final class StringList implements Serializable {
    private transient int size   = 0;
    private transient Entry head = null;

    // No longer Serializable!
    private static class Entry {
        String data;
        Entry  next;
        Entry  previous;
    }

    // Appends the specified string to the list
    public final void add(String s) { ... }

    /**
     * Serialize this {@code StringList} instance.
     *
     * @serialData The size of the list (the number of strings
     * it contains) is emitted ({@code int}), followed by all of
     * its elements (each a {@code String}), in the proper
     * sequence.
     */
    private void writeObject(ObjectOutputStream s)
            throws IOException {
        s.defaultWriteObject();
        s.writeInt(size);

        // Write out all elements in the proper order.
        for (Entry e = head; e != null; e = e.next)
            s.writeObject(e.data);
    }

    private void readObject(ObjectInputStream s)
            throws IOException, ClassNotFoundException {
        s.defaultReadObject();
        int numElements = s.readInt();

        // Read in all elements and insert them in list
        for (int i = 0; i < numElements; i++)
            add((String) s.readObject());
    }

    ... // Remainder omitted
}

writeObject做的第一件事就是调用defaultWriteObject方法,而readObject做的第一件事就是调用defaultReadObject,即使所有StringList的属性都是瞬时状态(transient)的。 你可能会听到它说如果所有类的实例属性都是瞬时状态的,那么可以省去调用defaultWriteObject和defaultReadObject,但序列化规范要求无论如何都要调用它们。 这些调用的存在使得可以在以后的版本中添加非瞬时状态的实例属性,同时保持向后和向前兼容性。 如果实例在更高版本中序列化,并在早期版本中反序列化,则添加的属性将被忽略。 如果早期版本的readObject方法无法调用defaultReadObject,则反序列化将失败,抛出StreamCorruptedException异常。

请注意,writeObject方法有一个文档注释,即使它是私有的。 这类似于Name类中私有属性的文档注释。 此私有方法定义了一个公共API,它是序列化形式,并且应该记录公共API。 与属性的@serial标签一样,方法的@serialData标签告诉Javadoc实用程序将此文档放在序列化形式的页面上。

为了给前面的性能讨论提供一定的伸缩性,如果平均字符串长度是10个字符,那么经过修改的StringList的序列化形式占用的空间大约是原始字符串序列化形式的一半。在我的机器上,长度为10的列表,序列化修订后的StringList的速度是序列化原始版本的两倍多。最后,在修改后的序列化形式中没有堆栈溢出问题,因此对于可序列化的StringList的大小没有实际的上限。

虽然默认的序列化形式对于StringList来说是不好的,但是对于有些类会可能更糟糕。 对于StringList,默认的序列化形式是不灵活的,并且执行得很糟糕,但是在序列化和反序列化StringList实例,它产生了原始对象的忠实副本,其所有不变性都是完整的。 对于其不变性与特定实现的详细信息相关联的任何对象,情况并非如此。

例如,考虑哈希表(hash table)的情况。它的物理表示是一系列包含键值(key-value)项的哈希桶。每一项所在桶的位置,是其键的散列代码的方法决定的,通常情况下,不能保证从一个实现到另一个实现是相同的。事实上,它甚至不能保证每次运行都是相同的。因此,接受哈希表的默认序列化形式会构成严重的错误。对哈希表进行序列化和反序列化可能会产生一个不变性严重损坏的对象。

无论是否接受默认的序列化形式,当调用defaultWriteObject方法时,没有标记为transient的每个实例属性都会被序列化。因此,可以声明为transient的每个实例属性都应该是。这包括派生(derived)属性,其值可以从主要数据属性(primary data fields)(如缓存的哈希值)计算。它还包括一些属性,这些属性的值与JVM的一个特定运行相关联,比如表示指向本地数据结构指针的long型属性。在决定使非瞬时状态的属性之前,请确信它的值是对象逻辑状态的一部分。如果使用自定义序列化形式,则大多数或所有实例属性都应该标记为transient,如上面的StringList示例所示。

如果使用默认的序列化形式,并且标记了一个或多个属性为transient,请记住,当反序列化实例时,这些属性将初始化为默认值:对象引用属性为null,基本数字类型的属性为0,布尔属性为false [JLS, 4.12.5]。如果这些值对于任何瞬时状态的属性都不可接受,则必须提供一个readObject方法,该方法调用defaultReadObject方法,然后将瞬时状态的属性恢复为可接受的值(条目 88)。或者,这些属性可以在第一次使用时进行延迟初始化(条目 83)。

无论是否使用默认的序列化形式,必须对对象序列化加以同步,也要对读取对象的整个状态的任何方法施加同步。。 因此,例如如果有一个线程安全的对象(条目 82)通过同步每个方法来实现其线程安全,并且选择使用默认的序列化形式,请使用以下write-Object方法:

// writeObject for synchronized class with default serialized form
private synchronized void writeObject(ObjectOutputStream s)
        throws IOException {
    s.defaultWriteObject();
}

如果将同步放在writeObject方法中,则必须确保它遵守与其他活动相同的锁排序( lock-ordering)约束,否则将面临资源排(resource-ordering)序死锁的风险[Goetz06, 10.1.5]。

无论选择哪种序列化形式,都要在编写的每个可序列化类中声明显式的序列版本UID。这消除了序列版本UID作为不兼容性的潜在来源(条目 86)。还有一个小的性能优势。如果没有提供序列版本UID,则需要执行昂贵的计算来在运行时生成一个UID。

声明序列版本UID很简单。只需要在类中添加这一行:

private static final long serialVersionUID = randomLongValue;

如编写一个新类,为randomLongValue选择什么值并不重要。可以通过在类上运行serialver实用程序来生成该值,但是也可以凭空选择一个数字。序列版本UID不需要是惟一的。如果修改缺少序列版本UID的现有类,并且希望新版本接受现有的序列化实例,则必须使用为旧版本自动生成的值。可以通过在类的旧版本上运行serialver实用程序(序列化实例存在于旧版本上)来获得这个数字。

如果想要创建与现有版本不兼容的类的新版本,只需更改序列版本UID声明中的值即可。 这将导致尝试反序列化先前版本的序列化实例抛出InvalidClassException异常。 不要更改序列版本UID,除非想破坏与类的所有现有序列化实例的兼容性

总而言之,如果你已确定某个类应该可序列化(条目 86),请仔细考虑序列化形式应该是什么。 仅当它是对象逻辑状态的合理描述时,才使用默认的序列化形式;否则设计一个适当描述对象的自定义序列化形式。 在分配设计导出方法时,应该分配尽可能多的时间来设计类的序列化形式(条目 51)。 正如无法从将来的版本中删除导出的方法一样,也无法从序列化形式中删除属性;必须永久保存它们以确保序列化兼容性。 选择错误的序列化形式会对类的复杂性和性能产生永久性的负面影响。