更新到第 45 条

This commit is contained in:
sjsdfg 2019-05-27 16:50:23 +08:00
parent dc4ec6e5df
commit 8d889908b4
8 changed files with 40 additions and 65 deletions

View File

@ -1,6 +1,6 @@
# 38. 使用接口模拟可扩展的枚举
  在几乎所有方面,枚举类型都优于本书第一版中描述的类型安全模式[Bloch01]。 从表面上看,一个例外涉及可扩展性,这在原始模式下是可能的,但不受语言结构支持。 换句话说,使用该模式,有可能使一个枚举类型扩展为另一个; 使用语言功能特性,它不能这样做。 这不是偶然的。 大多数情况下,枚举的可扩展性是一个糟糕的主意。 令人困惑的是,扩展类型的元素是基类型的实例,反之亦然。 枚举基本类型及其扩展的所有元素没有好的方法。 最后,可扩展性会使设计和实现的很多方面复杂化。
  在几乎所有方面,枚举类型都优于本书第一版中描述的类型安全模式[Bloch01]。 从表面上看,一个例外涉及可扩展性,这在原始模式下是可能的,但不受语言结构支持。 换句话说,使用该模式,有可能使一个枚举类型扩展为另一个使用语言功能特性,它不能这样做。 这不是偶然的。 大多数情况下,枚举的可扩展性是一个糟糕的主意。 令人困惑的是,扩展类型的元素是基类型的实例,反之亦然。 枚举基本类型及其扩展的所有元素没有好的方法。 最后,可扩展性会使设计和实现的很多方面复杂化。
  也就是说对于可扩展枚举类型至少有一个有说服力的用例这就是操作码operation codes也称为 opcodes。 操作码是枚举类型,其元素表示某些机器上的操作,例如条目 34 中的 `Operation` 类型,它表示简单计算器上的功能。 有时需要让 API 的用户提供他们自己的操作,从而有效地扩展 API 提供的操作集。
