diff --git a/docs/README.md b/docs/README.md
index bfbc5ae..7f41899 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -97,6 +97,10 @@
- [89. 对于实例控制,枚举类型优于 readResolve.md](notes/89.%20对于实例控制,枚举类型优于%20readResolve.md)
- [90. 考虑用序列化代理代替序列化实例.md](notes/90.%20考虑用序列化代理代替序列化实例.md)
+## 💻 高效 Java 第三版完整版阅读
+
+- [高效 Java 第三版完整版](doc/effective-java-3rd-chinese.md)
+
## 😋 Give me a Favor
diff --git a/docs/doc/effective-java-3rd-chinese.md b/docs/doc/effective-java-3rd-chinese.md
new file mode 100644
index 0000000..c348727
--- /dev/null
+++ b/docs/doc/effective-java-3rd-chinese.md
@@ -0,0 +1,9167 @@
+# 1. 考虑使用静态工厂方法替代构造方法
+
+
+ 一个类允许客户端获取其实例的传统方式是提供一个公共构造方法。 其实还有另一种技术应该成为每个程序员工具箱的一部分。 一个类可以提供一个公共静态工厂方法,它只是一个返回类实例的静态方法。 下面是一个 `Boolean` 简单的例子(`boolean` 基本类型的包装类)。 此方法将 `boolean` 基本类型转换为 `Boolean` 对象引用:
+
+```java
+public static Boolean valueOf(boolean b) {
+ return b ? Boolean.TRUE : Boolean.FALSE;
+}
+```
+
+ 注意,静态工厂方法与设计模式中的工厂方法模式不同[Gamma95]。本条目中描述的静态工厂方法在设计模式中没有直接的等价。
+
+ 类可以为其客户端提供静态工厂方法,而不是公共构造方法。提供静态工厂方法而不是公共构造方法有优点也有缺点。
+
+ **静态工厂方法的一个优点是,不像构造方法,它们是有名字的。** 如果构造方法的参数本身并不描述被返回的对象,则具有精心选择名称的静态工厂更易于使用,并且生成的客户端代码更易于阅读。 例如,返回一个可能为素数的 `BigInteger` 的构造方法 `BigInteger(int,int,Random)` 可以更好地表示为名为 `BigInteger.probablePrime` 的静态工厂方法。 (这个方法是在 Java 1.4 中添加的。)
+
+
+ 一个类只能有一个给定签名的构造方法。 程序员知道通过提供两个构造方法来解决这个限制,这两个构造方法的参数列表只有它们的参数类型的顺序不同。 这是一个非常糟糕的主意。 这样的 API 用户将永远不会记得哪个构造方法是哪个,最终会错误地调用。 阅读使用这些构造方法的代码的人只有在参考类文档的情况下才知道代码的作用。
+
+ 因为他们有名字,所以静态工厂方法不会受到上面讨论中的限制。在类中似乎需要具有相同签名的多个构造方法的情况下,用静态工厂方法替换构造方法,并仔细选择名称来突出它们的差异。
+
+ **静态工厂方法的第二个优点是,与构造方法不同,它们不需要每次调用时都创建一个新对象。** 这允许不可变的类 (条目 17) 使用预先构建的实例,或者在构造时缓存实例,并反复分配它们以避免创建不必要的重复对象。`Boolean.valueof(boolean)` 方法说明了这种方法:它从不创建对象。这种技术类似于 `Flyweight` 模式[Gamma95]。如果经常请求等价对象,那么它可以极大地提高性能,特别是如果在创建它们非常昂贵的情况下。
+
+ 静态工厂方法从重复调用返回相同对象的能力允许类保持在任何时候存在的实例的严格控制。这样做的类被称为实例控制( instance-controlled)。编写实例控制类的原因有很多。实例控制允许一个类来保证它是一个单例 (3) 项或不可实例化的 (条目 4)。同时,它允许一个不可变的值类 (条目 17) 保证不存在两个相同的实例:当且仅当 `a == b` 时 `a.equals(b)`。这是享元模式的基础[Gamma95]。`Enum` 类型 (条目 34) 提供了这个保证。
+
+ **静态工厂方法的第三个优点是,与构造方法不同,它们可以返回其返回类型的任何子类型的对象。** 这为你在选择返回对象的类时提供了很大的灵活性。
+
+ 这种灵活性的一个应用是 API 可以返回对象而不需要公开它的类。 以这种方式隐藏实现类会使 API 非常紧凑 I。 这种技术适用于基于接口的框架(条目 20),其中接口为静态工厂方法提供自然返回类型。
+
+ 在 Java 8 之前,接口不能有静态方法。根据约定,一个名为 `Type` 的接口的静态工厂方法被放入一个非实例化的伙伴类 (companion class)(条目 4)`Types` 类中。例如,Java 集合框架有 45 个接口的实用工具实现,提供不可修改的集合、同步集合等等。几乎所有这些实现都是通过静态工厂方法在一个非实例类 (`java .util. collections`) 中导出的。返回对象的类都是非公开的。
+
+ `Collections` 框架 API 的规模要比它之前输出的 45 个单独的公共类要小得多,每个类有个便利类的实现。不仅是 API 的大部分减少了,还包括概念上的权重:程序员必须掌握的概念的数量和难度,才能使用 API。程序员知道返回的对象恰好有其接口指定的 API,因此不需要为实现类读阅读额外的类文档。此外,使用这种静态工厂方法需要客户端通过接口而不是实现类来引用返回的对象,这通常是良好的实践 (条目 64)。
+
+ 从 Java 8 开始,接口不能包含静态方法的限制被取消了,所以通常没有理由为接口提供一个不可实例化的伴随类。 很多公开的静态成员应该放在这个接口本身。 但是,请注意,将这些静态方法的大部分实现代码放在单独的包私有类中仍然是必要的。 这是因为 Java 8 要求所有接口的静态成员都是公共的。 Java 9 允许私有静态方法,但静态字段和静态成员类仍然需要公开。
+
+ **静态工厂的第四个优点是返回对象的类可以根据输入参数的不同而不同。** 声明的返回类型的任何子类都是允许的。 返回对象的类也可以随每次发布而不同。
+
+ `EnumSet` 类(条目 36)没有公共构造方法,只有静态工厂。 在 OpenJDK 实现中,它们根据底层枚举类型的大小返回两个子类中的一个的实例:如果大多数枚举类型具有 64 个或更少的元素,静态工厂将返回一个 `RegularEnumSet` 实例, 返回一个 `long` 类型;如果枚举类型具有六十五个或更多元素,则工厂将返回一个 `JumboEnumSet` 实例,返回一个 `long` 类型的数组。
+
+ 这两个实现类的存在对于客户是不可见的。 如果 `RegularEnumSet` 不再为小枚举类型提供性能优势,则可以在未来版本中将其淘汰,而不会产生任何不良影响。 同样,未来的版本可能会添加 `EnumSet` 的第三个或第四个实现,如果它证明有利于性能。 客户既不知道也不关心他们从工厂返回的对象的类别; 他们只关心它是 `EnumSet` 的一些子类。
+
+ **静态工厂的第 5 个优点是,在编写包含该方法的类时,返回的对象的类不需要存在。** 这种灵活的静态工厂方法构成了服务提供者框架的基础,比如 Java 数据库连接 API(JDBC)。服务提供者框架是提供者实现服务的系统,并且系统使得实现对客户端可用,从而将客户端从实现中分离出来。
+
+ 服务提供者框架中有三个基本组:服务接口,它表示实现;提供者注册 API,提供者用来注册实现;以及服务访问 API,客户端使用该 API 获取服务的实例。服务访问 API 允许客户指定选择实现的标准。在缺少这样的标准的情况下,API 返回一个默认实现的实例,或者允许客户通过所有可用的实现进行遍历。服务访问 API 是灵活的静态工厂,它构成了服务提供者框架的基础。
+
+ 服务提供者框架的一个可选的第四个组件是一个服务提供者接口,它描述了一个生成服务接口实例的工厂对象。在没有服务提供者接口的情况下,必须对实现进行反射实例化 (条目 65)。在 JDBC 的情况下,`Connection` 扮演服务接口的一部分,`DriverManager.registerDriver` 提供程序注册 API、`DriverManager.getConnection` 是服务访问 API,`Driver` 是服务提供者接口。
+
+ 服务提供者框架模式有许多变种。 例如,服务访问 API 可以向客户端返回比提供者提供的更丰富的服务接口。 这是桥接模式[Gamma95]。 依赖注入框架(条目 5)可以被看作是强大的服务提供者。 从 Java 6 开始,平台包含一个通用的服务提供者框架 `java.util.ServiceLoader`,所以你不需要,一般也不应该自己编写(条目 59)。 JDBC 不使用 `ServiceLoader`,因为前者早于后者。
+
+ **只提供静态工厂方法的主要限制是,没有公共或受保护构造方法的类不能被子类化。** 例如,在 `Collections` 框架中不可能将任何方便实现类子类化。可以说,这可能是因祸得福,因为它鼓励程序员使用组合而不是继承 (条目 18),并且是不可变类型 (条目 17)。
+
+ **静态工厂方法的第二个缺点是,程序员很难找到它们。** 它们不像构造方法那样在 API 文档中突出,因此很难找出如何实例化一个提供静态工厂方法而不是构造方法的类。Javadoc 工具可能有一天会引起对静态工厂方法的注意。与此同时,可以通过将注意力吸引到类或接口文档中的静态工厂以及遵守通用的命名约定来减少这个问题。下面是一些静态工厂方法的常用名称。以下清单并非完整:
+
+ - from——A 类型转换方法,它接受单个参数并返回此类型的相应实例,例如:**Date d = Date.from(instant)**;
+ - of——一个聚合方法,接受多个参数并返回该类型的实例,并把他们合并在一起,例如:**Set faceCards = EnumSet.of(JACK, QUEEN, KING)**;
+ - valueOf——from 和 to 更为详细的替代 方式,例如:**BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE)**;
+ - instance 或 getinstance——返回一个由其参数 (如果有的话) 描述的实例,但不能说它具有相同的值,例如:**StackWalker luke = StackWalker.getInstance(options)**;
+ - create 或 newInstance——与 instance 或 getInstance 类似,除了该方法保证每个调用返回一个新的实例,例如:**Object newArray = Array.newInstance(classObject, arrayLen)**;
+ - getType——与 getInstance 类似,但是如果在工厂方法中不同的类中使用。**Type** 是工厂方法返回的对象类型,例如:**FileStore fs = Files.getFileStore(path)**;
+ - newType——与 newInstance 类似,但是如果在工厂方法中不同的类中使用。Type 是工厂方法返回的对象类型,例如:**BufferedReader br = Files.newBufferedReader(path)**;
+ - type—— getType 和 newType 简洁的替代方式,例如:**List litany = Collections.list(legacyLitany)**;
+
+ 总之,静态工厂方法和公共构造方法都有它们的用途,并且了解它们的相对优点是值得的。通常,静态工厂更可取,因此避免在没有考虑静态工厂的情况下提供公共构造方法。
+
+
+
+# 2. 当构造方法参数过多时使用 builder 模式
+
+
+ 静态工厂和构造方法都有一个限制:它们不能很好地扩展到很多可选参数的情景。请考虑一个代表包装食品上的营养成分标签的例子。这些标签有几个必需的属性——每次建议的摄入量,每罐的份量和每份卡路里 ,以及超过 20 个可选的属性——总脂肪、饱和脂肪、反式脂肪、胆固醇、钠等等。大多数产品都有非零值,只有少数几个可选属性。
+
+ 应该为这样的类编写什么样的构造方法或静态工厂?传统上,程序员使用了可伸缩(telescoping constructor)构造方法模式,在这种模式中,只提供了一个只所需参数的构造函数,另一个只有一个可选参数,第三个有两个可选参数,等等,最终在构造函数中包含所有可选参数。这就是它在实践中的样子。为了简便起见,只显示了四个可选属性:
+
+```java
+// Telescoping constructor pattern - does not scale well!
+
+public class NutritionFacts {
+ private final int servingSize; // (mL) required
+ private final int servings; // (per container) required
+ private final int calories; // (per serving) optional
+ private final int fat; // (g/serving) optional
+ private final int sodium; // (mg/serving) optional
+ private final int carbohydrate; // (g/serving) optional
+
+ public NutritionFacts(int servingSize, int servings) {
+ this(servingSize, servings, 0);
+ }
+
+ public NutritionFacts(int servingSize, int servings,
+ int calories) {
+ this(servingSize, servings, calories, 0);
+ }
+
+ public NutritionFacts(int servingSize, int servings,
+ int calories, int fat) {
+ this(servingSize, servings, calories, fat, 0);
+ }
+
+ public NutritionFacts(int servingSize, int servings,
+ int calories, int fat, int sodium) {
+ this(servingSize, servings, calories, fat, sodium, 0);
+ }
+
+ public NutritionFacts(int servingSize, int servings,
+ int calories, int fat, int sodium, int carbohydrate) {
+ this.servingSize = servingSize;
+ this.servings = servings;
+ this.calories = calories;
+ this.fat = fat;
+ this.sodium = sodium;
+ this.carbohydrate = carbohydrate;
+ }
+}
+```
+
+ 当想要创建一个实例时,可以使用包含所有要设置的参数的最短参数列表的构造方法:
+
+```java
+NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);
+```
+
+ 通常情况下,这个构造方法的调用需要许多你不想设置的参数,但是你不得不为它们传递一个值。 在这种情况下,我们为 `fat` 属性传递了 0 值。 『只有』六个参数可能看起来并不那么糟糕,但随着参数数量的增加,它会很快失控。
+
+ 简而言之,可伸缩构造方法模式是有效的,但是当有很多参数时,很难编写客户端代码,而且很难读懂它。读者不知道这些值是什么意思,并且必须仔细地计算参数才能找到答案。一长串相同类型的参数可能会导致一些细微的 bug。如果客户端意外地反转了两个这样的参数,编译器并不会抱怨,但是程序在运行时会出现错误行为 (条目 51)。
+
+当在构造方法中遇到许多可选参数时,另一种选择是 JavaBeans 模式,在这种模式中,调用一个无参数的构造函数来创建对象,然后调用 `setter` 方法来设置每个必需的参数和可选参数:
+
+```java
+// JavaBeans Pattern - allows inconsistency, mandates mutability
+
+public class NutritionFacts {
+ // Parameters initialized to default values (if any)
+ private int servingSize = -1; // Required; no default value
+ private int servings = -1; // Required; no default value
+ private int calories = 0;
+ private int fat = 0;
+ private int sodium = 0;
+ private int carbohydrate = 0;
+
+ public NutritionFacts() { }
+
+ // Setters
+ public void setServingSize(int val) { servingSize = val; }
+ public void setServings(int val) { servings = val; }
+ public void setCalories(int val) { calories = val; }
+ public void setFat(int val) { fat = val; }
+ public void setSodium(int val) { sodium = val; }
+ public void setCarbohydrate(int val) { carbohydrate = val; }
+}
+```
+
+ 这种模式没有伸缩构造方法模式的缺点。有点冗长,但创建实例很容易,并且易于阅读所生成的代码:
+
+```java
+NutritionFacts cocaCola = new NutritionFacts();
+cocaCola.setServingSize(240);
+cocaCola.setServings(8);
+cocaCola.setCalories(100);
+cocaCola.setSodium(35);
+cocaCola.setCarbohydrate(27);
+```
+
+ 不幸的是,JavaBeans 模式本身有严重的缺陷。由于构造方法在多次调用中被分割,所以在构造过程中 JavaBean 可能处于不一致的状态。该类没有通过检查构造参数参数的有效性来执行一致性的选项。在不一致的状态下尝试使用对象可能会导致与包含 bug 的代码大相径庭的错误,因此很难调试。一个相关的缺点是,JavaBeans 模式排除了让类不可变的可能性 (条目 17),并且需要在程序员的部分增加工作以确保线程安全。
+
+ 当它的构造完成时,手动“冻结”对象,并且不允许它在解冻之前使用,可以减少这些缺点,但是这种变体在实践中很难使用并且很少使用。 而且,在运行时会导致错误,因为编译器无法确保程序员在使用对象之前调用 `freeze` 方法。
+
+ 幸运的是,还有第三种选择,它结合了可伸缩构造方法模式的安全性和 JavaBean 模式的可读性。 它是 Builder 模式[Gamma95] 的一种形式。客户端不直接调用所需的对象,而是调用构造方法 (或静态工厂),并使用所有必需的参数,并获得一个 builder 对象。然后,客户端调用 builder 对象的 `setter` 相似方法来设置每个可选参数。最后,客户端调用一个无参的 `build` 方法来生成对象,该对象通常是不可变的。Builder 通常是它所构建的类的一个静态成员类 (条目 24)。以下是它在实践中的示例:
+
+```java
+// Builder Pattern
+
+public class NutritionFacts {
+ private final int servingSize;
+ private final int servings;
+ private final int calories;
+ private final int fat;
+ private final int sodium;
+ private final int carbohydrate;
+
+ public static class Builder {
+ // Required parameters
+ private final int servingSize;
+ private final int servings;
+
+ // Optional parameters - initialized to default values
+ private int calories = 0;
+ private int fat = 0;
+ private int sodium = 0;
+ private int carbohydrate = 0;
+
+ public Builder(int servingSize, int servings) {
+ this.servingSize = servingSize;
+ this.servings = servings;
+ }
+
+ public Builder calories(int val) {
+ calories = val;
+ return this;
+ }
+
+ public Builder fat(int val) {
+ fat = val;
+ return this;
+ }
+
+ public Builder sodium(int val) {
+ sodium = val;
+ return this;
+ }
+
+ public Builder carbohydrate(int val) {
+ carbohydrate = val;
+ return this;
+ }
+
+ public NutritionFacts build() {
+ return new NutritionFacts(this);
+ }
+ }
+
+ private NutritionFacts(Builder builder) {
+ servingSize = builder.servingSize;
+ servings = builder.servings;
+ calories = builder.calories;
+ fat = builder.fat;
+ sodium = builder.sodium;
+ carbohydrate = builder.carbohydrate;
+ }
+}
+```
+
+ `NutritionFacts` 类是不可变的,所有的参数默认值都在一个地方。builder 的 setter 方法返回 builder 本身,这样调用就可以被链接起来,从而生成一个流畅的 API。下面是客户端代码的示例:
+
+```java
+NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
+ .calories(100).sodium(35).carbohydrate(27).build();
+```
+
+ 这个客户端代码很容易编写,更重要的是易于阅读。 Builder 模式模拟 Python 和 Scala 中的命名可选参数。
+
+ 为了简洁起见,省略了有效性检查。 要尽快检测无效参数,检查 builder 的构造方法和方法中的参数有效性。 在 `build` 方法调用的构造方法中检查包含多个参数的不变性。为了确保这些不变性不受攻击,在从 builder 复制参数后对对象属性进行检查(条目 50)。 如果检查失败,则抛出 `IllegalArgumentException` 异常(条目 72),其详细消息指示哪些参数无效(条目 75)。
+
+ Builder 模式非常适合类层次结构。 使用平行层次的 builder,每个嵌套在相应的类中。 抽象类有抽象的 builder; 具体的类有具体的 builder。 例如,考虑代表各种比萨饼的根层次结构的抽象类:
+
+```java
+// Builder pattern for class hierarchies
+
+import java.util.EnumSet;
+import java.util.Objects;
+import java.util.Set;
+
+public abstract class Pizza {
+ public enum Topping {HAM, MUSHROOM, ONION, PEPPER, SAUSAGE}
+ final Set toppings;
+
+ abstract static class Builder> {
+ EnumSet toppings = EnumSet.noneOf(Topping.class);
+
+ public T addTopping(Topping topping) {
+ toppings.add(Objects.requireNonNull(topping));
+ return self();
+ }
+
+ abstract Pizza build();
+
+ // Subclasses must override this method to return "this"
+ protected abstract T self();
+ }
+
+ Pizza(Builder> builder) {
+ toppings = builder.toppings.clone(); // See Item 50
+ }
+}
+```
+
+ 请注意,`Pizza.Builder` 是一个带有递归类型参数( recursive type parameter)(条目 30)的泛型类型。 这与抽象的 `self` 方法一起,允许方法链在子类中正常工作,而不需要强制转换。 Java 缺乏自我类型的这种变通解决方法被称为模拟自我类型(simulated self-type)的习惯用法。
+
+ 这里有两个具体的 `Pizza` 的子类,其中一个代表标准的纽约风格的披萨,另一个是半圆形烤乳酪馅饼。前者有一个所需的尺寸参数,而后者则允许指定酱汁是否应该在里面或在外面:
+
+```java
+import java.util.Objects;
+
+public class NyPizza extends Pizza {
+ public enum Size { SMALL, MEDIUM, LARGE }
+ private final Size size;
+
+ public static class Builder extends Pizza.Builder {
+ private final Size size;
+
+ public Builder(Size size) {
+ this.size = Objects.requireNonNull(size);
+ }
+
+ @Override public NyPizza build() {
+ return new NyPizza(this);
+ }
+
+ @Override protected Builder self() {
+ return this;
+ }
+ }
+
+ private NyPizza(Builder builder) {
+ super(builder);
+ size = builder.size;
+ }
+}
+
+public class Calzone extends Pizza {
+ private final boolean sauceInside;
+
+ public static class Builder extends Pizza.Builder {
+ private boolean sauceInside = false; // Default
+
+ public Builder sauceInside() {
+ sauceInside = true;
+ return this;
+ }
+
+ @Override public Calzone build() {
+ return new Calzone(this);
+ }
+
+ @Override protected Builder self() {
+ return this;
+ }
+ }
+
+ private Calzone(Builder builder) {
+ super(builder);
+ sauceInside = builder.sauceInside;
+ }
+}
+```
+
+ 请注意,每个子类 builder 中的 `build` 方法被声明为返回正确的子类:`NyPizza.Builder` 的 `build` 方法返回 `NyPizza`,而 `Calzone.Builder` 中的 `build` 方法返回 `Calzone`。 这种技术,其一个子类的方法被声明为返回在超类中声明的返回类型的子类型,称为协变返回类型 ( covariant return typing)。 它允许客户端使用这些 builder,而不需要强制转换。
+
+ 这些“分层 builder”的客户端代码基本上与简单的 `NutritionFacts` builder 的代码相同。为了简洁起见,下面显示的示例客户端代码假设枚举常量的静态导入:
+
+```java
+NyPizza pizza = new NyPizza.Builder(SMALL)
+ .addTopping(SAUSAGE).addTopping(ONION).build();
+Calzone calzone = new Calzone.Builder()
+ .addTopping(HAM).sauceInside().build();
+```
+
+ builder 对构造方法的一个微小的优势是,builder 可以有多个可变参数,因为每个参数都是在它自己的方法中指定的。或者,builder 可以将传递给多个调用的参数聚合到单个属性中,如前面的 `addTopping` 方法所演示的那样。
+
+ Builder 模式非常灵活。 单个 builder 可以重复使用来构建多个对象。 builder 的参数可以在构建方法的调用之间进行调整,以改变创建的对象。 builder 可以在创建对象时自动填充一些属性,例如每次创建对象时增加的序列号。
+
+ Builder 模式也有缺点。为了创建对象,首先必须创建它的 builder。虽然创建这个 builder 的成本在实践中不太可能被注意到,但在性能关键的情况下可能会出现问题。而且,builder 模式比伸缩构造方法模式更冗长,因此只有在有足够的参数时才值得使用它,比如四个或更多。但是请记住,如果希望在将来添加更多的参数。但是,如果从构造方法或静态工厂开始,并切换到 builder,当类演化到参数数量失控的时候,过时的构造方法或静态工厂就会面临尴尬的处境。因此,所以,最好从一开始就创建一个 builder。
+
+ 总而言之,当设计类的构造方法或静态工厂的参数超过几个时,Builder 模式是一个不错的选择,特别是如果许多参数是可选的或相同类型的。客户端代码比使用伸缩构造方法(telescoping constructors)更容易读写,并且 builder 比 JavaBeans 更安全。
+# 3. 使用私有构造方法或枚类实现 Singleton 属性
+
+
+ 单例是一个仅实例化一次的类[Gamma95]。单例对象通常表示无状态对象,如函数 (条目 24) 或一个本质上唯一的系统组件。让一个类成为单例会使测试它的客户变得困难,因为除非实现一个作为它类型的接口,否则不可能用一个模拟实现替代单例。
+
+ 有两种常见的方法来实现单例。两者都基于保持构造方法私有和导出公共静态成员以提供对唯一实例的访问。在第一种方法中,成员是 `final` 修饰的属性:
+
+```java
+// Singleton with public final field
+public class Elvis {
+ public static final Elvis INSTANCE = new Elvis();
+ private Elvis() { ... }
+ public void leaveTheBuilding() { ... }
+}
+```
+
+ 私有构造方法只调用一次,来初始化公共静态 final `Elvis.INSTANCE` 属性。缺少一个公共的或受保护的构造方法,保证了全局的唯一性:一旦 Elvis 类被初始化,一个 Elvis 的实例就会存在——不多也不少。客户端所做的任何事情都不能改变这一点,但需要注意的是:特权客户端可以使用 `AccessibleObject.setAccessible` 方法,以反射方式调用私有构造方法 (条目 65)。如果需要防御此攻击,请修改构造函数,使其在请求创建第二个实例时抛出异常。
+
+ 在第二个实现单例的方法中,公共成员是一个静态的工厂方法:
+
+```java
+// Singleton with static factory
+public class Elvis {
+ private static final Elvis INSTANCE = new Elvis();
+ private Elvis() { ... }
+ public static Elvis getInstance() { return INSTANCE; }
+
+ public void leaveTheBuilding() { ... }
+}
+```
+
+ 所有对 `Elvis.getInstance` 的调用都返回相同的对象引用,并且不会创建其他的 Elvis 实例(与前面提到的警告相同)。
+
+ 公共属性方法的主要优点是 API 明确表示该类是一个单例:公共静态属性是 final 的,所以它总是包含相同的对象引用。 第二个好处是它更简单。
+
+ 静态工厂方法的一个优点是,它可以灵活地改变你的想法,无论该类是否为单例而不必更改其 API。 工厂方法返回唯一的实例,但是可以修改,比如,返回调用它的每个线程的单独实例。 第二个好处是,如果你的应用程序需要它,可以编写一个泛型单例工厂(generic singleton factory )(条目 30)。 使用静态工厂的最后一个优点是方法引用可以用 `supplier`,例如 `Elvis::instance` 等同于 `Supplier`。 除非与这些优点相关的,否则公共属性方法是可取的。
+
+ 创建一个使用这两种方法的单例类 (第 12 章),仅仅将 `implements Serializable` 添加到声明中是不够的。为了维护单例的保证,声明所有的实例属性为 `transient`,并提供一个 `readResolve` 方法 (条目 89)。否则,每当序列化实例被反序列化时,就会创建一个新的实例,在我们的例子中,导致出现新的 Elvis 实例。为了防止这种情况发生,将这个 `readResolve` 方法添加到 Elvis 类:
+
+```java
+// readResolve method to preserve singleton property
+private Object readResolve() {
+ // Return the one true Elvis and let the garbage collector
+ // take care of the Elvis impersonator.
+ return INSTANCE;
+}
+```
+ 实现一个单例的第三种方法是声明单一元素的枚举类:
+
+```java
+// Enum singleton - the preferred approach
+public enum Elvis {
+ INSTANCE;
+
+ public void leaveTheBuilding() { ... }
+}
+```
+
+ 这种方式类似于公共属性方法,但更简洁,提供了免费的序列化机制,并提供了针对多个实例化的坚固保证,即使是在复杂的序列化或反射攻击的情况下。这种方法可能感觉有点不自然,但是单一元素枚举类通常是实现单例的最佳方式。注意,如果单例必须继承 `Enum` 以外的父类 (尽管可以声明一个 `Enum` 来实现接口),那么就不能使用这种方法。
+# 4. 使用私有构造方法执行非实例化
+
+
+ 偶尔你会想写一个类,它只是一组静态方法和静态属性。 这样的类获得了不好的名声,因为有些人滥用这些类而避免以面向对象方式思考,但是它们确实有着特殊的用途。 它们可以用来按照 `java.lang.Math` 或 `java.util.Arrays` 的方式,在基本类型的数值或数组上组织相关的方法。 它们也可以用于将静态方法(包括工厂(条目 1))分组,用于实现某个接口的对象,其方式为 `java.util.Collections`。 (从 Java 8 开始,你也可以将这些方法放在接口中,假如它是你自己修改的。)最后,这样的类可以用于在 final 类上对方法进行分组,因为不能将它们放在子类中。
+
+ 这样的实用类(utility classes)不是设计用来被实例化的:一个实例是没有意义的。然而,在没有显式构造方法的情况下,编译器提供了一个公共的、无参的默认构造方法。对于用户来说,该构造方法与其他构造方法没有什么区别。在已发布的 API 中经常看到无意识的被实例的类。
+
+ **试图通过创建抽象类来强制执行非实例化是行不通的。** 该类可以被子类化,子类可以被实例化。此外,它误导用户认为该类是为继承而设计的 (条目 19)。不过,有一个简单的方法来确保非实例化。只有当类不包含显式构造方法时,才会生成一个默认构造方法,**因此可以通过包含一个私有构造方法来实现类的非实例化:**
+
+```java
+// Noninstantiable utility class
+public class UtilityClass {
+ // Suppress default constructor for noninstantiability
+ private UtilityClass() {
+ throw new AssertionError();
+ }
+ ... // Remainder omitted
+}
+```
+
+ 因为显式构造方法是私有的,所以在类之外是不可访问的。`AssertionError` 异常不是严格要求的,但是它提供了一种保证,以防在类中意外地调用构造方法。它保证类在任何情况下都不会被实例化。这个习惯用法有点违反直觉,好像构造方法就是设计成不能调用的一样。因此,如前面所示,添加注释是种明智的做法。
+
+ 这种习惯有一个副作用,阻止了类的子类化。所有的构造方法都必须显式或隐式地调用父类构造方法,而子类则没有可访问的父类构造方法来调用。
+# 05. 使用依赖注入取代硬连接资源(hardwiring resources)
+
+
+ 许多类依赖于一个或多个底层资源。例如,拼写检查器依赖于字典。将此类类实现为静态实用工具类并不少见 (条目 4):
+
+```java
+// Inappropriate use of static utility - inflexible & untestable!
+public class SpellChecker {
+ private static final Lexicon dictionary = ...;
+
+ private SpellChecker() {} // Noninstantiable
+
+ public static boolean isValid(String word) { ... }
+ public static List suggestions(String typo) { ... }
+}
+```
+
+ 同样地,将它们实现为单例也并不少见 (条目 3):
+
+
+```java
+// Inappropriate use of singleton - inflexible & untestable!
+public class SpellChecker {
+ private final Lexicon dictionary = ...;
+
+ private SpellChecker(...) {}
+ public static INSTANCE = new SpellChecker(...);
+
+ public boolean isValid(String word) { ... }
+ public List suggestions(String typo) { ... }
+}
+```
+
+ 这两种方法都不令人满意,因为他们假设只有一本字典值得使用。在实际中,每种语言都有自己的字典,特殊的字典被用于特殊的词汇表。另外,使用专门的字典来进行测试也是可取的。想当然地认为一本字典就足够了,这是一厢情愿的想法。
+
+ 可以通过使 `dictionary` 属性设置为非 `final`,并添加一个方法来更改现有拼写检查器中的字典,从而让拼写检查器支持多个字典,但是在并发环境中,这是笨拙的、容易出错的和不可行的。静态实用类和单例对于那些行为被底层资源参数化的类来说是不合适的。
+
+ 所需要的是能够支持类的多个实例 (在我们的示例中,即 `SpellChecker`),每个实例都使用客户端所期望的资源 (在我们的例子中是 `dictionary`)。满足这一需求的简单模式是在创建新实例时将资源传递到构造方法中。这是依赖项注入(dependency injection)的一种形式:字典是拼写检查器的一个依赖项,当它创建时被注入到拼写检查器中。
+
+
+```java
+// Dependency injection provides flexibility and testability
+public class SpellChecker {
+ private final Lexicon dictionary;
+
+ public SpellChecker(Lexicon dictionary) {
+ this.dictionary = Objects.requireNonNull(dictionary);
+ }
+
+ public boolean isValid(String word) { ... }
+ public List suggestions(String typo) { ... }
+}
+```
+ 依赖注入模式非常简单,许多程序员使用它多年而不知道它有一个名字。 虽然我们的拼写检查器的例子只有一个资源(字典),但是依赖项注入可以使用任意数量的资源和任意依赖图。 它保持了不变性(条目 17),因此多个客户端可以共享依赖对象(假设客户需要相同的底层资源)。 依赖注入同样适用于构造方法,静态工厂(条目 1)和 builder 模式(条目 2)。
+
+ 该模式的一个有用的变体是将资源工厂传递给构造方法。 工厂是可以重复调用以创建类型实例的对象。 这种工厂体现了工厂方法模式(Factory Method pattern )[Gamma95]。 Java 8 中引入的 `Supplier` 接口非常适合代表工厂。 在输入上采用 `Supplier` 的方法通常应该使用有界的通配符类型 ( bounded wildcard type)(条目 31)约束工厂的类型参数,以允许客户端传入工厂,创建指定类型的任何子类型。 例如,下面是一个使用客户端提供的工厂生成 tile 的方法:
+
+ Mosaic create(Supplier extends Tile> tileFactory) { ... }
+
+ 尽管依赖注入极大地提高了灵活性和可测试性,但它可能使大型项目变得混乱,这些项目通常包含数千个依赖项。使用依赖注入框架 (如 Dagger[Dagger]、Guice[Guice] 或 Spring[Spring]) 可以消除这些混乱。这些框架的使用超出了本书的范围,但是请注意,为手动依赖注入而设计的 API 非常适合这些框架的使用。
+
+ 总之,不要使用单例或静态的实用类来实现一个类,该类依赖于一个或多个底层资源,这些资源的行为会影响类的行为,并且不让类直接创建这些资源。相反,将资源或工厂传递给构造方法 (或静态工厂或 builder 模式)。这种称为依赖注入的实践将极大地增强类的灵活性、可重用性和可测试性。
+# 6. 避免创建不必要的对象
+
+
+ 在每次需要时重用一个对象而不是创建一个新的相同功能对象通常是恰当的。重用可以更快更流行。如果对象是不可变的 (条目 17),它总是可以被重用。
+
+ 作为一个不应该这样做的极端例子,请考虑以下语句:
+
+```java
+String s = new String("bikini"); // DON'T DO THIS!
+```
+
+ 语句每次执行时都会创建一个新的 String 实例,而这些对象的创建都不是必需的。String 构造方法 `("bikini")` 的参数本身就是一个 `bikini` 实例,它与构造方法创建的所有对象的功能相同。如果这种用法发生在循环中,或者在频繁调用的方法中,就可以毫无必要地创建数百万个 String 实例。
+
+ 改进后的版本如下:
+```java
+String s = "bikini";
+```
+
+ 该版本使用单个 String 实例,而不是每次执行时创建一个新实例。此外,它可以保证对象运行在同一虚拟机上的任何其他代码重用,而这些代码恰好包含相同的字符串字面量[JLS,3.10.5]。
+
+ 通过使用静态工厂方法 (static factory methods(条目 1),可以避免创建不需要的对象。例如,工厂方法 `Boolean.valueOf(String)` 比构造方法 `Boolean(String)` 更可取,后者在 Java 9 中被弃用。构造方法每次调用时都必须创建一个新对象,而工厂方法永远不需要这样做,在实践中也不需要。除了重用不可变对象,如果知道它们不会被修改,还可以重用可变对象。
+
+ 一些对象的创建比其他对象的创建要昂贵得多。 如果要重复使用这样一个“昂贵的对象”,建议将其缓存起来以便重复使用。 不幸的是,当创建这样一个对象时并不总是很直观明显的。 假设你想写一个方法来确定一个字符串是否是一个有效的罗马数字。 以下是使用正则表达式完成此操作时最简单方法:
+
+
+```java
+// Performance can be greatly improved!
+static boolean isRomanNumeral(String s) {
+ return s.matches("^(?=.)M*(C[MD]|D?C{0,3})"
+ + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
+}
+```
+
+ 这个实现的问题在于它依赖于 `String.matches` 方法。 虽然 `String.matches` 是检查字符串是否与正则表达式匹配的最简单方法,但它不适合在性能临界的情况下重复使用。 问题是它在内部为正则表达式创建一个 `Pattern` 实例,并且只使用它一次,之后它就有资格进行垃圾收集。 创建 `Pattern` 实例是昂贵的,因为它需要将正则表达式编译成有限状态机(finite state machine)。
+
+ 为了提高性能,作为类初始化的一部分,将正则表达式显式编译为一个 `Pattern` 实例(不可变),缓存它,并在 `isRomanNumeral` 方法的每个调用中重复使用相同的实例:
+
+```java
+// Reusing expensive object for improved performance
+public class RomanNumerals {
+ private static final Pattern ROMAN = Pattern.compile(
+ "^(?=.)M*(C[MD]|D?C{0,3})"
+ + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
+
+ static boolean isRomanNumeral(String s) {
+ return ROMAN.matcher(s).matches();
+ }
+}
+```
+
+ 如果经常调用,`isRomanNumeral` 的改进版本的性能会显著提升。 在我的机器上,原始版本在输入 8 个字符的字符串上需要 1.1 微秒,而改进的版本则需要 0.17 微秒,速度提高了 6.5 倍。 性能上不仅有所改善,而且更明确清晰了。 为不可见的 Pattern 实例创建静态 final 修饰的属性,并允许给它一个名字,这个名字比正则表达式本身更具可读性。
+
+ 如果包含 `isRomanNumeral` 方法的改进版本的类被初始化,但该方法从未被调用,则 ROMAN 属性则没必要初始化。 在第一次调用 `isRomanNumeral` 方法时,可以通过延迟初始化( lazily initializing)属性(条目 83)来排除初始化,但一般不建议这样做。 延迟初始化常常会导致实现复杂化,而性能没有可衡量的改进(条目 67)。
+
+ 当一个对象是不可变的时,很明显它可以被安全地重用,但是在其他情况下,它远没有那么明显,甚至是违反直觉的。考虑适配器(adapters)的情况[Gamma95],也称为视图(views)。一个适配器是一个对象,它委托一个支持对象(backing object),提供一个可替代的接口。由于适配器没有超出其支持对象的状态,因此不需要为给定对象创建多个给定适配器的实例。
+
+ 例如,Map 接口的 `keySet` 方法返回 Map 对象的 Set 视图,包含 Map 中的所有 key。 天真地说,似乎每次调用 `keySet` 都必须创建一个新的 Set 实例,但是对给定 Map 对象的 `keySet` 的每次调用都返回相同的 Set 实例。 尽管返回的 Set 实例通常是可变的,但是所有返回的对象在功能上都是相同的:当其中一个返回的对象发生变化时,所有其他对象也都变化,因为它们全部由相同的 Map 实例支持。 虽然创建 `keySet` 视图对象的多个实例基本上是无害的,但这是没有必要的,也没有任何好处。
+
+ 另一种创建不必要的对象的方法是自动装箱(autoboxing),它允许程序员混用基本类型和包装的基本类型,根据需要自动装箱和拆箱。 自动装箱模糊不清,但不会消除基本类型和装箱基本类型之间的区别。 有微妙的语义区别和不那么细微的性能差异(条目 61)。 考虑下面的方法,它计算所有正整数的总和。 要做到这一点,程序必须使用 `long` 类型,因为 `int` 类型不足以保存所有正整数的总和:
+
+```java
+// Hideously slow! Can you spot the object creation?
+private static long sum() {
+ Long sum = 0L;
+ for (long i = 0; i <= Integer.MAX_VALUE; i++)
+ sum += i;
+ return sum;
+}
+```
+
+ 这个程序的结果是正确的,但由于写错了一个字符,运行的结果要比实际慢很多。变量 `sum` 被声明成了 `Long` 而不是 `long`,这意味着程序构造了大约 231 不必要的 `Long` 实例(大约每次往 `Long` 类型的 `sum` 变量中增加一个 `long` 类型构造的实例),把 `sum` 变量的类型由 `Long` 改为 `long`,在我的机器上运行时间从 6.3 秒降低到 0.59 秒。这个教训很明显:优先使用基本类型而不是装箱的基本类型,也要注意无意识的自动装箱。
+
+ 这个条目不应该被误解为暗示对象创建是昂贵的,应该避免创建对象。 相反,使用构造方法创建和回收小的对象是非常廉价,构造方法只会做很少的显示工作,尤其是在现代 JVM 实现上。 创建额外的对象以增强程序的清晰度,简单性或功能性通常是件好事。
+
+ 相反,除非池中的对象非常重量级,否则通过维护自己的对象池来避免对象创建是一个坏主意。对象池的典型例子就是数据库连接。建立连接的成本非常高,因此重用这些对象是有意义的。但是,一般来说,维护自己的对象池会使代码混乱,增加内存占用,并损害性能。现代 JVM 实现具有高度优化的垃圾收集器,它们在轻量级对象上轻松胜过此类对象池。
+
+ 这个条目的对应点是针对条目 50 的防御性复制(defensive copying)。 目前的条目说:“当你应该重用一个现有的对象时,不要创建一个新的对象”,而条目 50 说:“不要重复使用现有的对象,当你应该创建一个新的对象时。”请注意,重用防御性复制所要求的对象所付出的代价,要远远大于不必要地创建重复的对象。 未能在需要的情况下防御性复制会导致潜在的错误和安全漏洞;而不必要地创建对象只会影响程序的风格和性能。
+# 7. 消除过期的对象引用
+
+
+ 如果你从使用手动内存管理的语言 (如 C 或 C++) 切换到像 Java 这样的带有垃圾收集机制的语言,那么作为程序员的工作就会变得容易多了,因为你的对象在使用完毕以后就自动回收了。当你第一次体验它的时候,它就像魔法一样。这很容易让人觉得你不需要考虑内存管理,但这并不完全正确。
+
+ 考虑以下简单的堆栈实现:
+
+```java
+// Can you spot the "memory leak"?
+public class Stack {
+ private Object[] elements;
+ private int size = 0;
+ private static final int DEFAULT_INITIAL_CAPACITY = 16;
+
+ public Stack() {
+ elements = new Object[DEFAULT_INITIAL_CAPACITY];
+ }
+
+ public void push(Object e) {
+ ensureCapacity();
+ elements[size++] = e;
+ }
+
+ public Object pop() {
+ if (size == 0)
+ throw new EmptyStackException();
+ return elements[--size];
+ }
+
+ /**
+ * Ensure space for at least one more element, roughly
+ * doubling the capacity each time the array needs to grow.
+ */
+ private void ensureCapacity() {
+ if (elements.length == size)
+ elements = Arrays.copyOf(elements, 2 * size + 1);
+ }
+}
+```
+ 这个程序没有什么明显的错误(但是对于泛型版本,请参阅条目 29)。 你可以对它进行详尽的测试,它都会成功地通过每一项测试,但有一个潜在的问题。 笼统地说,程序有一个“内存泄漏”,由于垃圾回收器的活动的增加,或内存占用的增加,静默地表现为性能下降。 在极端的情况下,这样的内存泄漏可能会导致磁盘分页( disk paging),甚至导致内存溢出(OutOfMemoryError)的失败,但是这样的故障相对较少。
+
+ 那么哪里发生了内存泄漏? 如果一个栈增长后收缩,那么从栈弹出的对象不会被垃圾收集,即使使用栈的程序不再引用这些对象。 这是因为栈维护对这些对象的过期引用( obsolete references)。 过期引用简单来说就是永远不会解除的引用。 在这种情况下,元素数组“活动部分(active portion)”之外的任何引用都是过期的。 活动部分是由索引下标小于 size 的元素组成。
+
+ 垃圾收集语言中的内存泄漏(更适当地称为无意的对象保留 unintentional object retentions)是隐蔽的。 如果无意中保留了对象引用,那么不仅这个对象排除在垃圾回收之外,而且该对象引用的任何对象也是如此。 即使只有少数对象引用被无意地保留下来,也可以阻止垃圾回收机制对许多对象的回收,这对性能产生很大的影响。
+
+ 这类问题的解决方法很简单:一旦对象引用过期,将它们设置为 null。 在我们的 `Stack` 类的情景下,只要从栈中弹出,元素的引用就设置为过期。 `pop` 方法的修正版本如下所示:
+
+```java
+public Object pop() {
+ if (size == 0)
+ throw new EmptyStackException();
+ Object result = elements[--size];
+ elements[size] = null; // Eliminate obsolete reference
+ return result;
+}
+```
+ 取消过期引用的另一个好处是,如果它们随后被错误地引用,程序立即抛出 `NullPointerException` 异常,而不是悄悄地做继续做错误的事情。尽可能快地发现程序中的错误是有好处的。
+
+ 当程序员第一次被这个问题困扰时,他们可能会在程序结束后立即清空所有对象引用。这既不是必要的,也不是可取的;它不必要地搞乱了程序。**清空对象引用应该是例外而不是规范。**消除过期引用的最好方法是让包含引用的变量超出范围。如果在最近的作用域范围内定义每个变量 (条目 57),这种自然就会出现这种情况。
+
+ 那么什么时候应该清空一个引用呢?`Stack` 类的哪个方面使它容易受到内存泄漏的影响?简单地说,它管理自己的内存。存储池(storage pool)由 `elements` 数组的元素组成 (对象引用单元,而不是对象本身)。数组中活动部分的元素 (如前面定义的) 被分配,其余的元素都是空闲的。垃圾收集器没有办法知道这些;对于垃圾收集器来说,`elements` 数组中的所有对象引用都同样有效。只有程序员知道数组的非活动部分不重要。程序员可以向垃圾收集器传达这样一个事实,一旦数组中的元素变成非活动的一部分,就可以手动清空这些元素的引用。
+
+ 一般来说,**当一个类自己管理内存时,程序员应该警惕内存泄漏问题。** 每当一个元素被释放时,元素中包含的任何对象引用都应该被清除。
+
+ **另一个常见的内存泄漏来源是缓存。** 一旦将对象引用放入缓存中,很容易忘记它的存在,并且在它变得无关紧要之后,仍然保留在缓存中。对于这个问题有几种解决方案。如果你正好想实现了一个缓存:只要在缓存之外存在对某个项(entry)的键(key)引用,那么这项就是明确有关联的,就可以用 `WeakHashMap` 来表示缓存;这些项在过期之后自动删除。记住,只有当缓存中某个项的生命周期是由外部引用到键(key)而不是值(value)决定时,`WeakHashMap` 才有用。
+
+ 更常见的情况是,缓存项有用的生命周期不太明确,随着时间的推移一些项变得越来越没有价值。在这种情况下,缓存应该偶尔清理掉已经废弃的项。这可以通过一个后台线程 (也许是 `ScheduledThreadPoolExecutor`) 或将新的项添加到缓存时顺便清理。`LinkedHashMap` 类使用它的 `removeEldestEntry` 方法实现了后一种方案。对于更复杂的缓存,可能直接需要使用 `java.lang.ref`。
+
+ 第三个常见的内存泄漏来源是监听器和其他回调。如果你实现了一个 API,其客户端注册回调,但是没有显式地撤销注册回调,除非采取一些操作,否则它们将会累积。确保回调是垃圾收集的一种方法是只存储弱引用(weak references),例如,仅将它们保存在 `WeakHashMap` 的键(key)中。
+
+ 因为内存泄漏通常不会表现为明显的故障,所以它们可能会在系统中保持多年。 通常仅在仔细的代码检查或借助堆分析器( heap profiler)的调试工具才会被发现。 因此,学习如何预见这些问题,并防止这些问题发生,是非常值得的。
+
+
+
+
+# 8. 避免使用 Finalizer 和 Cleaner 机制
+
+ Finalizer 机制是不可预知的,往往是危险的,而且通常是不必要的。 它们的使用会导致不稳定的行为,糟糕的性能和移植性问题。 Finalizer 机制有一些特殊的用途,我们稍后会在这个条目中介绍,但是通常应该避免它们。 从 Java 9 开始,Finalizer 机制已被弃用,但仍被 Java 类库所使用。 Java 9 中 Cleaner 机制代替了 Finalizer 机制。 Cleaner 机制不如 Finalizer 机制那样危险,但仍然是不可预测,运行缓慢并且通常是不必要的。
+
+ 提醒 C++程序员不要把 Java 中的 Finalizer 或 Cleaner 机制当成的 C ++析构函数的等价物。 在 C++中,析构函数是回收对象相关资源的正常方式,是与构造方法相对应的。 在 Java 中,当一个对象变得不可达时,垃圾收集器回收与对象相关联的存储空间,不需要开发人员做额外的工作。 C ++析构函数也被用来回收其他非内存资源。 在 Java 中,try-with-resources 或 try-finally 块用于此目的(条目 9)。
+
+ Finalizer 和 Cleaner 机制的一个缺点是不能保证他们能够及时执行[JLS,12.6]。 在一个对象变得无法访问时,到 Finalizer 和 Cleaner 机制开始运行时,这期间的时间是任意长的。 这意味着你永远不应该 Finalizer 和 Cleaner 机制做任何时间敏感(time-critical)的事情。 例如,依赖于 Finalizer 和 Cleaner 机制来关闭文件是严重的错误,因为打开的文件描述符是有限的资源。 如果由于系统迟迟没有运行 Finalizer 和 Cleaner 机制而导致许多文件被打开,程序可能会失败,因为它不能再打开文件了。
+
+ 及时执行 Finalizer 和 Cleaner 机制是垃圾收集算法的一个功能,这种算法在不同的实现中有很大的不同。程序的行为依赖于 Finalizer 和 Cleaner 机制的及时执行,其行为也可能大不不同。 这样的程序完全可以在你测试的 JVM 上完美运行,然而在你最重要的客户的机器上可能运行就会失败。
+
+ 延迟终结(finalization)不只是一个理论问题。为一个类提供一个 Finalizer 机制可以任意拖延它的实例的回收。一位同事调试了一个长时间运行的 GUI 应用程序,这个应用程序正在被一个 OutOfMemoryError 错误神秘地死掉。分析显示,在它死亡的时候,应用程序的 Finalizer 机制队列上有成千上万的图形对象正在等待被终结和回收。不幸的是,Finalizer 机制线程的运行优先级低于其他应用程序线程,所以对象被回收的速度低于进入队列的速度。语言规范并不保证哪个线程执行 Finalizer 机制,因此除了避免使用 Finalizer 机制之外,没有轻便的方法来防止这类问题。在这方面, Cleaner 机制比 Finalizer 机制要好一些,因为 Java 类的创建者可以控制自己 cleaner 机制的线程,但 cleaner 机制仍然在后台运行,在垃圾回收器的控制下运行,但不能保证及时清理。
+
+ Java 规范不能保证 Finalizer 和 Cleaner 机制能及时运行;它甚至不能能保证它们是否会运行。当一个程序结束后,一些不可达对象上的 Finalizer 和 Cleaner 机制仍然没有运行。因此,不应该依赖于 Finalizer 和 Cleaner 机制来更新持久化状态。例如,依赖于 Finalizer 和 Cleaner 机制来释放对共享资源 (如数据库) 的持久锁,这是一个使整个分布式系统陷入停滞的好方法。
+
+ 不要相信 `System.gc` 和 `System.runFinalization` 方法。 他们可能会增加 Finalizer 和 Cleaner 机制被执行的几率,但不能保证一定会执行。 曾经声称做出这种保证的两个方法:`System.runFinalizersOnExit` 和它的孪生兄弟 `Runtime.runFinalizersOnExit`,包含致命的缺陷,并已被弃用了几十年[ThreadStop]。
+
+ Finalizer 机制的另一个问题是在执行 Finalizer 机制过程中,未捕获的异常会被忽略,并且该对象的 Finalizer 机制也会终止 [JLS, 12.6]。未捕获的异常会使其他对象陷入一种损坏的状态(corrupt state)。如果另一个线程试图使用这样一个损坏的对象,可能会导致任意不确定的行为。通常情况下,未捕获的异常将终止线程并打印堆栈跟踪( stacktrace),但如果发生在 Finalizer 机制中,则不会发出警告。Cleaner 机制没有这个问题,因为使用 Cleaner 机制的类库可以控制其线程。
+
+ 使用 finalizer 和 cleaner 机制会导致严重的性能损失。 在我的机器上,创建一个简单的 `AutoCloseable` 对象,使用 try-with-resources 关闭它,并让垃圾回收器回收它的时间大约是 12 纳秒。 使用 finalizer 机制,而时间增加到 550 纳秒。 换句话说,使用 finalizer 机制创建和销毁对象的速度要慢 50 倍。 这主要是因为 finalizer 机制会阻碍有效的垃圾收集。 如果使用它们来清理类的所有实例 (在我的机器上的每个实例大约是 500 纳秒),那么 cleaner 机制的速度与 finalizer 机制的速度相当,但是如果仅将它们用作安全网( safety net),则 cleaner 机制要快得多,如下所述。 在这种环境下,创建,清理和销毁一个对象在我的机器上需要大约 66 纳秒,这意味着如果你不使用安全网的话,需要支付 5 倍 (而不是 50 倍) 的保险。
+
+ finalizer 机制有一个严重的安全问题:它们会打开你的类来进行 finalizer 机制攻击。finalizer 机制攻击的想法很简单:如果一个异常是从构造方法或它的序列化中抛出的——`readObject` 和 `readResolve` 方法 (第 12 章)——恶意子类的 finalizer 机制可以运行在本应该“中途夭折(died on the vine)”的部分构造对象上。finalizer 机制可以在静态字属性记录对对象的引用,防止其被垃圾收集。一旦记录了有缺陷的对象,就可以简单地调用该对象上的任意方法,而这些方法本来就不应该允许存在。从构造方法中抛出异常应该足以防止对象出现;而在 finalizer 机制存在下,则不是。这样的攻击会带来可怕的后果。Final 类不受 finalizer 机制攻击的影响,因为没有人可以编写一个 final 类的恶意子类。为了保护非 final 类不受 finalizer 机制攻击,编写一个 final 的 `finalize` 方法,它什么都不做。
+
+ 那么,你应该怎样做呢?为对象封装需要结束的资源 (如文件或线程),而不是为该类编写 Finalizer 和 Cleaner 机制?让你的类实现 `AutoCloseable` 接口即可,并要求客户在在不再需要时调用每个实例 close 方法,通常使用 try-with-resources 确保终止,即使面对有异常抛出情况(条目 9)。一个值得一提的细节是实例必须跟踪是否已经关闭:close 方法必须记录在对象里不再有效的属性,其他方法必须检查该属性,如果在对象关闭后调用它们,则抛出 IllegalStateException 异常。
+
+ 那么,Finalizer 和 Cleaner 机制有什么好处呢?它们可能有两个合法用途。一个是作为一个安全网(safety net),以防资源的拥有者忽略了它的 `close` 方法。虽然不能保证 Finalizer 和 Cleaner 机制会迅速运行 (或者根本就没有运行),最好是把资源释放晚点出来,也要好过客户端没有这样做。如果你正在考虑编写这样的安全网 Finalizer 机制,请仔细考虑一下这样保护是否值得付出对应的代价。一些 Java 库类,如 `FileInputStream`、`FileOutputStream`、`ThreadPoolExecutor` 和 `java.sql.Connection`,都有作为安全网的 Finalizer 机制。
+
+ 第二种合理使用 Cleaner 机制的方法与本地对等类(native peers)有关。本地对等类是一个由普通对象委托的本地 (非 Java) 对象。由于本地对等类不是普通的 Java 对象,所以垃圾收集器并不知道它,当它的 Java 对等对象被回收时,本地对等类也不会回收。假设性能是可以接受的,并且本地对等类没有关键的资源,那么 Finalizer 和 Cleaner 机制可能是这项任务的合适的工具。但如果性能是不可接受的,或者本地对等类持有必须迅速回收的资源,那么类应该有一个 `close` 方法,正如前面所述。
+
+ Cleaner 机制使用起来有点棘手。下面是演示该功能的一个简单的 `Room` 类。假设 `Room` 对象必须在被回收前清理干净。`Room` 类实现 `AutoCloseable` 接口;它的自动清理安全网使用的是一个 Cleaner 机制,这仅仅是一个实现细节。与 Finalizer 机制不同,Cleaner 机制不污染一个类的公共 API:
+
+```java
+// An autocloseable class using a cleaner as a safety net
+public class Room implements AutoCloseable {
+ private static final Cleaner cleaner = Cleaner.create();
+
+ // Resource that requires cleaning. Must not refer to Room!
+ private static class State implements Runnable {
+ int numJunkPiles; // Number of junk piles in this room
+
+ State(int numJunkPiles) {
+ this.numJunkPiles = numJunkPiles;
+ }
+
+ // Invoked by close method or cleaner
+ @Override
+ public void run() {
+ System.out.println("Cleaning room");
+ numJunkPiles = 0;
+ }
+ }
+
+ // The state of this room, shared with our cleanable
+ private final State state;
+
+ // Our cleanable. Cleans the room when it’s eligible for gc
+ private final Cleaner.Cleanable cleanable;
+
+ public Room(int numJunkPiles) {
+ state = new State(numJunkPiles);
+ cleanable = cleaner.register(this, state);
+ }
+
+ @Override
+ public void close() {
+ cleanable.clean();
+ }
+}
+```
+ 静态内部 `State` 类拥有 Cleaner 机制清理房间所需的资源。 在这里,它仅仅包含 `numJunkPiles` 属性,它代表混乱房间的数量。 更实际地说,它可能是一个 final 修饰的 `long` 类型的指向本地对等类的指针。 `State` 类实现了 `Runnable` 接口,其 `run` 方法最多只能调用一次,只能被我们在 Room 构造方法中用 `Cleaner` 机制注册 `State` 实例时得到的 `Cleanable` 调用。 对 `run` 方法的调用通过以下两种方法触发:通常,通过调用 `Room` 的 `close` 方法内调用 `Cleanable` 的 `clean` 方法来触发。 如果在 `Room` 实例有资格进行垃圾回收的时候客户端没有调用 `close` 方法,那么 `Cleaner` 机制将(希望)调用 `State` 的 `run` 方法。
+
+ 一个 `State` 实例不引用它的 `Room` 实例是非常重要的。如果它引用了,则创建了一个循环,阻止了 `Room` 实例成为垃圾收集的资格 (以及自动清除)。因此,`State` 必须是静态的嵌内部类,因为非静态内部类包含对其宿主类的实例的引用 (条目 24)。同样,使用 lambda 表达式也是不明智的,因为它们很容易获取对宿主类对象的引用。
+
+ 就像我们之前说的,`Room` 的 Cleaner 机制仅仅被用作一个安全网。如果客户将所有 `Room` 的实例放在 try-with-resource 块中,则永远不需要自动清理。行为良好的客户端如下所示:
+
+```java
+public class Adult {
+ public static void main(String[] args) {
+ try (Room myRoom = new Room(7)) {
+ System.out.println("Goodbye");
+ }
+ }
+}
+```
+ 正如你所预料的,运行 `Adult` 程序会打印 `Goodbye` 字符串,随后打印 `Cleaning room` 字符串。但是如果时不合规矩的程序,它从来不清理它的房间会是什么样的?
+
+```java
+public class Teenager {
+ public static void main(String[] args) {
+ new Room(99);
+ System.out.println("Peace out");
+ }
+}
+```
+ 你可能期望它打印出 `Peace out`,然后打印 `Cleaning room` 字符串,但在我的机器上,它从不打印 `Cleaning room` 字符串;仅仅是程序退出了。 这是我们之前谈到的不可预见性。 Cleaner 机制的规范说:“`System.exit` 方法期间的清理行为是特定于实现的。 不保证清理行为是否被调用。”虽然规范没有说明,但对于正常的程序退出也是如此。 在我的机器上,将 `System.gc()` 方法添加到 `Teenager` 类的 `main` 方法足以让程序退出之前打印 `Cleaning room`,但不能保证在你的机器上会看到相同的行为。
+
+ 总之,除了作为一个安全网或者终止非关键的本地资源,不要使用 Cleaner 机制,或者是在 Java 9 发布之前的 finalizers 机制。即使是这样,也要当心不确定性和性能影响。
+
+
+# 9. 使用 try-with-resources 语句替代 try-finally 语句
+
+ Java 类库中包含许多必须通过调用 `close` 方法手动关闭的资源。 比如 `InputStream`,`OutputStream` 和 `java.sql.Connection`。 客户经常忽视关闭资源,其性能结果可想而知。 尽管这些资源中有很多使用 finalizer 机制作为安全网,但 finalizer 机制却不能很好地工作(条目 8)。
+
+ 从以往来看,try-finally 语句是保证资源正确关闭的最佳方式,即使是在程序抛出异常或返回的情况下:
+
+```java
+// try-finally - No longer the best way to close resources!
+static String firstLineOfFile(String path) throws IOException {
+ BufferedReader br = new BufferedReader(new FileReader(path));
+ try {
+ return br.readLine();
+ } finally {
+ br.close();
+ }
+}
+```
+
+ 这可能看起来并不坏,但是当添加第二个资源时,情况会变得更糟:
+
+```java
+// try-finally is ugly when used with more than one resource!
+static void copy(String src, String dst) throws IOException {
+ InputStream in = new FileInputStream(src);
+ try {
+ OutputStream out = new FileOutputStream(dst);
+ try {
+ byte[] buf = new byte[BUFFER_SIZE];
+ int n;
+ while ((n = in.read(buf)) >= 0)
+ out.write(buf, 0, n);
+ } finally {
+ out.close();
+ }
+ } finally {
+ in.close();
+ }
+}
+```
+ 这可能很难相信,但即使是优秀的程序员,大多数时候也会犯错误。首先,我在 Java Puzzlers[Bloch05] 的第 88 页上弄错了,多年来没有人注意到。事实上,2007 年 Java 类库中使用 `close` 方法的三分之二都是错误的。
+
+ 即使是用 try-finally 语句关闭资源的正确代码,如前面两个代码示例所示,也有一个微妙的缺陷。 try-with-resources 块和 finally 块中的代码都可以抛出异常。 例如,在 `firstLineOfFile` 方法中,由于底层物理设备发生故障,对 `readLine` 方法的调用可能会引发异常,并且由于相同的原因,调用 `close` 方法可能会失败。 在这种情况下,第二个异常完全冲掉了第一个异常。 在异常堆栈跟踪中没有第一个异常的记录,这可能使实际系统中的调试非常复杂——通常这是你想要诊断问题的第一个异常。 虽然可以编写代码来抑制第二个异常,但是实际上没有人这样做,因为它太冗长了。
+
+ 当 Java 7 引入了 try-with-resources 语句时,所有这些问题一下子都得到了解决[JLS,14.20.3]。要使用这个构造,资源必须实现 `AutoCloseable` 接口,该接口由一个返回为 `void` 的 `close` 组成。Java 类库和第三方类库中的许多类和接口现在都实现或继承了 `AutoCloseable` 接口。如果你编写的类表示必须关闭的资源,那么这个类也应该实现 `AutoCloseable` 接口。
+
+ 以下是我们的第一个使用 try-with-resources 的示例:
+
+```java
+// try-with-resources - the the best way to close resources!
+static String firstLineOfFile(String path) throws IOException {
+ try (BufferedReader br = new BufferedReader(
+ new FileReader(path))) {
+ return br.readLine();
+ }
+}
+```
+
+ 以下是我们的第二个使用 try-with-resources 的示例:
+
+```java
+// try-with-resources on multiple resources - short and sweet
+static void copy(String src, String dst) throws IOException {
+ try (InputStream in = new FileInputStream(src);
+ OutputStream out = new FileOutputStream(dst)) {
+ byte[] buf = new byte[BUFFER_SIZE];
+ int n;
+ while ((n = in.read(buf)) >= 0)
+ out.write(buf, 0, n);
+ }
+}
+```
+ 不仅 try-with-resources 版本比原始版本更精简,更好的可读性,而且它们提供了更好的诊断。 考虑 `firstLineOfFile` 方法。 如果调用 `readLine` 和(不可见)`close` 方法都抛出异常,则后一个异常将被抑制(suppressed),而不是前者。 事实上,为了保留你真正想看到的异常,可能会抑制多个异常。 这些抑制的异常没有被抛弃, 而是打印在堆栈跟踪中,并标注为被抑制了。 你也可以使用 `getSuppressed` 方法以编程方式访问它们,该方法在 Java 7 中已添加到的 `Throwable` 中。
+
+ 可以在 try-with-resources 语句中添加 catch 子句,就像在常规的 try-finally 语句中一样。这允许你处理异常,而不会在另一层嵌套中污染代码。作为一个稍微有些做作的例子,这里有一个版本的 `firstLineOfFile` 方法,它不会抛出异常,但是如果它不能打开或读取文件,则返回默认值:
+
+
+```java
+// try-with-resources with a catch clause
+static String firstLineOfFile(String path, String defaultVal) {
+ try (BufferedReader br = new BufferedReader(
+ new FileReader(path))) {
+ return br.readLine();
+ } catch (IOException e) {
+ return defaultVal;
+ }
+}
+```
+
+ 结论明确:在处理必须关闭的资源时,使用 try-with-resources 语句替代 try-finally 语句。 生成的代码更简洁,更清晰,并且生成的异常更有用。 try-with-resources 语句在编写必须关闭资源的代码时会更容易,也不会出错,而使用 try-finally 语句实际上是不可能的。
+
+
+# 10. 重写 equals 方法时遵守通用约定
+
+
+ 虽然 `Object` 是一个具体的类,但它主要是为继承而设计的。它的所有非 final 方法 (equals、hashCode、toString、clone 和 finalize) 都有清晰的通用约定( general contracts),因为它们被设计为被子类重写。任何类都有义务重写这些方法,以遵从他们的通用约定;如果不这样做,将会阻止其他依赖于约定的类 (例如 HashMap 和 HashSet) 与此类一起正常工作。
+
+ 本章论述何时以及如何重写 `Object` 类的非 final 的方法。这一章省略了 finalize 方法,因为它在条目 8 中进行了讨论。`Comparable.compareTo` 方法虽然不是 `Object` 中的方法,因为具有很多的相似性,所以也在这里讨论。
+
+ 重写 equals 方法看起来很简单,但是有很多方式会导致重写出错,其结果可能是可怕的。避免此问题的最简单方法是不覆盖 equals 方法,在这种情况下,类的每个实例只与自身相等。如果满足以下任一下条件,则说明是正确的做法:
+
+ - 每个类的实例都是固有唯一的。 对于像 Thread 这样代表活动实体而不是值的类来说,这是正确的。 Object 提供的 equals 实现对这些类完全是正确的行为。
+ - 类不需要提供一个“逻辑相等(logical equality)”的测试功能。例如 `java.util.regex.Pattern` 可以重写 equals 方法检查两个是否代表完全相同的正则表达式 Pattern 实例,但是设计者并不认为客户需要或希望使用此功能。在这种情况下,从 Object 继承的 equals 实现是最合适的。
+ - 父类已经重写了 equals 方法,则父类行为完全适合于该子类。例如,大多数 Set 从 AbstractSet 继承了 equals 实现、List 从 AbstractList 继承了 equals 实现,Map 从 AbstractMap 的 Map 继承了 equals 实现。
+ - 类是私有的或包级私有的,可以确定它的 equals 方法永远不会被调用。如果你非常厌恶风险,可以重写 equals 方法,以确保不会被意外调用:
+
+```java
+@Override
+public boolean equals(Object o) {
+ throw new AssertionError(); // Method is never called
+}
+```
+
+ 什么时候需要重写 equals 方法呢?如果一个类包含一个逻辑相等( logical equality)的概念,此概念有别于对象标识(object identity),而且父类还没有重写过 equals 方法。这通常用在值类( value classes)的情况。值类只是一个表示值的类,例如 Integer 或 String 类。程序员使用 equals 方法比较值对象的引用,期望发现它们在逻辑上是否相等,而不是引用相同的对象。重写 equals 方法不仅可以满足程序员的期望,它还支持重写过 equals 的实例作为 Map 的键(key),或者 Set 里的元素,以满足预期和期望的行为。
+
+ 一种不需要 equals 方法重写的值类是使用实例控制(instance control)(条目 1)的类,以确保每个值至多存在一个对象。 枚举类型(条目 34)属于这个类别。 对于这些类,逻辑相等与对象标识是一样的,所以 Object 的 equals 方法作用逻辑 equals 方法。
+
+ 当你重写 equals 方法时,必须遵守它的通用约定。Object 的规范如下:
+equals 方法实现了一个等价关系(equivalence relation)。它有以下这些属性:
+
+ - **自反性:** 对于任何非空引用 x,`x.equals(x)` 必须返回 true。
+ - **对称性:** 对于任何非空引用 x 和 y,如果且仅当 `y.equals(x)` 返回 true 时 `x.equals(y)` 必须返回 true。
+ - **传递性:** 对于任何非空引用 x、y、z,如果 `x.equals(y)` 返回 true,`y.equals(z)` 返回 true,则 `x.equals(z)` 必须返回 true。
+ - **一致性:** 对于任何非空引用 x 和 y,如果在 equals 比较中使用的信息没有修改,则 `x.equals(y)` 的多次调用必须始终返回 true 或始终返回 false。
+ - 对于任何非空引用 x,`x.equals(null)` 必须返回 false。
+
+ 除非你喜欢数学,否则这看起来有点吓人,但不要忽略它!如果一旦违反了它,很可能会发现你的程序运行异常或崩溃,并且很难确定失败的根源。套用约翰·多恩 (John Donne) 的说法,没有哪个类是孤立存在的。一个类的实例常常被传递给另一个类的实例。许多类,包括所有的集合类,都依赖于传递给它们遵守 equals 约定的对象。
+
+ 既然已经意识到违反 equals 约定的危险,让我们详细地讨论一下这个约定。好消息是,表面上看,这并不是很复杂。一旦你理解了,就不难遵守这一约定。
+
+ 那么什么是等价关系? 笼统地说,它是一个运算符,它将一组元素划分为彼此元素相等的子集。 这些子集被称为等价类(equivalence classes)。 为了使 equals 方法有用,每个等价类中的所有元素必须从用户的角度来说是可以互换(interchangeable)的。 现在让我们依次看下这个五个要求:
+
+ **自反性(Reflexivity**)——第一个要求只是说一个对象必须与自身相等。 很难想象无意中违反了这个规定。 如果你违反了它,然后把类的实例添加到一个集合中,那么 `contains` 方法可能会说集合中没有包含刚添加的实例。
+
+ **对称性(Symmetry)**——第二个要求是,任何两个对象必须在是否相等的问题上达成一致。与第一个要求不同的是,我们不难想象在无意中违反了这一要求。例如,考虑下面的类,它实现了不区分大小写的字符串。字符串被 toString 保存,但在 equals 比较中被忽略:
+
+```java
+import java.util.Objects;
+
+public final class CaseInsensitiveString {
+ private final String s;
+
+ public CaseInsensitiveString(String s) {
+ this.s = Objects.requireNonNull(s);
+ }
+
+ // Broken - violates symmetry!
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof CaseInsensitiveString)
+ return s.equalsIgnoreCase(
+ ((CaseInsensitiveString) o).s);
+ if (o instanceof String) // One-way interoperability!
+ return s.equalsIgnoreCase((String) o);
+ return false;
+ }
+ ...// Remainder omitted
+}
+```
+
+ 上面类中的 equals 试图与正常的字符串进行操作,假设我们有一个不区分大小写的字符串和一个正常的字符串:
+```java
+CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
+String s = "polish";
+
+System.out.println(cis.equals(s)); // true
+System.out.println(s.equals(cis)); // false
+```
+
+ 正如所料,`cis.equals(s)` 返回 true。 问题是,尽管 `CaseInsensitiveString` 类中的 equals 方法知道正常字符串,但 String 类中的 equals 方法却忽略了不区分大小写的字符串。 因此,`s.equals(cis)` 返回 false,明显违反对称性。 假设把一个不区分大小写的字符串放入一个集合中:
+
+```java
+List list = new ArrayList<>();
+list.add(cis);List list = new ArrayList<>();
+list.add(cis);
+```
+
+ `list.contains(s)` 返回了什么?谁知道呢?在当前的 OpenJDK 实现中,它会返回 false,但这只是一个实现构件。在另一个实现中,它可以很容易地返回 true 或抛出运行时异常。一旦违反了 equals 约定,就不知道其他对象在面对你的对象时会如何表现了。
+
+ 要消除这个问题,只需删除 equals 方法中与 String 类相互操作的恶意尝试。这样做之后,可以将该方法重构为单个返回语句:
+
+```java
+@Override
+public boolean equals(Object o) {
+ return o instanceof CaseInsensitiveString &&
+ ((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
+}
+```
+ **传递性(Transitivity)**——equals 约定的第三个要求是,如果第一个对象等于第二个对象,第二个对象等于第三个对象,那么第一个对象必须等于第三个对象。同样,也不难想象,无意中违反了这一要求。考虑子类的情况, 将新值组件( value component)添加到其父类中。换句话说,子类添加了一个信息,它影响了 equals 方法比较。让我们从一个简单不可变的二维整数类型 Point 类开始:
+
+```java
+public class Point {
+ private final int x;
+ private final int y;
+
+ public Point(int x, int y) {
+ this.x = x;
+ this.y = y;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof Point))
+ return false;
+ Point p = (Point) o;
+ return p.x == x && p.y == y;
+ }
+
+ ... // Remainder omitted
+}
+```
+
+ 假设想继承这个类,将表示颜色的 Color 类添加到 Point 类中:
+
+```java
+public class ColorPoint extends Point {
+ private final Color color;
+
+ public ColorPoint(int x, int y, Color color) {
+ super(x, y);
+ this.color = color;
+ }
+
+ ... // Remainder omitted
+}
+```
+
+ equals 方法应该是什么样子?如果完全忽略,则实现是从 Point 类上继承的,颜色信息在 equals 方法比较中被忽略。虽然这并不违反 equals 约定,但这显然是不可接受的。假设你写了一个 equals 方法,它只在它的参数是另一个具有相同位置和颜色的 ColorPoint 实例时返回 true:
+
+
+```java
+// Broken - violates symmetry!
+@Override
+public boolean equals(Object o) {
+ if (!(o instanceof ColorPoint))
+ return false;
+ return super.equals(o) && ((ColorPoint) o).color == color;
+}
+```
+
+ 当你比较 Point 对象和 ColorPoint 对象时,可以会得到不同的结果,反之亦然。前者的比较忽略了颜色属性,而后者的比较会一直返回 false,因为参数的类型是错误的。为了让问题更加具体,我们创建一个 Point 对象和 ColorPoint 对象:
+
+```java
+Point p = new Point(1, 2);
+ColorPoint cp = new ColorPoint(1, 2, Color.RED);
+```
+
+ p.equals(cp) 返回 true,但是 cp.equals(p) 返回 false。你可能想使用 ColorPoint.equals 通过混合比较的方式来解决这个问题。
+
+```java
+@Override
+public boolean equals(Object o) {
+ if (!(o instanceof Point))
+ return false;
+
+ // If o is a normal Point, do a color-blind comparison
+ if (!(o instanceof ColorPoint))
+ return o.equals(this);
+
+ // o is a ColorPoint; do a full comparison
+ return super.equals(o) && ((ColorPoint) o).color == color;
+}
+```
+
+ 这种方法确实提供了对称性,但是丧失了传递性:
+
+```java
+ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
+Point p2 = new Point(1, 2);
+ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
+```
+
+ 现在,`p1.equals(p2)` 和 `p2.equals(p3)` 返回了 true,但是 `p1.equals(p3)` 却返回了 false,很明显违背了传递性的要求。前两个比较都是不考虑颜色信息的,而第三个比较时却包含颜色信息。
+
+ 此外,这种方法可能导致无限递归:假设有两个 Point 的子类,比如 ColorPoint 和 SmellPoint,每个都有这种 equals 方法。 然后调用 `myColorPoint.equals(mySmellPoint)` 将抛出一个 StackOverflowError 异常。
+
+ 那么解决方案是什么? 事实证明,这是面向对象语言中关于等价关系的一个基本问题。 除非您愿意放弃面向对象抽象的好处,否则无法继承可实例化的类,并在保留 equals 约定的同时添加一个值组件。
+
+ 你可能听说过,可以继承一个可实例化的类并添加一个值组件,同时通过在 equals 方法中使用一个 getClass 测试代替 instanceof 测试来保留 equals 约定:
+
+```java
+@Override
+public boolean equals(Object o) {
+ if (o == null || o.getClass() != getClass())
+ return false;
+ Point p = (Point) o;
+ return p.x == x && p.y == y;
+}
+```
+
+ 只有当对象具有相同的实现类时,才会产生相同的效果。这看起来可能不是那么糟糕,但是结果是不可接受的:一个 Point 类子类的实例仍然是一个 Point 的实例,它仍然需要作为一个 Point 来运行,但是如果你采用这个方法,就会失败!假设我们要写一个方法来判断一个 Point 对象是否在 unitCircle 集合中。我们可以这样做:
+
+```java
+private static final Set unitCircle = Set.of(
+ new Point( 1, 0), new Point( 0, 1),
+ new Point(-1, 0), new Point( 0, -1));
+
+public static boolean onUnitCircle(Point p) {
+ return unitCircle.contains(p);
+}
+```
+
+ 虽然这可能不是实现功能的最快方法,但它可以正常工作。假设以一种不添加值组件的简单方式继承 Point 类,比如让它的构造方法跟踪记录创建了多少实例:
+
+```java
+public class CounterPoint extends Point {
+ private static final AtomicInteger counter =
+ new AtomicInteger();
+
+ public CounterPoint(int x, int y) {
+ super(x, y);
+ counter.incrementAndGet();
+ }
+
+ public static int numberCreated() {
+ return counter.get();
+ }
+}
+```
+
+ 里氏替代原则( Liskov substitution principle)指出,任何类型的重要属性都应该适用于所有的子类型,因此任何为这种类型编写的方法都应该在其子类上同样适用[Liskov87]。 这是我们之前声明的一个正式陈述,即 Point 的子类(如 CounterPoint)仍然是一个 Point,必须作为一个 Point 类来看待。 但是,假设我们将一个 CounterPoint 对象传递给 onUnitCircle 方法。 如果 Point 类使用基于 getClass 的 equals 方法,则无论 CounterPoint 实例的 x 和 y 坐标如何,onUnitCircle 方法都将返回 false。 这是因为大多数集合(包括 onUnitCircle 方法使用的 HashSet)都使用 equals 方法来测试是否包含元素,并且 CounterPoint 实例并不等于任何 Point 实例。 但是,如果在 Point 上使用了适当的基于 `instanceof` 的 equals 方法,则在使用 CounterPoint 实例呈现时,同样的 onUnitCircle 方法可以正常工作。
+
+ 虽然没有令人满意的方法来继承一个可实例化的类并添加一个值组件,但是有一个很好的变通方法:按照条目 18 的建议,“优先使用组合而不是继承”。取代继承 Point 类的 ColorPoint 类,可以在 ColorPoint 类中定义一个私有 Point 属性,和一个公共的试图(view)(条目 6)方法,用来返回具有相同位置的 ColorPoint 对象。
+
+```java
+// Adds a value component without violating the equals contract
+public class ColorPoint {
+ private final Point point;
+ private final Color color;
+
+ public ColorPoint(int x, int y, Color color) {
+ point = new Point(x, y);
+ this.color = Objects.requireNonNull(color);
+ }
+
+ /**
+ * Returns the point-view of this color point.
+ */
+ public Point asPoint() {
+ return point;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof ColorPoint))
+ return false;
+ ColorPoint cp = (ColorPoint) o;
+ return cp.point.equals(point) && cp.color.equals(color);
+ }
+
+ ... // Remainder omitted
+}
+```
+
+ Java 平台类库中有一些类可以继承可实例化的类并添加一个值组件。 例如,`java.sql.Timestamp` 继承了 `java.util.Date` 并添加了一个 nanoseconds 字段。 Timestamp 的等价 equals 确实违反了对称性,并且如果 Timestamp 和 Date 对象在同一个集合中使用,或者以其他方式混合使用,则可能导致不稳定的行为。 Timestamp 类有一个免责声明,告诫程序员不要混用 Timestamp 和 Date。 虽然只要将它们分开使用就不会遇到麻烦,但没有什么可以阻止你将它们混合在一起,并且由此产生的错误可能很难调试。 Timestamp 类的这种行为是一个错误,不应该被仿效。
+
+ 你可以将值组件添加到抽象类的子类中,而不会违反 equals 约定。这对于通过遵循第 23 个条目中“优先考虑类层级(class hierarchies)来代替标记类(tagged classes)”中的建议而获得的类层级,是非常重要的。例如,可以有一个没有值组件的抽象类 Shape,子类 Circle 有一个 radius 属性,另一个子类 Rectangle 包含 length 和 width 属性 。 只要不直接创建父类实例,就不会出现前面所示的问题。
+
+ **一致性(Consistent)**——equals 约定的第四个要求是,如果两个对象是相等的,除非一个(或两个)对象被修改了, 那么它们必须始终保持相等。 换句话说,可变对象可以在不同时期可以与不同的对象相等,而不可变对象则不会。 当你写一个类时,要认真思考它是否应该设计为不可变的(条目 17)。 如果你认为应该这样做,那么确保你的 equals 方法强制执行这样的限制:相等的对象永远相等,不相等的对象永远都不会相等。
+
+ 不管一个类是不是不可变的,都不要写一个依赖于不可靠资源的 equals 方法。 如果违反这一禁令,满足一致性要求是非常困难的。 例如,`java.net.URL` 类中的 equals 方法依赖于与 URL 关联的主机的 IP 地址的比较。 将主机名转换为 IP 地址可能需要访问网络,并且不能保证随着时间的推移会产生相同的结果。 这可能会导致 URL 类的 equals 方法违反 equals 约定,并在实践中造成问题。 URL 类的 equals 方法的行为是一个很大的错误,不应该被效仿。 不幸的是,由于兼容性的要求,它不能改变。 为了避免这种问题,equals 方法应该只对内存驻留对象执行确定性计算。
+
+ **非空性(Non-nullity)**——最后 equals 约定的要求没有官方的名称,所以我冒昧地称之为“非空性”。意思是说说所有的对象都必须不等于 null。虽然很难想象在调用 `o.equals(null)` 的响应中意外地返回 true,但不难想象不小心抛出 `NullPointerException` 异常的情况。通用的约定禁止抛出这样的异常。许多类中的 equals 方法都会明确阻止对象为 null 的情况:
+
+```java
+@Override
+public boolean equals(Object o) {
+ if (o == null)
+ return false;
+ ...
+}
+```
+
+ 这个判断是不必要的。 为了测试它的参数是否相等,equals 方法必须首先将其参数转换为合适类型,以便调用访问器或允许访问的属性。 在执行类型转换之前,该方法必须使用 instanceof 运算符来检查其参数是否是正确的类型:
+
+```java
+@Override
+public boolean equals(Object o) {
+ if (!(o instanceof MyType))
+ return false;
+ MyType mt = (MyType) o;
+ ...
+}
+```
+
+ 如果此类型检查漏掉,并且 equals 方法传递了错误类型的参数,那么 equals 方法将抛出 `ClassCastException` 异常,这违反了 equals 约定。 但是,如果第一个操作数为 null,则指定 instanceof 运算符返回 false,而不管第二个操作数中出现何种类型[JLS,15.20.2]。 因此,如果传入 null,类型检查将返回 false,因此不需要 明确的 null 检查。
+
+ 综合起来,以下是编写高质量 equals 方法的配方(recipe):
+
+ 1. 使用 == 运算符检查参数是否为该对象的引用。如果是,返回 true。这只是一种性能优化,但是如果这种比较可能很昂贵的话,那就值得去做。
+ 2. 使用 `instanceof` 运算符来检查参数是否具有正确的类型。 如果不是,则返回 false。 通常,正确的类型是 equals 方法所在的那个类。 有时候,改类实现了一些接口。 如果类实现了一个接口,该接口可以改进 equals 约定以允许实现接口的类进行比较,那么使用接口。 集合接口(如 Set,List,Map 和 Map.Entry)具有此特性。
+ 3. 参数转换为正确的类型。因为转换操作在 instanceof 中已经处理过,所以它肯定会成功。
+ 4. 对于类中的每个“重要”的属性,请检查该参数属性是否与该对象对应的属性相匹配。如果所有这些测试成功,返回 true,否则返回 false。如果步骤 2 中的类型是一个接口,那么必须通过接口方法访问参数的属性;如果类型是类,则可以直接访问属性,这取决于属性的访问权限。
+
+ 对于类型为非 float 或 double 的基本类型,使用 == 运算符进行比较;对于对象引用属性,递归地调用 equals 方法;对于 float 基本类型的属性,使用静态 `Float.compare(float, float)` 方法;对于 double 基本类型的属性,使用 `Double.compare(double, double)` 方法。由于存在 `Float.NaN`,`-0.0f` 和类似的 double 类型的值,所以需要对 float 和 double 属性进行特殊的处理;有关详细信息,请参阅 JLS 15.21.1 或 Float.equals 方法的详细文档。 虽然你可以使用静态方法 Float.equals 和 Double.equals 方法对 float 和 double 基本类型的属性进行比较,这会导致每次比较时发生自动装箱,引发非常差的性能。 对于数组属性,将这些准则应用于每个元素。 如果数组属性中的每个元素都很重要,请使用其中一个重载的 Arrays.equals 方法。
+
+ 某些对象引用的属性可能合法地包含 null。 为避免出现 NullPointerException 异常,请使用静态方法 Objects.equals(Object, Object) 检查这些属性是否相等。
+
+ 对于一些类,例如上的 `CaseInsensitiveString` 类,属性比较相对于简单的相等性测试要复杂得多。在这种情况下,你想要保存属性的一个规范形式( canonical form),这样 equals 方法就可以基于这个规范形式去做开销很小的精确比较,来取代开销很大的非标准比较。这种方式其实最适合不可变类(条目 17)。一旦对象发生改变,一定要确保把对应的规范形式更新到最新。
+
+ equals 方法的性能可能受到属性比较顺序的影响。 为了获得最佳性能,你应该首先比较最可能不同的属性,开销比较小的属性,或者最好是两者都满足(derived fields)。 你不要比较不属于对象逻辑状态的属性,例如用于同步操作的 lock 属性。 不需要比较可以从“重要属性”计算出来的派生属性,但是这样做可以提高 equals 方法的性能。 如果派生属性相当于对整个对象的摘要描述,比较这个属性将节省在比较失败时再去比较实际数据的开销。 例如,假设有一个 Polygon 类,并缓存该区域。 如果两个多边形的面积不相等,则不必费心比较它们的边和顶点。
+
+ 当你完成编写完 equals 方法时,问你自己三个问题:它是对称的吗?它是传递吗?它是一致的吗?除此而外,编写单元测试加以排查,除非使用 AutoValue 框架 (第 49 页) 来生成 equals 方法,在这种情况下可以安全地省略测试。如果持有的属性失败,找出原因,并相应地修改 equals 方法。当然,equals 方法也必须满足其他两个属性 (自反性和非空性),但这两个属性通常都会满足。
+
+ 在下面这个简单的 `PhoneNumber` 类中展示了根据之前的配方构建的 equals 方法:
+
+```java
+public final class PhoneNumber {
+
+ private final short areaCode, prefix, lineNum;
+
+ public PhoneNumber(int areaCode, int prefix, int lineNum) {
+ this.areaCode = rangeCheck(areaCode, 999, "area code");
+ this.prefix = rangeCheck(prefix, 999, "prefix");
+ this.lineNum = rangeCheck(lineNum, 9999, "line num");
+ }
+
+ private static short rangeCheck(int val, int max, String arg) {
+ if (val < 0 || val > max)
+ throw new IllegalArgumentException(arg + ": " + val);
+
+ return (short) val;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this)
+ return true;
+ if (!(o instanceof PhoneNumber))
+ return false;
+
+ PhoneNumber pn = (PhoneNumber) o;
+
+ return pn.lineNum == lineNum && pn.prefix == prefix
+ && pn.areaCode == areaCode;
+ }
+
+ ... // Remainder omitted
+}
+```
+
+ 以下是一些最后提醒:
+
+ 1. **当重写 equals 方法时,同时也要重写 hashCode 方法(条目 11)**。
+ 2. **不要让 equals 方法试图太聪明。**如果只是简单地测试用于相等的属性,那么要遵守 equals 约定并不困难。如果你在寻找相等方面过于激进,那么很容易陷入麻烦。一般来说,考虑到任何形式的别名通常是一个坏主意。例如,File 类不应该试图将引用的符号链接等同于同一文件对象。幸好 File 类并没这么做。
+ 3. **在 equal 时方法声明中,不要将参数 Object 替换成其他类型。**对于程序员来说,编写一个看起来像这样的 equals 方法并不少见,然后花上几个小时苦苦思索为什么它不能正常工作:在 equal 时方法声明中,不要将参数 Object 替换成其他类型。对于程序员来说,编写一个看起来像这样的 equals 方法并不少见,然后花上几个小时苦苦思索为什么它不能正常工作。
+
+```java
+// Broken - parameter type must be Object!
+public boolean equals(MyClass o) {
+ …
+}
+```
+
+ 问题在于这个方法并没有重写 Object.equals 方法,它的参数是 Object 类型的,这样写只是重载了 equals 方法(Item 52)。 即使除了正常的方法之外,提供这种“强类型”的 equals 方法也是不可接受的,因为它可能会导致子类中的 Override 注解产生误报,提供不安全的错觉。
+在这里,使用 Override 注解会阻止你犯这个错误 (条目 40)。这个 equals 方法不会编译,错误消息会告诉你到底错在哪里:
+
+```java
+// Still broken, but won’t compile
+@Override
+public boolean equals(MyClass o) {
+ …
+}
+```
+ 编写和测试 equals(和 hashCode) 方法很繁琐,生的代码也很普通。替代手动编写和测试这些方法的优雅的手段是,使用谷歌 AutoValue 开源框架,该框架自动为你生成这些方法,只需在类上添加一个注解即可。在大多数情况下,AutoValue 框架生成的方法与你自己编写的方法本质上是相同的。
+
+ 很多 IDE(例如 Eclipse,NetBeans,IntelliJ IDEA 等)也有生成 equals 和 hashCode 方法的功能,但是生成的源代码比使用 AutoValue 框架的代码更冗长、可读性更差,不会自动跟踪类中的更改,因此需要进行测试。这就是说,使用 IDE 工具生成 equals(和 hashCode) 方法通常比手动编写它们更可取,因为 IDE 工具不会犯粗心大意的错误,而人类则会。
+
+ 总之,除非必须:在很多情况下,不要重写 equals 方法,从 Object 继承的实现完全是你想要的。 如果你确实重写了 equals 方法,那么一定要比较这个类的所有重要属性,并且以保护前面 equals 约定里五个规定的方式去比较。
+# 11. 重写 equals 方法时同时也要重写 hashcode 方法
+
+ **在每个类中,在重写 equals 方法的时侯,一定要重写 hashcode 方法。** 如果不这样做,你的类违反了 hashCode 的通用约定,这会阻止它在 HashMap 和 HashSet 这样的集合中正常工作。根据 Object 规范,以下时具体约定。
+
+ 1. 当在一个应用程序执行过程中,如果在 equals 方法比较中没有修改任何信息,在一个对象上重复调用 hashCode 方法时,它必须始终返回相同的值。从一个应用程序到另一个应用程序的每一次执行返回的值可以是不一致的。
+ 2. 如果两个对象根据 equals(Object) 方法比较是相等的,那么在两个对象上调用 hashCode 就必须产生的结果是相同的整数。
+ 3. 如果两个对象根据 equals(Object) 方法比较并不相等,则不要求在每个对象上调用 hashCode 都必须产生不同的结果。 但是,程序员应该意识到,为不相等的对象生成不同的结果可能会提高散列表(hash tables)的性能。
+
+ **当无法重写 hashCode 时,所违反第二个关键条款是:相等的对象必须具有相等的哈希码( hash codes)。** 根据类的 equals 方法,两个不同的实例可能在逻辑上是相同的,但是对于 Object 类的 hashCode 方法,它们只是两个没有什么共同之处的对象。因此, Object 类的 hashCode 方法返回两个看似随机的数字,而不是按约定要求的两个相等的数字。
+
+举例说明,假设你使用条目 10 中的 `PhoneNumber` 类的实例做为 HashMap 的键(key):
+
+```java
+Map m = new HashMap<>();
+
+m.put(new PhoneNumber(707, 867, 5309), "Jenny");
+```
+
+ 你可能期望 `m.get(new PhoneNumber(707, 867, 5309))` 方法返回 `Jenny` 字符串,但实际上,返回了 null。注意,这里涉及到两个 `PhoneNumber` 实例:一个实例插入到 HashMap 中,另一个作为判断相等的实例用来检索。`PhoneNumber` 类没有重写 hashCode 方法导致两个相等的实例返回了不同的哈希码,违反了 hashCode 约定。put 方法把 `PhoneNumber` 实例保存在了一个哈希桶( hash bucket)中,但 get 方法却是从不同的哈希桶中去查找,即使恰好两个实例放在同一个哈希桶中,get 方法几乎肯定也会返回 null。因为 HashMap 做了优化,缓存了与每一项(entry)相关的哈希码,如果哈希码不匹配,则不会检查对象是否相等了。
+
+ 解决这个问题很简单,只需要为 `PhoneNumber` 类重写一个合适的 hashCode 方法。hashCode 方法是什么样的?写一个不规范的方法的是很简单的。以下示例,虽然永远是合法的,但绝对不能这样使用:
+
+
+```java
+// The worst possible legal hashCode implementation - never use!
+
+@Override public int hashCode() { return 42; }
+```
+
+ 这是合法的,因为它确保了相等的对象具有相同的哈希码。这很糟糕,因为它确保了每个对象都有相同的哈希码。因此,每个对象哈希到同一个桶中,哈希表退化为链表。应该在线性时间内运行的程序,运行时间变成了平方级别。对于数据很大的哈希表而言,会影响到能够正常工作。
+
+ 一个好的 hash 方法趋向于为不相等的实例生成不相等的哈希码。这也正是 hashCode 约定中第三条的表达。理想情况下,hash 方法为集合中不相等的实例均匀地分配 int 范围内的哈希码。实现这种理想情况可能是困难的。 幸运的是,要获得一个合理的近似的方式并不难。 以下是一个简单的配方:
+
+ 1. 声明一个 int 类型的变量 result,并将其初始化为对象中第一个重要属性 `c` 的哈希码,如下面步骤 2.a 中所计算的那样。(回顾条目 10,重要的属性是影响比较相等的领域。)
+ 2. 对于对象中剩余的重要属性 `f`,请执行以下操作:
+ a. 比较属性 `f` 与属性 `c` 的 int 类型的哈希码:
+
+ -- i. 如果这个属性是基本类型的,使用 `Type.hashCode(f)` 方法计算,其中 `Type` 类是对应属性 `f` 基本类型的包装类。
+ -- ii. 如果该属性是一个对象引用,并且该类的 equals 方法通过递归调用 equals 来比较该属性,并递归地调用 hashCode 方法。 如果需要更复杂的比较,则计算此字段的“范式(“canonical representation)”,并在范式上调用 hashCode。 如果该字段的值为空,则使用 0(也可以使用其他常数,但通常来使用 0 表示)。
+ -- iii. 如果属性 `f` 是一个数组,把它看作每个重要的元素都是一个独立的属性。 也就是说,通过递归地应用这些规则计算每个重要元素的哈希码,并且将每个步骤 2.b 的值合并。 如果数组没有重要的元素,则使用一个常量,最好不要为 0。如果所有元素都很重要,则使用 `Arrays.hashCode` 方法。
+
+ b. 将步骤 2.a 中属性 c 计算出的哈希码合并为如下结果:`result = 31 * result + c;`
+
+ 3. 返回 result 值。
+
+ 当你写完 hashCode 方法后,问自己是否相等的实例有相同的哈希码。 编写单元测试来验证你的直觉(除非你使用 AutoValue 框架来生成你的 equals 和 hashCode 方法,在这种情况下,你可以放心地忽略这些测试)。 如果相同的实例有不相等的哈希码,找出原因并解决问题。
+
+ 可以从哈希码计算中排除派生属性(derived fields)。换句话说,如果一个属性的值可以根据参与计算的其他属性值计算出来,那么可以忽略这样的属性。您必须排除在 equals 比较中没有使用的任何属性,否则可能会违反 hashCode 约定的第二条。
+
+ 步骤 2.b 中的乘法计算结果取决于属性的顺序,如果类中具有多个相似属性,则产生更好的散列函数。 例如,如果乘法计算从一个 String 散列函数中被省略,则所有的字符将具有相同的散列码。 之所以选择 31,因为它是一个奇数的素数。 如果它是偶数,并且乘法溢出,信息将会丢失,因为乘以 2 相当于移位。 使用素数的好处不太明显,但习惯上都是这么做的。 31 的一个很好的特性,是在一些体系结构中乘法可以被替换为移位和减法以获得更好的性能:`31 * i ==(i << 5) - i`。 现代 JVM 可以自动进行这种优化。
+
+ 让我们把上述办法应用到 `PhoneNumber` 类中:
+
+```java
+// Typical hashCode method
+
+@Override
+public int hashCode() {
+
+ int result = Short.hashCode(areaCode);
+
+ result = 31 * result + Short.hashCode(prefix);
+
+ result = 31 * result + Short.hashCode(lineNum);
+
+ return result;
+
+}
+```
+
+ 因为这个方法返回一个简单的确定性计算的结果,它的唯一的输入是 `PhoneNumber` 实例中的三个重要的属性,所以显然相等的 `PhoneNumber` 实例具有相同的哈希码。 实际上,这个方法是 `PhoneNumber` 的一个非常好的 hashCode 实现,与 Java 平台类库中的实现一样。 它很简单,速度相当快,并且合理地将不相同的电话号码分散到不同的哈希桶中。
+
+ 虽然在这个项目的方法产生相当好的哈希函数,但并不是最先进的。 它们的质量与 Java 平台类库的值类型中找到的哈希函数相当,对于大多数用途来说都是足够的。 如果真的需要哈希函数而不太可能产生碰撞,请参阅 Guava 框架的的[com.google.common.hash.Hashing][1] [Guava] 方法。
+
+ `Objects` 类有一个静态方法,它接受任意数量的对象并为它们返回一个哈希码。 这个名为 hash 的方法可以让你编写一行 hashCode 方法,其质量与根据这个项目中的上面编写的方法相当。 不幸的是,它们的运行速度更慢,因为它们需要创建数组以传递可变数量的参数,以及如果任何参数是基本类型,则进行装箱和取消装箱。 这种哈希函数的风格建议仅在性能不重要的情况下使用。 以下是使用这种技术编写的 `PhoneNumber` 的哈希函数:
+
+
+```java
+// One-line hashCode method - mediocre performance
+
+@Override
+public int hashCode() {
+
+ return Objects.hash(lineNum, prefix, areaCode);
+
+}
+```
+
+ 如果一个类是不可变的,并且计算哈希码的代价很大,那么可以考虑在对象中缓存哈希码,而不是在每次请求时重新计算哈希码。 如果你认为这种类型的大多数对象将被用作哈希键,那么应该在创建实例时计算哈希码。 否则,可以选择在首次调用 hashCode 时延迟初始化(lazily initialize)哈希码。 需要注意确保类在存在延迟初始化属性的情况下保持线程安全(项目 83)。 `PhoneNumber` 类不适合这种情况,但只是为了展示它是如何完成的。 请注意,属性 hashCode 的初始值(在本例中为 0)不应该是通常创建的实例的哈希码:
+
+```java
+// hashCode method with lazily initialized cached hash code
+
+private int hashCode; // Automatically initialized to 0
+
+@Override
+public int hashCode() {
+
+ int result = hashCode;
+
+ if (result == 0) {
+
+ result = Short.hashCode(areaCode);
+
+ result = 31 * result + Short.hashCode(prefix);
+
+ result = 31 * result + Short.hashCode(lineNum);
+
+ hashCode = result;
+
+ }
+
+ return result;
+
+}
+```
+
+ **不要试图从哈希码计算中排除重要的属性来提高性能。** 由此产生的哈希函数可能运行得更快,但其质量较差可能会降低哈希表的性能,使其无法使用。 具体来说,哈希函数可能会遇到大量不同的实例,这些实例主要在你忽略的区域中有所不同。 如果发生这种情况,哈希函数将把所有这些实例映射到少许哈希码上,而应该以线性时间运行的程序将会运行平方级的时间。
+
+ 这不仅仅是一个理论问题。 在 Java 2 之前,String 类哈希函数在整个字符串中最多使用 16 个字符,从第一个字符开始,在整个字符串中均匀地选取。 对于大量的带有层次名称的集合(如 URL),此功能正好显示了前面描述的病态行为。
+
+ **不要为 hashCode 返回的值提供详细的规范,因此客户端不能合理地依赖它; 你可以改变它的灵活性。** Java 类库中的许多类(例如 String 和 Integer)都将 hashCode 方法返回的确切值指定为实例值的函数。 这不是一个好主意,而是一个我们不得不忍受的错误:它妨碍了在未来版本中改进哈希函数的能力。 如果未指定细节并在散列函数中发现缺陷,或者发现了更好的哈希函数,则可以在后续版本中对其进行更改。
+
+ 总之,每次重写 equals 方法时都必须重写 hashCode 方法,否则程序将无法正常运行。你的 hashCode 方法必须遵从 Object 类指定的常规约定,并且必须执行合理的工作,将不相等的哈希码分配给不相等的实例。如果使用第 51 页的配方,这很容易实现。如条目 10 所述,AutoValue 框架为手动编写 equals 和 hashCode 方法提供了一个很好的选择,IDE 也提供了一些这样的功能。
+
+
+
+ [1]: http://com.google.common.hash.hashing/
+# 12. 始终重写 toString 方法
+
+ 虽然 Object 类提供了 toString 方法的实现,但它返回的字符串通常不是你的类的用户想要看到的。 它由类名后跟一个“at”符号(@)和哈希码的无符号十六进制表示组成,例如 `PhoneNumber@163b91`。 toString 的通用约定要求,返回的字符串应该是“一个简洁但内容丰富的表示,对人们来说是很容易阅读的”。虽然可以认为 `PhoneNumber@163b91` 简洁易读,但相比于 `707-867-5309`,但并不是很丰富 。 toString 通用约定“建议所有的子类重写这个方法”。好的建议,的确如此!
+
+ 虽然它并不像遵守 equals 和 hashCode 约定那样重要 (条目 10 和 11),但是提供一个良好的 toString 实现使你的类更易于使用,并对使用此类的系统更易于调试。当对象被传递到 println、printf、字符串连接操作符或断言,或者由调试器打印时,toString 方法会自动被调用。即使你从不调用对象上的 toString,其他人也可以。例如,对对象有引用的组件可能包含在日志错误消息中对象的字符串表示。如果未能重写 toString,则消息可能是无用的。
+
+ 如果为 `PhoneNumber` 提供了一个很好的 toString 方法,那么生成一个有用的诊断消息就像下面这样简单:
+
+```java
+System.out.println("Failed to connect to " + phoneNumber);
+```
+
+ 程序员将以这种方式生成诊断消息,不管你是否重写 toString,但是除非你这样做,否则这些消息将不会有用。 提供一个很好的 toString 方法的好处不仅包括类的实例,同样有益于包含实例引用的对象,特别是集合。 打印 map 对象时你会看到哪一个,`{Jenny=PhoneNumber@163b91}` 还是 `{Jenny=707-867-5309}`?
+
+ 实际上,toString 方法应该返回对象中包含的所有需要关注的信息,如电话号码示例中所示。 如果对象很大或者包含不利于字符串表示的状态,这是不切实际的。 在这种情况下,toString 应该返回一个摘要,如 `Manhattan residential phone directory (1487536 listings)` 或线程`[main,5,main]`。 理想情况下,字符串应该是不言自明的(线程示例并没有遵守这点)。 如果未能将所有对象的值得关注的信息包含在字符串表示中,则会导致一个特别烦人的处罚:测试失败报告如下所示:
+
+
+```java
+Assertion failure: expected {abc, 123}, but was {abc, 123}.
+```
+
+ 实现 toString 方法时,必须做出的一个重要决定是:在文档中指定返回值的格式。 建议你对值类进行此操作,例如电话号码或矩阵类。 指定格式的好处是它可以作为标准的,明确的,可读的对象表示。 这种表示形式可以用于输入、输出以及持久化可读性的数据对象,如 CSV 文件。 如果指定了格式,通常提供一个匹配的静态工厂或构造方法,是个好主意,所以程序员可以轻松地在对象和字符串表示之间来回转换。 Java 平台类库中的许多值类都采用了这种方法,包括 BigInteger,BigDecimal 和大部分基本类型包装类。
+
+ 指定 toString 返回值的格式的缺点是,假设你的类被广泛使用,一旦指定了格式,就会终身使用。程序员将编写代码来解析表达式,生成它,并将其嵌入到持久数据中。如果在将来的版本中更改了格式的表示,那么会破坏他们的代码和数据,并且还会抱怨。但通过选择不指定格式,就可以保留在后续版本中添加信息或改进格式的灵活性。
+
+ 无论是否决定指定格式,你都应该清楚地在文档中表明你的意图。如果指定了格式,则应该这样做。例如,这里有一个 toString 方法,该方法在条目 11 中使用 `PhoneNumber` 类:
+
+```java
+/**
+ * Returns the string representation of this phone number.
+ * The string consists of twelve characters whose format is
+ * "XXX-YYY-ZZZZ", where XXX is the area code, YYY is the
+ * prefix, and ZZZZ is the line number. Each of the capital
+ * letters represents a single decimal digit.
+ *
+ * If any of the three parts of this phone number is too small
+ * to fill up its field, the field is padded with leading zeros.
+ * For example, if the value of the line number is 123, the last
+ * four characters of the string representation will be "0123".
+ */
+@Override
+public String toString() {
+ return String.format("%03d-%03d-%04d",
+ areaCode, prefix, lineNum);
+}
+```
+
+ 如果你决定不指定格式,那么文档注释应该是这样的:
+
+```java
+/**
+ * Returns a brief description of this potion. The exact details
+ * of the representation are unspecified and subject to change,
+ * but the following may be regarded as typical:
+ *
+ * "[Potion #9: type=love, smell=turpentine, look=india ink]"
+ */
+@Override
+public String toString() { ... }
+```
+
+ 在阅读了这条注释之后,那些生成依赖于格式细节的代码或持久化数据的程序员,在这种格式发生改变的时候,只能怪他们自己。
+
+ 无论是否指定格式,都可以通过编程方式访问 toString 返回的值中包含的信息。 例如,`PhoneNumber` 类应该包含 areaCode, prefix, lineNum 这三个属性。 如果不这样做,就会强迫程序员需要这些信息来解析字符串。 除了降低性能和程序员做不必要的工作之外,这个过程很容易出错,如果改变格式就会中断,并导致脆弱的系统。 由于未能提供访问器,即使已指定格式可能会更改,也可以将字符串格式转换为事实上的 API。
+
+ 在静态工具类(条目 4)中编写 toString 方法是没有意义的。 你也不应该在大多数枚举类型(条目 34)中写一个 toString 方法,因为 Java 为你提供了一个非常好的方法。 但是,你应该在任何抽象类中定义 toString 方法,该类的子类共享一个公共字符串表示形式。 例如,大多数集合实现上的 toString 方法都是从抽象集合类继承的。
+
+ Google 的开放源代码 AutoValue 工具在条目 10 中讨论过,它为你生成一个 toString 方法,就像大多数 IDE 工具一样。 这些方法非常适合告诉你每个属性的内容,但并不是专门针对类的含义。 因此,例如,为我们的 `PhoneNumber` 类使用自动生成的 toString 方法是不合适的(因为电话号码具有标准的字符串表示形式),但是对于我们的 `Potion` 类来说,这是完全可以接受的。 也就是说,自动生成的 toString 方法比从 Object 继承的方法要好得多,它不会告诉你对象的值。
+
+ 回顾一下,除非父类已经这样做了,否则在每个实例化的类中重写 Object 的 toString 实现。 它使得类更加舒适地使用和协助调试。 toString 方法应该以一种美观的格式返回对象的简明有用的描述。
+
+
+
+
+# 13. 谨慎地重写 clone 方法
+
+ Cloneable 接口的目的是作为一个 mixin 接口 (条目 20),公布这样的类允许克隆。不幸的是,它没有达到这个目的。它的主要缺点是缺少 clone 方法,而 Object 的 clone 方法是受保护的。你不能,不借助反射 (条目 65),仅仅因为它实现了 Cloneable 接口,就调用对象上的 clone 方法。即使是反射调用也可能失败,因为不能保证对象具有可访问的 clone 方法。尽管存在许多缺陷,该机制在合理的范围内使用,所以理解它是值得的。这个条目告诉你如何实现一个行为良好的 clone 方法,在适当的时候讨论这个方法,并提出替代方案。
+
+ 既然 Cloneable 接口不包含任何方法,那它用来做什么? 它决定了 Object 的受保护的 clone 方法实现的行为:如果一个类实现了 Cloneable 接口,那么 Object 的 clone 方法将返回该对象的逐个属性(field-by-field)拷贝;否则会抛出 `CloneNotSupportedException` 异常。这是一个非常反常的接口使用,而不应该被效仿。 通常情况下,实现一个接口用来表示可以为客户做什么。但对于 Cloneable 接口,它会修改父类上受保护方法的行为。
+
+ 虽然规范并没有说明,但在实践中,实现 Cloneable 接口的类希望提供一个正常运行的公共 clone 方法。为了实现这一目标,该类及其所有父类必须遵循一个复杂的、不可执行的、稀疏的文档协议。由此产生的机制是脆弱的、危险的和不受语言影响的(extralinguistic):它创建对象而不需要调用构造方法。
+
+ clone 方法的通用规范很薄弱的。 以下内容是从 Object 规范中复制出来的:
+
+ 创建并返回此对象的副本。 “复制(copy)”的确切含义可能取决于对象的类。 一般意图是,对于任何对象 x,表达式 `x.clone() != x` 返回 true,并且 `x.clone().getClass() == x.getClass()` 也返回 true,但它们不是绝对的要求,但通常情况下,`x.clone().equals(x)` 返回 true,当然这个要求也不是绝对的。
+
+ 根据约定,这个方法返回的对象应该通过调用 `super.clone` 方法获得的。 如果一个类和它的所有父类(Object 除外)都遵守这个约定,情况就是如此,`x.clone().getClass() == x.getClass()`。
+
+ 根据约定,返回的对象应该独立于被克隆的对象。 为了实现这种独立性,在返回对象之前,可能需要修改由 super.clone 返回的对象的一个或多个属性。
+
+ 这种机制与构造方法链(chaining)很相似,只是它没有被强制执行;如果一个类的 clone 方法返回一个通过调用构造方法获得而不是通过调用 super.clone 的实例,那么编译器不会抱怨,但是如果一个类的子类调用了 super.clone,那么返回的对象包含错误的类,从而阻止子类 clone 方法正常执行。如果一个类重写的 clone 方法是有 final 修饰的,那么这个约定可以被安全地忽略,因为子类不需要担心。但是,如果一个 final 类有一个不调用 super.clone 的 clone 方法,那么这个类没有理由实现 Cloneable 接口,因为它不依赖于 Object 的 clone 实现的行为。
+
+ 假设你希望在一个类中实现 Cloneable 接口,它的父类提供了一个行为良好的 clone 方法。首先调用 super.clone。 得到的对象将是原始的完全功能的复制品。 在你的类中声明的任何属性将具有与原始属性相同的值。 如果每个属性包含原始值或对不可变对象的引用,则返回的对象可能正是你所需要的,在这种情况下,不需要进一步的处理。 例如,对于条目 11 中的 `PhoneNumber` 类,情况就是这样,但是请注意,不可变类永远不应该提供 clone 方法,因为这只会浪费复制。 有了这个警告,以下是 `PhoneNumber` 类的 clone 方法:
+
+```java
+// Clone method for class with no references to mutable state
+@Override public PhoneNumber clone() {
+ try {
+ return (PhoneNumber) super.clone();
+ } catch (CloneNotSupportedException e) {
+ throw new AssertionError(); // Can't happen
+ }
+}
+```
+
+ 为了使这个方法起作用,`PhoneNumber` 的类声明必须被修改,以表明它实现了 Cloneable 接口。 虽然 Object 类的 clone 方法返回 Object 类,但是这个 clone 方法返回 `PhoneNumber` 类。 这样做是合法和可取的,因为 Java 支持协变返回类型。 换句话说,重写方法的返回类型可以是重写方法的返回类型的子类。 这消除了在客户端转换的需要。 在返回之前,我们必须将 Object 的 super.clone 的结果强制转换为 `PhoneNumber`,但保证强制转换成功。
+
+ super.clone 的调用包含在一个 try-catch 块中。 这是因为 Object 声明了它的 clone 方法来抛出 CloneNotSupportedException 异常,这是一个检查时异常。 由于 `PhoneNumber` 实现了 Cloneable 接口,所以我们知道调用 super.clone 会成功。 这里引用的需要表明 `CloneNotSupportedException` 应该是未被检查的(条目 71)。
+
+ 如果对象包含引用可变对象的属性,则前面显示的简单 clone 实现可能是灾难性的。 例如,考虑条目 7 中的 Stack 类:
+
+```java
+public class Stack {
+
+ private Object[] elements;
+ private int size = 0;
+ private static final int DEFAULT_INITIAL_CAPACITY = 16;
+
+ public Stack() {
+ this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
+ }
+
+ public void push(Object e) {
+ ensureCapacity();
+ elements[size++] = e;
+ }
+
+ public Object pop() {
+ if (size == 0)
+ throw new EmptyStackException();
+ Object result = elements[--size];
+
+ elements[size] = null; // Eliminate obsolete reference
+ return result;
+ }
+
+ // Ensure space for at least one more element.
+ private void ensureCapacity() {
+ if (elements.length == size)
+ elements = Arrays.copyOf(elements, 2 * size + 1);
+ }
+}
+```
+
+ 假设你想让这个类可以克隆。 如果 clone 方法仅返回 super.clone() 调用的对象,那么生成的 Stack 实例在其 size 属性中具有正确的值,但 elements 属性引用与原始 Stack 实例相同的数组。 修改原始实例将破坏克隆中的不变量,反之亦然。 你会很快发现你的程序产生了无意义的结果,或者抛出 `NullPointerException` 异常。
+
+ 这种情况永远不会发生,因为调用 Stack 类中的唯一构造方法。 实际上,clone 方法作为另一种构造方法; 必须确保它不会损坏原始对象,并且可以在克隆上正确建立不变量。 为了使 Stack 上的 clone 方法正常工作,它必须复制 stack 对象的内部。 最简单的方法是对元素数组递归调用 clone 方法:
+
+```java
+// Clone method for class with references to mutable state
+@Override public Stack clone() {
+ try {
+ Stack result = (Stack) super.clone();
+ result.elements = elements.clone();
+ return result;
+ } catch (CloneNotSupportedException e) {
+ throw new AssertionError();
+ }
+}
+```
+
+ 请注意,我们不必将 elements.clone 的结果转换为 Object[] 数组。 在数组上调用 clone 会返回一个数组,其运行时和编译时类型与被克隆的数组相同。 这是复制数组的首选习语。 事实上,数组是 clone 机制的唯一有力的用途。
+
+ 还要注意,如果 elements 属性是 final 的,则以前的解决方案将不起作用,因为克隆将被禁止向该属性分配新的值。 这是一个基本的问题:像序列化一样,Cloneable 体系结构与引用可变对象的 final 属性的正常使用不兼容,除非可变对象可以在对象和其克隆之间安全地共享。 为了使一个类可以克隆,可能需要从一些属性中移除 final 修饰符。
+
+ 仅仅递归地调用 clone 方法并不总是足够的。 例如,假设您正在为哈希表编写一个 clone 方法,其内部包含一个哈希桶数组,每个哈希桶都指向“键-值”对链表的第一项。 为了提高性能,该类实现了自己的轻量级单链表,而没有使用 java 内部提供的 java.util.LinkedList:
+
+```java
+public class HashTable implements Cloneable {
+ private Entry[] buckets = ...;
+ private static class Entry {
+ final Object key;
+ Object value;
+ Entry next;
+
+ Entry(Object key, Object value, Entry next) {
+ this.key = key;
+ this.value = value;
+ this.next = next;
+ }
+ }
+ ... // Remainder omitted
+}
+```
+ 假设你只是递归地克隆哈希桶数组,就像我们为 Stack 所做的那样:
+```java
+// Broken clone method - results in shared mutable state!
+@Override public HashTable clone() {
+ try {
+ HashTable result = (HashTable) super.clone();
+ result.buckets = buckets.clone();
+ return result;
+ } catch (CloneNotSupportedException e) {
+ throw new AssertionError();
+ }
+}
+```
+ 虽然被克隆的对象有自己的哈希桶数组,但是这个数组引用与原始数组相同的链表,这很容易导致克隆对象和原始对象中的不确定性行为。 要解决这个问题,你必须复制包含每个桶的链表。 下面是一种常见的方法:
+```java
+// Recursive clone method for class with complex mutable state
+public class HashTable implements Cloneable {
+ private Entry[] buckets = ...;
+
+ private static class Entry {
+ final Object key;
+ Object value;
+ Entry next;
+
+ Entry(Object key, Object value, Entry next) {
+ this.key = key;
+ this.value = value;
+ this.next = next;
+ }
+
+ // Recursively copy the linked list headed by this Entry
+ Entry deepCopy() {
+ return new Entry(key, value,
+ next == null ? null : next.deepCopy());
+ }
+ }
+
+ @Override public HashTable clone() {
+ try {
+ HashTable result = (HashTable) super.clone();
+ result.buckets = new Entry[buckets.length];
+ for (int i = 0; i < buckets.length; i++)
+ if (buckets[i] != null)
+ result.buckets[i] = buckets[i].deepCopy();
+ return result;
+ } catch (CloneNotSupportedException e) {
+ throw new AssertionError();
+ }
+ }
+ ... // Remainder omitted
+}
+```
+ 私有类 HashTable.Entry 已被扩充以支持“深度复制”方法。 HashTable 上的 clone 方法分配一个合适大小的新哈希桶数组,迭代原来哈希桶数组,深度复制每个非空的哈希桶。 Entry 上的 deepCopy 方法递归地调用它自己以复制由头节点开始的整个链表。 如果哈希桶不是太长,这种技术很聪明并且工作正常。但是,克隆链表不是一个好方法,因为它为列表中的每个元素消耗一个栈帧(stack frame)。 如果列表很长,这很容易导致堆栈溢出。 为了防止这种情况发生,可以用迭代来替换 deepCopy 中的递归:
+```java
+// Iteratively copy the linked list headed by this Entry
+Entry deepCopy() {
+ Entry result = new Entry(key, value, next);
+ for (Entry p = result; p.next != null; p = p.next)
+ p.next = new Entry(p.next.key, p.next.value, p.next.next);
+ return result;
+}
+```
+ 克隆复杂可变对象的最后一种方法是调用 super.clone,将结果对象中的所有属性设置为其初始状态,然后调用更高级别的方法来重新生成原始对象的状态。 以 HashTable 为例,bucket 属性将被初始化为一个新的 bucket 数组,并且 put(key, value) 方法(未示出)被调用用于被克隆的哈希表中的键值映射。 这种方法通常产生一个简单,合理的优雅 clone 方法,其运行速度不如直接操纵克隆内部的方法快。 虽然这种方法是干净的,但它与整个 Cloneable 体系结构是对立的,因为它会盲目地重写构成体系结构基础的逐个属性对象复制。
+
+ 与构造方法一样,clone 方法绝对不可以在构建过程中,调用一个可以重写的方法(条目 19)。如果 clone 方法调用一个在子类中重写的方法,则在子类有机会在克隆中修复它的状态之前执行该方法,很可能导致克隆和原始对象的损坏。因此,我们在前面讨论的 put(key, value) 方法应该时 final 或 private 修饰的。(如果时 private 修饰,那么大概是一个非 final 公共方法的辅助方法)。
+
+ Object 类的 clone 方法被声明为抛出 CloneNotSupportedException 异常,但重写方法时不需要。 公共 clone 方法应该省略 throws 子句,因为不抛出检查时异常的方法更容易使用(条目 71)。
+
+ 在为继承设计一个类时(条目 19),通常有两种选择,但无论选择哪一种,都不应该实现 Clonable 接口。你可以选择通过实现正确运行的受保护的 clone 方法来模仿 Object 的行为,该方法声明为抛出 CloneNotSupportedException 异常。 这给了子类实现 Cloneable 接口的自由,就像直接继承 Object 一样。 或者,可以选择不实现工作的 clone 方法,并通过提供以下简并 clone 实现来阻止子类实现它:
+```java
+// clone method for extendable class not supporting Cloneable
+@Override
+protected final Object clone() throws CloneNotSupportedException {
+ throw new CloneNotSupportedException();
+}
+```
+ 还有一个值得注意的细节。 如果你编写一个实现了 Cloneable 的线程安全的类,记得它的 clone 方法必须和其他方法一样(条目 78)需要正确的同步。 Object 类的 clone 方法是不同步的,所以即使它的实现是令人满意的,也可能需要编写一个返回 super.clone() 的同步 clone 方法。
+
+ 回顾一下,实现 Cloneable 的所有类应该重写公共 clone 方法,而这个方法的返回类型是类本身。 这个方法应该首先调用 super.clone,然后修复任何需要修复的属性。 通常,这意味着复制任何包含内部“深层结构”的可变对象,并用指向新对象的引用来代替原来指向这些对象的引用。虽然这些内部拷贝通常可以通过递归调用 clone 来实现,但这并不总是最好的方法。 如果类只包含基本类型或对不可变对象的引用,那么很可能是没有属性需要修复的情况。 这个规则也有例外。 例如,表示序列号或其他唯一 ID 的属性即使是基本类型的或不可变的,也需要被修正。
+
+ 这么复杂是否真的有必要?很少。 如果你继承一个已经实现了 Cloneable 接口的类,你别无选择,只能实现一个行为良好的 clone 方法。 否则,通常你最好提供另一种对象复制方法。 对象复制更好的方法是提供一个复制构造方法或复制工厂。 复制构造方法接受参数,其类型为包含此构造方法的类,例如:
+```java
+// Copy constructor
+public Yum(Yum yum) { ... };
+```
+ 复制工厂类似于复制构造方法的静态工厂:
+```java
+// Copy factory
+public static Yum newInstance(Yum yum) { ... };
+```
+ 复制构造方法及其静态工厂变体与 Cloneable/clone 相比有许多优点:它们不依赖风险很大的语言外的对象创建机制;不要求遵守那些不太明确的惯例;不会与 final 属性的正确使用相冲突; 不会抛出不必要的检查异常; 而且不需要类型转换。
+
+ 此外,复制构造方法或复制工厂可以接受类型为该类实现的接口的参数。 例如,按照惯例,所有通用集合实现都提供了一个构造方法,其参数的类型为 Collection 或 Map。 基于接口的复制构造方法和复制工厂(更适当地称为转换构造方法和转换工厂)允许客户端选择复制的实现类型,而不是强制客户端接受原始实现类型。 例如,假设你有一个 HashSet,并且你想把它复制为一个 TreeSet。 clone 方法不能提供这种功能,但使用转换构造方法很容易:`new TreeSet<>(s)`。
+
+ 考虑到与 Cloneable 接口相关的所有问题,新的接口不应该继承它,新的可扩展类不应该实现它。 虽然实现 Cloneable 接口对于 final 类没有什么危害,但应该将其视为性能优化的角度,仅在极少数情况下才是合理的(条目 67)。 通常,复制功能最好由构造方法或工厂提供。 这个规则的一个明显的例外是数组,它最好用 clone 方法复制。
+
+
+
+
+# 14. 考虑实现 Comparable 接口
+
+ 与本章讨论的其他方法不同,`compareTo` 方法并没有在 `Object` 类中声明。 相反,它是 ``Comparable`` 接口中的唯一方法。 它与 Object 类的 equals 方法在性质上是相似的,除了它允许在简单的相等比较之外的顺序比较,它是泛型的。 通过实现 `Comparable` 接口,一个类表明它的实例有一个自然顺序( natural ordering)。 对实现 `Comparable` 接口的对象数组排序非常简单,如下所示:
+
+```java
+Arrays.sort(a);
+```
+
+ 它很容易查找,计算极端数值,以及维护 `Comparable` 对象集合的自动排序。例如,在下面的代码中,依赖于 String 类实现了 `Comparable` 接口,去除命令行参数输入重复的字符串,并按照字母顺序排序:
+
+```java
+public class WordList {
+
+ public static void main(String[] args) {
+ Set s = new TreeSet<>();
+ Collections.addAll(s, args);
+ System.out.println(s);
+ }
+}
+```
+ 通过实现 `Comparable` 接口,可以让你的类与所有依赖此接口的通用算法和集合实现进行互操作。 只需少量的努力就可以获得巨大的能量。 几乎 Java 平台类库中的所有值类以及所有枚举类型(条目 34)都实现了 `Comparable` 接口。 如果你正在编写具有明显自然顺序(如字母顺序,数字顺序或时间顺序)的值类,则应该实现 `Comparable` 接口:
+```java
+public interface Comparable {
+ int compareTo(T t);
+}
+```
+ `compareTo` 方法的通用约定与 `equals` 相似:
+
+ 将此对象与指定的对象按照排序进行比较。 返回值可能为负整数,零或正整数,因为此对象对应小于,等于或大于指定的对象。 如果指定对象的类型与此对象不能进行比较,则引发 `ClassCastException` 异常。
+
+ 下面的描述中,符号 sgn(expression) 表示数学中的 signum 函数,它根据表达式的值为负数、零、正数,对应返回-1、0 和 1。
+
+ - 实现类必须确保所有 `x` 和 `y` 都满足 `sgn(x.compareTo(y)) == -sgn(y. compareTo(x))`。 (这意味着当且仅当 `y.compareTo(x)` 抛出异常时,`x.compareTo(y)` 必须抛出异常。)
+ - 实现类还必须确保该关系是可传递的:`(x. compareTo(y) > 0 && y.compareTo(z) > 0)` 意味着 `x.compareTo(z) > 0`。
+ - 最后,对于所有的 z,实现类必须确保 `x.compareTo(y) == 0` 意味着 `sgn(x.compareTo(z)) == sgn(y.compareTo(z))`。
+ - 强烈推荐 `x.compareTo(y) == 0) == (x.equals(y))`,但不是必需的。 一般来说,任何实现了 `Comparable` 接口的类违反了这个条件都应该清楚地说明这个事实。 推荐的语言是“注意:这个类有一个自然顺序,与 `equals` 不一致”。
+
+ 与 `equals` 方法一样,不要被上述约定的数学特性所退缩。这个约定并不像看起来那么复杂。 与 `equals` 方法不同,`equals` 方法在所有对象上施加了全局等价关系,`compareTo` 不必跨越不同类型的对象:当遇到不同类型的对象时,`compareTo` 被允许抛出 `ClassCastException` 异常。 通常,这正是它所做的。 约定确实允许进行不同类型间比较,这种比较通常在由被比较的对象实现的接口中定义。
+
+ 正如一个违反 hashCode 约定的类可能会破坏依赖于哈希的其他类一样,违反 `compareTo` 约定的类可能会破坏依赖于比较的其他类。 依赖于比较的类,包括排序后的集合 `TreeSet` 和 TreeMap 类,以及包含搜索和排序算法的实用程序类 `Collections` 和 `Arrays`。
+
+ 我们来看看 `compareTo` 约定的规定。 第一条规定,如果反转两个对象引用之间的比较方向,则会发生预期的事情:如果第一个对象小于第二个对象,那么第二个对象必须大于第一个; 如果第一个对象等于第二个,那么第二个对象必须等于第一个; 如果第一个对象大于第二个,那么第二个必须小于第一个。 第二项约定说,如果一个对象大于第二个对象,而第二个对象大于第三个对象,则第一个对象必须大于第三个对象。 最后一条规定,所有比较相等的对象与任何其他对象相比,都必须得到相同的结果。
+
+ 这三条规定的一个结果是,`compareTo` 方法所实施的平等测试必须遵守 equals 方法约定所施加的相同限制:自反性,对称性和传递性。 因此,同样需要注意的是:除非你愿意放弃面向对象抽象(条目 10)的好处,否则无法在保留 `compareTo` 约定的情况下使用新的值组件继承可实例化的类。 同样的解决方法也适用。 如果要将值组件添加到实现 `Comparable` 的类中,请不要继承它;编写一个包含第一个类实例的不相关的类。 然后提供一个返回包含实例的“视图”方法。 这使你可以在包含类上实现任何 `compareTo` 方法,同时客户端在需要时,把包含类的实例视同以一个类的实例。
+
+ `compareTo` 约定的最后一段是一个强烈的建议,而不是一个真正的要求,只是声明 `compareTo` 方法施加的相等性测试,通常应该返回与 `equals` 方法相同的结果。 如果遵守这个约定,则 `compareTo` 方法施加的顺序被认为与 `equals` 相一致。 如果违反,顺序关系被认为与 `equals` 不一致。 其 `compareTo` 方法施加与 `equals` 不一致顺序关系的类仍然有效,但包含该类元素的有序集合可能不服从相应集合接口(Collection,Set 或 Map)的一般约定。 这是因为这些接口的通用约定是用 `equals` 方法定义的,但是排序后的集合使用 `compareTo` 强加的相等性测试来代替 `equals`。 如果发生这种情况,虽然不是一场灾难,但仍是一件值得注意的事情。
+
+ 例如,考虑 `BigDecimal` 类,其 `compareTo` 方法与 `equals` 不一致。 如果你创建一个空的 HashSet 实例,然后添加 `new BigDecimal("1.0")` 和 `new BigDecimal("1.00")`,则该集合将包含两个元素,因为与 `equals` 方法进行比较时,添加到集合的两个 `BigDecimal` 实例是不相等的。 但是,如果使用 `TreeSet` 而不是 `HashSet` 执行相同的过程,则该集合将只包含一个元素,因为使用 `compareTo` 方法进行比较时,两个 BigDecimal 实例是相等的。 (有关详细信息,请参阅 BigDecimal 文档。)
+
+ 编写 `compareTo` 方法与编写 `equals` 方法类似,但是有一些关键的区别。 因为 `Comparable` 接口是参数化的,`compareTo` 方法是静态类型的,所以你不需要输入检查或者转换它的参数。 如果参数是错误的类型,那么调用将不会编译。 如果参数为 null,则调用应该抛出一个 `NullPointerException` 异常,并且一旦该方法尝试访问其成员,它就会立即抛出这个异常。
+
+ 在 `compareTo` 方法中,比较属性的顺序而不是相等。 要比较对象引用属性,请递归调用 `compareTo` 方法。 如果一个属性没有实现 Comparable,或者你需要一个非标准的顺序,那么使用 `Comparator` 接口。 可以编写自己的比较器或使用现有的比较器,如在条目 10 中的 `CaseInsensitiveString` 类的 `compareTo` 方法中:
+
+```java
+// Single-field Comparable with object reference field
+public final class CaseInsensitiveString
+ implements Comparable {
+ public int compareTo(CaseInsensitiveString cis) {
+ return String.CASE_INSENSITIVE_[ORDER.compare(s](http://ORDER.compare(s), cis.s);
+ }
+ ... // Remainder omitted
+}
+```
+
+ 请注意,`CaseInsensitiveString` 类实现了 `Comparable` 接口。 这意味着 `CaseInsensitiveString` 引用只能与另一个 `CaseInsensitiveString` 引用进行比较。 当声明一个类来实现 `Comparable` 接口时,这是正常模式。
+
+ 在本书第二版中,曾经推荐如果比较整型基本类型的属性,使用关系运算符“<” 和 “>”,对于浮点类型基本类型的属性,使用 `Double.compare` 和 `Float.compare` 静态方法。在 Java 7 中,静态比较方法被添加到 Java 的所有包装类中。 在 `compareTo` 方法中使用关系运算符“<” 和“>”是冗长且容易出错的,不再推荐。
+
+ 如果一个类有多个重要的属性,那么比较他们的顺序是至关重要的。 从最重要的属性开始,逐步比较所有的重要属性。 如果比较结果不是零(零表示相等),则表示比较完成; 只是返回结果。 如果最重要的字段是相等的,比较下一个重要的属性,依此类推,直到找到不相等的属性或比较剩余不那么重要的属性。 以下是条目 11 中 `PhoneNumber` 类的 `compareTo` 方法,演示了这种方法:
+
+```java
+// Multiple-field `Comparable` with primitive fields
+public int compareTo(PhoneNumber pn) {
+ int result = [Short.compare(areaCode](http://Short.compare(areaCode), pn.areaCode);
+ if (result == 0) {
+ result = [Short.compare(prefix](http://Short.compare(prefix), pn.prefix);
+ if (result == 0)
+ result = [Short.compare(lineNum](http://Short.compare(lineNum), pn.lineNum);
+ }
+ return result;
+}
+```
+
+ 在 Java 8 中 `Comparator` 接口提供了一系列比较器方法,可以使比较器流畅地构建。 这些比较器可以用来实现 `compareTo` 方法,就像 `Comparable` 接口所要求的那样。 许多程序员更喜欢这种方法的简洁性,尽管它的性能并不出众:在我的机器上排序 PhoneNumber 实例的数组速度慢了大约 10%。 在使用这种方法时,考虑使用 Java 的静态导入,以便可以通过其简单名称来引用比较器静态方法,以使其清晰简洁。 以下是 PhoneNumber 的 `compareTo` 方法的使用方法:
+
+```java
+// Comparable with comparator construction methods
+private static final Comparator COMPARATOR =
+ comparingInt((PhoneNumber pn) -> pn.areaCode)
+ .thenComparingInt(pn -> pn.prefix)
+ .thenComparingInt(pn -> pn.lineNum);
+
+public int compareTo(PhoneNumber pn) {
+ return COMPARATOR.compare(this, pn);
+}
+```
+
+ 此实现在类初始化时构建比较器,使用两个比较器构建方法。第一个是 `comparingInt` 方法。它是一个静态方法,它使用一个键提取器函数式接口( key extractor function)作为参数,将对象引用映射为 int 类型的键,并返回一个根据该键排序的实例的比较器。在前面的示例中,`comparingInt` 方法使用 lambda 表达式,它从 `PhoneNumber` 中提取区域代码,并返回一个 `Comparator`,根据它们的区域代码来排序电话号码。注意,lambda 表达式显式指定了其输入参数的类型 (PhoneNumber pn)。事实证明,在这种情况下,Java 的类型推断功能不够强大,无法自行判断类型,因此我们不得不帮助它以使程序编译。
+
+ 如果两个电话号码实例具有相同的区号,则需要进一步细化比较,这正是第二个比较器构建方法,即 `thenComparingInt` 方法做的。 它是 `Comparator` 上的一个实例方法,接受一个 int 类型键提取器函数式接口( key extractor function)作为参数,并返回一个比较器,该比较器首先应用原始比较器,然后使用提取的键来打破连接。 你可以按照喜欢的方式多次调用 `thenComparingInt` 方法,从而产生一个字典顺序。 在上面的例子中,我们将两个调用叠加到 `thenComparingInt`,产生一个排序,它的二级键是 prefix,而其三级键是 lineNum。 请注意,我们不必指定传递给 `thenComparingInt` 的任何一个调用的键提取器函数式接口的参数类型:Java 的类型推断足够聪明,可以自己推断出参数的类型。
+
+ `Comparator` 类具有完整的构建方法。对于 long 和 double 基本类型,也有对应的类似于 comparingInt 和 `thenComparingInt` 的方法,int 版本的方法也可以应用于取值范围小于 int 的类型上,如 short 类型,如 PhoneNumber 实例中所示。对于 double 版本的方法也可以用在 float 类型上。这提供了所有 Java 的基本数字类型的覆盖。
+
+ 也有对象引用类型的比较器构建方法。静态方法 `comparing` 有两个重载方式。第一个方法使用键提取器函数式接口并按键的自然顺序。第二种方法是键提取器函数式接口和比较器,用于键的排序。`thenComparing` 方法有三种重载。第一个重载只需要一个比较器,并使用它来提供一个二级排序。第二次重载只需要一个键提取器函数式接口,并使用键的自然顺序作为二级排序。最后的重载方法同时使用一个键提取器函数式接口和一个比较器来用在提取的键上。
+
+ 有时,你可能会看到 `compareTo` 或 `compare` 方法依赖于两个值之间的差值,如果第一个值小于第二个值,则为负;如果两个值相等则为零,如果第一个值大于,则为正值。这是一个例子:
+
+```java
+// BROKEN difference-based comparator - violates transitivity!
+
+static Comparator