@ -12,7 +12,6 @@ public interface Operation {
double apply(double x, double y);
}
public enum BasicOperation implements Operation {
PLUS("+") {
public double apply(double x, double y) { return x + y; }
@ -28,12 +27,10 @@ public enum BasicOperation implements Operation {
};
private final String symbol;
BasicOperation(String symbol) {
this.symbol = symbol;
}
@Override public String toString() {
return symbol;
}
@ -70,7 +67,7 @@ public enum ExtendedOperation implements Operation {
  只要 API 编写为接口类型(`Operation`),而不是实现(`BasicOperation`),现在就可以在任何可以使用基本操作的地方使用新操作。请注意,不必在枚举中声明 `apply` 抽象方法,就像您在具有实例特定方法实现的非扩展枚举中所做的那样(第 162 页)。 这是因为抽象方法(`apply`)是接口(`Operation`)的成员。
  不仅可以在任何需要“基本枚举”的地方传递“扩展枚举”的单个实例,而且还可以传入整个扩展枚举类型,并使用其元素。 例如,这里是第 163 页上的一个测试程序版本,它执行之前定义的所有扩展操作:
  不仅可以在任何需要「基本枚举」的地方传递「扩展枚举」的单个实例,而且还可以传入整个扩展枚举类型,并使用其元素。 例如,这里是第 163 页上的一个测试程序版本,它执行之前定义的所有扩展操作:
```java
public static void main(String[] args) {
@ -88,9 +85,9 @@ private static <T extends Enum<T> & Operation> void test(
}
```
  注意,扩展的操作类型的类字面文字(`ExtendedOperation`.class`main` 方法里传递给了 `test` 方法,用来描述扩展操作的集合。这个类的字面文字用作限定的类型令牌(条目 33)。`opEnumType` 参数中复杂的声明`<T extends Enum<T> & Operation> Class<T>`)确保了 `Class` 对象既是枚举又是 `Operation` 的子类,这正是遍历元素和执行每个元素相关联的操作时所需要的。
  注意,扩展的操作类型的类字面文字(`ExtendedOperation.class`)从 `main` 方法里传递给了 `test` 方法,用来描述扩展操作的集合。这个类的字面文字用作限定的类型令牌(详见第 33 条)。`opEnumType` 参数中复杂的声明`<T extends Enum<T> & Operation> Class<T>`)确保了 `Class` 对象既是枚举又是 `Operation` 的子类,这正是遍历元素和执行每个元素相关联的操作时所需要的。
  第二种方式是传递一个 `Collection<? extends Operation>`,这是一个限定通配符类型(条目 31),而不是传递了一个 `class` 对象:
  第二种方式是传递一个 `Collection<? extends Operation>`,这是一个限定通配符类型(详见第 31 条),而不是传递了一个 `class` 对象:
```java
public static void main(String[] args) {
@ -107,7 +104,7 @@ private static void test(Collection<? extends Operation> opSet,
}
```
  生成的代码稍微不那么复杂,`test` 方法灵活一点:它允许调用者将多个实现类型的操作组合在一起。另一方面,也放弃了在指定操作上使用 `EnumSet`(条目 36) 和 `EnumMap`(条目 37) 的能力。
  生成的代码稍微不那么复杂,`test` 方法灵活一点:它允许调用者将多个实现类型的操作组合在一起。另一方面,也放弃了在指定操作上使用 `EnumSet`(详见第 36 条)和 `EnumMap`(详见第 37 条)的能力。
  上面的两个程序在运行命令行输入参数 4 和 2 时生成以下输出:
@ -116,7 +113,7 @@ private static void test(Collection<? extends Operation> opSet,
4.000000 % 2.000000 = 0.000000
```
  使用接口来模拟可扩展枚举的一个小缺点是,实现不能从一个枚举类型继承到另一个枚举类型。如果实现代码不依赖于任何状态,则可以使用默认实现 (条目 20) 将其放置在接口中。在我们的 `Operation` 示例中,存储和检索与操作关联的符号的逻辑必须在 `BasicOperation``ExtendedOperation` 中重复。在这种情况下,这并不重要,因为很少的代码是冗余的。如果有更多的共享功能,可以将其封装在辅助类或静态辅助方法中,以消除代码冗余。
  使用接口来模拟可扩展枚举的一个小缺点是,实现不能从一个枚举类型继承到另一个枚举类型。如果实现代码不依赖于任何状态,则可以使用默认实现(详见第 20 条)将其放置在接口中。在我们的 `Operation` 示例中,存储和检索与操作关联的符号的逻辑必须在 `BasicOperation``ExtendedOperation` 中重复。在这种情况下,这并不重要,因为很少的代码是冗余的。如果有更多的共享功能,可以将其封装在辅助类或静态辅助方法中,以消除代码冗余。
  该条目中描述的模式在 `Java` 类库中有所使用。例如,`java.nio.file.LinkOption` 枚举类型实现了 `CopyOption``OpenOption` 接口。

View File

@ -4,7 +4,7 @@
  命名模式的第二个缺点是无法确保它们仅用于适当的程序元素。 例如,假设调用了 `TestSafetyMechanisms` 类,希望 `JUnit 3` 能够自动测试其所有方法,而不管它们的名称如何。 同样,`JUnit 3` 也不会出错,但它也不会执行测试。
  命名模式的第三个缺点是它们没有提供将参数值与程序元素相关联的好的方法。 例如,假设想支持只有在抛出特定异常时才能成功的测试类别。 异常类型基本上是测试的一个参数。 你可以使用一些精心设计的命名模式将异常类型名称编码到测试方法名称中,但这会变得丑陋和脆弱(条目 62)。 编译器无法知道要检查应该命名为异常的字符串是否确实存在。 如果命名的类不存在或者不是异常,那么直到尝试运行测试时才会发现。
  命名模式的第三个缺点是它们没有提供将参数值与程序元素相关联的好的方法。 例如,假设想支持只有在抛出特定异常时才能成功的测试类别。 异常类型基本上是测试的一个参数。 你可以使用一些精心设计的命名模式将异常类型名称编码到测试方法名称中,但这会变得丑陋和脆弱(详见第 62 条)。 编译器无法知道要检查应该命名为异常的字符串是否确实存在。 如果命名的类不存在或者不是异常,那么直到尝试运行测试时才会发现。
  注解[JLS9.7] 很好地解决了所有这些问题,`JUnit` 从第 4 版开始采用它们。在这个项目中,我们将编写我们自己的测试框架来显示注解的工作方式。 假设你想定义一个注解类型来指定自动运行的简单测试,并且如果它们抛出一个异常就会失败。 以下是名为 Test 的这种注解类型的定义:
@ -120,7 +120,7 @@ public @interface ExceptionTest {
}
```
  此注解的参数类型是 `Class<? extends Throwable>`。毫无疑问,这种通配符是拗口的。 在英文中,它表示“扩展 `Throwable` 的某个类的 `Class` 对象”,它允许注解的用户指定任何异常(或错误)类型。 这个用法是一个限定类型标记的例子(条目 33)。 以下是注解在实践中的例子。 请注意,类名字被用作注解参数值:
  此注解的参数类型是 `Class<? extends Throwable>`。毫无疑问,这种通配符是拗口的。 在英文中,它表示“扩展 `Throwable` 的某个类的 `Class` 对象”,它允许注解的用户指定任何异常(或错误)类型。 这个用法是一个限定类型标记的例子(详见第 33 条)。 以下是注解在实践中的例子。 请注意,类名字被用作注解参数值:
```java
// Program containing annotations with a parameter
@ -277,4 +277,4 @@ if (m.isAnnotationPresent(ExceptionTest.class)
  这个项目中的测试框架只是一个演示,但它清楚地表明了注解相对于命名模式的优越性,而且它仅仅描绘了你可以用它们做什么的外观。 如果编写的工具要求程序员将信息添加到源代码中,请定义适当的注解类型。 **当可以使用注解代替时,没有理由使用命名模式。**
  这就是说除了特定的开发者toolsmith之外大多数程序员都不需要定义注解类型。 **但所有程序员都应该使用 Java 提供的预定义注解类型**条目 4027)。 另外,请考虑使用 IDE 或静态分析工具提供的注解。 这些注解可以提高这些工具提供的诊断信息的质量。 但请注意,这些注解尚未标准化,因此如果切换工具或标准出现,可能额外需要做一些工作。
  这就是说除了特定的开发者toolsmith之外大多数程序员都不需要定义注解类型。 **但所有程序员都应该使用 Java 提供的预定义注解类型**详见第 27 和 40 条)。 另外,请考虑使用 IDE 或静态分析工具提供的注解。 这些注解可以提高这些工具提供的诊断信息的质量。 但请注意,这些注解尚未标准化,因此如果切换工具或标准出现,可能额外需要做一些工作。

View File

@ -33,7 +33,7 @@ public class Bigram {
  主程序重复添加二十六个双字母组合到集合中,每个双字母组合由两个相同的小写字母组成。 然后它会打印集合的大小。 你可能希望程序打印 26因为集合不能包含重复项。 如果你尝试运行程序,你会发现它打印的不是 26而是 260。它有什么问题
  显然,`Bigram` 类的作者打算重写 `equals` 方法(条目 10甚至记得重写 `hashCode`(条目 11。 不幸的是,我们倒霉的程序员没有重写 `equals`,而是重载它(条目 52)。 要重写 `Object.equals`,必须定义一个 `equals` 方法,其参数的类型为 `Object`,但 `Bigram``equals` 方法的参数不是 `Object` 类型的,因此 `Bigram` 继承 `Object``equals` 方法,这个 `equals` 方法测试对象的引用是否是同一个,就像 `==` 运算符一样。 每个祖母组合的 10 个副本中的每一个都与其他 9 个副本不同,所以它们被 `Object.equals` 视为不相等,这就解释了程序打印 260 的原因。
  显然,`Bigram` 类的作者打算重写 `equals` 方法(详见第 10 条),甚至记得重写 `hashCode`(详见第 11 条)。 不幸的是,我们倒霉的程序员没有重写 `equals`,而是重载它(详见第 52 条)。 要重写 `Object.equals`,必须定义一个 `equals` 方法,其参数的类型为 `Object`,但 `Bigram``equals` 方法的参数不是 `Object` 类型的,因此 `Bigram` 继承 `Object``equals` 方法,这个 `equals` 方法测试对象的引用是否是同一个,就像 `==` 运算符一样。 每个祖母组合的 10 个副本中的每一个都与其他 9 个副本不同,所以它们被 `Object.equals` 视为不相等,这就解释了程序打印 260 的原因。
  幸运的是,编译器可以帮助你找到这个错误,但只有当你通过告诉它你打算重写 `Object.equals` 来帮助你。 要做到这一点,用 `@Override` 注解 `Bigram.equals` 方法,如下所示:
@ -52,7 +52,7 @@ from a supertype
^
```
  你会立刻意识到你做错了什么,在额头上狠狠地打了一下,用一个正确的(条目 10)来替换出错的 `equals` 实现:
  你会立刻意识到你做错了什么,在额头上狠狠地打了一下,用一个正确的(详见第 10 条)来替换出错的 `equals` 实现:
```java
@Override public boolean equals(Object o) {

View File

@ -1,8 +1,8 @@
# 41. 使用标记接口定义类型
  标记接口marker interface不包含方法声明只是指定“标记”)一个类实现了具有某些属性的接口。 例如,考虑 `Serializable` 接口(第 12 章)。通过实现这个接口,一个类表明它的实例可以写入 `ObjectOutputStream`(或“序列化”)。
  标记接口marker interface不包含方法声明只是指定「标记」)一个类实现了具有某些属性的接口。 例如,考虑 `Serializable` 接口(第 12 章)。通过实现这个接口,一个类表明它的实例可以写入 `ObjectOutputStream`(或「序列化」)。
  你可能会听说过标记注解(条目 39)标记一个接口是废弃过时的。 这个断言是不正确的。 标记接口与标记注解相比具有两个优点。
  你可能会听说过标记注解(详见第 39 条)标记一个接口是废弃过时的。 这个断言是不正确的。 标记接口与标记注解相比具有两个优点。
  首先,**标记接口定义了一个由标记类实例实现的类型;标记注解则不会。** 标记接口类型的存在允许在编译时捕获错误,如果使用标记注解,则直到运行时才能捕获错误。
@ -18,7 +18,7 @@
  总之,标记接口和标记注释都有其用处。 如果你想定义一个没有任何关联的新方法的类型,一个标记接口是一种可行的方法。 如果要标记除类和接口以外的程序元素,或者将标记符合到已经大量使用注解类型的框架中,那么标记注解是正确的选择。 **如果发现自己正在编写目标为 `ElementType.TYPE` 的标记注解类型,请花点时间确定它是否应该是注释类型,是不是标记接口是否更合适。**
  从某种意义来说,本条目与条目 22 的的意思正好相反,条目 22 的意思是:“如果你不想定义一个类型,不要使用接口”。本条目的意思是:如果想定义一个类型,一定要使用接口。
  从某种意义来说,本条目与条目 22 的的意思正好相反,条目 22 的意思是:「如果你不想定义一个类型,不要使用接口」。本条目的意思是:如果想定义一个类型,一定要使用接口。

View File

@ -1,8 +1,8 @@
# 42. lambda 表达式优于匿名类
  在 Java 8 中,添加了函数式接口,`lambda` 表达式和方法引用,以便更容易地创建函数对象。 Stream API 随着其他语言的修改一同被添加进来,为处理数据元素序列提供类库支持。 在本章中,我们将讨论如何充分利用这些功能。
  
  以往,使用单一抽象方法的接口(或者很少使用抽象类)被用作函数类型。 它们的实例称为函数对象表示函数functions或行动actions。 自从 JDK 1.1 于 1997 年发布以来,创建函数对象的主要手段就是匿名类(条目 24)。 下面是一段代码片段,按照字符串长度顺序对列表进行排序,使用匿名类创建排序的比较方法(强制排序顺序):
  以往,使用单一抽象方法的接口(或者很少使用抽象类)被用作函数类型。 它们的实例称为函数对象表示函数functions或行动actions。 自从 JDK 1.1 于 1997 年发布以来,创建函数对象的主要手段就是匿名类(详见第 24 条)。 下面是一段代码片段,按照字符串长度顺序对列表进行排序,使用匿名类创建排序的比较方法(强制排序顺序):
```java
// Anonymous class instance as a function object - obsolete!
@ -12,9 +12,8 @@ Collections.sort(words, new Comparator<String>() {
}
});
```
  匿名类适用于需要函数对象的经典面向对象设计模式,特别是策略模式[Gamma95]。 比较器接口表示排序的抽象策略; 上面的匿名类是排序字符串的具体策略。 然而,匿名类的冗长,使得 Java 中的函数式编程成为一种吸引人的前景。
  
  在 Java 8 中,语言形式化了这样的概念,即使用单个抽象方法的接口是特别的,应该得到特别的对待。 这些接口现在称为函数式接口,并且该语言允许你使用 **lambda** 表达式或简称 **lambda** 来创建这些接口的实例。 **Lambdas** 在功能上与匿名类相似,但更为简洁。 下面的代码使用 **lambdas** 替换上面的匿名类。 样板不见了,行为清晰明了:
```java
@ -23,11 +22,11 @@ Collections.sort(words,
(s1, s2) -> Integer.compare(s1.length(), s2.length()));
```
  请注意,代码中不存在 **lambda**`Comparator <String>`其参数s1 和 s2都是 `String` 类型及其返回值int的类型。 编译器使用称为类型推断的过程从上下文中推导出这些类型。 在某些情况下,编译器将无法确定类型,必须指定它们。 类型推断的规则很复杂:他们在 JLS 中占据了整个章节[JLS18]。 很少有程序员详细了解这些规则,但没关系。 除非它们的存在使你的程序更清晰,否则省略所有 **lambda** 参数的类型。 如果编译器生成一个错误,告诉你它不能推断出 **lambda** 参数的类型,那么指定它。 有时你可能不得不强制转换返回值或整个 **lambda** 表达式,但这很少见。
  
  关于类型推断需要注意一点。 条目 26 告诉你不要使用原始类型,条目 29 告诉你偏好泛型类型,条目 30 告诉你偏向泛型方法。 当使用 **lambda** 表达式时,这个建议是非常重要的,因为编译器获得了大部分允许它从泛型进行类型推断的类型信息。 如果你没有提供这些信息,编译器将无法进行类型推断,你必须在 **lambdas** 中手动指定类型,这将大大增加它们的冗余度。 举例来说,如果变量被声明为原始类型 `List` 而不是参数化类型 `List<String>`,则上面的代码片段将不会编译。
  
  顺便提一句,如果使用比较器构造方法代替 **lambda**,则代码中的比较器可以变得更加简洁(条目 1443
  请注意,代码中不存在 **lambda**`Comparator <String>`其参数s1 和 s2都是 `String` 类型及其返回值int的类型。 编译器使用称为类型推断的过程从上下文中推导出这些类型。 在某些情况下,编译器将无法确定类型,必须指定它们。 类型推断的规则很复杂:他们在 JLS 中占据了整个章节[JLS18]。 很少有程序员详细了解这些规则,但没关系。 除非它们的存在使你的程序更清晰,否则省略所有 **lambda** 参数的类型。 如果编译器生成一个错误,告诉你它不能推断出 **lambda** 参数的类型,那么指定它。 有时你可能不得不强制转换返回值或整个 **lambda** 表达式,但这很少见。  
  关于类型推断需要注意一点。 条目 26 告诉你不要使用原始类型,条目 29 告诉你偏好泛型类型,条目 30 告诉你偏向泛型方法。 当使用 **lambda** 表达式时,这个建议是非常重要的,因为编译器获得了大部分允许它从泛型进行类型推断的类型信息。 如果你没有提供这些信息,编译器将无法进行类型推断,你必须在 **lambdas** 中手动指定类型,这将大大增加它们的冗余度。 举例来说,如果变量被声明为原始类型 `List` 而不是参数化类型 `List<String>`,则上面的代码片段将不会编译。  
  顺便提一句,如果使用比较器构造方法代替 **lambda**,则代码中的比较器可以变得更加简洁(详见第 14 和 43 条
```java
Collections.sort(words, comparingInt(String::length));
@ -95,13 +94,13 @@ public enum Operation {
}
```
  请注意,我们使用表示枚举常量行为的 **lambdas**`DoubleBinaryOperator` 接口。 这是 `java.util.function` 中许多预定义的函数接口之一(条目 44)。 它表示一个函数,它接受两个 **double** 类型参数并返回 **double** 类型的结果。
  请注意,我们使用表示枚举常量行为的 **lambdas**`DoubleBinaryOperator` 接口。 这是 `java.util.function` 中许多预定义的函数接口之一(详见第 44 条)。 它表示一个函数,它接受两个 **double** 类型参数并返回 **double** 类型的结果。
  看看基于 **lambda**`Operation` 枚举,你可能会认为常量特定的方法体已经失去了它们的用处,但事实并非如此。 与方法和类不同,**lambda 没有名称和文档; 如果计算不是自解释的,或者超过几行,则不要将其放入 lambda 表达式中。** 一行代码对于 **lambda** 说是理想的,三行代码是合理的最大值。 如果违反这一规定,可能会严重损害程序的可读性。 如果一个 **lambda** 很长或很难阅读,要么找到一种方法来简化它或重构你的程序来消除它。 此外,传递给枚举构造方法的参数在静态上下文中进行评估。 因此,枚举构造方法中的 **lambda** 表达式不能访问枚举的实例成员。 如果枚举类型具有难以理解的常量特定行为,无法在几行内实现,或者需要访问实例属性或方法,那么常量特定的类主体仍然是行之有效的方法。
  同样,你可能会认为匿名类在 **lambda** 时代已经过时了。 这更接近事实,但有些事情你可以用匿名类来做,而却不能用 **lambdas** 做。 **Lambda** 仅限于函数式接口。 如果你想创建一个抽象类的实例,你可以使用匿名类来实现,但不能使用 **lambda**。 同样,你可以使用匿名类来创建具有多个抽象方法的接口实例。 最后,**lambda** 不能获得对自身的引用。 在 **lambda** 中,`this` 关键字引用封闭实例,这通常是你想要的。 在匿名类中,`this` 关键字引用匿名类实例。 如果你需要从其内部访问函数对象,则必须使用匿名类。
  **Lambdas** 与匿名类共享无法可靠地序列化和反序列化实现的属性。**因此,应该很少 (如果有的话) 序列化一个 lambda(或一个匿名类实例)。** 如果有一个想要进行序列化的函数对象,比如一个 `Comparator`,那么使用一个私有静态嵌套类的实例(条目 24)。
  **Lambdas** 与匿名类共享无法可靠地序列化和反序列化实现的属性。**因此,应该很少 (如果有的话) 序列化一个 lambda(或一个匿名类实例)。** 如果有一个想要进行序列化的函数对象,比如一个 `Comparator`,那么使用一个私有静态嵌套类的实例(详见第 24 条)。
  综上所述,从 Java 8 开始,**lambda** 是迄今为止表示小函数对象的最佳方式。 **除非必须创建非函数式接口类型的实例,否则不要使用匿名类作为函数对象。** 另外,请记住,**lambda** 表达式使代表小函数对象变得如此简单,以至于它为功能性编程技术打开了一扇门,这些技术在 Java 中以前并不实用。

View File

@ -1,7 +1,7 @@
# 43. 方法引用优于 lambda 表达式
  lambda 优于匿名类的主要优点是它更简洁。Java 提供了一种生成函数对象的方法,比 lambda 还要简洁,那就是:方法引用( method references。下面是一段程序代码片段它维护一个从任意键到整数值的映射。如果将该值解释为键的实例个数则该程序是一个多重集合的实现。该代码的功能是根据键找到整数值然后在此基础上加 1
  lambda 优于匿名类的主要优点是它更简洁。Java 提供了一种生成函数对象的方法,比 lambda 还要简洁那就是方法引用method references。下面是一段程序代码片段它维护一个从任意键到整数值的映射。如果将该值解释为键的实例个数则该程序是一个多重集合的实现。该代码的功能是根据键找到整数值然后在此基础上加 1
```java
map.merge(key, 1, (count, incr) -> count + incr);

View File

@ -22,7 +22,7 @@ interface EldestEntryRemovalFunction<K,V>{
  这个接口可以正常工作,但是你不应该使用它,因为你不需要为此目的声明一个新的接口。 `java.util.function` 包提供了大量标准函数式接口供你使用。 如果其中一个标准函数式接口完成这项工作,则通常应该优先使用它,而不是专门构建的函数式接口。 这将使你的 API 更容易学习,通过减少其不必要概念,并将提供重要的互操作性好处,因为许多标准函数式接口提供了有用的默认方法。 例如,`Predicate` 接口提供了组合判断的方法。 在我们的 `LinkedHashMap` 示例中,标准的 `BiPredicate<Map<K,V>, Map.Entry<K,V>>` 接口应优先于自定义的 `EldestEntryRemovalFunction` 接口的使用。
  在 `java.util.Function` 中有 43 个接口。不能指望全部记住它们,但是如果记住了六个基本接口,就可以在需要它们时派生出其余的接口。基本接口操作于对象引用类型。`Operator` 接口表示方法的结果和参数类型相同。`Predicate` 接口表示其方法接受一个参数并返回一个布尔值。`Function` 接口表示方法其参数和返回类型不同。`Supplier` 接口表示一个不接受参数和返回值 (或“供应”) 的方法。最后,`Consumer` 表示该方法接受一个参数而不返回任何东西,本质上就是使用它的参数。六种基本函数式接口概述如下:
  在 `java.util.Function` 中有 43 个接口。不能指望全部记住它们,但是如果记住了六个基本接口,就可以在需要它们时派生出其余的接口。基本接口操作于对象引用类型。`Operator` 接口表示方法的结果和参数类型相同。`Predicate` 接口表示其方法接受一个参数并返回一个布尔值。`Function` 接口表示方法其参数和返回类型不同。`Supplier` 接口表示一个不接受参数和返回值 (或「供应」) 的方法。最后,`Consumer` 表示该方法接受一个参数而不返回任何东西,本质上就是使用它的参数。六种基本函数式接口概述如下:
|接口|方法|示例|
|:--:|:--:|:--:|
@ -33,7 +33,7 @@ interface EldestEntryRemovalFunction<K,V>{
|`Supplier<T>`|T get()|Instant::now|
|`Consumer<T>`|void accept(T t)|System.out::println|
  在处理基本类型 intlong 和 double 的操作上,六个基本接口中还有三个变体。 它们的名字是通过在基本接口前加一个基本类型而得到的。 因此,例如,一个接受 int 的 `Predicate` 是一个 `IntPredicate`,而一个接受两个 long 值并返回一个 long 的二元运算符是一个 `LongBinaryOperator`。 除 `Function` 接口变体通过返回类型进行了参数化,其他变体类型都没有参数化。 例如,`LongFunction<int[]>` 使用 long 类型作为参数并返回了 int [] 类型。
  在处理基本类型 intlong 和 double 的操作上,六个基本接口中还有三个变体。 它们的名字是通过在基本接口前加一个基本类型而得到的。 因此,例如,一个接受 int 的 `Predicate` 是一个 `IntPredicate`,而一个接受两个 long 值并返回一个 long 的二元运算符是一个 `LongBinaryOperator`。 除 `Function` 接口变体通过返回类型进行了参数化,其他变体类型都没有参数化。 例如,`LongFunction<int[]>` 使用 long 类型作为参数并返回了 int[] 类型。
  `Function` 接口还有九个额外的变体,当结果类型为基本类型时使用。 源和结果类型总是不同,因为从类型到它自身的函数是 `UnaryOperator`。 如果源类型和结果类型都是基本类型,则使用带有 `SrcToResult` 的前缀 `Function`,例如 `LongToIntFunction`(六个变体)。如果源是一个基本类型,返回结果是一个对象引用,那么带有 `ToObj` 的前缀 `Function`,例如 `DoubleToObjFunction` (三种变体)。
@ -41,23 +41,22 @@ interface EldestEntryRemovalFunction<K,V>{
  最后,还有一个 `BooleanSupplier` 接口,它是 `Supplier` 的一个变体,它返回布尔值。 这是任何标准函数式接口名称中唯一明确提及的布尔类型,但布尔返回值通过 `Predicate` 及其四种变体形式支持。 前面段落中介绍的 `BooleanSupplier` 接口和 42 个接口占所有四十三个标准功能接口。 无可否认,这是非常难以接受的,并且不是非常正交的。 另一方面,你所需要的大部分功能接口都是为你写的,而且它们的名字是经常性的,所以在你需要的时候不应该有太多的麻烦。
  大多数标准函数式接口仅用于提供对基本类型的支持。 **不要试图使用基本的函数式接口来装箱基本类型的包装类而不是基本类型的函数式接口。** 虽然它起作用,但它违反了第 61 条中的建议:“优先使用基本类型而不是基本类型的包装类”。使用装箱基本类型的包装类进行批量操作的性能后果可能是致命的。
  大多数标准函数式接口仅用于提供对基本类型的支持。 **不要试图使用基本的函数式接口来装箱基本类型的包装类而不是基本类型的函数式接口。** 虽然它起作用,但它违反了第 61 条中的建议:「优先使用基本类型而不是基本类型的包装类」。使用装箱基本类型的包装类进行批量操作的性能后果可能是致命的。
  现在你知道你应该通常使用标准的函数式接口来优先编写自己的接口。 但是,你应该什么时候写自己的接口? 当然,如果没有一个标准模块能够满足您的需求,例如,如果需要一个带有三个参数的 `Predicate`,或者一个抛出检查异常的 `Predicate`,那么需要编写自己的代码。 但有时候你应该编写自己的函数式接口,即使与其中一个标准的函数式接口的结构相同。
  考虑我们的老朋友 `Comparator <T>`,它的结构与 `ToIntBiFunction <T, T>` 接口相同。 即使将前者添加到类库时后者的接口已经存在,使用它也是错误的。 `Comparator` 值得拥有自己的接口有以下几个原因。 首先,它的名称每次在 API 中使用时都会提供优秀的文档,并且使用了很多。 其次,`Comparator` 接口对构成有效实例的构成有强大的要求,这些要求构成了它的普遍契约。 通过实现接口,就要承诺遵守契约。 第三,接口配备很多了有用的默认方法来转换和组合多个比较器。
  如果需要一个函数式接口与 `Comparator` 共享以下一个或多个特性,应该认真考虑编写一个专用函数式接口,而不是使用标准函数式接口:
  
- 它将被广泛使用,并且可以从描述性名称中受益。
- 它拥有强大的契约。
- 它会受益于自定义的默认方法。
  如果选择编写你自己的函数式接口,请记住它是一个接口,因此应非常小心地设计(条目 21)。
  如果选择编写你自己的函数式接口,请记住它是一个接口,因此应非常小心地设计(详见第 21 条)。
  请注意,`EldestEntryRemovalFunction` 接口(第 199 页)`标有 @FunctionalInterface` 注解。 这种注解在类型类似于 `@Override`。 这是一个程序员意图的陈述,它有三个目的:它告诉读者该类和它的文档,该接口是为了实现 lambda 表达式而设计的;它使你保持可靠,因为除非只有一个抽象方法,否则接口不会编译; 它可以防止维护人员在接口发生变化时不小心地将抽象方法添加到接口中。 **始终使用 `@FunctionalInterface` 注解标注你的函数式接口。**
  最后一点应该是关于在 api 中使用函数接口的问题。不要提供具有多个重载的方法,这些重载在相同的参数位置上使用不同的函数式接口,如果这样做可能会在客户端中产生歧义。这不仅仅是一个理论问题。`ExecutorService` 的 `submit` 方法可以采用 `Callable<T>``Runnable` 接口,并且可以编写需要强制类型转换以指示正确的重载的客户端程序 (条目 52)。避免此问题的最简单方法是不要编写在相同的参数位置中使用不同函数式接口的重载。这是条目 52 中建议的一个特例,“明智地使用重载”
  最后一点应该是关于在 api 中使用函数接口的问题。不要提供具有多个重载的方法,这些重载在相同的参数位置上使用不同的函数式接口,如果这样做可能会在客户端中产生歧义。这不仅仅是一个理论问题。`ExecutorService` 的 `submit` 方法可以采用 `Callable<T>``Runnable` 接口,并且可以编写需要强制类型转换以指示正确的重载的客户端程序(详见第 52 条)。避免此问题的最简单方法是不要编写在相同的参数位置中使用不同函数式接口的重载。这是条目 52 中建议的一个特例,「明智地使用重载」
  总之,现在 Java 已经有了 lambda 表达式,因此必须考虑 lambda 表达式来设计你的 API。 在输入上接受函数式接口类型并在输出中返回它们。 一般来说,最好使用 `java.util.function.Function` 中提供的标准接口,但请注意,在相对罕见的情况下,最好编写自己的函数式接口。

View File

@ -8,23 +8,11 @@
  Stream API 流式的fluent:它设计允许所有组成管道的调用被链接到一个表达式中。事实上,多个管道可以链接在一起形成一个表达式。
  默认情况下,流管道按顺序 (sequentially) 运行。 使管道并行执行就像在管道中的任何流上调用并行方法一样简单,但很少这样做(第 48 )。
  默认情况下,流管道按顺序sequentially运行。 使管道并行执行就像在管道中的任何流上调用并行方法一样简单,但很少这样做(详见第 48 条)。
  Stream API 具有足够的通用性,实际上任何计算都可以使用 Stream 执行,但仅仅因为可以,并不意味着应该这样做。如果使用得当,流可以使程序更短更清晰;如果使用不当,它们会使程序难以阅读和维护。对于何时使用流没有硬性的规则,但是有一些启发。
  考虑以下程序该程序从字典文件中读取单词并打印其大小符合用户指定的最小值的所有变位词anagram组。如果两个单词由长度相通不同顺序的相同字母组成则它们是变位词。程序从用户指定的字典文件中读取每个单词并将单词放入 map 对象中。map 对象的键是按照字母排序的单词因此『staple』的键是『aelpst』『petals』的键也是『aelpst』这两个单词就是同位词所有的同位词共享相同的依字母顺序排列的形式或称之为 alphagram。map 对象的值是包含共享字母顺序形式的所有单词的列表。 处理完字典文件后,每个列表都是一个完整的同位词组。然后程序遍历 map 对象的 `values()` 的视图并打印每个大小符合阈值的列表:
  流管道由源流source stream的零或多个中间操作和一个终结操作组成。每个中间操作都以某种方式转换流例如将每个元素映射到该元素的函数或过滤掉所有不满足某些条件的元素。中间操作都将一个流转换为另一个流其元素类型可能与输入流相同或不同。终结操作对流执行最后一次中间操作产生的最终计算例如将其元素存储到集合中、返回某个元素或打印其所有元素。
  管道延迟lazily计算求值计算直到终结操作被调用后才开始而为了完成终结操作而不需要的数据元素永远不会被计算出来。 这种延迟计算求值的方式使得可以使用无限流。 请注意,没有终结操作的流管道是静默无操作的,所以不要忘记包含一个。
  Stream API 流式的fluent:它设计允许所有组成管道的调用被链接到一个表达式中。事实上,多个管道可以链接在一起形成一个表达式。
  默认情况下,流管道按顺序 (sequentially) 运行。 使管道并行执行就像在管道中的任何流上调用并行方法一样简单,但很少这样做(第 48 个条目)。
  Stream API 具有足够的通用性,实际上任何计算都可以使用 Stream 执行,但仅仅因为可以,并不意味着应该这样做。如果使用得当,流可以使程序更短更清晰;如果使用不当,它们会使程序难以阅读和维护。对于何时使用流没有硬性的规则,但是有一些启发。
  考虑以下程序该程序从字典文件中读取单词并打印其大小符合用户指定的最小值的所有变位词anagram组。如果两个单词由长度相通不同顺序的相同字母组成则它们是变位词。程序从用户指定的字典文件中读取每个单词并将单词放入 map 对象中。map 对象的键是按照字母排序的单词因此『staple』的键是『aelpst』『petals』的键也是『aelpst』这两个单词就是同位词所有的同位词共享相同的依字母顺序排列的形式或称之为 alphagram。map 对象的值是包含共享字母顺序形式的所有单词的列表。 处理完字典文件后,每个列表都是一个完整的同位词组。然后程序遍历 map 对象的 values() 的视图并打印每个大小符合阈值的列表:
  考虑以下程序该程序从字典文件中读取单词并打印其大小符合用户指定的最小值的所有变位词anagram组。如果两个单词由长度相通不同顺序的相同字母组成则它们是变位词。程序从用户指定的字典文件中读取每个单词并将单词放入 map 对象中。map 对象的键是按照字母排序的单词因此「staple」的键是「aelpst」「petals」的键也是「aelpst」这两个单词就是同位词所有的同位词共享相同的依字母顺序排列的形式或称之为 alphagram。map 对象的值是包含共享字母顺序形式的所有单词的列表。 处理完字典文件后,每个列表都是一个完整的同位词组。然后程序遍历 map 对象的 `values()` 的视图并打印每个大小符合阈值的列表:
```java
@ -36,9 +24,9 @@ public class Anagrams {
Map<String, Set<String>> groups = new HashMap<>();
try (Scanner s = new Scanner(dictionary)) {
while (s.hasNext()) {
String word = s.next();
groups.computeIfAbsent(alphabetize(word), (unused) -> new TreeSet<>()).add(word);
groups.computeIfAbsent(alphabetize(word),
(unused) -> new TreeSet<>()).add(word);
}
}
@ -55,7 +43,7 @@ public class Anagrams {
}
```
  这个程序中的一个步骤值得注意。将每个单词插入到 map 中 (以粗体显示) 中使用了 `computeIfAbsent` 方法,该方法是在 Java 8 中添加的。这个方法在 map 中查找一个键:如果键存在,该方法只返回与其关联的值。如果没有,该方法通过将给定的函数对象应用于键来计算值,将该值与键关联,并返回计算值。`computeIfAbsent` 方法简化了将多个值与每个键关联的 map 的实现。
  这个程序中的一个步骤值得注意。将每个单词插入到 map 中(以粗体显示)中使用了 `computeIfAbsent` 方法,该方法是在 Java 8 中添加的。这个方法在 map 中查找一个键:如果键存在,该方法只返回与其关联的值。如果没有,该方法通过将给定的函数对象应用于键来计算值,将该值与键关联,并返回计算值。`computeIfAbsent` 方法简化了将多个值与每个键关联的 map 的实现。
  现在考虑以下程序,它解决了同样的问题,但大量过度使用了流。 请注意,整个程序(打开字典文件的代码除外)包含在单个表达式中。 在单独的表达式中打开字典文件的唯一原因是允许使用 `try-with-resources` 语句,该语句确保关闭字典文件:
@ -105,7 +93,7 @@ public class Anagrams {
  即使以前很少接触流,这个程序也不难理解。它在一个 `try-with-resources` 块中打开字典文件,获得一个由文件中的所有行组成的流。流变量命名为 words表示流中的每个元素都是一个单词。此流上的管道没有中间操作它的终结操作将所有单词收集到个 map 对象中,按照字母排列的形式对单词进行分组 (第 46 项)。这与之前两个版本的程序构造的 map 完全相同。然后在 map 的 `values()` 视图上打开一个新的流 `List<String>`。当然,这个流中的元素是同位词组。对流进行过滤,以便忽略大小小于 minGroupSize 的所有组,最后由终结操作 forEach 打印剩下的同位词组。
  请注意,仔细选择 lambda 参数名称。 上面程序中参数 g 应该真正命名为 group但是生成的代码行对于本书来说太宽了。 在没有显式类型的情况下,仔细命名 lambda 参数对于流管道的可读性至关重要。
  请注意,仔细选择 lambda 参数名称。 上面程序中参数 g 应该真正命名为 group但是生成的代码行对于本书来说太宽了。 **在没有显式类型的情况下,仔细命名 lambda 参数对于流管道的可读性至关重要。**
  另请注意,单词字母化是在单独的 `alphabetize` 方法中完成的。 这通过提供操作名称并将实现细节保留在主程序之外来增强可读性。 **使用辅助方法对于流管道中的可读性比在迭代代码中更为重要,因为管道缺少显式类型信息和命名临时变量。**
@ -116,17 +104,9 @@ public class Anagrams {
```
  你可能希望它打印 `Hello world!`,但如果运行它,发现它打印 721011081081113211911111410810033。这是因为 `“Hello world!”.chars()` 返回的流的元素不是 char 值,而是 int 值,因此调用了 `print` 的 int 重载。无可否认,一个名为 chars 的方法返回一个 int 值流是令人困惑的。可以通过强制调用正确的重载来修复该程序:
  你可能希望它打印 `Hello world!`,但如果运行它,发现它打印 721011081081113211911111410810033。这是因为 `“Hello world!”.chars()` 返回的流的元素不是 char 值,而是 int 值,因此调用了 `print` 的 int 重载。无可否认,一个名为 chars 的方法返回一个 int 值流是令人困惑的。可以通过强制调用正确的重载来修复该程序
  请注意,仔细选择 lambda 参数名称。 上面程序中参数 g 应该真正命名为 group但是生成的代码行对于本书来说太宽了。 **在没有显式类型的情况下,仔细命名 lambda 参数对于流管道的可读性至关重要。**
```java
"Hello world!".chars().forEach(x -> System.out.print((char) x));
```
  **但理想情况下,应该避免使用流来处理 char 值。**
  当开始使用流时,你可能会感到想要将所有循环语句转换为流方式的冲动,但请抵制这种冲动。尽管这是可能的,但可能会损害代码库的可读性和可维护性。 通常,使用流和迭代的某种组合可以最好地完成中等复杂的任务,如上面的 `Anagrams` 程序所示。 **因此,重构现有代码以使用流,并仅在有意义的情况下在新代码中使用它们。**
  **但理想情况下,应该避免使用流来处理 char 值。 **当开始使用流时,你可能会感到想要将所有循环语句转换为流方式的冲动,但请抵制这种冲动。尽管这是可能的,但可能会损害代码库的可读性和可维护性。 通常,使用流和迭代的某种组合可以最好地完成中等复杂的任务,如上面的 `Anagrams` 程序所示。 **因此,重构现有代码以使用流,并仅在有意义的情况下在新代码中使用它们。**
  如本项目中的程序所示,流管道使用函数对象 (通常为 lambdas 或方法引用) 表示重复计算,而迭代代码使用代码块表示重复计算。从代码块中可以做一些从函数对象中不能做的事情:
@ -143,7 +123,7 @@ public class Anagrams {
  如果使用这些技术最好地表达计算,那么使用流是这些场景很好的候选者。
  对于流来说,很难做到的一件事是同时访问管道的多个阶段中的相应元素:一旦将值映射到其他值,原始值就会丢失。一种解决方案是将每个值映射到一个包含原始值和新值的 `pair` 对象,但这不是一个令人满意的解决方案,尤其是在管道的多个阶段需要一对对象时更是如此。生成的代码既混乱又冗长,破坏了流的主要用途。当它适用时,一个更好的解决方案是在需要访问早期阶段值时转换映射。
  对于流来说,很难做到的一件事是同时访问管道的多个阶段中的相应元素一旦将值映射到其他值,原始值就会丢失。一种解决方案是将每个值映射到一个包含原始值和新值的 `pair` 对象,但这不是一个令人满意的解决方案,尤其是在管道的多个阶段需要一对对象时更是如此。生成的代码既混乱又冗长,破坏了流的主要用途。当它适用时,一个更好的解决方案是在需要访问早期阶段值时转换映射。
  例如,让我们编写一个程序来打印前 20 个梅森素数 (Mersenne primes)。 梅森素数是一个 2p 1 形式的数字。如果 p 是素数,相应的梅森数可能是素数; 如果是这样的话,那就是梅森素数。 作为我们管道中的初始流,我们需要所有素数。 这里有一个返回该(无限)流的方法。 我们假设使用静态导入来轻松访问 `BigInteger` 的静态成员: