diff --git a/docs/book/01-What-is-an-Object.md b/docs/book/01-What-is-an-Object.md index b434eaa5..5c4c94ad 100644 --- a/docs/book/01-What-is-an-Object.md +++ b/docs/book/01-What-is-an-Object.md @@ -102,7 +102,7 @@ Java 有三个显式关键字来设置类中的访问权限:`public`(公开 使用“组合”关系给我们的程序带来极大的灵活性。通常新建的类中,成员对象会使用 `private` 访问权限,这样应用程序员则无法对其直接访问。我们就可以在不影响客户代码的前提下,从容地修改那些成员。我们也可以在“运行时"改变成员对象从而动态地改变程序的行为,这进一步增大了灵活性。下面一节要讲到的“继承”并不具备这种灵活性,因为编译器对通过继承创建的类进行了限制。 -在面向对象编程中经常重点强调“继承”。在新手程序员的印象里,或许先入为主地认为“继承应当随处可见”。沿着这种思路产生的程序设计通常拙劣又复杂。相反,在创建新类时首先要考虑“组合”,因为它更简单灵活,而且设计更加清晰。等我们有一些编程经验后,一旦需要用到继承,就会明显意识到这一点。 +在面向对象编程中经常重点强调“继承”。在新手程序员的印象里,或许先入为主地认为“继承应当随处可见”。沿着这种思路产生的程序设计通常拙劣又复杂。相反,在创建新类时首先要考虑“组合”,因为它更简单灵活,而且设计更加清晰。等我们有一些编程经验后,一旦需要用到继承,就会明显意识到这一点。(合成/聚合复用原则:尽量使用合成/聚合,而不是继承来达到复用的目的) ## 继承 @@ -130,7 +130,7 @@ Java 有三个显式关键字来设置类中的访问权限:`public`(公开 ### "是一个"与"像是一个"的关系 -对于继承可能会引发争论:继承应该只覆盖基类的方法(不应该添加基类中没有的方法)吗?如果这样的话,基类和派生类就是相同的类型了,因为它们具有相同的接口。这会造成,你可以用一个派生类对象完全替代基类对象,这叫作"纯粹替代",也经常被称作"替代原则"。在某种意义上,这是一种处理继承的理想方式。我们经常把这种基类和派生类的关系称为是一个(is-a)关系,因为可以说"圆是一个形状"。判断是否继承,就看在你的类之间有无这种 is-a 关系。 +对于继承可能会引发争论:继承应该只覆盖基类的方法(不应该添加基类中没有的方法)吗?如果这样的话,基类和派生类就是相同的类型了,因为它们具有相同的接口。这会造成,你可以用一个派生类对象完全替代基类对象,这叫作"纯粹替代",也经常被称作"替代原则(里式替换)"。在某种意义上,这是一种处理继承的理想方式。我们经常把这种基类和派生类的关系称为是一个(is-a)关系,因为可以说"圆是一个形状"。判断是否继承,就看在你的类之间有无这种 is-a 关系。 有时你在派生类添加了新的接口元素,从而扩展接口。虽然新类型仍然可以替代基类,但是这种替代不完美,原因在于基类无法访问新添加的方法。这种关系称为像是一个(is-like-a)关系。新类型不但拥有旧类型的接口,而且包含其他方法,所以不能说新旧类型完全相同。 diff --git a/docs/book/09-Polymorphism.md b/docs/book/09-Polymorphism.md index ddca482d..52af1bc4 100644 --- a/docs/book/09-Polymorphism.md +++ b/docs/book/09-Polymorphism.md @@ -591,7 +591,7 @@ sub.field = 1, sub.getField() = 1, sub.getSuperField() = 0 当 **Sub** 对象向上转型为 **Super** 引用时,任何属性访问都被编译器解析,因此不是多态的。在这个例子中,**Super.field** 和 **Sub.field** 被分配了不同的存储空间,因此,**Sub** 实际上包含了两个称为 **field** 的属性:它自己的和来自 **Super** 的。然而,在引用 **Sub** 的 **field** 时,默认的 **field** 属性并不是 **Super** 版本的 **field** 属性。为了获取 **Super** 的 **field** 属性,需要显式地指明 **super.field**。 -尽管这看起来是个令人困惑的问题,实际上基本不会发生。首先,通常会将所有的属性都指明为 **private**,因此不能直接访问它们,只能通过方法来访问。此外,你可能也不会给基类属性和派生类属性起相同的名字,这样做会令人困惑。 +尽管这看起来是个令人困惑的问题,实际上基本不会发生。首先,通常会将所有的属性都指明为 **private**,因此不能直接访问它们,只能通过方法来访问(利用函数的副作用)。此外,你可能也不会给基类属性和派生类属性起相同的名字,这样做会令人困惑。 如果一个方法是静态(**static**)的,它的行为就不具有多态性: diff --git a/docs/book/10-Interfaces.md b/docs/book/10-Interfaces.md index f2257f52..100eda86 100644 --- a/docs/book/10-Interfaces.md +++ b/docs/book/10-Interfaces.md @@ -648,7 +648,7 @@ Crack Twist ``` -这里展示了创建 **Operations** 的不同方式:一个外部类(Bing),一个匿名类,一个方法引用和 lambda 表达式——毫无疑问用在这里是最好的解决方法。 +这里展示了创建 **Operations** 的不同方式(self-note:其实并没有展示。。):一个外部类(Bing),一个匿名类,一个方法引用, 一个lambda 表达式——毫无疑问用在这里是最好的解决方法。 这个特性是一项改善,因为它允许把静态方法放在更合适的地方。 diff --git a/docs/book/11-Inner-Classes.md b/docs/book/11-Inner-Classes.md index 66d3d5a1..1ba39158 100755 --- a/docs/book/11-Inner-Classes.md +++ b/docs/book/11-Inner-Classes.md @@ -368,9 +368,11 @@ public class Parcel5 { } ``` -**PDestination** 类是 `destination()` 方法的一部分,而不是 **Parcel5** 的一部分。所以,在 `destination()` 之外不能访问 **PDestination**,注意出现在 **return** 语句中的向上转型-返回的是 **Destination** 的引用,它是 **PDestination** 的基类。当然,在 `destination()` 中定义了内部类 **PDestination**,并不意味着一旦 `destination()` 方法执行完毕,**PDestination** 就不可用了。 +**PDestination** 类是 `destination()` 方法的一部分,而不是 **Parcel5** 的一部分。所以,在 `destination()` 之外不能访问 **PDestination**,注意出现在 **return** 语句中的向上转型-返回的是 **Destination** 的引用,它是 **PDestination** 的基类。 -你可以在同一个子目录下的任意类中对某个内部类使用类标识符 **PDestination**,这并不会有命名冲突。 +当然,在 `destination()` 中定义了内部类 **PDestination**,并不是说一旦 `destination()` 方法执行完毕,**PDestination** 就不可用了。( 外部可持有该引用 ) + +由于作用域在方法内部,因此你可以在同一个子目录下的任意类中对某个内部类使用类标识符 **PDestination**,这并不会有命名冲突。 下面的例子展示了如何在任意的作用域内嵌入一个内部类: @@ -1150,7 +1152,7 @@ public class GreenhouseControls extends Controller { 一个由 **Event** 对象组成的数组被递交给 **Restart**,该数组要加到控制器上。由于 `Restart()` 也是一个 **Event** 对象,所以同样可以将 **Restart** 对象添加到 `Restart.action()` 中,以使系统能够有规律地重新启动自己。 -下面的类通过创建一个 **GreenhouseControls** 对象,并添加各种不同的 **Event** 对象来配置该系统,这是 *命令* 设计模式的一个例子—**eventList** 中的每个对象都被封装成对象的请求: +下面的类通过创建一个 GreenhouseControls 对象,并添加各种不同的 Event 对象来配置该系统,这是 *命令* 设计模式的一个例子—eventList 中的每个对象都是“请求对象”(包含与请求相关的所有信息的独立对象,这样封装的好处是让你能根据不同的请求将方法参数化、 延迟请求执行或将其放入队列中, 且能实现可撤销操作): ```java // innerclasses/GreenhouseController.java @@ -1411,9 +1413,15 @@ Anonymous inner 8 Anonymous inner 9 ``` -**Counter** 返回的是序列中的下一个值。我们分别使用局部内部类和匿名内部类实现了这个功能,它们具有相同的行为和能力,既然局部内部类的名字在方法外是不可见的,那为什么我们仍然使用局部内部类而不是匿名内部类呢?唯一的理由是,我们需要一个已命名的构造器,或者需要重载构造器,而匿名内部类只能使用实例初始化。 +**Counter** 返回的是序列中的下一个值。我们分别使用局部内部类和匿名内部类实现了这个功能,它们具有相同的行为和能力,既然局部内部类的名字在方法外是不可见的,那为什么我们仍然使用局部内部类而不是匿名内部类呢? + +使用局部内部类而不使用匿名内部类的第一个理由是, + +我们需要一个已命名的构造器,或者需要重载构造器,而匿名内部类只能使用实例初始化。 + +使用局部内部类而不使用匿名内部类的第二个理由就是, -所以使用局部内部类而不使用匿名内部类的另一个理由就是,需要不止一个该内部类的对象。 +需要不止一个内部类,因此要用两个局部内部类,每个都必须有名字。 diff --git a/docs/book/13-Functional-Programming.md b/docs/book/13-Functional-Programming.md index 65f3ece4..80e4b61f 100644 --- a/docs/book/13-Functional-Programming.md +++ b/docs/book/13-Functional-Programming.md @@ -23,7 +23,7 @@ OO(object oriented,面向对象)是抽象数据,FP(functional programming,函数式编程)是抽象行为。 -纯粹的函数式语言在安全性方面更进一步。它强加了额外的约束,即所有数据必须是不可变的:设置一次,永不改变。将值传递给函数,该函数然后生成新值但从不修改自身外部的任何东西(包括其参数或该函数范围之外的元素)。当强制执行此操作时,你知道任何错误都不是由所谓的副作用引起的,因为该函数仅创建并返回结果,而不是其他任何错误。 +纯粹的函数式语言在安全性方面更进一步。它强加了额外的约束,即所有数据必须是不可变的:设置一次,永不改变。将值传递给函数,该函数然后生成新值但从不修改自身外部的任何东西(包括其参数或该函数范围之外的元素)。当强制执行此操作时,你知道任何错误都不是由所谓的副作用引起的,因为该函数仅创建并返回结果,不做其它操作。 更好的是,“不可变对象和无副作用”范式解决了并发编程中最基本和最棘手的问题之一(当程序的某些部分同时在多个处理器上运行时)。这是可变共享状态的问题,这意味着代码的不同部分(在不同的处理器上运行)可以尝试同时修改同一块内存(谁赢了?没人知道)。如果函数永远不会修改现有值但只生成新值,则不会对内存产生争用,这是纯函数式语言的定义。 因此,经常提出纯函数式语言作为并行编程的解决方案(还有其他可行的解决方案)。 diff --git a/docs/book/14-Streams.md b/docs/book/14-Streams.md index 253968be..69bf9346 100644 --- a/docs/book/14-Streams.md +++ b/docs/book/14-Streams.md @@ -840,15 +840,15 @@ public class Prime { ### 应用函数到元素 -- `map(Function)`:将函数操作应用在输入流的元素中,并将返回值传递到输出流中。 +- `map(Function)`:将函数`Function`应用在输入流的每一个元素上,并将所有结果值作为输出流。 -- `mapToInt(ToIntFunction)`:操作同上,但结果是 **IntStream**。 +- `mapToInt(ToIntFunction)`:操作同上,但所有结果作为 **IntStream**。 -- `mapToLong(ToLongFunction)`:操作同上,但结果是 **LongStream**。 +- `mapToLong(ToLongFunction)`:操作同上,但所有结果作为 **LongStream**。 -- `mapToDouble(ToDoubleFunction)`:操作同上,但结果是 **DoubleStream**。 +- `mapToDouble(ToDoubleFunction)`:操作同上,但所有结果作为 **DoubleStream**。 -在这里,我们使用 `map()` 映射多种函数到一个字符串流中。代码示例: +在这里,我们使用 `map()` 映射几种不同的函数到一个字符串流中。代码示例: ```java // streams/FunctionMap.java @@ -911,9 +911,9 @@ class FunctionMap { ``` -在上面的自增示例中,我们用 `Integer.parseInt()` 尝试将一个字符串转化为整数。如果字符串不能被转化成为整数就会抛出 `NumberFormatException` 异常,此时我们就回过头来把原始字符串放到输出流中。 +在上面的`Increment`示例中,我们用 `Integer.parseInt()` 尝试将一个字符串转化为整数。如果字符串不能被转化成为整数就会抛出 `NumberFormatException` 异常,此时我们就把原始字符串放回输出流(fallback机制)。 -在以上例子中,`map()` 将一个字符串映射为另一个字符串,但是我们完全可以产生和接收类型完全不同的类型,从而改变流的数据类型。下面代码示例: +在以上例子中,`map()` 将一个字符串映射为另一个字符串,但是我们完全可以产出和入参不同的类型,从而改变流的数据类型。下面代码示例: ```java // streams/FunctionMap2.java @@ -952,7 +952,7 @@ Numbered(13) 我们将获取到的整数通过构造器 `Numbered::new` 转化成为 `Numbered` 类型。 -如果使用 **Function** 返回的结果是数值类型的一种,我们必须使用合适的 `mapTo数值类型` 进行替代。代码示例: +如果使用 **Function** 返回的结果是数值类型的一种,我们必须用合适的 "`mapTo`操作" 替代。代码示例: ```java // streams/FunctionMap3.java @@ -984,23 +984,23 @@ class FunctionMap3 { 17.000000 1.900000 0.230000 ``` -遗憾的是,Java 设计者并没有尽最大努力去消除基本类型。 +遗憾的是,Java 设计者并没有尽任何努力去消除基本类型。 ### 在 `map()` 中组合流 -假设我们现在有了一个传入的元素流,并且打算对流元素使用 `map()` 函数。现在你已经找到了一些可爱并独一无二的函数功能,但是问题来了:这个函数功能是产生一个流。我们想要产生一个元素流,而实际却产生了一个元素流的流。 +假设我们有一个输入的元素流,并且打算对这些元素使用 `map()` 函数。现在你已经给`map()` 找了一些可爱并独一无二的功能,但是问题来了:这些功能会产生一个流。此时其实我们只想要一个元素输出流,却输出了一个包含着几组元素流的输出流。 -`flatMap()` 做了两件事:将产生流的函数应用在每个元素上(与 `map()` 所做的相同),然后将每个流都扁平化为元素,因而最终产生的仅仅是元素。 +`flatMap()` 做了两件事:1.将产生流的函数应用在每个元素上(这点与 `map()` 所做的相同);2.将每个流都"扁平化"为元素,因而最终产生的只有元素。 -`flatMap(Function)`:当 `Function` 产生流时使用。 +`flatMap(Function)`:当 `Function` 产出流时使用。 -`flatMapToInt(Function)`:当 `Function` 产生 `IntStream` 时使用。 +`flatMapToInt(Function)`:当 `Function` 产出 `IntStream` 时使用。 -`flatMapToLong(Function)`:当 `Function` 产生 `LongStream` 时使用。 +`flatMapToLong(Function)`:当 `Function` 产出 `LongStream` 时使用。 -`flatMapToDouble(Function)`:当 `Function` 产生 `DoubleStream` 时使用。 +`flatMapToDouble(Function)`:当 `Function` 产出 `DoubleStream` 时使用。 -为了弄清它的工作原理,我们从传入一个刻意设计的函数给 `map()` 开始。该函数接受一个整数并产生一个字符串流: +为了弄清它的工作原理,我们从传入一个刻意设计的函数给 `map()` 开始。该函数接受一个整数并产出一个字符串流: ```java // streams/StreamOfStreams.java @@ -1023,7 +1023,7 @@ java.util.stream.ReferencePipeline$Head java.util.stream.ReferencePipeline$Head ``` -我们天真地希望能够得到字符串流,但实际得到的却是“Head”流的流。我们可以使用 `flatMap()` 解决这个问题: +我们天真地希望能够得到一个字符串流,但实际得到的却是一个囊括着“Head”流的流。我们可以使用 `flatMap()` 轻易解决这个问题: ```java // streams/FlatMap.java @@ -1051,7 +1051,7 @@ Fozzie Beaker ``` -从映射返回的每个流都会自动扁平为组成它的字符串。 +从映射(`Mapping`)之后返回的每个流都会自动扁平化,作为最后输出字符串流的一个组件。 下面是另一个演示,我们从一个整数流开始,然后使用每一个整数去创建更多的随机数。 @@ -1076,9 +1076,9 @@ public class StreamOfRandoms { 58 -1 55 93 -1 61 61 29 -1 68 0 22 7 -1 88 28 51 89 9 -1 ``` -在这里我们引入了 `concat()`,它以参数顺序组合两个流。 如此,我们在每个随机 `Integer` 流的末尾添加一个 -1 作为标记。你可以看到最终流确实是从一组扁平流中创建的。 +在这里我们引入了 `concat()`,它以参数顺序合并两个流。 这样做,就能在每个随机 `Integer` 流的末尾添加一个 -1 作为标记。可以看到,最终的流确实是从一组流扁平化得来的。 -因为 `rand.ints()` 产生的是一个 `IntStream`,所以我必须使用 `flatMap()`、`concat()` 和 `of()` 的特定整数形式。 +因为 `rand.ints()` 产生的是一个 `IntStream`,所以我必须使用 `flatMap()`、`concat()` 和 `of()` 的特定整数形式(`IntStream,ToInt`)。 让我们再看一下将文件划分为单词流的任务。我们最后使用到的是 **FileToWordsRegexp.java**,它的问题是需要将整个文件读入行列表中 —— 显然需要存储该列表。而我们真正想要的是创建一个不需要中间存储层的单词流。 @@ -1101,15 +1101,15 @@ public class FileToWords { `stream()` 现在是一个静态方法,因为它可以自己完成整个流创建过程。 -注意:`\\W+` 是一个正则表达式。表示“非单词字符”,`+` 表示“可以出现一次或者多次”。小写形式的 `\\w` 表示“单词字符”。 +注意:`\\W+` 是一个正则表达式。表示“非单词字符”(它等价于" `[^a-zA-Z0-9_]` "),`+` 表示“可以出现一次或者多次”。小写形式的 `\\w` 表示“单词字符” (它等价于" `[A-Za-z0-9_]` ")。 -我们之前遇到的问题是 `Pattern.compile().splitAsStream()` 产生的结果为流,这意味着当我们只是想要一个简单的单词流时,在传入的行流(stream of lines)上调用 `map()` 会产生一个单词流的流。幸运的是,`flatMap()` 可以将元素流的流扁平化为一个简单的元素流。或者,我们可以使用 `String.split()` 生成一个数组,其可以被 `Arrays.stream()` 转化成为流: +我们之前遇到的问题是 `Pattern.compile().splitAsStream()` 产生的是一个流,这意味着当我们只是想要一个简单的单词流时,对输入流调用 `map()` 会产生一个囊括单词流的流。幸运的是,`flatMap()` 可以将囊括元素流的流扁平化为一个简单的元素流。或者,我们还可以用 `String.split()` 生成一个数组,数组又会被 `Arrays.stream()` 转化成为流: ```java .flatMap(line -> Arrays.stream(line.split("\\W+")))) ``` -因为有了真正的流(而不是`FileToWordsRegexp.java` 中基于集合存储的流),所以每次需要一个新的流时,我们都必须从头开始创建,因为流不能被复用: +因为有了真正的流(而不是`FileToWordsRegexp.java` 中基于集合存储的流),所以每次需要一个新的流时都必须从头开始创建,因为流不能被复用: ```java // streams/FileToWordsTest.java @@ -1140,7 +1140,7 @@ is it ## Optional类 -在我们学习终端操作(Terminal Operations)之前,我们必须考虑在一个空流中获取元素会发生什么。我们喜欢沿着“快乐路径”[^1]把流连接起来,同时假设流不会中断。然而,在流中放置 `null` 却会轻易令其中断。那么是否存在某种对象,可以在持有流元素的同时,即使在我们查找的元素不存在时,也能友好地对我们进行提示(也就是说,不会产生异常)? +在我们学习终端操作(Terminal Operations)之前,我们必须考虑在一个没有任何元素的、空的流中获取元素会发生什么。我们喜欢沿着“快乐路径”[^1]把流连接起来,同时认为流不会中断。然而,在流中放置 `null` 却会轻易令其中断。那么是否存在某种对象,可以在持有流元素的同时,即使在我们查找的元素不存在时,也能友好地对我们进行提示(也就是说,不会产生异常)? **Optional** 可以实现这样的功能。一些标准流操作返回 **Optional** 对象,因为它们并不能保证预期结果一定存在。包括: @@ -1148,9 +1148,9 @@ is it - `findAny()` 返回包含任意元素的 **Optional** 对象,如果流为空则返回 **Optional.empty** - `max()` 和 `min()` 返回一个包含最大值或者最小值的 **Optional** 对象,如果流为空则返回 **Optional.empty** - `reduce()` 不再以 `identity` 形式开头,而是将其返回值包装在 **Optional** 中。(`identity` 对象成为其他形式的 `reduce()` 的默认结果,因此不存在空结果的风险) +- `reduce()` 几个重载方法中, 没有`identity`参数(该参数充当初始值)的那个方法,其返回值包装在 **Optional** 中。(其它重载 `reduce()` 有初始值作为默认的结果,因此不存在空的风险) -对于数字流 **IntStream**、**LongStream** 和 **DoubleStream**,`average()` 会将结果包装在 **Optional** 以防止流为空。 +- 对于数字流 **IntStream**、**LongStream** 和 **DoubleStream**,`average()` 会将结果包装到 **Optional** ,防止流为空。 以下是对空流进行所有这些操作的简单测试: @@ -1189,7 +1189,7 @@ OptionalDouble.empty 当流为空的时候你会获得一个 **Optional.empty** 对象,而不是抛出异常。**Optional** 拥有 `toString()` 方法可以用于展示有用信息。 -注意,空流是通过 `Stream.empty()` 创建的。如果你在没有任何上下文环境的情况下调用 `Stream.empty()`,Java 并不知道它的数据类型;这个语法解决了这个问题。如果编译器拥有了足够的上下文信息,比如: +注意,如果你在没有任何上下文环境的情况下调用 `Stream.empty()`,Java 并不知道它的数据类型;空流通过 `Stream.empty()` 这个语法创建,解决了这个问题。如果编译器拥有了足够的上下文信息,比如: ```java Stream s = Stream.empty(); @@ -1197,7 +1197,7 @@ Stream s = Stream.empty(); 就可以在调用 `empty()` 时推断类型。 -这个示例展示了 **Optional** 的两个基本用法: +这个示例展示了 **Optional** 的两个基本用法(`isPresent() get()`): ```java // streams/OptionalBasics.java @@ -1232,12 +1232,12 @@ Nothing inside! 有许多便利函数可以解包 **Optional** ,这简化了上述“对所包含的对象的检查和执行操作”的过程: -- `ifPresent(Consumer)`:当值存在时调用 **Consumer**,否则什么也不做。 -- `orElse(otherObject)`:如果值存在则直接返回,否则生成 **otherObject**。 -- `orElseGet(Supplier)`:如果值存在则直接返回,否则使用 **Supplier** 函数生成一个可替代对象。 -- `orElseThrow(Supplier)`:如果值存在直接返回,否则使用 **Supplier** 函数生成一个异常。 +- `ifPresent(Consumer)`:若值存在则调用 **Consumer**,否则什么也不做。 +- `orElse(otherObject)`:若值存在则直接返回,否则生成 **otherObject**。 +- `orElseGet(Supplier)`:若值存在则直接返回,否则通过使用 **Supplier** 函数生成一个替代对象。 +- `orElseThrow(Supplier)`:若值存在则直接返回,否则通过使用 **Supplier** 函数生成一个异常。 -如下是针对不同便利函数的简单演示: +如下是不同便利函数的简单演示: ```java // streams/Optionals.java @@ -1303,7 +1303,7 @@ Epithets Caught java.lang.Exception: Supplied ``` -`test()` 通过传入所有方法都适用的 **Consumer** 来避免重复代码。 +`test()` 通过传入所有方法都适用的 **Consumer** 来避免代码重复。 `orElseThrow()` 通过 **catch** 关键字来捕获抛出的异常。更多细节,将在 [异常](./15-Exceptions.md) 这一章节中学习。 @@ -1363,13 +1363,13 @@ Null 当我们的流管道生成了 **Optional** 对象,下面 3 个方法可使得 **Optional** 的后续能做更多的操作: -- `filter(Predicate)`:对 **Optional** 中的内容应用**Predicate** 并将结果返回。如果 **Optional** 不满足 **Predicate** ,将 **Optional** 转化为空 **Optional** 。如果 **Optional** 已经为空,则直接返回空**Optional** 。 +- `filter(Predicate)`:对 **Optional** 对象中的内容应用 **Predicate** 并将结果返回。如果 **Optional** 不满足 **Predicate** ,将 **Optional** 转化为 **Optional.empty** 。如果 **Optional** 已经为空,则直接返回 **Optional.empty** 。 -- `map(Function)`:如果 **Optional** 不为空,应用 **Function** 于 **Optional** 中的内容,并返回结果。否则直接返回 **Optional.empty**。 +- `map(Function)`:如果 **Optional** 不为空,对 **Optional** 中的内容应用 **Function** 并返回结果。否则直接返回 **Optional.empty**。 -- `flatMap(Function)`:同 `map()`,但是提供的映射函数将结果包装在 **Optional** 对象中,因此 `flatMap()` 不会在最后进行任何包装。 +- `flatMap(Function)`:同 `map()`,但提供的映射函数`Function`将结果包装在 **Optional** 对象中,因此 `flatMap()` 不会在最后进行任何包装。 -以上方法都不适用于数值型 **Optional**。一般来说,流的 `filter()` 会在 **Predicate** 返回 `false` 时移除流元素。而 `Optional.filter()` 在失败时不会删除 **Optional**,而是将其保留下来,并转化为空。下面请看代码示例: +以上方法都不适用于数值型 **Optional**。一般来说,在 **Predicate** 返回 `false` 时,流的 `filter()` 会移除流元素;而 `Optional.filter()` 不会删除 **Optional**,而是将其保留下来,并转化为 **Optional.empty** 。下面请看代码示例: ```java @@ -1445,9 +1445,9 @@ Optional[Bingo] Optional.empty ``` -即使输出看起来像流,要特别注意 `test()` 中的 for 循环。每一次的for循环都重新启动流,然后跳过for循环索引指定的数量的元素,这就是流只剩后续元素的原因。然后调用`findFirst()` 获取剩余元素中的第一个元素,并包装在一个 `Optional`对象中。 +尽管打印输出看起来像流,要特别注意这里 `test()` 用的是 for 循环。每一次的for循环都重新启动流,跳过for循环索引设置的数目,这就是打印的“流”的元素看起来连续的原因。之后调用`findFirst()` 获取剩余元素中的第一个元素,并包装在一个 `Optional`对象中。 -**注意**,不同于普通 for 循环,这里的索引值范围并不是 `i < elements.length`, 而是 `i <= elements.length`。所以最后一个元素实际上超出了流。方便的是,这将自动成为 **Optional.empty**,你可以在每一个测试的结尾中看到。 +**注意**,不同于普通 for 循环,这里的索引值范围并不是 `i < elements.length`, 而是 `i <= elements.length`。所以最后一个元素实际上超出了流。很方便的是,这将自动成为 **Optional.empty**,因此你可以在每个`test()`结尾中看到。 同 `map()` 一样 , `Optional.map()` 执行一个函数。它仅在 **Optional** 不为空时才执行这个映射函数。并将 **Optional** 的内容提取出来,传递给映射函数。代码示例: @@ -1525,9 +1525,9 @@ Optional[5] Optional.empty ``` -映射函数的返回结果会自动包装成为 **Optional**。**Optional.empty** 会被直接跳过。 +映射函数的返回结果会自动包装成为 **Optional**。**Optional.empty** 会被直接略过。 -**Optional** 的 `flatMap()` 应用于已生成 **Optional** 的映射函数,所以 `flatMap()` 不会像 `map()` 那样将结果封装在 **Optional** 中。代码示例: +**Optional** 的 `flatMap()` 应用在已生成 **Optional** 的映射函数,所以 `flatMap()` 不会像 `map()` 那样将结果封装到 **Optional** 。代码示例: ```java // streams/OptionalFlatMap.java @@ -1605,7 +1605,7 @@ Optional[5] Optional.empty ``` -同 `map()`,`flatMap()` 将提取非空 **Optional** 的内容并将其应用在映射函数。唯一的区别就是 `flatMap()` 不会把结果包装在 **Optional** 中,因为映射函数已经被包装过了。在如上示例中,我们已经在每一个映射函数中显式地完成了包装,但是很显然 `Optional.flatMap()` 是为那些自己已经生成 **Optional** 的函数而设计的。 +同 `map()`,`flatMap()` 将提取非空 **Optional** 的内容并将其应用在映射函数。唯一的区别就是 `flatMap()` 不会把结果包装到 **Optional** ,因为映射函数已经被包装过了。在如上示例中,我们已经在每一个映射函数中显式地完成了包装,很明显 `Optional.flatMap()` 是为那些自己已经生成 **Optional** 的函数而设计的。 ### Optional 流 @@ -1681,20 +1681,20 @@ Signal(dash) Signal(dash) ``` -在这里,我们使用 `filter()` 来保留那些非空 **Optional**,然后在 `map()` 中使用 `get()` 获取元素。由于每种情况都需要定义“空值”的含义,所以通常我们要为每个应用程序采用不同的方法。 +在这里,我们使用 `filter()` 来保留那些非空 **Optional**,然后在 `map()` 中使用 `get()` 获取元素。由于每一种情况都需要你决定“空值”的含义,所以通常要为每个应用程序采用不同的方法。 ## 终端操作 -以下操作将会获取流的最终结果。至此我们无法再继续往后传递流。可以说,终端操作(Terminal Operations)总是我们在流管道中所做的最后一件事。 +这种操作将会获取流并产生最终结果,不再继续往后传递任何东西。可以说,终端操作(Terminal Operations)总是我们在整条管线所做的最后一件事。 ### 数组 - `toArray()`:将流转换成适当类型的数组。 -- `toArray(generator)`:在特殊情况下,生成自定义类型的数组。 +- `toArray(generator)`:在特殊情况下,生成自定义类型的数组(generator分配数组存储)。 当我们需要得到数组类型的数据以便于后续操作时,上面的方法就很有用。假设我们需要复用流产生的随机数时,就可以这么使用。代码示例: @@ -1755,7 +1755,7 @@ public class ForEach { 258 555 693 861 961 429 868 200 522 207 288 128 551 589 ``` -为了方便测试不同大小的流,我们抽离出了 `SZ` 变量。然而即使 `SZ` 值为14也产生了有趣的结果。在第一个流中,未使用 `parallel()` ,因此以元素从 `rands()`出来的顺序输出结果。在第二个流中,引入`parallel()` ,即便流很小,输出的结果的顺序也和前面不一样。这是由于多处理器并行操作的原因,如果你将程序多运行几次,你会发现输出都不相同,这是多处理器并行操作的不确定性造成的结果。 +为了方便测试不同大小的流,我们抽离出了 `SZ` 变量。可即使 `SZ` 赋值14也产生了有趣的结果。在第一个流中,未使用 `parallel()` ,因此以元素从 `rands()`出来的顺序输出结果。在第二个流中,引入`parallel()` ,即便流很小,输出的结果的顺序也和前面不一样。这是由于多处理器并行操作的原因,如果你将程序多运行几次,你会发现输出都不相同,这是多处理器并行操作的不确定性造成的结果。 在最后一个流中,同时使用了 `parallel()` 和 `forEachOrdered()` 来强制保持原始流顺序。因此,对非并行流使用 `forEachOrdered()` 是没有任何影响的。 @@ -1764,7 +1764,7 @@ public class ForEach { ### 集合 - `collect(Collector)`:使用 **Collector** 收集流元素到结果集合中。 -- `collect(Supplier, BiConsumer, BiConsumer)`:同上,第一个参数 **Supplier** 创建了一个新的结果集合,第二个参数 **BiConsumer** 将下一个元素收集到结果集合中,第三个参数 **BiConsumer** 用于将两个结果集合合并起来。 +- `collect(Supplier, BiConsumer, BiConsumer)`:同上,但第一个参数 **Supplier** 创建了一个新的结果集合,第二个参数 **BiConsumer** 将下一个元素收集到结果集合中,第三个参数 **BiConsumer** 用于合并两个结果集合。 在这里我们只是简单介绍了几个 **Collectors** 的运用示例。实际上,它还有一些非常复杂的操作实现,可通过查看 `java.util.stream.Collectors` 的 API 文档了解。例如,我们可以将元素收集到任意一种特定的集合中。 @@ -1854,11 +1854,11 @@ public class MapCollector { {688=W, 309=C, 293=B, 761=N, 858=N, 668=G, 622=F, 751=N} ``` -**Pair** 只是一个基础的数据对象。**RandomPair** 创建了随机生成的 **Pair** 对象流。在 Java 中,我们不能直接以某种方式组合两个流。所以我创建了一个整数流,并且使用 `mapToObj()` 将整数流转化成为 **Pair** 流。 **capChars**的随机大写字母迭代器创建了流,然后`next()`让我们可以在`stream()`中使用这个流。就我所知,这是将多个流组合成新的对象流的唯一方法。 +**Pair** 只是一个基础的数据对象。**RandomPair** 创建了随机生成的 **Pair** 对象流。在 Java 中,我们不能直接以某种方式组合两个流。所以我创建了一个整数流,并且使用 `mapToObj()` 将整数流转化成为 **Pair** 流。 **capChars**的随机大写字母迭代器创建了流,然后`next()`让我们可以在`stream()`中使用这个流。就我所知,这是将多个流(这里是随机整数流和随机字母流)组合成新的对象流的唯一方法。 在这里,我们只使用最简单形式的 `Collectors.toMap()`,这个方法只需要两个从流中获取键和值的函数。还有其他重载形式,其中一种当是键发生冲突时,使用一个函数来处理冲突。 -大多数情况下,`java.util.stream.Collectors` 中预设的 **Collector** 就能满足我们的要求。除此之外,你还可以使用第二种形式的 `collect()`。 我把它留作更高级的练习,下例给出基本用法: +大多数情况下,`java.util.stream.Collectors` 中预设的 **Collector** 就能满足我们的要求。除此之外,你还可以使用第二种形式的 `collect()`。 我把它留作更高级的练习,下例给出基本概念: ```java // streams/SpecialCollector.java @@ -1942,20 +1942,20 @@ Frobnitz(7) Frobnitz(29) ``` -**Frobnitz** 包含一个可生成自身的生成器 `supply()` ;因为 `supply()` 方法作为一个 `Supplier` 是签名兼容的,我们可以把 `supply()` 作为一个方法引用传递给 `Stream.generate()` (这种签名兼容性被称作结构一致性)。我们使用了没有“初始值”作为第一个参数的 `reduce()`方法,所以产生的结果是 **Optional** 类型。`Optional.ifPresent()` 方法只有在结果非空的时候才会调用 `Consumer` (`println` 方法可以被调用是因为 **Frobnitz** 可以通过 `toString()` 方法转换成 **String**)。 +**Frobnitz** 包含一个可生成自身的生成器 `supply()` ;因为 `supply()` 方法作为一个 `Supplier` 是签名兼容的,所以我们可以把 `supply()` 作为一个方法引用传递给 `Stream.generate()` (这种签名兼容性被称作结构一致性)。我们使用了没有“初始值”作为第一个参数的 `reduce()`方法,所以产生的结果是 **Optional** 类型。`Optional.ifPresent()` 方法只有在结果非空的时候才会调用 `Consumer` (`println` 方法可以被调用是因为 **Frobnitz** 可以通过 `toString()` 方法转换成 **String**)。 Lambda 表达式中的第一个参数 `fr0` 是 `reduce()` 中上一次调用的结果。而第二个参数 `fr1` 是从流传递过来的值。 -`reduce()` 中的 Lambda 表达式使用了三元表达式来获取结果,当 `fr0` 的 `size` 值小于 50 的时候,将 `fr0` 作为结果,否则将序列中的下一个元素即 `fr1`作为结果。当取得第一个 `size` 值小于 50 的 `Frobnitz`,只要得到这个结果就会忽略流中其他元素。这是个非常奇怪的限制, 但也确实让我们对 `reduce()` 有了更多的了解。 +`reduce()` 中的 Lambda 表达式使用了三元表达式来获取结果,当 `fr0` 的 `size` 值小于 50 的时候,将 `fr0` 作为结果,否则将序列中的下一个元素即 `fr1`作为结果。当取得第一个 `size` 值小于 50 的 `Frobnitz`,只要得到这个结果就会忽略流中其他元素。这是个非常奇怪的限制, 但也令我们对 `reduce()` 有更多认识。 ### 匹配 -- `allMatch(Predicate)` :如果流的每个元素提供给 **Predicate** 都返回 true ,结果返回为 true。在第一个 false 时,则停止执行计算。 -- `anyMatch(Predicate)`:如果流的任意一个元素提供给 **Predicate** 返回 true ,结果返回为 true。在第一个 true 是停止执行计算。 +- `allMatch(Predicate)` :如果流的每个元素提供给 **Predicate** 都返回 true ,结果返回为 true。在第一个 false 时停止执行计算。 +- `anyMatch(Predicate)`:如果流的任意一个元素提供给 **Predicate** 返回 true ,结果返回为 true。在第一个 true 时停止执行计算。 - `noneMatch(Predicate)`:如果流的每个元素提供给 **Predicate** 都返回 false 时,结果返回为 true。在第一个 true 时停止执行计算。 -我们已经在 `Prime.java` 中看到了 `noneMatch()` 的示例;`allMatch()` 和 `anyMatch()` 的用法基本上是等同的。下面我们来探究一下短路行为。为了消除冗余代码,我们创建了 `show()`。首先我们必须知道如何统一地描述这三个匹配器的操作,然后再将其转换为 **Matcher** 接口。代码示例: +我们已经在 `Prime.java` 中看到了 `noneMatch()` 的示例;`allMatch()` 和 `anyMatch()` 的用法基本上是等同的。下面我们来探究一下短路行为。为了消除冗余代码,我们创建了 `show()`。首先我们必须知道如何统一描述上述的这3个匹配器的操作,然后把这描述的概念创建为 **Matcher** 接口。代码示例: ```java // streams/Matching.java @@ -1999,7 +1999,7 @@ public class Matching { **BiPredicate** 是一个二元谓词,它接受两个参数并返回 true 或者 false。第一个参数是我们要测试的流,第二个参数是一个谓词 **Predicate**。**Matcher** 可以匹配所有的 **Stream::\*Match** 方法,所以可以将每一个**Stream::\*Match**方法引用传递到 `show()` 中。对`match.test()` 的调用会被转换成 对方法引用**Stream::\*Match** 的调用。 -`show()` 接受一个**Matcher**和一个 `val` 参数,`val` 在判断测试 `n < val`中指定了最大值。`show()` 方法生成了整数1-9组成的一个流。`peek()`用来展示在测试短路之前测试进行到了哪一步。从输出中可以看到每次都发生了短路。 +`show()` 接受一个**Matcher**和一个 `val` 参数,`val` 在判断测试 `n < val`中指定了最大值。`show()` 方法生成了整数1-9组成的一个流。`peek()`用来展示在测试短路之前测试进行到了哪一步。从输出中可以看到:每次都发生了短路。 ### 查找 @@ -2034,9 +2034,9 @@ public class SelectElement { 242 ``` -无论流是否为并行化,`findFirst()` 总是会选择流中的第一个元素。对于非并行流,`findAny()`会选择流中的第一个元素(即使从定义上来看是选择任意元素)。在这个例子中,用 `parallel()` 将流并行化,以展示 `findAny()` 不选择流的第一个元素的可能性。 +无论流是否并行,`findFirst()` 总是会选择流中的第一个元素。对于非并行流,`findAny()`则会选择流中的第一个元素(即使从语义来看是选择任意元素)。在这个例子中,用 `parallel()` 将流并行化,以展示 `findAny()` 不选择流的第一个元素的可能性。 -如果必须选择流中最后一个元素,那就使用 `reduce()`。代码示例: +如果要选择流中最后一个元素,就使用 `reduce()`。代码示例: ```java // streams/LastElement.java @@ -2064,15 +2064,15 @@ public class LastElement { three ``` -`reduce()` 的参数只是用最后一个元素替换了最后两个元素,最终只生成最后一个元素。如果为数字流,你必须使用相近的数字 **Optional** 类型( numeric optional type),否则使用 **Optional** 类型,就像上例中的 `Optional`。 +`reduce()` 的参数只是用最后一个元素替换了最后两个元素,最终只生成最后一个元素。如果是数字流,你必须使用相近的数字 **Optional** 类型( numeric optional type)(上例中的`OptionalInt`),非数字流则使用 **Optional** 类型(上例中的 `Optional`)。 ### 信息 - `count()`:流中的元素个数。 -- `max(Comparator)`:根据所传入的 **Comparator** 所决定的“最大”元素。 -- `min(Comparator)`:根据所传入的 **Comparator** 所决定的“最小”元素。 +- `max(Comparator)`:根据所传入的 **Comparator** 所定义的“最大”元素。 +- `min(Comparator)`:根据所传入的 **Comparator** 所定义的“最小”元素。 **String** 类型有预设的 **Comparator** 实现。代码示例: @@ -2113,7 +2113,7 @@ you - `average()` :求取流元素平均值。 - `max()` 和 `min()`:数值流操作无需 **Comparator**。 - `sum()`:对所有流元素进行求和。 -- `summaryStatistics()`:生成可能有用的数据。目前并不太清楚这个方法存在的必要性,因为我们其实可以用更直接的方法获得需要的数据。 +- `summaryStatistics()`:生成潜在有用的数据。目前并不太清楚这个方法存在的必要性,因为我们其实可以用更直接的方法获得需要的数据。 ```java // streams/NumericStreamInfo.java @@ -2144,9 +2144,9 @@ IntSummaryStatistics{count=100, sum=50794, min=8, average=507.940000, max=998} ## 本章小结 -流式操作改变并极大地提升了 Java 语言的可编程性,并可能极大地阻止了 Java 编程人员向诸如 Scala 这种函数式语言的流转。在本书的剩余部分,我们将尽可能地使用流。 +流式操作改变并极大地提升了 Java 语言的可编程性,并可能明显阻止了 Java 编程人员向诸如 Scala 这种函数式语言的流转。在本书的剩余部分,我们将尽可能地使用流。 -[^1]: 在软件或信息建模的上下文中,快乐路径(有时称为快乐流)是没有异常或错误条件的默认场景。例如,验证信用卡号的函数的快乐路径应该是任何验证规则都不会出现错误的地方,从而让执行成功地继续到最后,生成一个积极的响应。[见 wikipedia: happy path](https://en.wikipedia.org/wiki/Happy_path) +[^1]: 在软件或信息建模的上下文中,快乐路径(有时称为快乐流)是指没有异常或错误情形导致退出的默认场景。例如,验证信用卡号的函数的快乐路径应该是任何验证规则都不会出现错误而中断,从而让执行成功地延续到最后,生成一个积极的响应让使用者快乐。[见 wikipedia: happy path](https://en.wikipedia.org/wiki/Happy_path) diff --git a/docs/book/15-Exceptions.md b/docs/book/15-Exceptions.md index ecb6ff8f..a49b2ff0 100644 --- a/docs/book/15-Exceptions.md +++ b/docs/book/15-Exceptions.md @@ -11,17 +11,17 @@ > 要想创建健壮的系统,它的每一个构件都必须是健壮的。 -Java 使用异常来提供一致的错误报告模型,使得构件能够与客户端代码可靠地沟通问题。 +Java 使用异常来提供一致的错误报告模型,使得组件能够与客户端代码可靠地沟通问题。 -Java 中的异常处理的目的在于通过使用少于目前数量的代码来简化大型、可靠的程序的生成,并且通过这种方式可以使你更加确信:你的应用中没有未处理的错误。异常的相关知识学起来并非艰涩难懂,并且它属于那种可以使你的项目受益明显、立竿见影的特性之一。 +Java 中的异常处理的目的——通过使用比目前更少的代码来简化大型、可靠的程序的生成, 这样做可以让你更有信心,你的应用程序不会有未处理的错误。异常的相关知识学起来并非艰涩难懂,并且它属于那种可以使你的项目受益明显、立竿见影的特性之一。 -因为异常处理是 Java 中唯一官方的错误报告机制,并且通过编译器强制执行,所以不学习异常处理的话,你也就只能写出书中那么些例子了。本章将教你如何编写正确的异常处理程序,以及当方法出问题的时候,如何产生自定义的异常。 +因为异常处理是 Java 中唯一官方的错误报告机制,并且通过编译器执行,所以不学习异常处理的话,对此的认识就到此为止了。本章将教你如何编写正确的异常处理程序,以及当方法出问题的时候,如何产生自定义的异常。 ## 异常概念 -C 以及其他早期语言常常具有多种错误处理模式,这些模式往往建立在约定俗成的基础之上,而并不属于语言的一部分。通常会返回某个特殊值或者设置某个标志,并且假定接收者将对这个返回值或标志进行检查,以判定是否发生了错误。然而,随着时间的推移,人们发现,高傲的程序员们在使用程序库的时候更倾向于认为:“对,错误也许会发生,但那是别人造成的,不关我的事”。所以,程序员不去检查错误情形也就不足为奇了(何况对某些错误情形的检查确实很无聊)。如果的确在每次调用方法的时候都彻底地进行错误检查,代码很可能会变得难以阅读。正是由于程序员还仍然用这些方式拼凑系统,所以他们拒绝承认这样一个事实:对于构造大型、健壮、可维护的程序而言,这种错误处理模式已经成为了主要障碍。 +C 以及其他早期语言常常具有多种错误处理模式,这些模式往往建立在约定俗成的基础之上,而并不属于语言的一部分。通常会返回某个特殊值或者设置某个标志,并且假定接收者将对这个返回值或标志进行检查,以判定是否发生了错误。然而,随着时间的推移,人们发现,使用lib库的程序员们会觉得自己无敌:“对,错误也许会发生,但那是别人造成的,不是我的代码问题”。所以,程序员不去检查错误情形也就不足为奇了(何况对某些错误情形的检查确实很无聊)。如果在每次调用方法的时候都彻底进行错误排查,代码很可能会变成难以阅读的噩梦。但由于程序员还仍然用这些语言(C 以及其他早期语言)拼凑系统,所以他们拒绝承认这样一个事实:对于构造大型、健壮、可维护的程序而言,这种错误处理模式已经成为了主要障碍。 解决的办法是,用强制规定的形式来消除错误处理过程中随心所欲的因素。这种做法由来已久,对异常处理的实现可以追溯到 20 世纪 60 年代的操作系统,甚至于 BASIC 语言中的“on error goto”语句。而 C++的异常处理机制基于 Ada,Java 中的异常处理机制则建立在 C++ 的基础之上(尽管看上去更像 Object Pascal)。 @@ -33,11 +33,11 @@ C 以及其他早期语言常常具有多种错误处理模式,这些模式往 ## 基本异常 -异常情形(exceptional condition)是指阻止当前方法或作用域继续执行的问题。把异常情形与普通问题相区分很重要,所谓的普通问题是指,在当前环境下能得到足够的信息,总能处理这个错误。而对于异常情形,就不能继续下去了,因为在当前环境下无法获得必要的信息来解决问题。你所能做的就是从当前环境跳出,并且把问题提交给上一级环境。这就是抛出异常时所发生的事情。 +*异常情形*(exceptional condition)是指阻止当前方法或作用域继续执行的问题。把异常情形与普通问题相区分很重要,所谓的普通问题是指,在当前环境下能得到足够的信息,总能处理这个错误。而对于异常情形,就不能继续下去了,因为在当前环境下无法获得必要的信息来解决问题。你所能做的就是从当前环境跳出,并且把问题提交给上一级环境。这就是抛出异常时所发生的事情。 除法就是一个简单的例子。除数有可能为 0,所以先进行检查很有必要。但除数为 0 代表的究竟是什么意思呢?通过当前正在解决的问题环境,或许能知道该如何处理除数为 0 的情况。但如果这是一个意料之外的值,你也不清楚该如何处理,那就要抛出异常,而不是顺着原来的路径继续执行下去。 -当抛出异常后,有几件事会随之发生。首先,同 Java 中其他对象的创建一样,将使用 new 在堆上创建异常对象。然后,当前的执行路径(它不能继续下去了)被终止,并且从当前环境中弹出对异常对象的引用。此时,异常处理机制接管程序,并开始寻找一个恰当的地方来继续执行程序。这个恰当的地方就是异常处理程序,它的任务是将程序从错误状态中恢复,以使程序能要么换一种方式运行,要么继续运行下去。 +当抛出异常后,有几件事会随之发生。首先,同 Java 中其他对象的创建一样,将使用 new 在堆上创建异常对象(exception object)。然后,当前的执行路径(它不能继续下去了)被终止,并且从当前环境中弹出对异常对象的引用。此时,异常处理机制接管程序,并开始寻找一个恰当的地方来继续执行程序。这个恰当的地方就是异常处理程序,它的任务是将程序从错误状态中恢复,以使程序能要么换一种方式运行,要么继续运行下去。 举一个抛出异常的简单例子。对于对象引用 t,传给你的时候可能尚未被初始化。所以在使用这个对象引用调用其方法之前,会先对引用进行检查。可以创建一个代表错误信息的对象,并且将它从当前环境中“抛出”,这样就把错误信息传播到了“更大”的环境中。这被称为*抛出一个异常*,看起来像这样: @@ -60,13 +60,13 @@ if(t == null) throw new NullPointerException("t = null"); ``` -不久读者将看到,要把这个字符串的内容提取出来可以有多种不同的方法。 +等下读者将看到,要把这个字符串的内容提取出来可以有多种不同的方法。 关键字 **throw** 将产生许多有趣的结果。在使用 **new** 创建了异常对象之后,此对象的引用将传给 **throw**。尽管异常对象的类型通常与方法设计的返回类型不同,但从效果上看,它就像是从方法“返回”的。可以简单地把异常处理看成一种不同的返回机制,当然若过分强调这种类比的话,就会有麻烦了。另外还能用抛出异常的方式从当前的作用域退出。在这两种情况下,将会返回一个异常对象,然后退出方法或作用域。 抛出异常与方法正常返回的相似之处到此为止。因为异常返回的“地点”与普通方法调用返回的“地点”完全不同。(异常将在一个恰当的异常处理程序中得到解决,它的位置可能离异常被抛出的地方很远,也可能会跨越方法调用栈的许多层级。) -此外,能够抛出任意类型的 **Throwable** 对象,它是异常类型的根类。通常,对于不同类型的错误,要抛出相应的异常。错误信息可以保存在异常对象内部或者用异常类的名称来暗示。上一层环境通过这些信息来决定如何处理异常。(通常,唯一的信息只有异常的类型名,而在异常对象内部没有任何有意义的信息。) +此外,能够抛出任意类型的 **Throwable** 对象,它是异常类型的基类。通常,为每个不同类型的错误抛出不同的异常类。错误信息可以保存在异常对象内部或者用异常类的名称来暗示。上一级环境通过这些信息来决定如何处理异常。(通常,唯一的信息只有异常的类型名,而在异常对象内部没有任何有意义的信息。) ## 异常捕获 @@ -113,7 +113,7 @@ try { 另一种称为恢复模型。意思是异常处理程序的工作是修正错误,然后重新尝试调用出问题的方法,并认为第二次能成功。对于恢复模型,通常希望异常被处理之后能继续执行程序。如果想要用 Java 实现类似恢复的行为,那么在遇见错误时就不能抛出异常,而是调用方法来修正该错误。或者,把 try 块放在 while 循环里,这样就不断地进入 try 块,直到得到满意的结果。 -在过去,使用支持恢复模型异常处理的操作系统的程序员们最终还是转向使用类似“终止模型”的代码,并且忽略恢复行为。所以虽然恢复模型开始显得很吸引人,但不是很实用。其中的主要原因可能是它所导致的耦合:恢复性的处理程序需要了解异常抛出的地点,这势必要包含依赖于抛出位置的非通用性代码。这增加了代码编写和维护的困难,对于异常可能会从许多地方抛出的大型程序来说,更是如此。 +在过去,使用支持恢复模型异常处理的操作系统的程序员们最终会使用类似“终止模型”的代码并跳过恢复。因此, 虽然恢复模型开始显得很吸引人,但在实践中却没有那么有用。其中的主要原因可能是它所导致的耦合:一个恢复处理程序需要了解异常抛出的位置,并包含根据抛出位置特制的非通用性代码。这增加了代码编写和维护的困难,对于异常可能会从许多地方抛出的大型程序来说,更是如此。 @@ -121,7 +121,7 @@ try { 不必拘泥于 Java 已有的异常类型。Java异常体系不可能预见你将报告的所有错误,所以你可以创建自己的异常类,来表示你的程序中可能遇到的问题。 -要自己定义异常类,必须从已有的异常类继承,最好是选择意思相近的异常类继承(不过这样的异常并不容易找)。建立新的异常类型最简单的方法就是让编译器为你产生无参构造器,所以这几乎不用写多少代码: +要自己定义异常类,必须从已有的异常类继承,最好是选择含义相近的异常类继承(不过这样的异常并不容易找)。建立新的异常类型最简单的方法就是让编译器为你产生无参构造器,所以这几乎不用写多少代码: ```java // exceptions/InheritingExceptions.java @@ -153,9 +153,17 @@ Throw SimpleException from f() Caught it! ``` -编译器创建了无参构造器,它将自动调用基类的无参构造器。本例中不会得到像 SimpleException(String) 这样的构造器,这种构造器也不实用。你将看到,对异常来说,最重要的部分就是类名,所以本例中建立的异常类在大多数情况下已经够用了。 +编译器创建了无参构造器,它将自动调用基类的无参构造器。本例中不会有像 **SimpleException(String)** 这样的构造器,这种构造器也不会应用太多。你将看到,对异常来说,最重要的部分就是类名,所以本例中建立的异常类在大多数情况下已经够用了。 本例的结果被显示在控制台。你也可以通过写入 System.err 而将错误发送给标准错误流。通常这比把错误信息输出到 System.out 要好,因为 System.out 也许会被重定向。如果把结果送到 System.err,它就不会随 System.out 一起被重定向,所以用户就更容易注意到它。 +```java +public static void main(String[] args) throws Exception { + String str = "sout"; + PrintStream out = new PrintStream("c:/test.txt"); + System.setOut(out); + System.out.println(str); //这种情况叫做重定向 +} +``` 你也可以为异常类创建一个接受字符串参数的构造器: @@ -204,13 +212,13 @@ MyException: Originated in g() 新增的代码非常简短:两个构造器定义了 MyException 类型对象的创建方式。对于第二个构造器,使用 super 关键字明确调用了其基类构造器,它接受一个字符串作为参数。 -在异常处理程序中,调用了在 Throwable 类声明(Exception 即从此类继承)的 printStackTrace() 方法。就像从输出中看到的,它将打印“从方法调用处直到异常抛出处”的方法调用序列。这里,信息被发送到了 System.out,并自动地被捕获和显示在输出中。但是,如果调用默认版本: +在异常处理程序中,调用了在 Throwable 类声明(Exception 即从此类继承)的 printStackTrace() 方法。就像从输出中看到的,它将打印“从方法调用处直到异常抛出处”的方法调用序列。这里,信息被发送到了 System.out,并自动地被捕获和显示在输出中(白字)。但是,如果调用默认版本: ```java e.printStackTrace(); ``` -信息就会被输出到标准错误流。 +信息就会被输出到标准错误流(红字)。 ### 异常与记录日志 @@ -263,9 +271,9 @@ LoggingExceptions.main(LoggingExceptions.java:25) Caught LoggingException ``` -静态的 Logger.getLogger() 方法创建了一个 String 参数相关联的 Logger 对象(通常与错误相关的包名和类名),这个 Logger 对象会将其输出发送到 System.err。向 Logger 写入的最简单方式就是直接调用与日志记录消息的级别相关联的方法,这里使用的是 severe()。为了产生日志记录消息,我们欲获取异常抛出处的栈轨迹,但是 printStackTrace() 不会默认地产生字符串。为了获取字符串,我们需要使用重载的 printStackTrace() 方法,它接受一个 java.io.PrintWriter 对象作为参数(PrintWriter 会在 [附录:I/O 流 ](./Appendix-IO-Streams.md) 一章详细介绍)。如果我们将一个 java.io.StringWriter 对象传递给这个 PrintWriter 的构造器,那么通过调用 toString() 方法,就可以将输出抽取为一个 String。 +静态的 Logger.getLogger() 方法创建了一个 String 参数相关联的 Logger 对象(通常与错误相关的包名和类名),这个 Logger 对象会将它的输出发送到 System.err。向 Logger 写入的最简单方式就是直接调用与日志记录消息的级别相关联的方法,这里使用的是 severe()。为了产生日志记录消息,我们欲获取异常抛出处的栈轨迹,但是 printStackTrace() 不会默认地产生字符串。为了获取字符串,我们需要使用重载的 printStackTrace() 方法,它接受一个 java.io.PrintWriter 对象作为参数(PrintWriter 会在 [附录:I/O 流 ](./Appendix-IO-Streams.md) 一章详细介绍)。如果我们将一个 java.io.StringWriter 对象传递给这个 PrintWriter 的构造器,那么通过调用 toString() 方法,就可以将输出抽取为一个 String。 -尽管由于 LoggingException 将所有记录日志的基础设施都构建在异常自身中,使得它所使用的方式非常方便,并因此不需要客户端程序员的干预就可以自动运行,但是更常见的情形是我们需要捕获和记录其他人编写的异常,因此我们必须在异常处理程序中生成日志消息; +尽管由于 LoggingException 将所有记录日志的基础设施都构建在异常自身中,使得它所使用的方式非常方便(new Exception会自动记录日志),并因此不需要客户端程序员的干预就可以自动运行,但是更常见的情形是我们需要捕获和记录其他人编写的异常(因而不能侵入别人的异常代码的构造器),因此我们必须在异常处理程序中手动生成日志消息; ```java // exceptions/LoggingExceptions2.java @@ -376,8 +384,8 @@ at ExtraFeatures.main(ExtraFeatures.java:48) e.val() = 47 ``` -新的异常添加了字段 x 以及设定 x 值的构造器和读取数据的方法。此外,还覆盖了 Throwable. -getMessage() 方法,以产生更详细的信息。对于异常类来说,getMessage() 方法有点类似于 toString() 方法。 +新的异常添加了字段 x 以及设定 x 值的构造器和读取数据的方法。此外,还重写了 Throwable. +getMessage() 方法,以产生更详细的信息。对于异常类来说,getMessage() 方法类似于 toString() 方法。 既然异常也是对象的一种,所以可以继续修改这个异常类,以得到更强的功能。但要记住,使用程序包的客户端程序员可能仅仅只是查看一下抛出的异常类型,其他的就不管了(大多数 Java 库里的异常都是这么用的),所以对异常所添加的其他功能也许根本用不上。 @@ -403,7 +411,7 @@ void f() { // ... 不过还是有个能“作弊”的地方:可以声明方法将抛出异常,实际上却不抛出。编译器相信了这个声明,并强制此方法的用户像真的抛出异常那样使用这个方法。这样做的好处是,为异常先占个位子,以后就可以抛出这种异常而不用修改已有的代码。在定义抽象基类和接口时这种能力很重要,这样派生类或接口实现就能够抛出这些预先声明的异常。 -这种在编译时被强制检查的异常称为被检查的异常。 +这种在编译时被强制检查的异常称为*被检查的异常*(**checked exceptions**)。 ## 捕获所有异常 @@ -692,7 +700,7 @@ at Rethrowing.h(Rethrowing.java:27) at Rethrowing.main(Rethrowing.java:38) ``` -调用 fillInStackTrace() 的那一行就成了异常的新发生地了。 +调用 fillInStackTrace() 的那一行就成了异常的新发生地了。(减少爬栈, 提高效率) 有可能在捕获异常之后抛出另一种异常。这么做的话,得到的效果类似于使用 fillInStackTrace(),有关原来异常发生点的信息会丢失,剩下的是与新的抛出点有关的信息: @@ -745,7 +753,7 @@ at RethrowNew.main(RethrowNew.java:26) 最后那个异常仅知道自己来自 main(),而对 f() 一无所知。 -永远不必为清理前一个异常对象而担心,或者说为异常对象的清理而担心。它们都是用 new 在堆上创建的对象,所以垃圾回收器会自动把它们清理掉。 +永远不必为清理之前的 or 任何的异常对象而担心。它们都是用 new 在堆上创建的对象,所以垃圾回收器会自动把它们清理掉。 ### 精准的重新抛出异常 @@ -766,7 +774,7 @@ public class PreciseRethrow { } ``` -因为 catch 捕获了一个 BaseException,编译器强迫你声明 catcher() 抛出 BaseException,即使它实际上抛出了更具体的 DerivedException。从 Java 7 开始,这段代码就可以编译,这是一个很小但很有用的修复。 +因为 catch 捕获了一个 BaseException,Java7 前的编译器强迫你的方法声明 catcher() 只能抛出 BaseException,即使它实际上抛出了更具体的 DerivedException。从 Java 7 开始,这段代码就可以编译,这是一个很小但很有用的修复。 ### 异常链 @@ -1161,7 +1169,7 @@ Caught FourException in 1st try block finally in 1st try block ``` -当涉及 break 和 continue 语句的时候,finally 子句也会得到执行。请注意,如果把 finally 子句和带标签的 break 及 continue 配合使用,在 Java 里就没必要使用 goto 语句了。 +当涉及 break 和 continue 语句的时候,finally 子句也会得到执行。请注意,如果把 finally 子句和带标签的 break 及 continue 配合使用, 就免去了对 goto 语句的需求了。 ### 在 return 中使用 finally @@ -1290,7 +1298,7 @@ public class ExceptionSilencer { ## 异常限制 -当覆盖方法的时候,只能抛出在基类方法的异常说明里列出的那些异常。这个限制很有用,因为这意味着与基类一起工作的代码,也能和导出类一起正常工作(这是面向对象的基本概念),异常也不例外。 +当重写方法的时候,只能抛出在基类方法的异常说明里列出的那些异常。这个限制很有用,因为这意味着与基类一起工作的代码,也能和导出类一起正常工作(这是面向对象的基本概念),异常也不例外。 下面例子演示了这种(在编译时)施加在异常上面的限制: @@ -1371,21 +1379,21 @@ public class StormyInning extends Inning implements Storm { } ``` -在 Inning 类中,可以看到构造器和 event() 方法都声明将抛出异常,但实际上没有抛出。这种方式使你能强制用户去捕获可能在覆盖后的 event() 版本中增加的异常,所以它很合理。这对于抽象方法同样成立,比如 atBat()。 +在 Inning 类中,可以看到构造器和 event() 方法都声明将抛出异常,但实际上没有抛出。这种方式使你能强制用户去捕获可能在重写后的 event() 版本中增加的异常,所以它很合理。这对于抽象方法同样成立,比如 atBat()。 接口 Storm 包含了一个在 Inning 中定义的方法 event() 和一个不在 Inning 中定义的方法 rainHard()。这两个方法都抛出新的异常 RainedOut,如果 StormyInning 类在扩展 Inning 类的同时又实现了 Storm 接口,那么 Storm 里的 event() 方法就不能改变在 Inning 中的 event() 方法的异常接口。否则的话,在使用基类的时候就不能判断是否捕获了正确的异常,所以这也很合理。当然,如果接口里定义的方法不是来自于基类,比如 rainHard(),那么此方法抛出什么样的异常都没有问题。 -异常限制对构造器不起作用。你会发现 StormyInning 的构造器可以抛出任何异常,而不必理会基类构造器所抛出的异常。然而,因为基类构造器必须以这样或那样的方式被调用(这里默认构造器将自动被调用),派生类构造器的异常说明必须包含基类构造器的异常说明。 +异常限制对构造器不起作用。你会发现 StormyInning 的构造器可以抛出任何异常,而不必受限于基类构造器所抛出的异常。然而,因为基类构造器必须以这样或那样的方式被调用(这里默认构造器将自动被调用),派生类构造器的异常说明必须包含基类构造器的异常说明。 派生类构造器不能捕获基类构造器抛出的异常。 StormyInning.walk() 不能通过编译是因为它抛出了一个 Inning.walk() 中没有声明的异常。如果编译器允许这么做的话,就可以编写调用Inning.walk()却不处理任何异常的代码。 但是当使用 `Inning`派生类的对象时,就会抛出异常,从而导致程序出现问题。通过强制派生类遵守基类方法的异常说明,对象的可替换性得到了保证。 -覆盖后的 event() 方法表明,派生类版的方法可以不抛出任何异常,即使基类版的方法抛出了异常。因为这样做不会破坏那些假定基类版的方法会抛出异常的代码。类似的情况出现在 `atBat()`上,它抛出的异常`PopFoul`是由基类版`atBat()`抛出的`Foul` 异常派生而来。如果你写的代码同 `Inning` 一起工作,并且调用了 `atBat()`的话,那么肯定能捕获 `Foul` 。又因为 `PopFoul` 是由 `Foul`派生而来,因此异常处理程序也能捕获 `PopFoul`。 +重写后的 event() 方法表明,派生类版的方法可以不抛出任何异常,即使基类版的方法抛出了异常。因为这样做不会破坏那些假定基类版的方法会抛出异常的代码。类似的情况出现在 `atBat()`上,它抛出的异常`PopFoul`是由基类版`atBat()`抛出的`Foul` 异常派生而来。如果你写的代码同 `Inning` 一起工作,并且调用了 `atBat()`的话,那么肯定能捕获 `Foul` 。又因为 `PopFoul` 是由 `Foul`派生而来,因此异常处理程序也能捕获 `PopFoul`。 最后一个有趣的地方在 `main()`。如果处理的刚好是 Stormylnning 对象的话,编译器只要求捕获这个类所抛出的异常。但是如果将它向上转型成基类型,那么编译器就会准确地要求捕获基类的异常。所有这些限制都是为了能产生更为健壮的异常处理代码。 -尽管在继承过程中,编译器会对异常说明做强制要求,但异常说明本身并不属于方法类型的一部分,方法类型是由方法的名字与参数的类型组成的。因此,不能基于异常说明来重载方法。此外,一个出现在基类方法的异常说明中的异常,不一定会出现在派生类方法的异常说明里。这点同继承的规则明显不同,在继承中,基类的方法必须出现在派生类里,换句话说,在继承和覆盖的过程中,某个特定方法的“异常说明的接口”不是变大了而是变小了——这恰好和类接口在继承时的情形相反。 +尽管在继承过程中,编译器会对异常说明做强制要求,但异常说明本身并不属于方法类型的一部分,方法类型是由方法的名字与参数的类型组成的。因此,不能基于异常说明来重载方法。此外,一个出现在基类方法的异常说明中的异常,不一定会出现在派生类方法的异常说明里。(这点与继承的规则明显不同,在继承中,基类的方法必须出现在派生类里。)换句话说,在继承和重写的过程中,子类特定方法的“异常说明的接口” **不能扩大只能减小** ——这与继承时的类接口规则( 访问权限 **不能减小只能扩大** )正好相反。 @@ -1564,7 +1572,7 @@ NeedsCleanup 4 disposed - [2] 为了构造和清理,可以看到将具有不能失败的构造器的对象分组在一起。 - [3] 展示了如何处理那些具有可以失败的构造器,且需要清理的对象。为了正确处理这种情况,事情变得很棘手,因为对于每一个构造,都必须包含在其自己的 try-finally 语句块中,并且每一个对象构造必须都跟随一个 try-finally 语句块以确保清理。 -本例中异常处理的混乱情形,有力的论证了应该创建不会抛出异常的构造器,尽管这并不总会实现。 +本例中异常处理的混乱情形,有力的论证了应该创建不会抛出异常的构造器,尽管这并非总能实现。 注意,如果 dispose() 可以抛出异常,那么你可能需要额外的 try 语句块。基本上,你应该仔细考虑所有的可能性,并确保正确处理每一种情况。 @@ -1945,7 +1953,7 @@ try { ## 其他可选方式 -异常处理系统就像一个活门(trap door),使你能放弃程序的正常执行序列。当“异常情形”发生的时候,正常的执行已变得不可能或者不需要了,这时就要用到这个“活门"。异常代表了当前方法不能继续执行的情形。开发异常处理系统的原因是,如果为每个方法所有可能发生的错误都进行处理的话,任务就显得过于繁重了,程序员也不愿意这么做。结果常常是将错误忽略。应该注意到,开发异常处理的初衷是为了方便程序员处理错误。 +异常处理系统就像一个*活板门*(trapdoor),使你能放弃程序的正常执行序列。当“异常情形”发生的时候,正常的执行已变得不可能或者不需要了,这时就要用到这个“*活板门*"。异常代表了当前方法不能继续执行的情形。开发异常处理系统的原因是,如果为每个方法所有可能发生的错误都进行处理的话,任务就显得过于繁重了,程序员也不愿意这么做。结果常常是将错误忽略。应该注意到,开发异常处理的初衷是为了方便程序员处理错误。 异常处理的一个重要原则是“只有在你知道如何处理的情况下才捕获异常"。实际上,异常处理的一个重要目标就是把处理错误的代码同错误发生的地点相分离。这使你能在一段代码中专注于要完成的事情,至于如何处理错误,则放在另一段代码中完成。这样一来,主要代码就不会与错误处理逻辑混在一起,也更容易理解和维护。通过允许一个处理程序去处理多个出错点,异常处理还使得错误处理代码的数量趋于减少。 @@ -2144,8 +2152,8 @@ SomeOtherException: SomeOtherException 3. 解决问题并且重新调用产生异常的方法。 4. 进行少许修补,然后绕过异常发生的地方继续执行。 5. 用别的数据进行计算,以代替方法预计会返回的值。 -6. 把当前运行环境下能做的事情尽量做完,然后把相同的异常重抛到更高层。 -7. 把当前运行环境下能做的事情尽量做完,然后把不同的异常抛到更高层。 +6. 把当前运行环境下能做的事情尽量做完,然后把相同的异常重新抛出到更高层。 +7. 把当前运行环境下能做的事情尽量做完,然后把一个不同的异常抛到更高层。 8. 终止程序。 9. 进行简化。(如果你的异常模式使问题变得太复杂,那用起来会非常痛苦也很烦人。) 10. 让类库和程序更安全。(这既是在为调试做短期投资,也是在为程序的健壮性做长期投资。) diff --git a/docs/book/16-Validating-Your-Code.md b/docs/book/16-Validating-Your-Code.md index 26917eab..5b41625b 100644 --- a/docs/book/16-Validating-Your-Code.md +++ b/docs/book/16-Validating-Your-Code.md @@ -20,9 +20,9 @@ Java是一个静态类型的语言,程序员经常对一种编程语言明显 这个过程是将集成测试构建到你创建的所有代码中,并在每次构建系统时运行这些测试。这样,构建过程不仅能检查语法的错误,同时也能检查语义的错误。 -“单元”是指测试一小部分代码 。通常,每个类都有测试来检查它所有方法的行为。“系统”测试则是不同的,它检查的是整个程序是否满足要求。 +“单元”是指测试一小部分代码 。通常,每个类都有测试来检查它所有方法的行为。“系统”测试则不同,它检查的是整个程序是否满足要求。 -C 风格的语言,尤其是 C++,通常会认为性能比安全更重要。用 Java 编程比 C++(一般认为大概快两倍)快的原因是 Java 的安全性保障:比如垃圾回收以及改良的类型检测等特性。通过将单元测试集成到构建过程中,你扩大了这个安全保障,因而有了更快的开发效率。当发现设计或实现的缺陷时,可以更容易、更大胆地重构你的代码。 +C 风格的语言,尤其是 C++,通常会认为性能比安全更重要。用 Java 开发程序比 C++(一般认为大概快两倍)快的原因是 Java 的安全性保障:比如垃圾回收以及改良的类型检测等特性。通过将单元测试集成到构建过程中,你扩大了这个安全保障,因而有了更快的开发效率。当发现设计或实现的缺陷时,可以更容易、更大胆地重构你的代码。 我自己的测试经历开始于我意识到要确保书中代码的正确性,书中的所有程序必须能够通过合适的构建系统自动提取、编译。这本书所使用的构建系统是 Gradle。 你只要在安装 JDK 后输入 **gradlew compileJava**,就能编译本书的所有代码。自动提取和自动编译的效果对本书代码的质量是如此的直接和引人注目,(在我看来)这会很快成为任何编程书籍的必备条件——你怎么能相信没有编译的代码呢? 我还发现我可以使用搜索和替换在整本书进行大范围的修改,如果引入了一个错误,代码提取器和构建系统就会清除它。随着程序越来越复杂,我在系统中发现了一个严重的漏洞。编译程序毫无疑问是重要的第一步, 对于一本要出版的书而言,这看来是相当具有革命意义的发现(由于出版压力, 你经常打开一本程序设计的书会发现书中代码的错误)。但是,我收到了来自读者反馈代码中存在语义问题。当然,这些问题可以通过运行代码发现。我在早期实现一个自动化执行测试系统时尝试了一些不太有效的方式,但迫于出版压力,我明白我的程序绝对有问题,并会以 bug 报告的方式让我自食恶果。我也经常收到读者的抱怨说,我没有显示足够的代码输出。我需要验证程序的输出,并且在书中显示验证的输出。我以前的意见是读者应该一边看书一边运行代码,许多读者就是这么做的并且从中受益。然而,这种态度背后的原因是,我无法保证书中的输出是正确的。从经验来看,我知道随着时间的推移,会发生一些事情,使得输出不再正确(或者一开始就不正确)。为了解决这个问题,我利用 Python 创建了一个工具(你将在下载的示例中找到此工具)。本书中的大多数程序都产生控制台输出,该工具将该输出与源代码清单末尾的注释中显示的预期输出进行比较,所以读者可以看到预期的输出,并且知道这个输出已经被构建程序验证过。 @@ -160,9 +160,9 @@ Cleaning up 4 */ ``` -**@BeforeAll** 注解是在任何其他测试操作之前运行一次的方法。 **@AfterAll** 是所有其他测试操作之后只运行一次的方法。两个方法都必须是静态的。 +**@BeforeAll** 注解是在任何其他测试操作之前运行一次的方法。 **@AfterAll** 是所有其他测试操作之后只运行一次的方法。两个方法都必须是静态的。(注: junit4里面是 **@BeforeClass** **@AfterClass**) -**@BeforeEach**注解是通常用于创建和初始化公共对象的方法,并在每次测试前运行。可以将所有这样的初始化放在测试类的构造函数中,尽管我认为 **@BeforeEach** 更加清晰。JUnit为每个测试创建一个对象,确保测试运行之间没有副作用。然而,所有测试的所有对象都是同时创建的(而不是在测试之前创建对象),所以使用 **@BeforeEach** 和构造函数之间的唯一区别是 **@BeforeEach** 在测试前直接调用。在大多数情况下,这不是问题,如果你愿意,可以使用构造函数方法。 +**@BeforeEach**注解是通常用于创建和初始化公共对象的方法,并在每次测试前运行。可以将所有这样的初始化放在测试类的构造函数中,尽管我认为 **@BeforeEach** 更加清晰。JUnit为每个测试创建一个对象,确保测试运行之间没有副作用。然而,所有测试的所有对象都是同时创建的(而不是在测试之前创建对象),所以使用 **@BeforeEach** 和构造函数之间的唯一区别是 **@BeforeEach** 在测试前直接调用。在大多数情况下,这不是问题,如果你愿意,可以使用构造函数方法。(注: junit4里面是 **@Before** **@After**) 如果你必须在每次测试后执行清理(如果修改了需要恢复的静态文件,打开文件需要关闭,打开数据库或者网络连接,etc),那就用注解 **@AfterEach**。 @@ -186,13 +186,13 @@ JUnit 是 Java 最流行的单元测试框架,但也有其它可以替代的 #### 测试覆盖率的幻觉 -测试覆盖率,同样也称为代码覆盖率,度量代码的测试百分比。百分比越高,测试的覆盖率越大。这里有很多[方法](https://en.wikipedia.org/wiki/Code_coverage) +测试覆盖率,同样也称为代码覆盖率,度量代码被测试的百分比。百分比越高,测试的覆盖率越大。这里有很多[方法](https://en.wikipedia.org/wiki/Code_coverage) 计算覆盖率,还有有帮助的文章[Java代码覆盖工具](https://en.wikipedia.org/wiki/Java_Code_Coverage_Tools)。 -对于没有知识但处于控制地位的人来说,很容易在没有任何了解的情况下也有概念认为 100% 的测试覆盖是唯一可接受的值。这有一个问题,因为 100% 并不意味着是对测试有效性的良好测量。你可以测试所有需要它的东西,但是只需要 65% 的覆盖率。如果需要 100% 的覆盖,你将浪费大量时间来生成剩余的代码,并且在向项目添加代码时浪费的时间更多。 +对于没有知识但有控制权的人来说,很容易决定100%覆盖是唯一可接受的值。这有一个问题,因为 100% 并不能很好地衡量测试效果。你可以测试所有需要它的东西,但是只需要 65% 的覆盖率。如果需要 100% 的覆盖,你将浪费大量时间来生成剩余的代码,并浪费更多的时间在向项目添加代码。 -当分析一个未知的代码库时,测试覆盖率作为一个粗略的度量是有用的。如果覆盖率工具报告的值特别低(比如,少于百分之40),则说明覆盖不够充分。然而,一个非常高的值也同样值得怀疑,这表明对编程领域了解不足的人迫使团队做出了武断的决定。覆盖工具的最佳用途是发现代码库中未测试的部分。但是,不要依赖覆盖率来得到测试质量的任何信息。 +当分析一个未知的代码库时,测试覆盖率作为一个粗略的度量是有用的。如果覆盖率工具报告的值特别低(比如,少于百分之40),则说明覆盖不够充分。然而,一个非常高的值也同样值得怀疑,这表明对编程领域了解不足的人迫使团队做出了武断的决定。覆盖率工具的最佳用途是发现代码库中未测试的部分。但是,不要依赖覆盖率来得到测试质量的任何信息。 @@ -240,7 +240,7 @@ at Assert1.main(Assert1.java:9) */ ``` -如果你正常运行程序,没有任何特殊的断言标志,则不会发生任何事情。你需要在运行程序时显式启用断言。一种简单的方法是使用 **-ea** 标志, 它也可以表示为: **-enableassertion**, 这将运行程序并执行任何断言语句。 +如果你正常运行程序,没有任何特殊的断言标志,则不会发生任何事情。你需要在运行程序时显式启用断言。一种简单的方法是使用 **-ea** 标志, 它也可以这样拼写: **-enableassertion**, 这将运行程序并执行所有断言语句。 输出中并没有包含多少有用的信息。另一方面,如果你使用 **information-expression** , 将生成一条有用的消息作为异常堆栈跟踪的一部分。最有用的 **information-expression** 通常是一串针对程序员的文本: @@ -373,13 +373,13 @@ Shouldn't be null: arg s #### 使用断言进行契约式设计 -*契约式设计(DbC)*是 Eiffel 语言的发明者 Bertrand Meyer 提出的一个概念,通过确保对象遵循某些规则来帮助创建健壮的程序。这些规则是由正在解决的问题的性质决定的,这超出了编译器可以验证的范围。虽然断言没有直接实现 **DBC**(Eiffel 语言也是如此),但是它们创建了一种非正式的 DbC 编程风格。DbC 假定服务供应者与该服务的消费者或客户之间存在明确指定的契约。在面向对象编程中,服务通常由对象提供,对象的边界 — 供应者和消费者之间的划分 — 是对象类的接口。当客户端调用特定的公共方法时,它们希望该调用具有特定的行为:对象状态改变,以及一个可预测的返回值。 +*契约式设计(Design by Contract DbC)*是 Eiffel 语言的发明者 Bertrand Meyer 提出的一个概念,通过确保对象遵循某些规则来帮助创建健壮的程序。这些规则是由正在解决的问题的性质决定的,这超出了编译器可以验证的范围。虽然断言没有直接实现 **DBC**(Eiffel 语言也是如此),但是它们创建了一种非正式的 DbC 编程风格。DbC 假定服务供应者与该服务的消费者或客户之间存在明确指定的契约。在面向对象编程中,服务通常由对象提供,对象的边界 — 供应者和消费者之间的划分 — 是对象类的接口。当客户端调用特定的公共方法时,它们希望该调用具有特定的行为:对象状态改变,以及一个可预测的返回值。 **Meyer** 认为: 1.应该明确指定行为,就好像它是一个契约一样。 -2.通过实现某些运行时检查来保证这种行为,他将这些检查称为前置条件、后置条件和不变项。 +2.通过实现某些运行时检查来保证这种行为,他将这些检查称为**前置条件**、**后置条件**和**不变项**。 不管你是否同意,第一条总是对的,在大多数情况下,DbC 确实是一种有用的方法。(我认为,与任何解决方案一样,它的有用性也有界限。但如果你知道这些界限,你就知道什么时候去尝试。)尤其是,设计过程中一个有价值的部分是特定类 DbC 约束的表达式;如果无法指定约束,则你可能对要构建的内容了解得不够。 @@ -886,7 +886,7 @@ public class NonNullConstruction { ## 测试驱动开发 -之所以可以有测试驱动开发(TDD)这种开发方式,是因为如果你在设计和编写代码时考虑到了测试,那么你不仅可以写出可测试性更好的代码,而且还可以得到更好的代码设计。 一般情况下这个说法都是正确的。 一旦我想到“我将如何测试我的代码?”,这个想法将使我的代码产生变化,并且往往是从“可测试”转变为“可用”。 +之所以可以有*测试驱动开发(Test-Driven Development TDD)*这种开发方式,是因为如果你在设计和编写代码时考虑到了测试,那么你不仅可以写出可测试性更好的代码,而且还可以得到更好的代码设计。 一般情况下这个说法都是正确的。 一旦我想到“我将如何测试我的代码?”,这个想法将使我的代码产生变化,并且往往是从“可测试”转变为“可用”。 纯粹的 TDD 主义者会在实现新功能之前就为其编写测试,这称为测试优先的开发。 我们采用一个简易的示例程序来进行说明,它的功能是反转 **String** 中字符的大小写。 让我们随意添加一些约束:**String** 必须小于或等于30个字符,并且必须只包含字母,空格,逗号和句号(英文)。 @@ -1037,7 +1037,7 @@ class DynamicStringInverterTests { 每个使用 **@TestFactory** 注释的方法都会生成一个 **DynamicTest** 对象的 **Stream**(通过 **testVersions()** ),每个 JUnit 都像常规的 **@Test** 方法一样执行。 -现在测试都已经准备好了,我们就可以开始实现 **StringInverter **了。 我们从一个仅返回其参数的假的实现类开始: +现在测试都已经准备好了,我们就可以开始实现 **StringInverter **了。 我们从一个仅返回其参数的形同虚设的实现类开始: ```java // validating/Inverter1.java @@ -1118,7 +1118,7 @@ public class Inverter4 implements StringInverter { 你将从测试输出中看到,每个版本的 **Inverter** 都几乎能通过所有测试。 当你在进行测试优先的开发时会有相同的体验。 -**DynamicStringInverterTests.java** 仅是为了显示 TDD 过程中不同 **StringInverter** 实现的开发。 通常,你只需编写一组如下所示的测试,并修改单个 **StringInverter** 类直到它满足所有测试: +**DynamicStringInverterTests.java** 仅是为了展示 TDD 过程中 **StringInverter** 的不同实现的开发。 通常,你只需编写一组如下所示的测试,并修改单个 **StringInverter** 类直到它满足所有测试: ```java // validating/tests/StringInverterTests.java @@ -1311,7 +1311,7 @@ public class SLF4JLevels { ### 使用 JDB 调试 -Java 调试器(JDB)是 JDK 内置的命令行工具。从调试的指令和命令行接口两方面看的话,JDB 至少从概念上是 GNU 调试器(GDB,受 Unix DB 的影响)的继承者。JDB 对于学习调试和执行简单的调试任务来说是有用的,而且知道只要安装了 JDK 就可以使用 JDB 是有帮助的。然而,对于大型项目来说,你可能想要一个图形化的调试器,这在后面会描述。 +Java 调试器(*Java Debugger JDB*)是 JDK 内置的命令行工具。从调试的指令和命令行接口两方面看的话,JDB 至少从概念上是 GNU 调试器(Gnu Debugger GDB,受 Unix DB 的影响)的继承者。JDB 对于学习调试和执行简单的调试任务来说是有用的,而且知道只要安装了 JDK 就可以使用 JDB 是有帮助的。然而,对于大型项目来说,你可能想要一个图形化的调试器,这在后面会描述。 假设你写了如下程序: @@ -1475,7 +1475,7 @@ SimpleDebugging.main(SimpleDebugging.java:20) 如果你发现自己正在过早优化的滑坡上,你可能浪费了几个月的时间(如果你雄心勃勃的话)。通常,一个简单直接的编码方法就足够好了。如果你进行了不必要的优化,就会使你的代码变得无谓的复杂和难以理解。 -基准测试意味着对代码或算法片段进行计时看哪个跑得更快,与下一节的分析和优化截然相反,分析优化是观察整个程序,找到程序中最耗时的部分。 +基准测试 **Benchmarking** 意味着对代码或算法片段进行计时看哪个跑得更快,与下一节的 **剖析和优化** 截然相反,分析优化是观察整个程序,找到程序中最慢的部分。 可以简单地对一个代码片段的执行计时吗?在像 C 这样直接的编程语言中,这个方法的确可行。在像 Java 这样拥有复杂的运行时系统的编程语言中,基准测试变得更有挑战性。为了生成可靠的数据,环境设置必须控制诸如 CPU 频率,节能特性,其他运行在相同机器上的进程,优化器选项等等。 @@ -1569,7 +1569,7 @@ parallelSetAll: 86 setAll: 39 ``` -**SplittableRandom** 是为并行算法设计的,它当然看起来比普通的 **Random** 在 **parallelSetAll()** 中运行得更快。 但是看上去还是比非并发的 **setAll()** 运行时间更长,有点难以置信(也许是真的,但我们不能通过一个坏的微基准测试得到这个结论)。 +**SplittableRandom** 是为并行算法设计的,它当然看起来比普通的 **Random** 在 **parallelSetAll()** 中运行得更快。 但是看上去还是比非并发的 **setAll()** 运行时间更长,有点难以置信(也许是真的,但我们不能通过一个坏的微基准测试来得到此结论)。 这只考虑了微基准测试的问题。Java 虚拟机 Hotspot 也非常影响性能。如果你在测试前没有通过运行代码给 JVM 预热,那么你就会得到“冷”的结果,不能反映出代码在 JVM 预热之后的运行速度(假如你运行的应用没有在预热的 JVM 上运行,你就可能得不到所预期的性能,甚至可能减缓速度)。 @@ -1618,7 +1618,7 @@ public class JMH1 { } ``` -“forks” 的默认值是 10,意味着每个测试都运行 10 次。为了减少运行时间,这里使用了 **@Fork** 注解来减少这个次数到 1。我还使用了 **@Warmup** 和 **@Measurement** 注解将它们默认的运行次数从 20 减少到 5 次。尽管这降低了整体的准确率,但是结果几乎与使用默认值相同。可以尝试将 **@Warmup**、**@Measurement** 和 **@Fork** 都注释掉然后看使用它们的默认值,结果会有多大显著的差异;一般来说,你应该只能看到长期运行的测试使错误因素减少,而结果没有多大变化。 +“forks” 的默认值是 10,意思是每次测试同时开了 10 个进程运行。为了加速,这里使用了 **@Fork** 注解来减少这个次数到 1 (表示一个进程)。我还使用了 **@Warmup** 和 **@Measurement** 注解,将预热次数和测试次数从默认的 20 减少到 5 次。尽管这降低了整体的准确率,但是结果几乎与使用默认值相同。可以尝试将 **@Warmup**、**@Measurement** 和 **@Fork** 都注释掉然后看使用它们的默认值,看看结果会有多大显著的差异;一般来说,你应该只能看到长期运行的测试减少了误差,而结果变化不大。 需要使用显式的 gradle 命令才能运行基准测试(在示例代码的根目录处运行)。这能防止耗时的基准测试运行其他的 **gradlew** 命令: @@ -1738,7 +1738,7 @@ public class JMH2 { **setAll()/parallelSetAll()** 中工作的计算量起很大影响吗?在前面的例子中,我们所做的只有对数组的赋值操作,这可能是最简单的任务。所以即使 **N** 值变大,**N*Q** 也仍然没有达到巨大,所以看起来像是我们没有为并行提供足够的机会(JMH 提供了一种模拟变量 Q 的途径;如果想了解更多的话,可搜索 **Blackhole.consumeCPU**)。 -我们通过使方法 **f()** 中的任务变得更加复杂,从而产生更多的并行机会: +我们通过下面的方法 **f()** 中的任务变得更加复杂,从而产生更多的并行机会: ```java // validating/jmh/JMH3.java @@ -1903,7 +1903,7 @@ public class JMH3 { ## 重构 -技术负债是指迭代发展的软件中为了应急而生的丑陋解决方案从而导致设计难以理解,代码难以阅读的部分。特别是当你必须修改和增加新特性的时候,这会造成麻烦。 +技术负债是指迭代发展的软件中使用为了应急而生的丑陋解决方案,从而导致设计难以理解、代码难以阅读的部分。特别是当你必须修改和增加新特性的时候,这会造成麻烦。 重构可以矫正技术负债。重构的关键是它能改善代码设计,结构和可读性(因而减少代码负债),但是它不能改变代码的行为。 diff --git a/docs/book/17-Files.md b/docs/book/17-Files.md index 7a22b051..8184bfee 100644 --- a/docs/book/17-Files.md +++ b/docs/book/17-Files.md @@ -8,10 +8,10 @@ 这种"困难方式"的全部细节都在 [Appendix: I/O Streams](./Appendix-IO-Streams.md)。如果你读过这个部分,就会认同 Java 设计者毫不在意他们的使用者的体验这一观念。打开并读取文件对于大多数编程语言来说是非常常用的,由于 I/O 糟糕的设计以至于 很少有人能够在不依赖其他参考代码的情况下完成打开文件的操作。 -好像 Java 设计者终于意识到了 Java 使用者多年来的痛苦,在 Java7 中对此引入了巨大的改进。这些新元素被放在 **java.nio.file** 包下面,过去人们通常把 **nio** 中的 **n** 理解为 **new** 即新的 **io**,现在更应该当成是 **non-blocking** 非阻塞 **io**(**io**就是*input/output输入/输出*)。**java.nio.file** 库终于将 Java 文件操作带到与其他编程语言相同的水平。最重要的是 Java8 新增的 streams 与文件结合使得文件操作编程变得更加优雅。我们将看一下文件操作的两个基本组件: +好像 Java 设计者终于意识到了 Java 使用者多年来的痛苦,在 Java7 中对此引入了巨大的改进。这些新元素被放在 **java.nio.file** 包下面,过去人们通常把 **nio** 中的 **n** 理解为 **new** 即新的 **io**,如今却意味着 **non-blocking** 非阻塞 **io**(**io**就是*input/output输入/输出*)。**java.nio.file** 库终于将 Java 文件操作带到与其他编程语言相同的水平。最重要的是 Java8 新增的 streams 与文件结合使得文件操作编程变得更加优雅。我们将看一下文件操作的两个基本组件: 1. 文件或者目录的路径; -2. 文件本身。 +2. 文件自身。 ## 文件和目录路径 @@ -121,14 +121,14 @@ true */ ``` -我已经在这一章第一个程序的 **main()** 方法添加了第一行用于展示操作系统的名称,因此你可以看到不同操作系统之间存在哪些差异。理想情况下,差别会相对较小,并且使用 **/** 或者 **\\** 路径分隔符进行分隔。你可以看到我运行在Windows 10 上的程序输出。 +我已经在 **main()** 方法的第一行用于展示操作系统的名称,因此你可以看到不同操作系统之间存在哪些差异。理想情况下,差别会相对较小,诸如 **/** 还是 **\\** 路径分隔符进行分隔之类的。你可以看到我运行在Windows 10 上的程序输出。 -当 **toString()** 方法生成完整形式的路径,你可以看到 **getFileName()** 方法总是返回当前文件名。 -通过使用 **Files** 工具类(我们接下来将会更多地使用它),可以测试一个文件是否存在,测试是否是一个"普通"文件还是一个目录等等。"Nofile.txt"这个示例展示我们描述的文件可能并不在指定的位置;这样可以允许你创建一个新的路径。"PathInfo.java"存在于当前目录中,最初它只是没有路径的文件名,但它仍然被检测为"存在"。一旦我们将其转换为绝对路径,我们将会得到一个从"C:"盘(因为我们是在Windows机器下进行测试)开始的完整路径,现在它也拥有一个父路径。“真实”路径的定义在文档中有点模糊,因为它取决于具体的文件系统。例如,如果文件名不区分大小写,即使路径由于大小写的缘故而不是完全相同,也可能得到肯定的匹配结果。在这样的平台上,**toRealPath()** 将返回实际情况下的 **Path**,并且还会删除任何冗余元素。 + **toString()** 生成完整路径, **getFileName()** 方法总是返回当前文件名。 +通过使用 **Files** 工具类(我们接下来将会更多地使用它),可以测试一个文件是否存在、是否"普通"文件、是否目录等等。"Nofile.txt"这个示例展示我们描述的文件可能并不在指定的位置;这样就允许你创建一个新的路径。"PathInfo.java"存在于当前目录中,最初它只是没有路径的文件名,但它仍然被检测为"存在"。一旦我们将其转换为绝对路径,我们将会得到一个从"C:"盘(因为我们是在Windows机器下进行测试)开始的完整路径,现在它也拥有一个父路径。“真实”路径的定义在文档中有点模糊,因为它取决于具体的文件系统。例如,如果文件名不区分大小写,即使路径由于大小写的缘故而不是完全相同,也可能得到肯定的匹配结果。在这样的平台上,**toRealPath()** 将返回实际情况下的 **Path**,并且还会删除任何冗余元素。 这里你会看到 **URI** 看起来只能用于描述文件,实际上 **URI** 可以用于描述更多的东西;通过 [维基百科](https://en.wikipedia.org/wiki/Uniform_Resource_Identifier) 可以了解更多细节。现在我们成功地将 **URI** 转为一个 **Path** 对象。 -最后,你会在 **Path** 中看到一些有点欺骗的东西,这就是调用 **toFile()** 方法会生成一个 **File** 对象。听起来似乎可以得到一个类似文件的东西(毕竟被称为 **File** ),但是这个方法的存在仅仅是为了向后兼容。虽然看上去应该被称为"路径",实际上却应该表示目录或者文件本身。这是个非常草率并且令人困惑的命名,但是由于 **java.nio.file** 的存在我们可以安全地忽略它的存在。 +最后,你会在 **Path** 中看到一些有点欺骗性的定义,这就是调用 **toFile()** 生成的 **File** 对象。这听起来好像得到了一个类似文件的东西(毕竟被称为 **File** ),但是这种描述仅仅是为了兼容老版本。**File** 对象 实际上表示文件或目录本身——应该被称为"路径**Path**",这是个非常草率并且令人困惑的命名,但是由于 **java.nio.file** 的存在我们可以安全地忽略它的存在。 (这个 **File** 对象是 **java.io.File**, 不兼容旧版本一般用不上) ### 选取路径部分片段 **Path** 对象可以非常容易地生成路径的某一部分: @@ -143,8 +143,8 @@ public class PartsOfPaths { Path p = Paths.get("PartsOfPaths.java").toAbsolutePath(); for(int i = 0; i < p.getNameCount(); i++) System.out.println(p.getName(i)); - System.out.println("ends with '.java': " + - p.endsWith(".java")); + System.err.println("ends with '.java'1: " + p.endsWith(".java")); //false + System.err.println("ends with '.java'2: " + p.endsWith("PartsOfPaths.java")); //true for(Path pp : p) { System.out.print(pp + ": "); System.out.print(p.startsWith(pp) + " : "); @@ -177,7 +177,7 @@ Starts with C:\ true */ ``` -可以通过 **getName()** 来索引 **Path** 的各个部分,直到达到上限 **getNameCount()**。**Path** 也实现了 **Iterable** 接口,因此我们也可以通过增强的 for-each 进行遍历。请注意,即使路径以 **.java** 结尾,使用 **endsWith()** 方法也会返回 **false**。这是因为使用 **endsWith()** 比较的是整个路径部分,而不会包含文件路径的后缀。通过使用 **startsWith()** 和 **endsWith()** 也可以完成路径的遍历。但是我们可以看到,遍历 **Path** 对象并不包含根路径,只有使用 **startsWith()** 检测根路径时才会返回 **true**。 +对于 **Path** 中的路径分隔符分隔的几个部分的名字(组件名),可通过 **getName(i)** 来索引,直至上限 **getNameCount()**。还可以通过 for-in 循环进行遍历 (**Path** 也实现了 **Iterable** 接口)。请注意,即使路径的确以 **.java** 结尾,使用 **endsWith()** 也返回 **false**。这是因为使用 **endsWith()** 比较的是 **Path** 的各个完整的组件名,而不仅是某个组件名的子串**.java**。这里展示了使用 **startsWith()** 和 **endsWith()** 在 for-in 循环中检测 **Path** 的当前部分。但是我们可以看到,遍历 **Path** 对象没有包含根路径(C盘D盘之类的盘符),只有使用 **startsWith()** 检测 **getRoot()** 时才会返回 **true**。 ### 路径分析 **Files** 工具类包含一系列完整的方法用于获得 **Path** 相关的信息。 @@ -239,10 +239,12 @@ SymbolicLink: false 在调用最后一个测试方法 **getPosixFilePermissions()** 之前我们需要确认一下当前文件系统是否支持 **Posix** 接口,否则会抛出运行时异常。 ### **Paths**的增减修改 -我们必须能通过对 **Path** 对象增加或者删除一部分来构造一个新的 **Path** 对象。我们使用 **relativize()** 移除 **Path** 的根路径,使用 **resolve()** 添加 **Path** 的尾路径(不一定是“可发现”的名称)。 +我们必须能通过对 **Path** 对象增加或者删除一部分来构造一个新的 **Path** 对象。 +删除:使用 **relativize()** 移除 **Path** 的根路径; +添加:使用 **resolve()** 添加到 **Path** 的末尾(不一定是“可发现”的名称)。(可以是不存在的目录和文件) -对于下面代码中的示例,我使用 **relativize()** 方法从所有的输出中移除根路径,部分原因是为了示范,部分原因是为了简化输出结果,这说明你可以使用该方法将绝对路径转为相对路径。 -这个版本的代码中包含 **id**,以便于跟踪输出结果: +对于下面代码中的示例,我使用 **relativize()** 方法从所有的输出中移除根路径,为了示范,也为了简化输出结果。事实证明只有绝对**isAbsolute()**才能使用 **relativize()** 方法。 +这个版本的代码中包含 **id** 以便跟踪输出结果: ```java // files/AddAndSubtractPaths.java @@ -324,7 +326,7 @@ C:\Users\Bruce\Documents\GitHub\onjava\ ExtractedExamples\files\nonexistent */ ``` -我还为 **toRealPath()** 添加了更多的测试,这是为了扩展和规则化,防止路径不存在时抛出运行时异常。 +我还对 **toRealPath()** 做了进一步测试,它会一直扩展和规则化 **Path** ,路径不存在时抛出运行时异常。 @@ -357,16 +359,16 @@ public class RmDir { } } ``` -删除目录树的方法实现依赖于 **Files.walkFileTree()**,"walking" 目录树意味着遍历每个子目录和文件。*Visitor* 设计模式提供了一种标准机制来访问集合中的每个对象,然后你需要提供在每个对象上执行的操作。 +删除目录树的方法实现依赖于 **Files.walkFileTree()**,"walking" 目录树意味着遍历每个子目录和文件。 设计模式—— *访问者模式* *Visitor* 提供了一种标准机制来访问集合中的每个对象,然后需要你提供(实现)在每个对象上执行的操作。 此操作的定义取决于实现的 **FileVisitor** 的四个抽象方法,包括: - 1. **preVisitDirectory()**:在访问目录中条目之前在目录上运行。 + 1. **preVisitDirectory()**:在目录上运行,在其内容被访问之前。 2. **visitFile()**:运行目录中的每一个文件。 3. **visitFileFailed()**:调用无法访问的文件。 - 4. **postVisitDirectory()**:在访问目录中条目之后在目录上运行,包括所有的子目录。 + 4. **postVisitDirectory()**:在目录上运行,在其内容(包括所有的子目录)被访问之后。 -为了简化,**java.nio.file.SimpleFileVisitor** 提供了所有方法的默认实现。这样,在我们的匿名内部类中,我们只需要重写非标准行为的方法:**visitFile()** 和 **postVisitDirectory()** 实现删除文件和删除目录。两者都应该返回标志位决定是否继续访问(这样就可以继续访问,直到找到所需要的)。 -作为探索目录操作的一部分,现在我们可以有条件地删除已存在的目录。在以下例子中,**makeVariant()** 接受基本目录测试,并通过旋转部件列表生成不同的子目录路径。这些旋转与路径分隔符 **sep** 使用 **String.join()** 贴在一起,然后返回一个 **Path** 对象。 +为了简化,**java.nio.file.SimpleFileVisitor** 提供了所有方法的默认实现。这样,在我们的匿名内部类中,我们只需要重写非标准行为的方法:**visitFile()** 和 **postVisitDirectory()** 实现删除文件和删除目录。两者都返回“继续访问”标志(这样就可以一直访问,直至找到所需为止)。 +作为我们探索创建和填充目录的一部分,现在我们可以有条件地删除已存在的目录。在以下例子中,**makeVariant()** 接受基本目录测试,并通过旋转组件名列表生成不同的子目录路径。这些旋转(组件名)和路径分隔符 **sep** 用 **String.join()** 粘贴在一起,然后返回一个 **Path** 对象。 ```java // files/Directories.java @@ -461,18 +463,24 @@ test\foo\bar\baz\bag\File.txt test\Hello.txt */ ``` -首先,**refreshTestDir()** 用于检测 **test** 目录是否已经存在。若存在,则使用我们新工具类 **rmdir()** 删除其整个目录。检查是否 **exists** 是多余的,但我想说明一点,因为如果你对于已经存在的目录调用 **createDirectory()** 将会抛出异常。**createFile()** 使用参数 **Path** 创建一个空文件; **resolve()** 将文件名添加到 **test Path** 的末尾。 +首先,**refreshTestDir()** 用于检测 **test** 目录是否已经存在。若存在,则使用我们新工具类 **rmdir()** 删除其整个目录。在这里检查是否 **exists()** 做了2次,是重复的,这是因为我是想阐明,如果你对于已存在的目录调用 **createDirectory()** 将会抛出异常。**createFile()** 以 **Path** 为参数创建了一个空文件; **resolve()** 将文件名添加到该目录路径的末尾。 -我们尝试使用 **createDirectory()** 来创建多级路径,但是这样会抛出异常,因为这个方法只能创建单级路径。我已经将 **populateTestDir()** 作为一个单独的方法,因为它将在后面的例子中被重用。对于每一个变量 **variant**,我们都能使用 **createDirectories()** 创建完整的目录路径,然后使用此文件的副本以不同的目标名称填充该终端目录。然后我们使用 **createTempFile()** 生成一个临时文件。 +我们尝试使用 **createDirectory()** 来创建多级路径,但是这样会抛出异常,因为这个方法只能创建单级路径。 -在调用 **populateTestDir()** 之后,我们在 **test** 目录下面创建一个临时目录。请注意,**createTempDirectory()** 只有名称的前缀选项。与 **createTempFile()** 不同,我们再次使用它将临时文件放入新的临时目录中。你可以从输出中看到,如果未指定后缀,它将默认使用".tmp"作为后缀。 +我已经将 **populateTestDir()** 作为一个单独的方法,因为它将在后面的例子中被重用。对于每一个 **variant** ( **Path** 对象),我们都能使用 **createDirectories()** 创建完整的目录路径(目录树),然后使用此文件(Directories.java)的拷贝以另一个的名称(File.txt)填充该目录树的终端。然后我们使用 **createTempFile()** 生成一个临时文件。(这里我们给 **createTempFile()** 传的参数是两个 **null** ) -为了展示结果,我们首次使用看起来很有希望的 **newDirectoryStream()**,但事实证明这个方法只是返回 **test** 目录内容的 Stream 流,并没有更多的内容。要获取目录树的全部内容的流,请使用 **Files.walk()**。 +在调用 **populateTestDir()** 之后,下一步的操作是在 **test** 目录下面创建一个临时目录。请注意,**createTempDirectory()** 只有名称的前缀(设置)选项,这一点和 **createTempFile()** 不同。再下一步,我们使用 **createTempFile()** 将临时文件放入新的临时目录中。你可以从输出中看到,如果未指定后缀,默认使用" **.tmp** "作为后缀。 + +为了列出上述操作的结果,我们开始试用很有前途的 **newDirectoryStream()**,但事实证明这个方法只是流式处理了(stream化) **test** 这一级的目录内容,并没有再深入。要获取目录树的全部内容的流,请使用 **Files.walk()**。 ## 文件系统 -为了完整起见,我们需要一种方法查找文件系统相关的其他信息。在这里,我们使用静态的 **FileSystems** 工具类获取"默认"的文件系统,但你同样也可以在 **Path** 对象上调用 **getFileSystem()** 以获取创建该 **Path** 的文件系统。你可以获得给定 *URI* 的文件系统,还可以构建新的文件系统(对于支持它的操作系统)。 +为了完整性,我们需要一种方法来找出有关文件系统的其余信息。在这里,我们可以: +- 使用静态的 **FileSystems** 工具类获取"默认"的文件系统;`FileSystems.getDefault()` +- 对 **Path** 对象调用 **getFileSystem()** 以获取创建该 **Path** 的文件系统;`xx.getFileSystem()` +- 获得给定 *URI* 对应的文件系统;`FileSystems.getFileSystem(URI)` +- 构建新的文件系统(对于支持该操作的操作系统)。`FileSystems.newFileSystem(URI,map)` ```java // files/FileSystemDemo.java import java.nio.file.*; @@ -514,12 +522,12 @@ sun.nio.fs.WindowsFileSystemProvider@6d06d69c File Attribute Views: [owner, dos, acl, basic, user] */ ``` -一个 **FileSystem** 对象也能生成 **WatchService** 和 **PathMatcher** 对象,将会在接下来两章中详细讲解。 +一个 **FileSystem** 对象也能生成 **WatchService** 和 **PathMatcher** 对象,会在接下来两节中讲解。 ## 路径监听 -通过 **WatchService** 可以设置一个进程对目录中的更改做出响应。在这个例子中,**delTxtFiles()** 作为一个单独的任务执行,该任务将遍历整个目录并删除以 **.txt** 结尾的所有文件,**WatchService** 会对文件删除操作做出反应: +通过 **WatchService** 可以设置一个进程,对目录中的更改做出响应。在这个例子中,**delTxtFiles()** 作为一个单独的任务执行,该任务将遍历整个目录并删除以 **.txt** 结尾的所有文件,**WatchService** 会对文件删除操作做出反应: ```java // files/PathWatcher.java @@ -581,15 +589,17 @@ evt.kind(): ENTRY_DELETE */ ``` -**delTxtFiles()** 中的 **try** 代码块看起来有些多余,因为它们捕获的是同一种类型的异常,外部的 **try** 语句似乎已经足够了。然而出于某种原因,Java 要求两者都必须存在(这也可能是一个 bug)。还要注意的是在 **filter()** 中,我们必须显式地使用 **f.toString()** 转为字符串,否则我们调用 **endsWith()** 将会与整个 **Path** 对象进行比较,而不是路径名称字符串的一部分进行比较。 +**delTxtFiles()** 中的 **try** 代码块看起来有重复,因为它们捕获的是同一种类型的异常,似乎只要外部的 **try** 语句已经足够了。然而出于某种原因,Java 要求两者都在(这也可能是一个 bug)。还要注意的是在 **filter()** 中,我们必须显式地使用 **f.toString()** 转为字符串,否则 **endsWith()** 会与整个 **Path** 对象进行比较,而不是只比较路径的字符串名称这部分。 + +一旦我们从 **FileSystem** 中得到了 **WatchService** 对象,就将 **WatchService** 对象及感兴趣项的参数列表一起注册到 **test** 对象,参数列表可选 **ENTRY_CREATE**,**ENTRY_DELETE** 或 **ENTRY_MODIFY**(其中创建 *CREATE* 和删除 *DELETE* 不属于修改)。 -一旦我们从 **FileSystem** 中得到了 **WatchService** 对象,我们将其注册到 **test** 路径以及我们感兴趣的项目的变量参数列表中,可以选择 **ENTRY_CREATE**,**ENTRY_DELETE** 或 **ENTRY_MODIFY**(其中创建和删除不属于修改)。 +因为接下来对 **watcher.take()** (开始监听事件)的调用会在监听到增删改之前一直阻塞线程,所以我们希望 **deltxtfiles()** 能够并行运行以便生成我们感兴趣的事件。(删完再监听,晚了,一直监听不到;先监听再删,删操作被阻塞。因此只能多线程并发) -因为接下来对 **watcher.take()** 的调用会在发生某些事情之前停止所有操作,所以我们希望 **deltxtfiles()** 能够并行运行以便生成我们感兴趣的事件。为了实现这个目的,我通过调用 **Executors.newSingleThreadScheduledExecutor()** 产生一个 **ScheduledExecutorService** 对象,然后调用 **schedule()** 方法传递所需函数的方法引用,并且设置在运行之前应该等待的时间。 +为了这一目的,我通过调用 **Executors.newSingleThreadScheduledExecutor()** 产生一个 **ScheduledExecutorService** 对象,然后调用 **schedule()** 方法传递所需函数的方法引用,并设置在开始监听( **watcher.take()** )之后延时(250ms)发生。 此时,**watcher.take()** 将等待并阻塞在这里。当目标事件发生时,会返回一个包含 **WatchEvent** 的 **Watchkey** 对象。展示的这三种方法是能对 **WatchEvent** 执行的全部操作。 -查看输出的具体内容。即使我们正在删除以 **.txt** 结尾的文件,在 **Hello.txt** 被删除之前,**WatchService** 也不会被触发。你可能认为,如果说"监视这个目录",自然会包含整个目录和下面子目录,但实际上:只会监视给定的目录,而不是下面的所有内容。如果需要监视整个树目录,必须在整个树的每个子目录上放置一个 **Watchservice**。 +查看输出的具体内容。即使我们正在删除以 **.txt** 结尾的文件,在 **Hello.txt** 被删除之前,**WatchService** 也不会被触发。你可能认为,如果说"监视这个目录",自然会包含整个目录和下面子目录,但实际上:只会监视给定的目录,不包括下面的。如果需要监视整个树目录,必须在整个树的每个子目录上放置一个 **Watchservice**。 ```java // files/TreeWatcher.java @@ -644,12 +654,12 @@ evt.kind(): ENTRY_DELETE */ ``` -在 **watchDir()** 方法中给 **WatchSevice** 提供参数 **ENTRY_DELETE**,并启动一个独立的线程来监视该**Watchservice**。这里我们没有使用 **schedule()** 进行启动,而是使用 **submit()** 启动线程。我们遍历整个目录树,并将 **watchDir()** 应用于每个子目录。现在,当我们运行 **deltxtfiles()** 时,其中一个 **Watchservice** 会检测到每一次文件删除。 +在 **watchDir()** 方法中给 **WatchSevice** 提供参数 **ENTRY_DELETE**,并启动一个独立的线程来监视该**Watchservice**。这里我们没有使用 **schedule()** 进行启动,而是使用 **submit()** 启动线程。我们遍历整个目录树,并将 **watchDir()** 应用于每个子目录。现在,当我们运行 **deltxtfiles()** 时,其中的一个 **Watchservice** 会检测到最开始的一次删除。 ## 文件查找 -到目前为止,为了找到文件,我们一直使用相当粗糙的方法,在 `path` 上调用 `toString()`,然后使用 `string` 操作查看结果。事实证明,`java.nio.file` 有更好的解决方案:通过在 `FileSystem` 对象上调用 `getPathMatcher()` 获得一个 `PathMatcher`,然后传入您感兴趣的模式。模式有两个选项:`glob` 和 `regex`。`glob` 比较简单,实际上功能非常强大,因此您可以使用 `glob` 解决许多问题。如果您的问题更复杂,可以使用 `regex`,这将在接下来的 `Strings` 一章中解释。 +到目前为止,为了找到文件,我们一直使用相当粗糙的方法,在 `path` 上调用 `toString()`,然后使用 `string` 相关操作查看结果。事实证明,`java.nio.file` 有更好的解决方案:`PathMatcher`。其通过在 `FileSystem` 对象上调用 `getPathMatcher()` 获得,然后传入您感兴趣的模式。模式有两个选项:`glob` 和 `regex`。`glob` 看似简单实际上功能十分强大,因此您可以使用 `glob` 解决许多问题。如果您的问题更复杂,可以使用 `regex`(这将在接下来的 `Strings` 一章中解释)。 在这里,我们使用 `glob` 查找以 `.tmp` 或 `.txt` 结尾的所有 `Path`: @@ -712,19 +722,19 @@ dir.tmp */ ``` -在 `matcher` 中,`glob` 表达式开头的 `**/` 表示“当前目录及所有子目录”,这在当你不仅仅要匹配当前目录下特定结尾的 `Path` 时非常有用。单 `*` 表示“任何东西”,然后是一个点,然后大括号表示一系列的可能性---我们正在寻找以 `.tmp` 或 `.txt` 结尾的东西。您可以在 `getPathMatcher()` 文档中找到更多详细信息。 +在 `matcher` 定义中,`glob` 表达式开头的 `**/` 表示“当前目录及所有子目录”,这在当你不仅仅要匹配当前目录下特定结尾的 `Path` 时非常有用。单 `*` 表示“任何东西”,然后是一个点,然后大括号表示一系列的可能性---我们正在寻找以 `.tmp` 或 `.txt` 结尾的东西。您可以在 `getPathMatcher()` 文档中找到更多详细信息。 -`matcher2` 只使用 `*.tmp`,通常不匹配任何内容,但是添加 `map()` 操作会将完整路径减少到末尾的名称。 +`matcher2` 只使用 `*.tmp`,通常不匹配任何内容,但是添加 `map()` 操作会将完整路径减少到末尾的名称 ( 因此现在有匹配内容了 )。 -注意,在这两种情况下,输出中都会出现 `dir.tmp`,即使它是一个目录而不是一个文件。要只查找文件,必须像在最后 `files.walk()` 中那样对其进行筛选。 +注意,在这两种情况下,输出中都会出现 `dir.tmp`,即使它是目录而不是文件。要只查找文件,必须像在最后 `files.walk()` 中那样对其进行筛选—— `.filter(Files::isRegularFile)`。 ## 文件读写 -此时,我们可以对路径和目录做任何事情。 现在让我们看一下操纵文件本身的内容。 +到此,我们可以对路径和目录做任何事情。 现在让我们看一下如何操纵文件本身的内容。 如果一个文件很“小”,也就是说“它运行得足够快且占用内存小”,那么 `java.nio.file.Files` 类中的实用程序将帮助你轻松读写文本和二进制文件。 -`Files.readAllLines()` 一次读取整个文件(因此,“小”文件很有必要),产生一个`List`。 对于示例文件,我们将重用`streams/Cheese.dat`: +`Files.readAllLines()` 一次读取整个文件(因此,“小”文件很有必要),产生一个`List`。 作为例子,我们又重用`streams/Cheese.dat`: ```java // files/ListOfLines.java @@ -788,13 +798,13 @@ Cheese.txt: 199 一个 `List` 被写入文件,任何 `Iterable` 对象也可以这么做。 -如果文件大小有问题怎么办? 比如说: +如果文件大小有问题怎么办? 比如: 1. 文件太大,如果你一次性读完整个文件,你可能会耗尽内存。 -2. 您只需要在文件的中途工作以获得所需的结果,因此读取整个文件会浪费时间。 +2. 您只需要通过文件进行局部操作就可以得到您想要的结果,因此读取整个文件会浪费时间。 -`Files.lines()` 方便地将文件转换为行的 `Stream`: +`Files.lines()` 方便地将文件转换以 **"行"** 为单位元素的 `Stream`: ```java // files/ReadLineStream.java @@ -813,9 +823,9 @@ public class ReadLineStream { */ ``` -这对本章中第一个示例代码做了流式处理,跳过 13 行,然后选择下一行并将其打印出来。 +这对本章开头的示例代码`PathInfo.java`做了流式处理,跳过 13 **行** ,然后选择下一行并将其打印出来。 -`Files.lines()` 对于把文件处理行的传入流时非常有用,但是如果你想在 `Stream` 中读取,处理或写入怎么办?这就需要稍微复杂的代码: +`Files.lines()` 对于处理文件(转为 **"行"** 为单位元素的输入流)非常有用,但如果你想读取、处理或写入全在一个 `Stream` 中又怎么做呢?这就需要稍微复杂的代码: ```java // files/StreamInAndOut.java @@ -845,7 +855,7 @@ public class StreamInAndOut { ## 本章小结 -虽然本章对文件和目录操作做了相当全面的介绍,但是仍然有没被介绍的类库中的功能——一定要研究 `java.nio.file` 的 Javadocs,尤其是 `java.nio.file.Files` 这个类。 +尽管本章对文件和目录操作做了相当全面的介绍,但还有类库中的功能未被介绍——一定要研究 `java.nio.file` 的 Javadocs,尤其是 `java.nio.file.Files` 这个类。 Java 7 和 8 对于处理文件和目录的类库做了大量改进。如果您刚刚开始使用 Java,那么您很幸运。在过去,它令人非常不愉快,我确信 Java 设计者以前对于文件操作不够重视才没做简化。对于初学者来说这是一件很棒的事,对于教学者来说也一样。我不明白为什么花了这么长时间来解决这个明显的问题,但不管怎么说它被解决了,我很高兴。使用文件现在很简单,甚至很有趣,这是你以前永远想不到的。 diff --git a/docs/book/18-Strings.md b/docs/book/18-Strings.md index b75429c5..83468d36 100755 --- a/docs/book/18-Strings.md +++ b/docs/book/18-Strings.md @@ -10,7 +10,7 @@ ## 字符串的不可变 -`String` 对象是不可变的。查看 JDK 文档你就会发现,`String` 类中每一个看起来会修改 `String` 值的方法,实际上都是创建了一个全新的 `String` 对象,以包含修改后的字符串内容。而最初的 `String` 对象则丝毫未动。 +`String` 对象是不可变的。查看 JDK 文档你就会发现,`String` 类中每一个看起来会修改 `String` 值的方法,实际上都是创建了一个全新的 `String` 对象,以包含修改后的字符串内容。而原来的 `String` 对象则丝毫未动。 看看下面的代码: ```java @@ -33,9 +33,9 @@ HOWDY howdy */ ``` -当把 `q` 传递给 `upcase()` 方法时,实际传递的是引用的一个拷贝。其实,每当把 String 对象作为方法的参数时,都会复制一份引用,而该引用所指向的对象其实一直待在单一的物理位置上,从未动过。 +当把对字符串"howdy"的引用—— `q` 被传进给 `upcase()` 方法时,实际上传进的是 `q` 的拷贝(新建了一个栈地址对字符串"howdy"的引用)。该引用所连接的对象(字符串"howdy")停留在单一物理位置上。引用在传递过程中会被复制。 -回到 `upcase()` 的定义,传入其中的引用有了名字 `s`,只有 `upcase()` 运行的时候,局部引用 `s` 才存在。一旦 `upcase()` 运行结束,`s` 就消失了。当然了,`upcase()` 的返回值,其实是最终结果的引用。这足以说明,`upcase()` 返回的引用已经指向了一个新的对象,而 `q` 仍然在原来的位置。 +再看 `upcase()` 的定义,传进的引用有了名字 `s`,只有 `upcase()` 运行的时候,局部引用 `s` 才存在。一旦 `upcase()` 运行结束,局部引用 `s` 就消失了。`upcase()` 返回的引用已经指向了一个新的对象(一个对原字符串的所有字符都设为大写的对象),而 `q` 仍然在原来的位置。 `String` 的这种行为正是我们想要的。例如: @@ -43,7 +43,7 @@ howdy String s = "asdf"; String x = Immutable.upcase(s); ``` -难道你真的希望 `upcase()` 方法改变其参数吗?对于一个方法而言,参数是为该方法提供信息的,而不是想让该方法改变自己的。在阅读这段代码时,读者自然会有这样的感觉。这一点很重要,正是有了这种保障,才使得代码易于编写和阅读。 +难道你真的希望 `upcase()` 方法改变它的方法参数吗?对代码的读者来说,一个方法的参数通常看起来像是提供给方法的一个信息,而不是需要被修改的东西。这是重要的保障,它使代码更易于编写和阅读。 @@ -67,9 +67,9 @@ public class Concatenation { abcmangodef47 */ ``` -可以想象一下,这段代码是这样工作的:`String` 可能有一个 `append()` 方法,它会生成一个新的 `String` 对象,以包含“abc”与 `mango` 连接后的字符串。该对象会再创建另一个新的 `String` 对象,然后与“def”相连,生成另一个新的对象,依此类推。 +可以想象一下,这段代码是这样工作的:字符串 “abc” 可能用一个 `append()` 方法,它生成一个新的字符串对象来存放 “abc” 与 `mango` 内容的联结结果。而该对象在与 “def” 相连时会再创建另一个新的字符串对象……依此类推。 -这种方式当然是可行的,但是为了生成最终的 `String` 对象,会产生一大堆需要垃圾回收的中间对象。我猜想,Java 设计者一开始就是这么做的(这也是软件设计中的一个教训:除非你用代码将系统实现,并让它运行起来,否则你无法真正了解它会有什么问题),然后他们发现其性能相当糟糕。 +这种方式当然是可行的,但是为了生成最终的 `String` 对象,产生了一大堆需要垃圾回收的中间对象。我猜 Java 设计者先试过这一做法(这也是软件设计中的一条经验:除非你用代码将系统实现,并让它运行起来,否则你无法真正了解它会有什么问题),然后他们发现其性能不可接受。 想看看以上代码到底是如何工作的吗?可以用 JDK 自带的 `javap` 工具来反编译以上代码。命令如下: ```java @@ -236,12 +236,12 @@ public class UsingStringBuilder { `string2()` 使用了 `Stream`,这样代码更加简洁美观。可以证明,`Collectors.joining()` 内部也是使用的 `StringBuilder`,这种写法不会影响性能! -`StringBuilder `是 Java SE5 引入的,在这之前用的是 `StringBuffer`。后者是线程安全的(参见[并发编程](./24-Concurrent-Programming.md)),因此开销也会大些。使用 `StringBuilder` 进行字符串操作更快一点。 +`StringBuilder`是 Java 5 引入的,在此之前,Java使用了`StringBuffer`,它是线程安全的(参见[并发编程](./24-Concurrent-Programming.md)),因此开销也会大些。使用 `StringBuilder` 进行字符串操作更快一点。 ## 意外递归 -Java 中的每个类从根本上都是继承自 `Object`,标准集合类也是如此,它们都有 `toString()` 方法,并且覆盖了该方法,使得它生成的 `String` 结果能够表达集合自身,以及集合包含的对象。例如 `ArrayList.toString()`,它会遍历 `ArrayList` 中包含的所有对象,调用每个元素上的 `toString()` 方法: +Java 中的标准集合类和其它类一样,最终的父类都是 `Object`,因此它们都有 `toString()` 方法。该方法被重写使得生成的 `String` 结果能够表达集合自身,以及集合包含的对象。例如 `ArrayList.toString()`,它会遍历 `ArrayList` 中包含的所有对象,调用每个元素上的 `toString()` 方法: ```java // strings/ArrayListDisplay.java import java.util.*; @@ -282,13 +282,13 @@ public class InfiniteRecursion { } } ``` -当你创建了 `InfiniteRecursion` 对象,并将其打印出来的时候,你会得到一串很长的异常信息。如果你将该 `InfiniteRecursion` 对象存入一个 `ArrayList` 中,然后打印该 `ArrayList`,同样也会抛出异常。其实,当运行到如下代码时: +当你创建一个 `InfiniteRecursion` 对象,并将其打印的时候,你会得到一长串的异常信息(`StackOverflowError`)。如果你将该 `InfiniteRecursion` 对象存入一个 `ArrayList` 中,然后打印该 `ArrayList`(如上),也是如此。其实,当运行到如下代码时: ```java "InfiniteRecursion address: " + this ``` -这里发生了自动类型转换,由 `InfiniteRecursion` 类型转换为 `String` 类型。因为编译器发现一个 `String` 对象后面跟着一个 “+”,而 “+” 后面的对象不是 `String`,于是编译器试着将 `this` 转换成一个 `String`。它怎么转换呢?正是通过调用 `this` 上的 `toString()` 方法,于是就发生了递归调用。 +这里发生了自动类型转换( *automatic type conversion* ),由 `InfiniteRecursion` 类型转换为 `String` 类型。因为编译器发现一个 `String` 对象后面跟着一个 “+”,而 “+” 后面的对象不是 `String`,于是编译器试着将 `this` 转换成一个 `String`。它怎么转换呢?正是通过调用 `this` 上的 `toString()` 方法,于是就发生了递归调用。 -如果你真的想要打印对象的内存地址,应该调用 `Object.toString()` 方法,这才是负责此任务的方法。所以,不要使用 `this`,而是应该调用 `super.toString()` 方法。 +如果你真的想要打印对象的内存地址,应该调用 `Object`类的 `toString()` 方法,这才是负责此任务的方法。所以,不要使用 `this`,而是应该调用 `super.toString()` 方法。 @@ -338,7 +338,7 @@ C 语言的 `printf()` 并不像 Java 那样连接字符串,它使用一个简 ```c System.out.printf("Row 1: [%d %f]%n", x, y); ``` -这一行代码在运行的时候,首先将 `x` 的值插入到 `%d_` 的位置,然后将 `y` 的值插入到 `%f` 的位置。这些占位符叫做*格式修饰符*,它们不仅指明了插入数据的位置,同时还指明了将会插入什么类型的变量,以及如何格式化。在这个例子中 `%d` 表示 `x` 是一个整数,`%f` 表示 `y` 是一个浮点数(`float` 或者 `double`)。 +这一行代码在运行的时候,首先将 `x` 的值插入到 `%d_` 的位置,然后将 `y` 的值插入到 `%f` 的位置。这些占位符叫做*格式修饰符*( format specifiers ),它们不仅指明了插入数据的位置,同时还指明了将会插入什么类型的变量,以及如何格式化。在这个例子中 `%d` 表示 `x` 是一个整数,`%f` 表示 `y` 是一个浮点数(`float` 或者 `double`)。 ### `System.out.format()` Java SE5 引入了 `format()` 方法,可用于 `PrintStream` 或者 `PrintWriter` 对象(你可以在 [附录:流式 I/O](./Appendix-IO-Streams.md) 了解更多内容),其中也包括 `System.out` 对象。`format()` 方法模仿了 C 语言的 `printf()`。如果你比较怀旧的话,也可以使用 `printf()`。以下是一个简单的示例: ```java @@ -409,12 +409,20 @@ Terry The Turtle is at (3,3) ``` 格式化修饰符 `%s` 表明这里需要 `String` 参数。 -所有的 `tommy` 都将输出到 `System.out`,而所有的 `terry` 则都输出到 `System.out` 的一个别名中。`Formatter` 的重载构造器支持输出到多个路径,不过最常用的还是 `PrintStream()`(如上例)、`OutputStream` 和 `File`。你可以在 [附录:流式 I/O](././Appendix-IO-Streams.md) 中了解更多信息。 +所有的 `tommy` 都将输出到 `System.out`,而所有的 `terry` 则都输出到 `System.out` 的一个别名(outAlias)中。`Formatter` 的重载构造器支持输出到多个路径,不过最常用的还是 `PrintStream()`(如上例)、`OutputStream` 和 `File`。你可以在 [附录:流式 I/O](././Appendix-IO-Streams.md) 中了解更多信息。 ### 格式化修饰符 在插入数据时,如果想要优化空格与对齐,你需要更精细复杂的格式修饰符。以下是其通用语法: ```java %[argument_index$][flags][width][.precision]conversion ``` + +- argument_index$:指定对应的内容参数位置,默认按照顺序依次对应。 +- flags:格式控制。 +- width:区域宽度。 +- .precision:对于浮点型数据,表示显示的小数位数;对于字符串数据,表示显示的字符数量。 +- conversion:类型转换字符。 + + 最常见的应用是控制一个字段的最小长度,这可以通过指定 *width* 来实现。`Formatter `对象通过在必要时添加空格,来确保一个字段至少达到设定长度。默认情况下,数据是右对齐的,不过可以通过使用 `-` 标志来改变对齐方向。 与 *width* 相对的是 *precision*,用于指定最大长度。*width* 可以应用于各种类型的数据转换,并且其行为方式都一样。*precision* 则不然,当应用于不同类型的数据转换时,*precision* 的意义也不同。在将 *precision* 应用于 `String` 时,它表示打印 `string` 时输出字符的最大数量。而在将 *precision* 应用于浮点数时,它表示小数部分要显示出来的位数(默认是 6 位小数),如果小数位数过多则舍入,太少则在尾部补零。由于整数没有小数部分,所以 *precision* 无法应用于整数,如果你对整数应用 *precision*,则会触发异常。 @@ -787,7 +795,7 @@ the mightiest banana in the forest...with... a banana! | 表达式 | 含义 | | :---- | :---- | -| `B` | 指定字符`B` | +| `B` | 具体字符`B` | | `\xhh` | 十六进制值为`0xhh`的字符 | | `\uhhhh` | 十六进制表现为`0xhhhh`的Unicode字符 | | `\t` | 制表符Tab | @@ -804,8 +812,8 @@ the mightiest banana in the forest...with... a banana! | `[abc]` |包含`a`、`b`或`c`的任何字符(和`a|b|c`作用相同)| | `[^abc]` | 除`a`、`b`和`c`之外的任何字符(否定) | | `[a-zA-Z]` | 从`a`到`z`或从`A`到`Z`的任何字符(范围) | -| `[abc[hij]]` | `a`、`b`、`c`、`h`、`i`、`j`中的任意字符(与`a|b|c|h|i|j`作用相同)(合并) | -| `[a-z&&[hij]]` | 任意`h`、`i`或`j`(交) | +| `[abc[hij]]` | `a`、`b`、`c`、`h`、`i`、`j`中的任意字符(与`a|b|c|h|i|j`作用相同)(并集) | +| `[a-z&&[hij]]` | `h`、`i`或`j`(交集) | | `\s` | 空白符(空格、tab、换行、换页、回车) | | `\S` | 非空白符(`[^\s]`) | | `\d` | 数字(`[0-9]`) | @@ -817,7 +825,7 @@ the mightiest banana in the forest...with... a banana! | 逻辑操作符 | 含义 | | :----: | :---- | -| `XY` | `Y`跟在`X`后面 | +| `XY` | `Y`跟在`X`后面 (X之后是Y)| | `X|Y` | `X`或`Y` | | `(X)` | 捕获组(capturing group)。可以在表达式中用`\i`引用第i个捕获组 | @@ -854,25 +862,25 @@ true ``` 我们的目的并不是编写最难理解的正则表达式,而是尽量编写能够完成任务的、最简单以及最必要的正则表达式。一旦真正开始使用正则表达式了,你就会发现,在编写新的表达式之前,你通常会参考代码中已经用到的正则表达式。 ### 量词 -量词描述了一个模式捕获输入文本的方式: +**量词 quantifier** 描述了一个模式(pattern)捕获输入文本的方式: + **贪婪型**: -量词总是贪婪的,除非有其他的选项被设置。贪婪表达式会为所有可能的模式发现尽可能多的匹配。导致此问题的一个典型理由就是假定我们的模式仅能匹配第一个可能的字符组,如果它是贪婪的,那么它就会继续往下匹配。 +量词总是贪婪的,除非有其他的选项被设置。贪婪表达式会为所有可能的模式发现尽可能多的匹配。问题的一个典型原因:假定在我们的模式只匹配了第一个字符组的时候,如果它是贪婪的,那么它就会继续直到最多匹配为止。 + **勉强型**: 用问号来指定,这个量词匹配满足模式所需的最少字符数。因此也被称作懒惰的、最少匹配的、非贪婪的或不贪婪的。 + **占有型**: -目前,这种类型的量词只有在 Java 语言中才可用(在其他语言中不可用),并且也更高级,因此我们大概不会立刻用到它。当正则表达式被应用于 `String` 时,它会产生相当多的状态,以便在匹配失败时可以回溯。而“占有的”量词并不保存这些中间状态,因此它们可以防止回溯。它们常常用于防止正则表达式失控,因此可以使正则表达式执行起来更高效。 +正则表达式被应用于 `String` 时,会产生相当多的状态,以便在匹配失败时可以回溯。而 **占有型** 量词并不保存这些中间状态,因此它们可以防止回溯。它们常常用于防止正则表达式失控,因此可以使正则表达式执行起来更高效。目前,这种类型的量词只有在 Java 语言中才可用(在其他语言中不可用),并且也更高级,因此我们大概不会立刻用到它。 | 贪婪型 | 勉强型 | 占有型 | 如何匹配 | | ---- | ---- | ---- | ---- | -| `X?` | `X??` | `X?+` | 一个或零个`X` | -| `X*` | `X*?` | `X*+` | 零个或多个`X` | -| `X+` | `X+?` | `X++` | 一个或多个`X` | +| `X?` | `X??` | `X?+` | 零个或一个`X` 相当于{0,1} | +| `X*` | `X*?` | `X*+` | 零个或多个`X` 相当于{0,} | +| `X+` | `X+?` | `X++` | 一个或多个`X` 相当于{1,0} | | `X{n}` | `X{n}?` | `X{n}+` | 恰好`n`次`X` | | `X{n,}` | `X{n,}?` | `X{n,}+` | 至少`n`次`X` | -| `X{n,m}` | `X{n,m}?` | `X{n,m}+` | `X`至少`n`次,但不超过`m`次 | +| `X{n,m}` | `X{n,m}?` | `X{n,m}+` | `X`至少`n`次,但不超过`m`次 (左闭右开)| 应该非常清楚地意识到,表达式 `X` 通常必须要用圆括号括起来,以便它能够按照我们期望的效果去执行。例如: ```java @@ -882,7 +890,7 @@ abc+ ```java (abc)+ ``` -你会发现,在使用正则表达式时很容易混淆,因为它是一种在 Java 之上的新语言。 +你会发现,在使用正则表达式时很容易混淆,因为它是一种在 Java 之上的正交语言。 ### `CharSequence` 接口 `CharSequence` 从 `CharBuffer`、`String`、`StringBuffer`、`StringBuilder` 类中抽象出了字符序列的一般化定义: ```java @@ -897,7 +905,11 @@ interface CharSequence { ### `Pattern` 和 `Matcher` 通常,比起功能有限的 `String` 类,我们更愿意构造功能强大的正则表达式对象。只需导入 `java.util.regex`包,然后用 `static Pattern.compile()` 方法来编译你的正则表达式即可。它会根据你的 `String` 类型的正则表达式生成一个 `Pattern` 对象。接下来,把你想要检索的字符串传入 `Pattern` 对象的 `matcher()` 方法。`matcher()` 方法会生成一个 `Matcher` 对象,它有很多功能可用(可以参考 `java.util.regext.Matcher` 的 JDK 文档)。例如,它的 `replaceAll()` 方法能将所有匹配的部分都替换成你传入的参数。 -作为第一个示例,下面的类可以用来测试正则表达式,看看它们能否匹配一个输入字符串。第一个控制台参数是将要用来搜索匹配的输入字符串,后面的一个或多个参数都是正则表达式,它们将被用来在输入的第一个字符串中查找匹配。在Unix/Linux上,命令行中的正则表达式必须用引号括起来。这个程序在测试正则表达式时很有用,特别是当你想验证它们是否具备你所期待的匹配功能的时候。[^3] +作为第一个示例,下面的类可以用来测试正则表达式,看看它们能否匹配一个输入字符串。 +- 第一个控制台参数(`args[0]`)是将要用来搜索匹配的输入字符串, +- 后面的一个或多个参数都是正则表达式,它们将被用来在输入的第一个字符串中查找匹配。 + +在Unix/Linux上,命令行中的正则表达式必须用引号括起来。这个程序在测试正则表达式时很有用,特别是当你想验证它们是否具备你所期待的匹配功能的时候。[^3] ```java // strings/TestRegularExpression.java // Simple regular expression demonstration @@ -952,12 +964,13 @@ static boolean matches(String regex, CharSequence input) 通过调用 `Pattern.matcher()` 方法,并传入一个字符串参数,我们得到了一个 `Matcher` 对象。使用 `Matcher` 上的方法,我们将能够判断各种不同类型的匹配是否成功: ```java -boolean matches() -boolean lookingAt() -boolean find() -boolean find(int start) +boolean matches() //(整个匹配,只有整个字符序列完全匹配成功,才返回True,否则返回False。但如果部分匹配成功,Matcher下次搜的起始位置为这次匹配的末端位置。) +boolean lookingAt() //(部分匹配,总是从第一个字符进行搜,匹配成功了不再继续匹配,匹配失败了,也不继续匹配。) +boolean find() //(部分匹配,从当前位置开始搜,找到一个匹配的子串,Matcher下次搜的起始位置为这次匹配的末端位置。) +boolean find(int start) //(部分匹配,从入参位置开始搜,找到一个匹配的子串,Matcher下次搜的起始位置为这次匹配的末端位置。) ``` -其中的 `matches()` 方法用来判断整个输入字符串是否匹配正则表达式模式,而 `lookingAt()` 则用来判断该字符串(不必是整个字符串)的起始部分是否能够匹配模式。 +其中的 `matches()` 方法用来判断整个输入字符串是否匹配正则表达式模式。 +而 `lookingAt()` 则用来判断该字符串(不必是整个字符串)的起始部分是否能够匹配模式。 ### `find()` `Matcher.find()` 方法可用来在 `CharSequence` 中查找多个匹配。例如: @@ -982,12 +995,10 @@ public class Finding { } /* Output: Evening is full of the linnet s wings -Evening vening ening ning ing ng g is is s full full -ull ll l of of f the the he e linnet linnet innet nnet -net et t s s wings wings ings ngs gs s +Evening vening ening ning ing ng g is is s full full ull ll l of of f the the he e linnet linnet innet nnet net et t s s wings wings ings ngs gs s */ ``` -模式 `\\w+` 将字符串划分为词。`find()` 方法像迭代器那样向前遍历输入字符串。而第二个重载的 `find()` 接收一个整型参数,该整数表示字符串中字符的位置,并以其作为搜索的起点。从结果可以看出,后一个版本的 `find()` 方法能够根据其参数的值,不断重新设定搜索的起始位置。 +模式 `\\w+` 将输入分组为词语。`find()` 方法像迭代器那样向前遍历这个输入的字符串。而第二个重载的 `find()` 接收一个整型参数,该整数表示字符串中字符的位置,并以其作为搜索的起点。从结果可以看出,后一个版本的 `find()` 方法能够根据其参数的值,不断重新设定搜索的起始位置。 ### 组(Groups) 组是用括号划分的正则表达式,可以根据组的编号来引用某个组。组号为 0 表示整个表达式,组号 1 表示被第一对括号括起来的组,以此类推。因此,下面这个表达式, ```java @@ -1030,17 +1041,14 @@ public class Groups { } } /* Output: -[the slithy toves][the][slithy toves][slithy][toves] -[in the wabe.][in][the wabe.][the][wabe.] -[were the borogoves,][were][the -borogoves,][the][borogoves,] -[mome raths outgrabe.][mome][raths -outgrabe.][raths][outgrabe.] -[Jabberwock, my son,][Jabberwock,][my son,][my][son,] -[claws that catch.][claws][that catch.][that][catch.] -[bird, and shun][bird,][and shun][and][shun] -[The frumious Bandersnatch.][The][frumious -Bandersnatch.][frumious][Bandersnatch.] +[the slithy toves][the][slithy toves][slithy][toves] +[in the wabe.][in][the wabe.][the][wabe.] +[were the borogoves,][were][the borogoves,][the][borogoves,] +[mome raths outgrabe.][mome][raths outgrabe.][raths][outgrabe.] +[Jabberwock, my son,][Jabberwock,][my son,][my][son,] +[claws that catch.][claws][that catch.][that][catch.] +[bird, and shun][bird,][and shun][and][shun] +[The frumious Bandersnatch.][The][frumious Bandersnatch.][frumious][Bandersnatch.] */ ``` 这首诗来自于 Lewis Carroll 所写的 *Through the Looking Glass* 中的 “Jabberwocky”。可以看到这个正则表达式模式有许多圆括号分组,由任意数目的非空白符(`\\S+`)及随后的任意数目的空白符(`\\s+`)所组成。目的是捕获每行的最后3个词,每行最后以 `\$` 结束。不过,在正常情况下是将 `\$` 与整个输入序列的末端相匹配。所以我们一定要显式地告知正则表达式注意输入序列中的换行符。这可以由序列开头的模式标记 `(?m)` 来完成(模式标记马上就会介绍)。 @@ -1139,13 +1147,13 @@ Pattern Pattern.compile(String regex, int flag) | 编译标记 | 效果 | | ---- |---- | -| `Pattern.CANON_EQ` | 当且仅当两个字符的完全规范分解相匹配时,才认为它们是匹配的。例如,如果我们指定这个标记,表达式`\u003F`就会匹配字符串`?`。默认情况下,匹配不考虑规范的等价性 | +| `Pattern.CANON_EQ` | 开启canonical equivalence。当且仅当两个字符的正规分解(canonical decomposition)都完全相同时,才认为它们是匹配的。例如,如果我们用了这个标记之后,表达式`\u003F`就会匹配字符串`?`。默认情况下,匹配不考虑规范的等价性(canonical equivalence) | | `Pattern.CASE_INSENSITIVE(?i)` | 默认情况下,大小写不敏感的匹配假定只有US-ASCII字符集中的字符才能进行。这个标记允许模式匹配不考虑大小写(大写或小写)。通过指定`UNICODE_CASE`标记及结合此标记。基于Unicode的大小写不敏感的匹配就可以开启了 | -| `Pattern.COMMENTS(?x)` | 在这种模式下,空格符将被忽略掉,并且以`#`开始直到行末的注释也会被忽略掉。通过嵌入的标记表达式也可以开启Unix的行模式 | +| `Pattern.COMMENTS(?x)` | 正则表达式中允许出现空白符(whitespace)和注解(comments),在这种模式下,空格符将被忽略掉,并且以`#`开始直到行末的注释也会被忽略掉。通过嵌入的标记表达式也可以开启Unix的行模式 | | `Pattern.DOTALL(?s)` | 在dotall模式下,表达式`.`匹配所有字符,包括行终止符。默认情况下,`.`不会匹配行终止符 | -| `Pattern.MULTILINE(?m)` | 在多行模式下,表达式`^`和`$`分别匹配一行的开始和结束。`^`还匹配输入字符串的开始,而`$`还匹配输入字符串的结尾。默认情况下,这些表达式仅匹配输入的完整字符串的开始和结束 | +| `Pattern.MULTILINE(?m)` | 启用多行匹配模式。在多行匹配模式下,模式中的`^`和`$`将逐次匹配每一行的行首和行尾。在默认情况下(即未启用多行匹配模式),`^`和`$`将匹配整个字符串的首部和尾部。等价于修饰符(?m)。 | | `Pattern.UNICODE_CASE(?u)` | 当指定这个标记,并且开启`CASE_INSENSITIVE`时,大小写不敏感的匹配将按照与Unicode标准相一致的方式进行。默认情况下,大小写不敏感的匹配假定只能在US-ASCII字符集中的字符才能进行 | -| `Pattern.UNIX_LINES(?d)` | 在这种模式下,在`.`、`^`和`$`的行为中,只识别行终止符`\n` | +| `Pattern.UNIX_LINES(?d)` | 启用Unix换行模式。在这种模式下,在`.`、`^`和`$`的行为中,只识别一种行终止符——`\n` | 在这些标记中,`Pattern.CASE_INSENSITIVE`、`Pattern.MULTILINE` 以及 `Pattern.COMMENTS`(对声明或文档有用)特别有用。请注意,你可以直接在正则表达式中使用其中的大多数标记,只需要将上表中括号括起来的字符插入到正则表达式中,你希望它起作用的位置即可。 @@ -1202,7 +1210,7 @@ public class SplitDemo { [This, unusual use, of exclamation!!points] */ ``` -第二种形式的 `split()` 方法可以限制将输入分割成字符串的数量。 +第二种形式的 `split()` 方法可以限制发生的分割次数。 ### 替换操作 正则表达式在进行文本替换时特别方便,它提供了许多方法: + `replaceFirst(String replacement)` 以参数字符串 `replacement` 替换掉第一个匹配成功的部分。 @@ -1269,7 +1277,7 @@ ExtrActEd blOck. `mInput` 匹配 `/*!` 和 `!*/` 之间的所有文字(注意分组的括号)。接下来,将存在两个或两个以上空格的地方,缩减为一个空格,并且删除每行开头部分的所有空格(为了使每一行都达到这个效果,而不仅仅是删除文本开头部分的空格,这里特意开启了多行模式)。这两个替换操作所使用的的 `replaceAll()` 是 `String` 对象自带的方法,在这里,使用此方法更方便。注意,因为这两个替换操作都只使用了一次 `replaceAll()`,所以,与其编译为 `Pattern`,不如直接使用 `String` 的 `replaceAll()` 方法,而且开销也更小些。 -`replaceFirst()` 只对找到的第一个匹配进行替换。此外,`replaceFirst()` 和 `replaceAll()` 方法用来替换的只是普通字符串,所以,如果想对这些替换字符串进行某些特殊处理,这两个方法时无法胜任的。如果你想要那么做,就应该使用 `appendReplacement()` 方法。该方法允许你在执行替换的过程中,操作用来替换的字符串。在这个例子中,先构造了 `sbuf` 用来保存最终结果,然后用 `group()` 选择一个组,并对其进行处理,将正则表达式找到的元音字母替换成大些字母。一般情况下,你应该遍历执行所有的替换操作,然后再调用 `appendTail()` 方法,但是,如果你想模拟 `replaceFirst()`(或替换n次)的行为,那就只需要执行一次替换,然后调用 `appendTail()` 方法,将剩余未处理的部分存入 `sbuf` 即可。 +`replaceFirst()` 只对找到的第一个匹配进行替换。此外,`replaceFirst()` 和 `replaceAll()` 方法用来替换的只是普通字符串,所以,如果想对这些替换字符串进行某些特殊处理,这两个方法时无法胜任的。如果你想要那么做,就应该使用 `appendReplacement()` 方法。该方法允许你在执行替换的过程中,操作用来替换的字符串。在这个例子中,先构造了 `sbuf` 用来保存最终结果,然后用 `group()` 选择一个组,并对其进行处理,将正则表达式找到的元音字母替换成大些字母。一般情况下,你应该遍历执行所有的替换操作,然后再调用 `appendTail()` 方法,但是,如果你想模拟 `replaceFirst()`(或替换n次)的行为,那就只需要执行一次替换,然后调用 `appendTail()` 方法将其余部分存入 `sbuf` 即可。 同时,`appendReplacement()` 方法还允许你通过 `\$g` 直接找到匹配的某个组,这里的 `g` 就是组号。然而,它只能应付一些简单的处理,无法实现类似前面这个例子中的功能。 ### `reset()` @@ -1297,7 +1305,10 @@ fix rig rag ``` 使用不带参数的 `reset()` 方法,可以将 `Matcher` 对象重新设置到当前字符序列的起始位置。 ### 正则表达式与 Java I/O -到目前为止,我们看到的例子都是将正则表达式用于静态的字符串。下面的例子将向你演示,如何应用正则表达式在一个文件中进行搜索匹配操作。`JGrep.java` 的灵感源自于 Unix 上的 *grep*。它有两个参数:文件名以及要匹配的正则表达式。输出的是每行有匹配的部分以及匹配部分在行中的位置。 +到目前为止,我们看到的例子都是将正则表达式用于静态的字符串。下面的例子将向你演示,如何应用正则表达式在一个文件中进行搜索匹配操作。`JGrep.java` 的灵感源自于 Unix 上的 *grep*。它有两个参数: + +`args[0]`文件名;`args[1]`要匹配的正则表达式。 输出的是每行有匹配的部分以及匹配部分在行中的位置。 + ```java // strings/JGrep.java // A very simple version of the "grep" program @@ -1424,9 +1435,9 @@ My favorite double is 0.809015. ``` `Scanner` 的构造器可以接收任意类型的输入对象,包括 `File`、`InputStream`、`String` 或者像此例中的`Readable` 实现类。`Readable` 是 Java SE5 中新加入的一个接口,表示“具有 `read()` 方法的某种东西”。上一个例子中的 `BufferedReader` 也归于这一类。 -有了 `Scanner`,所有的输入、分词、以及解析的操作都隐藏在不同类型的 `next` 方法中。普通的 `next()` 方法返回下一个 `String`。所有的基本类型(除 `char` 之外)都有对应的 `next` 方法,包括 `BigDecimal` 和 `BigInteger`。所有的 next 方法,只有在找到一个完整的分词之后才会返回。`Scanner` 还有相应的 `hasNext` 方法,用以判断下一个输入分词是否是所需的类型,如果是则返回 `true`。 +有了 `Scanner`,所有的输入、分词、以及解析的操作都隐藏在不同类型的 `next` 方法中。普通的 `next()` 方法返回下一个 `String`。所有的基本类型(除 `char` 之外)以及 `BigDecimal` 和 `BigInteger`都有对应的 `next` 方法。所有的 next 方法,只有在找到一个完整的分词之后才会返回。`Scanner` 还有相应的 `hasNext` 方法,用以判断下一个输入分词是否是所需的类型,如果是则返回 `true`。 -在 `BetterRead.java` 中没有用 `try` 区块捕获`IOException`。因为,`Scanner` 有一个假设,在输入结束时会抛出 `IOException`,所以 `Scanner` 会把 `IOException` 吞掉。不过,通过 `ioException()` 方法,你可以找到最近发生的异常,因此,你可以在必要时检查它。 +在 `BetterRead.java` 中没有用 `try` 区块捕获`IOException`。因为,`Scanner` 有一个假设,抛出 `IOException` 就表示输入结束,所以 `Scanner` 会把 `IOException` 吞掉。不过,通过 `ioException()` 方法,你可以找到最近发生的异常,因此,你可以在必要时检查它。 ### `Scanner` 分隔符 默认情况下,`Scanner` 根据空白字符对输入进行分词,但是你可以用正则表达式指定自己所需的分隔符: ```java @@ -1534,10 +1545,10 @@ But I'm not dead yet! I feel happy! [^3]: 网上还有很多实用并且成熟的正则表达式工具。 -[^4]: input来自于[Galaxy Quest](https://en.wikipedia.org/wiki/Galaxy_Quest)中Taggart司令的一篇演讲。 +[^4]: input来自于电影《惊爆银河系[Galaxy Quest](https://en.wikipedia.org/wiki/Galaxy_Quest)》中Taggart指挥官的一篇演讲。 -[^5]: 我不知道他们是如何想出这个方法名的,或者它到底指的什么。这只是代码审查很重要的原因之一。 +[^5]: 我不知道他们是如何想出这个方法名的,或者它到底指的什么。这就是代码审查很重要的原因之一。 diff --git a/docs/book/19-Type-Information.md b/docs/book/19-Type-Information.md index 6c51d949..0b5138cf 100755 --- a/docs/book/19-Type-Information.md +++ b/docs/book/19-Type-Information.md @@ -5,7 +5,7 @@ > RTTI(RunTime Type Information,运行时类型信息)能够在程序运行时发现和使用类型信息 -RTTI 把我们从只能在编译期进行面向类型操作的禁锢中解脱了出来,并且让我们可以使用某些非常强大的程序。对 RTTI 的需要,揭示了面向对象设计中许多有趣(并且复杂)的特性,同时也带来了关于如何组织程序的基本问题。 +RTTI (RunTime Type Information)把我们从只能在编译期进行面向类型操作的禁锢中解脱了出来,并且让我们可以使用某些非常强大的程序。对 RTTI 的需要,揭示了面向对象设计中许多有趣(并且复杂)的特性,同时也带来了关于如何组织程序的基本问题。 本章将讨论 Java 是如何在运行时识别对象和类信息的。主要有两种方式: @@ -20,7 +20,7 @@ RTTI 把我们从只能在编译期进行面向类型操作的禁锢中解脱了 ![多态例子Shape的类层次结构图](../images/image-20190409114913825-4781754.png) -这是一个典型的类层次结构图,基类位于顶部,派生类向下扩展。面向对象编程的一个基本目的是:让代码只操纵对基类(这里即 `Shape` )的引用。这样,如果你想添加一个新类(比如从 `Shape` 派生出 `Rhomboid`)来扩展程序,就不会影响原来的代码。在这个例子中,`Shape` 接口中动态绑定了 `draw()` 方法,这样做的目的就是让客户端程序员可以使用泛化的 `Shape` 引用来调用 `draw()`。`draw()` 方法在所有派生类里都会被覆盖,而且由于它是动态绑定的,所以即使通过 `Shape` 引用来调用它,也能产生恰当的行为,这就是多态。 +这是一个典型的类层次结构图,基类位于顶部,派生类向下扩展。面向对象编程的一个基本目的是:让代码只操纵对基类(这里即 `Shape` )的引用。这样,如果你想添加一个新类(比如从 `Shape` 派生出 `Rhomboid`)来扩展程序,就不会影响原来的代码。在这个例子中,`Shape` 接口中动态绑定了 `draw()` 方法,这样做的目的就是让客户端程序员可以使用泛化的 `Shape` 引用来调用 `draw()`。`draw()` 方法在所有派生类里都会被重写,而且由于它是动态绑定的,所以即使通过 `Shape` 引用来调用它,也能产生恰当的行为,这就是多态。 因此,我们通常会创建一个具体的对象(`Circle`、`Square` 或者 `Triangle`),把它向上转型成 `Shape` (忽略对象的具体类型),并且在后面的程序中使用 `Shape` 引用来调用在具体对象中被重载的方法(如 `draw()`)。 @@ -68,7 +68,7 @@ Square.draw() Triangle.draw() ``` -基类中包含 `draw()` 方法,它通过传递 `this` 参数传递给 `System.out.println()`,间接地使用 `toString()` 打印类标识符(注意:这里将 `toString()` 声明为 `abstract`,以此强制继承者覆盖该方法,并防止对 `Shape` 的实例化)。如果某个对象出现在字符串表达式中(涉及"+"和字符串对象的表达式),`toString()` 方法就会被自动调用,以生成表示该对象的 `String`。每个派生类都要覆盖(从 `Object` 继承来的)`toString()` 方法,这样 `draw()` 在不同情况下就打印出不同的消息(多态)。 +基类中包含 `draw()` 方法,它通过传递 `this` 参数传递给 `System.out.println()`,间接地使用 `toString()` 打印类标识符(注意:这里将 `toString()` 声明为 `abstract`,以此强制继承者重写该方法,并防止对 `Shape` 的实例化)。如果某个对象出现在字符串表达式中(涉及"+"和字符串对象的表达式),`toString()` 方法就会被自动调用,以生成表示该对象的 `String`。每个派生类都要重写(从 `Object` 继承来的)`toString()` 方法,这样 `draw()` 在不同情况下就打印出不同的消息(多态)。 这个例子中,在把 `Shape` 对象放入 `Stream` 中时就会进行向上转型(隐式),但在向上转型的时候也丢失了这些对象的具体类型。对 `stream` 而言,它们只是 `Shape` 对象。 @@ -156,7 +156,12 @@ After creating Cookie Class.forName("Gum"); ``` -所有 `Class` 对象都属于 `Class` 类,而且它跟其他普通对象一样,我们可以获取和操控它的引用(这也是类加载器的工作)。`forName()` 是 `Class` 类的一个静态方法,我们可以使用 `forName()` 根据目标类的类名(`String`)得到该类的 `Class` 对象。上面的代码忽略了 `forName()` 的返回值,因为那个调用是为了得到它产生的“副作用”。从结果可以看出,`forName()` 执行的副作用是如果 `Gum` 类没有被加载就加载它,而在加载的过程中,`Gum` 的 `static` 初始化块被执行了。 +所有 `Class` 对象都属于 `Class` 类,而且它跟其他普通对象一样,我们可以获取和操控它的引用(这也是类加载器的工作)。得到`Class` 对象的其中一种做法是使用`forName()`。`forName()` 是 `Class` 类的一个静态方法,我们可以使用 `forName()` 根据目标类的类名(`String`类型,注意拼写和大写)得到目标类的 `Class` 对象。上面的代码忽略了 `forName()` 的返回值,因为那个调用是为了得到它产生的“副作用”。从结果可以看出,`forName()` 执行的副作用是:如果 `Gum` 类没有被加载就加载它,而在加载的过程中,`Gum` 的 `static` 初始化块被执行了。 + +- Class.forName除了将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块。 +- 而classloader只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。 +- Class.forName(name,initialize,loader)带参数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象。 + 还需要注意的是,如果 `Class.forName()` 找不到要加载的类,它就会抛出异常 `ClassNotFoundException`。上面的例子中我们只是简单地报告了问题,但在更严密的程序里,就要考虑在异常处理程序中把问题解决掉(具体例子详见[设计模式](./25-Patterns)章节)。 @@ -228,16 +233,13 @@ public class ToyTest { 输出结果: ``` -Class name: typeinfo.toys.FancyToy is interface? -[false] +Class name: typeinfo.toys.FancyToy is interface? [false] Simple name: FancyToy Canonical name : typeinfo.toys.FancyToy -Class name: typeinfo.toys.HasBatteries is interface? -[true] +Class name: typeinfo.toys.HasBatteries is interface? [true] Simple name: HasBatteries Canonical name : typeinfo.toys.HasBatteries -Class name: typeinfo.toys.Waterproof is interface? -[true] +Class name: typeinfo.toys.Waterproof is interface? [true] Simple name: Waterproof Canonical name : typeinfo.toys.Waterproof Class name: typeinfo.toys.Shoots is interface? [true] @@ -248,11 +250,11 @@ Simple name: Toy Canonical name : typeinfo.toys.Toy ``` -`FancyToy` 继承自 `Toy` 并实现了 `HasBatteries`、`Waterproof` 和 `Shoots` 接口。在 `main` 方法中,我们创建了一个 `Class` 引用,然后在 `try` 语句里边用 `forName()` 方法创建了一个 `FancyToy` 的类对象并赋值给该引用。需要注意的是,传递给 `forName()` 的字符串必须使用类的全限定名(包含包名)。 +`FancyToy` 继承自 `Toy` 并实现了 `HasBatteries`、`Waterproof` 和 `Shoots` 接口。在 `main` 方法中,我们创建了一个 `Class` 引用,然后在 `try` 语句里边用 `forName()` 方法创建了一个 `FancyToy` 的类对象并赋值给该引用。需要注意的是,传递给 `forName()` 的字符串必须使用类的全限定名(由于文件不在根目录,因此需要包含包名)。 `printInfo()` 函数使用 `getName()` 来产生完整类名,使用 `getSimpleName()` 产生不带包名的类名,`getCanonicalName()` 也是产生完整类名(除内部类和数组外,对大部分类产生的结果与 `getName()` 相同)。`isInterface()` 用于判断某个 `Class` 对象代表的是否为一个接口。因此,通过 `Class` 对象,你可以得到关于该类型的所有信息。 -在主方法中调用的 `Class.getInterfaces()` 方法返回的是存放 `Class` 对象的数组,里面的 `Class` 对象表示的是那个类实现的接口。 +在主方法中调用的 `Class.getInterfaces()` 方法返回的是存放 `Class` 对象的数组。该数组里面的每一个 `Class` 对象,都分别是目标类所实现的每一个接口的`Class` 对象。 另外,你还可以调用 `getSuperclass()` 方法来得到父类的 `Class` 对象,再用父类的 `Class` 对象调用该方法,重复多次,你就可以得到一个对象完整的类继承结构。 @@ -312,7 +314,7 @@ Java 还提供了另一种方法来生成类对象的引用:**类字面常量* -我的建议是使用 `.class` 的形式,以保持与普通类的一致性。 +我的建议是最好使用 `.class` 的形式,来保持与普通类的一致性。 注意,有一点很有趣:当使用 `.class` 来创建对 `Class` 对象的引用时,不会自动地初始化该 `Class` 对象。为了使用类而做的准备工作实际包含三个步骤: @@ -322,7 +324,7 @@ Java 还提供了另一种方法来生成类对象的引用:**类字面常量* 3. **初始化**。如果该类具有超类,则先初始化超类,执行 `static` 初始化器和 `static` 初始化块。 -直到第一次引用一个 `static` 方法(构造器隐式地是 `static`)或者非常量的 `static` 字段,才会进行类初始化。 +直到第一次引用一个 `static` 方法(构造器是隐式的 `static`)或者 **非常量** 的 `static` 字段,才会进行类初始化。(当我们使用.class的方式时,初始化被延迟到了对静态方法或者是非常数静态域进行首次引用时才执行。) ```java // typeinfo/ClassInitialization.java @@ -386,17 +388,17 @@ After creating Initable3 ref 初始化有效地实现了尽可能的“惰性”,从对 `initable` 引用的创建中可以看到,仅使用 `.class` 语法来获得对类对象的引用不会引发初始化。但与此相反,使用 `Class.forName()` 来产生 `Class` 引用会立即就进行初始化,如 `initable3`。 -如果一个 `static final` 值是“编译期常量”(如 `Initable.staticFinal`),那么这个值不需要对 `Initable` 类进行初始化就可以被读取。但是,如果只是将一个字段设置成为 `static` 和 `final`,还不足以确保这种行为。例如,对 `Initable.staticFinal2` 的访问将强制进行类的初始化,因为它不是一个编译期常量。 +如果一个 `static final` 值是“编译期常量”(如 `Initable.staticFinal`),那么这个值不需要对 `Initable` 类进行初始化就可以被读取。但是,如果只是将一个字段设置成为 `static` 和 `final`,并不能保证类初始化一定不会发生。例如,对 `Initable.staticFinal2` 的访问将强制进行类的初始化,因为它不是一个编译期常量。 如果一个 `static` 字段不是 `final` 的,那么在对它访问时,总是要求在它被读取之前,要先进行链接(为这个字段分配存储空间)和初始化(初始化该存储空间),就像在对 `Initable2.staticNonFinal` 的访问中所看到的那样。 ### 泛化的 `Class` 引用 -`Class` 引用总是指向某个 `Class` 对象,而 `Class` 对象可以用于产生类的实例,并且包含可作用于这些实例的所有方法代码。它还包含该类的 `static` 成员,因此 `Class` 引用表明了它所指向对象的确切类型,而该对象便是 `Class` 类的一个对象。 +`Class` 引用总是指向某个 `Class` 对象,该 `Class` 对象可以用于产生类的实例,并且包含可作用于这些实例的所有方法代码。它还包含该类的 `static` 成员,因此 `Class` 引用确实表明了它所指向的确切类型: `Class` 类的一个对象。 -但是,Java 设计者看准机会,将它的类型变得更具体了一些。Java 引入泛型语法之后,我们可以使用泛型对 `Class` 引用所指向的 `Class` 对象的类型进行限定。在下面的实例中,两种语法都是正确的: +但是,Java 设计者看准机会,使“指向”变得更加具体:允许你使用泛型语法来约束`Class`引用指向的`Class`对象的类型。在下面的例子中,两种语法都是正确的: ```java // typeinfo/GenericClassReferences.java @@ -420,7 +422,7 @@ public class GenericClassReferences { Class geenericNumberClass = int.class; ``` -这看起来似乎是起作用的,因为 `Integer` 继承自 `Number`。但事实却是不行,因为 `Integer` 的 `Class` 对象并不是 `Number`的 `Class` 对象的子类(这看起来可能有点诡异,我们将在[泛型](./20-Generics)这一章详细讨论)。 +这看起来似乎是合理的,因为 `Integer` 继承自 `Number`。但事实却是不行,因为 `Integer` 的 `Class` 对象并不是 `Number`的 `Class` 对象的子类(这看起来可能有点诡异,我们将在[泛型](./20-Generics)这一章详细讨论)。 为了在使用 `Class` 引用时放松限制,我们使用了通配符,它是 Java 泛型中的一部分。通配符就是 `?`,表示“任何事物”。因此,我们可以在上例的普通 `Class` 引用中添加通配符,并产生相同的结果: @@ -435,7 +437,7 @@ public class WildcardClassReferences { } ``` -使用 `Class` 比单纯使用 `Class` 要好,虽然它们是等价的,并且单纯使用 `Class` 不会产生编译器警告信息。使用 `Class` 的好处是它表示你并非是碰巧或者由于疏忽才使用了一个非具体的类引用,而是特意为之。 +使用 `Class` 比单纯使用 `Class` 要好,虽然它们是等价的,并且单纯使用 `Class` 不会产生编译器警告信息。使用 `Class` 的好处是它表示你并非是意外或者出于无知才使用了一个非具体的类引用,而是特意为之。 为了创建一个限定指向某种类型或其子类的 `Class` 引用,我们需要将通配符与 `extends` 关键字配合使用,创建一个范围限定。这与仅仅声明 `Class` 不同,现在做如下声明: @@ -452,7 +454,7 @@ public class BoundedClassReferences { } ``` -向 `Class` 引用添加泛型语法的原因只是为了提供编译期类型检查,因此如果你操作有误,稍后就会发现这点。使用普通的 `Class` 引用你要确保自己不会犯错,因为一旦你犯了错误,就要等到运行时才能发现它,很不方便。 +之所以向 `Class` 引用添加泛型语法,只是为了提供编译期类型检查,所以如果你做错了什么,你会更早发现。使用普通的 `Class` 引用你要确保自己不会犯错,因为一旦你犯了错误,就要等到运行时才能发现它,很不方便。 下面的示例使用了泛型语法,它保存了一个类引用,稍后又用 `newInstance()` 方法产生类的对象: @@ -527,7 +529,7 @@ public class GenericToyTest { } ``` -如果你手头的是超类,那编译器将只允许你声明超类引用为“某个类,它是 `FancyToy` 的超类”,就像在表达式 `Class` 中所看到的那样。而不会接收 `Class` 这样的声明。这看上去显得有些怪,因为 `getSuperClass()` 方法返回的是基类(不是接口),并且编译器在编译期就知道它是什么类型了(在本例中就是 `Toy.class`),而不仅仅只是"某个类"。不管怎样,正是由于这种含糊性,`up.newInstance` 的返回值不是精确类型,而只是 `Object`。 +如果你使用 `getSuperclass()`,编译器只允许你将超类引用声明为“某个类,它是 `FancyToy` 的超类”(`Class`)。而不会接收 `Class` 这样的声明。这看上去有些奇怪,因为 `getSuperClass()` 方法返回的是基类(不是接口),而编译器在编译时就知道这个类是什么——是 `Toy.class`,而不仅仅只是"`FancyToy` 的超类"。不管怎样,由于这种模糊不清,`up.newInstance` 的返回值不是精确类型,而只是 `Object`。 ### `cast()` 方法 @@ -549,23 +551,23 @@ public class ClassCasts { } ``` -`cast()` 方法接受参数对象,并将其类型转换为 `Class` 引用的类型。但是,如果观察上面的代码,你就会发现,与实现了相同功能的 `main` 方法中最后一行相比,这种转型好像做了很多额外的工作。 +`cast()` 方法接受参数对象,并将其类型转换为 `Class` 引用的类型。但是,如果观察上面的代码,与实现了相同功能的 `main()` 方法中最后一行相比,这种转型似乎做了很多额外的工作。 `cast()` 在无法使用普通类型转换的情况下会显得非常有用,在你编写泛型代码(你将在[泛型](./20-Generics)这一章学习到)时,如果你保存了 `Class` 引用,并希望以后通过这个引用来执行转型,你就需要用到 `cast()`。但事实却是这种情况非常少见,我发现整个 Java 类库中,只有一处使用了 `cast()`(在 `com.sun.mirror.util.DeclarationFilter` 中)。 -Java 类库中另一个没有任何用处的特性就是 `Class.asSubclass()`,该方法允许你将一个 `Class` 对象转型为更加具体的类型。 +Java 类库中另一个没有任何用处的特性: `Class.asSubclass()`,该方法允许你将一个 `Class` 对象转型为更加具体的类型。 -## 类型转换检测 +## 类型转换前的检测 -直到现在,我们已知的 RTTI 类型包括: +直至目前,我们已知的 RTTI 的几种形式包括: -1. 传统的类型转换,如 “`(Shape)`”,由 RTTI 确保转换的正确性,如果执行了一个错误的类型转换,就会抛出一个 `ClassCastException` 异常。 +1. 传统的类型转换,如 “`(Shape)`”,由 RTTI 确保转换的正确性,如果执行了错误的类型转换,就会抛出一个 `ClassCastException` 异常。 -2. 代表对象类型的 `Class` 对象. 通过查询 `Class` 对象可以获取运行时所需的信息. +2. 代表对象类型的 `Class` 对象. 通过查询 `Class` 对象可以获得有用的运行时信息。 -在 C++ 中,经典的类型转换 “`(Shape)`” 并不使用 RTTI。它只是简单地告诉编译器将这个对象作为新的类型对待. 而 Java 会进行类型检查,这种类型转换一般被称作“类型安全的向下转型”。之所以称作“向下转型”,是因为传统上类继承图是这么画的。将 `Circle` 转换为 `Shape` 是一次向上转型, 将 `Shape` 转换为 `Circle` 是一次向下转型。但是, 因为我们知道 `Circle` 肯定是一个 `Shape`,所以编译器允许我们自由地做向上转型的赋值操作,且不需要任何显式的转型操作。当你给编译器一个 `Shape` 的时候,编译器并不知道它到底是什么类型的 `Shape`——它可能是 `Shape`,也可能是 `Shape` 的子类型,例如 `Circle`、`Square`、`Triangle` 或某种其他的类型。在编译期,编译器只能知道它是 `Shape`。因此,你需要使用显式地进行类型转换,以告知编译器你想转换的特定类型,否则编译器就不允许你执行向下转型赋值。 (编译器将会检查向下转型是否合理,因此它不允许向下转型到实际不是待转型类型的子类类型上)。 +在 C++ 中,经典的类型转换 “`(Shape)`” 并不使用 RTTI。它只是简单地告诉编译器将这个对象作为新的类型对待. 而 Java 会进行类型检查,这种类型转换一般被称作“类型安全的向下转型”。之所以称作“向下转型”,是因为传统上类继承图是这么画的。将 `Circle` 转换为 `Shape` 是一次向上转型, 将 `Shape` 转换为 `Circle` 是一次向下转型。但是, 因为我们知道 `Circle` 肯定是一个 `Shape`,所以编译器允许我们自由地做向上转型的赋值操作,且不需要任何显式的转型操作。当你给编译器一个 `Shape` 的时候,编译器并不知道它到底是什么类型的 `Shape`——它可能是 `Shape`,也可能是 `Shape` 的子类型,例如 `Circle`、`Square`、`Triangle` 或某种其他的类型。在编译期,编译器只能知道它是 `Shape`。因此,你需要使用显式地进行类型转换,以告知编译器你想转换的特定类型,否则编译器就不允许你执行向下转型赋值。 (编译器将会检查向下转型是否合理,因此它不允许向下转型到实际不是子类的类型)。 -RTTI 在 Java 中还有第三种形式,那就是关键字 `instanceof`。它返回一个布尔值,告诉我们对象是不是某个特定类型的实例,可以用提问的方式使用它,就像这个样子: +RTTI 在 Java 中还有**第三种形式**,那就是关键字 `instanceof`。它返回一个布尔值,告诉我们对象是不是某个特定类型的实例,可以用提问的方式使用它,就像这个样子: ```java if(x instanceof Dog) @@ -710,7 +712,7 @@ public class Hamster extends Rodent { 我们必须显式地为每一个子类编写无参构造器。因为我们有一个带一个参数的构造器,所以编译器不会自动地为我们加上无参构造器。 -接下来,我们需要一个类,它可以随机地创建不同类型的宠物,同时,它还可以创建宠物数组和持有宠物的 `List`。为了使这个类更加普遍适用,我们将其定义为抽象类: +接下来,我们需要一种方法来随机创建不同类型的宠物,为了方便,它应该还可以创建宠物数组和`List`。为了使这个工具能通过几个不同实现类来“进化”,我们将其定义为抽象类: ```java // typeinfo/pets/PetCreator.java @@ -737,9 +739,9 @@ public abstract class PetCreator implements Supplier { } ``` -抽象的 `types()` 方法需要子类来实现,以此来获取 `Class` 对象构成的 `List`(这是模板方法设计模式的一种变体)。注意,其中类的类型被定义为“任何从 `Pet` 导出的类型”,因此 `newInstance()` 不需要转型就可以产生 `Pet`。`get()` 随机的选取出一个 `Class` 对象,然后可以通过 `Class.newInstance()` 来生成该类的新实例。 +抽象的 `types()` 方法需要子类来实现,以此来获取 `Class` 对象构成的 `List`(这是模板方法设计模式的一种变体)。注意,其中类的类型被定义为“任何从 `Pet` 导出的类型(`Class`)”,因此 `newInstance()` 不需要转型就可以产生 `Pet`。`get()` 随机的选取出一个 `Class` 对象,然后可以通过 `Class.newInstance()` 来生成该类的新实例。 -在调用 `newInstance()` 时,可能会出现两种异常。在紧跟 `try` 语句块后面的 `catch` 子句中可以看到对它们的处理。异常的名字再次成为了一种对错误类型相对比较有用的解释(`IllegalAccessException` 违反了 Java 安全机制,在本例中,表示默认构造器为 `private` 的情况)。 +在调用 `newInstance()` 时,可能会出现两种异常。在紧跟 `try` 语句块后面的 `catch` 子句中可以看到对它们的处理。异常的名字再次成为了一种对错误类型相对比较有用的解释( `InstantiationException`系统无法调用无参数的构造函数实例化类;`IllegalAccessException` 违反了 Java 安全机制,在本例中,当默认构造器为 `private` 的时候会出现)。 当你创建 `PetCreator` 的子类时,你需要为 `get()` 方法提供 `Pet` 类型的 `List`。`types()` 方法会简单地返回一个静态 `List` 的引用。下面是使用 `forName()` 的一个具体实现: @@ -912,11 +914,11 @@ typeinfo.pets.Hamster] ``` -在即将到来的 `PetCount3.java` 示例中,我们用所有 `Pet` 类型预先加载一个 `Map`(不仅仅是随机生成的),因此 `ALL_TYPES` 类型的列表是必要的。`types` 列表是 `ALL_TYPES` 类型(使用 `List.subList()` 创建)的一部分,它包含精确的宠物类型,因此用于随机生成 `Pet`。 +在接下来的 `PetCount3.java` 示例中,我们用所有 `Pet` 类型预先加载一个 `Map`,因此 `ALL_TYPES` 类型的列表是必要的(这是为下文准备,而不只为随机生成而准备)。`types` 列表是 `ALL_TYPES` 类型(使用 `List.subList()` 创建)的一部分(子集),它包含精确的宠物类型,因此用于随机生成 `Pet`。 这次,`types` 的创建没有被 `try` 块包围,因为它是在编译时计算的,因此不会引发任何异常,不像 `Class.forName()`。 -我们现在在 `typeinfo.pets` 库中有两个 `PetCreator` 的实现。为了提供第二个作为默认实现,我们可以创建一个使用 `LiteralPetCreator` 的 *外观模式*: +我们现在在 `typeinfo.pets` 库中有两个 `PetCreator` 的实现。为了提供第二个作为默认实现,我们可以创建一个使用 `LiteralPetCreator` 的 *外观模式* (Facade设计模式,又称为门面模式): ```java // typeinfo/pets/Pets.java @@ -1044,15 +1046,15 @@ Pug Mouse Cymric EgyptianMau=2, Rodent=5, Hamster=1, Manx=7, Pet=20} ``` -为了计算所有不同类型的 `Pet`,`Counter Map` 预先加载了来自 `LiteralPetCreator.ALL_TYPES` 的类型。如果不预先加载 `Map`,将只计数随机生成的类型,而不是像 `Pet` 和 `Cat` 这样的基本类型。 +为了计数所有不同类型的 `Pet`,`Counter Map` 预先加载了来自 `LiteralPetCreator.ALL_TYPES` 的类型。如果不预先加载 `Map`,最后只会计数随机生成的类型,而不会计数是像 `Pet`、`Cat`、`Dog` 和 `Rodent` 这样的基本的类型。(译者注:意思是,`ALL_TYPES`有两个作用:1.利用其子集来生成随机详细的宠物类型,2.利用其全集来分类计数,分类包括笼统类/基本类,比如Pet=20,是因为生成的所有详细类都归属于Pet,基本类和详细类的计数都会增1) -`isInstance()` 方法消除了对 `instanceof` 表达式的需要。此外,这意味着你可以通过更改 `LiteralPetCreator.types` 数组来添加新类型的 `Pet`;程序的其余部分不需要修改(就像使用 `instanceof` 表达式时那样)。 +`isInstance()` 方法消除了对 `instanceof` 表达式的需要。此外,这意味着你可以通过更改 `LiteralPetCreator.types` 数组来添加新类型的 `Pet`;程序的其余部分不需要修改(就像使用 `instanceof` 表达式时那样)(解耦了!)。 -`toString()` 方法被重载,以便更容易读取输出,该输出仍与打印 `Map` 时看到的典型输出匹配。 +`toString()` 方法被重写,以便更容易读取输出,该输出仍与打印 `Map` 时看到的典型输出匹配。 ### 递归计数 -`PetCount3.Counter` 中的 `Map` 预先加载了所有不同的 `Pet` 类。我们可以使用 `Class.isAssignableFrom()` 而不是预加载 `Map` ,并创建一个不限于计数 `Pet` 的通用工具: +`PetCount3.Counter` 中的 `Map` 预加载了所有不同的 `Pet` 类。我们可以使用 `Class.isAssignableFrom()` 来代替预加载 `Map` 这一操作,并创建一个不仅限于计数 `Pet` 的通用工具: ```java // onjava/TypeCounter.java @@ -1081,8 +1083,7 @@ public class TypeCounter extends HashMap, Integer> { Integer quantity = get(type); put(type, quantity == null ? 1 : quantity + 1); Class superClass = type.getSuperclass(); - if(superClass != null && - baseType.isAssignableFrom(superClass)) + if(superClass != null && baseType.isAssignableFrom(superClass)) countClass(superClass); } @@ -1098,7 +1099,7 @@ public class TypeCounter extends HashMap, Integer> { } ``` -`count()` 方法获取其参数的 `Class`,并使用 `isAssignableFrom()` 进行运行时检查,以验证传递的对象实际上属于感兴趣的层次结构。`countClass()` 首先计算类的确切类型。然后,如果 `baseType` 可以从超类赋值,则在超类上递归调用 `countClass()`。 +`count()` 方法获取其参数的 `Class`,并使用 `isAssignableFrom()` 进行运行时检查,以验证传递的对象是否真的属于继承相关的层次结构。`countClass()` 首先对该类的确切类型计数,然后,如果 `baseType` 是该类的超类的父类或同类,则在该类的超类上递归调用 `countClass()`。 ```java // typeinfo/PetCount4.java @@ -1137,13 +1138,13 @@ Mouse=2} 从 `Pet` 层次结构生成对象的问题是,每当向层次结构中添加一种新类型的 `Pet` 时,必须记住将其添加到 `LiteralPetCreator.java` 的条目中。在一个定期添加更多类的系统中,这可能会成为问题。 -你可能会考虑向每个子类添加静态初始值设定项,因此初始值设定项会将其类添加到某个列表中。不幸的是,静态初始值设定项仅在首次加载类时调用,因此存在鸡和蛋的问题:生成器的列表中没有类,因此它无法创建该类的对象,因此类不会被加载并放入列表中。 +你可能会考虑向每个子类添加静态初始化块,初始化块会将其类添加到某个列表中。不幸的是,静态初始化块仅在首次加载类时调用,由此产生了先有鸡还是先有蛋的悖论:生成器的列表中没有该类,所以它永远无法创建该类的对象,所以该类不会被加载并放入列表中。 -基本上,你必须自己手工创建列表(除非你编写了一个工具来搜索和分析源代码,然后创建和编译列表)。所以你能做的最好的事情就是把列表集中放在一个明显的地方。层次结构的基类可能是最好的地方。 +基本上,你必须亲自创建这个列表(除非你编写了一个工具来搜索和分析源代码,然后创建和编译这个列表)。所以你能做的最好的事情就是把列表集中放在一个中央的、明显的地方。继承层次结构的基类可能是最好的地方。 -我们在这里所做的另一个更改是使用*工厂方法*设计模式将对象的创建推迟到类本身。工厂方法可以以多态方式调用,并为你创建适当类型的对象。事实证明,`java.util.function.Supplier` 用 `T get()` 描述了原型工厂方法。协变返回类型允许 `get()` 为 `Supplier` 的每个子类实现返回不同的类型。 +我们在这里所做的另一个更改是使用 *工厂方法 Factory Method* 设计模式——将【对象的创建】推迟到【类本身】(即:相比于直接new一个类,这方法使得 类本身可以创建继承自本类的子类型对象)。工厂方法可以以多态方式调用,并为你创建适当类型的对象。事实证明,`java.util.function.Supplier` 用 `T get()` 描述了原型工厂方法。协变返回类型允许 `get()` 为 `Supplier` 的每个子类实现返回不同的类型。 -在本例中,基类 `Part` 包含一个工厂对象的静态列表,列表成员类型为 `Supplier`。对于应该由 `get()` 方法生成的类型的工厂,通过将它们添加到 `prototypes` 列表向基类“注册”。奇怪的是,这些工厂本身就是对象的实例。此列表中的每个对象都是用于创建其他对象的*原型*: +在本例中,基类`Part`包含一个元素为工厂对象的静态列表,元素的类型为 `Supplier`。对于应该由 `get()` 方法产生的各种工厂,通过将它们添加至 `prototypes` 列表,向基类`Part`注册。奇怪的是,这些工厂本身就是对象的实例。此列表中的每个对象都是用于创建其他对象的*原型*: ```java // typeinfo/RegisteredFactories.java @@ -1253,9 +1254,9 @@ PowerSteeringBelt FuelFilter ``` -并非层次结构中的所有类都应实例化;这里的 `Filter` 和 `Belt` 只是分类器,这样你就不会创建任何一个类的实例,而是只创建它们的子类(请注意,如果尝试这样做,你将获得 `Part` 基类的行为)。 +不是所有在继承层次结构中的类都应该被实例化;这里的 `Filter` 和 `Belt` 只是分类器——你不会创建这两个类的实例,而是只创建它们的子类(请注意,如果尝试这样做(创建这两个类的实例),你将获得 `Part` 基类的行为,即使用的是基类中的方法)。 -因为 `Part implements Supplier`,`Part` 通过其 `get()` 方法供应其他 `Part`。如果为基类 `Part` 调用 `get()`(或者如果 `generate()` 调用 `get()`),它将创建随机特定的 `Part` 子类型,每个子类型最终都从 `Part` 继承,并重写相应的 `get()` 以生成它们中的一个。 +因为 `Part implements Supplier`,`Part` 通过其 `get()` 方法供应其他 `Part` (原型工厂)。如果对基类 `Part` 调用 `get()`(或者如果 `generate()` 调用 `get()`),它将创建(new)随机的具体的 `Part` 的子类型,其每一个最终都继承自 `Part` ,并通过重写了的 `get()` 方法来创建(new)它们本身。 @@ -1329,28 +1330,29 @@ x.getClass().equals(Base.class)) false x.getClass().equals(Derived.class)) true ``` -`test()` 方法使用两种形式的 `instanceof` 对其参数执行类型检查。然后,它获取 `Class` 引用,并使用 `==` 和 `equals()` 测试 `Class` 对象的相等性。令人放心的是,`instanceof` 和 `isInstance()` 产生的结果相同, `equals()` 和 `==` 产生的结果也相同。但测试本身得出了不同的结论。与类型的概念一致,`instanceof` 说的是“你是这个类,还是从这个类派生的类?”。而如果使用 `==` 比较实际的`Class` 对象,则与继承无关 —— 它要么是确切的类型,要么不是。 +`test()` 方法使用两种形式的 `instanceof` 对其参数执行类型检查。然后,它获取 `Class` 引用,并使用 `==` 和 `equals()` 测试 `Class` 对象的相等性。令人放心的是,`instanceof` 和 `isInstance()` 产生的结果相同, `equals()` 和 `==` 产生的结果也相同。但测试本身得出了不同的结论。与类型的概念一致,`instanceof` 的意思是:“你是这个类,又或者是 从这个类派生的类?”。另一方面,如果使用 `==` 比较实际的`Class` 对象,则与继承无关 —— 其 要么的确是该类型,要么不是。 + ## 反射:运行时类信息 如果你不知道对象的确切类型,RTTI 会告诉你。但是,有一个限制:必须在编译时知道类型,才能使用 RTTI 检测它,并对信息做一些有用的事情。换句话说,编译器必须知道你使用的所有类。 -起初,这看起来并没有那么大的限制,但是假设你引用了一个不在程序空间中的对象。实际上,该对象的类在编译时甚至对程序都不可用。也许你从磁盘文件或网络连接中获得了大量的字节,并被告知这些字节代表一个类。由于这个类在编译器为你的程序生成代码后很长时间才会出现,你如何使用这样的类? +起初,这看起来并不是什么大的限制,但是假设你引用了一个不在程序空间中的对象。实际上,该对象的类在编译时甚至对程序都不可用。也许你从磁盘文件或网络连接中获得了大量的字节,并被告知这些字节代表一个类。由于这个类在编译器为你的程序生成代码后很长时间才会出现,你如何使用这样的类? -在传统编程环境中,这是一个牵强的场景。但是,当我们进入一个更大的编程世界时,会有一些重要的情况发生。第一个是基于组件的编程,你可以在应用程序构建器*集成开发环境*中使用*快速应用程序开发*(RAD)构建项目。这是一种通过将表示组件的图标移动到窗体上来创建程序的可视化方法。然后,通过在编程时设置这些组件的一些值来配置这些组件。这种设计时配置要求任何组件都是可实例化的,它公开自己的部分,并且允许读取和修改其属性。此外,处理*图形用户界面*(GUI)事件的组件必须公开有关适当方法的信息,以便 IDE 可以帮助程序员覆写这些事件处理方法。反射提供了检测可用方法并生成方法名称的机制。 +在传统编程环境中,这是一个牵强的场景。但是,当我们进入一个更大的编程世界时,有一些重要的情况发生在这种情况下。第一个是基于组件的编程,你可以在应用程序构建器 *集成开发环境 (Integrated Development Environment IDE)* 中使用 *快速应用程序开发 (Rapid Application Development RAD)* 构建项目。这是一种通过将表示组件的图标移动到窗体上来创建程序的可视化方法。然后,通过在编程时设置这些组件的一些值来配置这些组件。这种【设计时配置】要求任何组件都是可实例化的,它公开自己的部分,并且允许读取和修改其属性。此外,处理*图形用户界面*(*Graphical User Interface* GUI)事件的组件必须公开有关适当方法的信息,以便 IDE 可以帮助程序员重写这些事件处理方法。反射提供了检测可用方法并生成方法名称的机制。 -在运行时发现类信息的另一个令人信服的动机是提供跨网络在远程平台上创建和执行对象的能力。这称为*远程方法调用*(RMI),它使 Java 程序的对象分布在许多机器上。这种分布有多种原因。如果你想加速一个计算密集型的任务,你可以把它分解成小块放到空闲的机器上。或者你可以将处理特定类型任务的代码(例如,多层次客户机/服务器体系结构中的“业务规则”)放在特定的机器上,这样机器就成为描述这些操作的公共存储库,并且可以很容易地更改它以影响系统中的每个人。分布式计算还支持专门的硬件,这些硬件可能擅长于某个特定的任务——例如矩阵转换——但对于通用编程来说不合适或过于昂贵。 +在运行时发现类信息的另一个令人信服的动机是提供跨网络在远程平台上创建和执行对象的能力。这称为*远程方法调用*(*Remote Method Invocation* RMI),它使 Java 程序的对象分布在许多机器上。这种分布有多种原因。如果你想加速一个计算密集型的任务,你可以把它分解成小块放到空闲的机器上。或者你可以将处理特定类型任务的代码(例如,多层次客户机/服务器体系结构中的“业务规则”)放在特定的机器上,这样机器就成为描述这些操作的公共存储库,并且可以很容易地更改它以影响系统中的每个人。分布式计算还支持专门的硬件,这些硬件可能擅长于某个特定的任务——例如矩阵转换——但对于通用编程来说不合适或过于昂贵。 -类 `Class` 支持*反射*的概念, `java.lang.reflect` 库中包含类 `Field`、`Method` 和 `Constructor`(每一个都实现了 `Member` 接口)。这些类型的对象由 JVM 在运行时创建,以表示未知类中的对应成员。然后,可以使用 `Constructor` 创建新对象,`get()` 和 `set()` 方法读取和修改与 `Field` 对象关联的字段,`invoke()` 方法调用与 `Method` 对象关联的方法。此外,还可以调用便利方法 `getFields()`、`getMethods()`、`getConstructors()` 等,以返回表示字段、方法和构造函数的对象数组。(你可以通过在 JDK 文档中查找类 `Class` 来了解更多信息。)因此,匿名对象的类信息可以在运行时完全确定,编译时不需要知道任何信息。 +`Class`类支持 *反射 reflection* 的概念 和 `java.lang.reflect` 库——该库包含类 `Field`、`Method` 和 `Constructor`(每一个都实现了 `Member` 接口)。这些类型的对象由 JVM 在运行时创建,以表示未知类中的对应成员。然后,可以使用 `Constructor` 创建新对象,`get()` 和 `set()` 方法读取和修改与 `Field` 对象关联的字段,`invoke()` 方法调用与 `Method` 对象关联的方法。此外,还可以调用便利方法 `getFields()`、`getMethods()`、`getConstructors()` 等,以返回表示字段、方法和构造函数的对象数组。(你可以通过在 JDK 文档中查找类 `Class` 来了解更多信息。)因此,匿名对象的类信息可以在运行时完全确定,编译时不需要知道任何信息。 -重要的是要意识到反射没有什么魔力。当你使用反射与未知类型的对象交互时,JVM 将查看该对象,并看到它属于特定的类(就像普通的 RTTI)。在对其执行任何操作之前,必须加载 `Class` 对象。因此,该特定类型的 `.class` 文件必须在本地计算机上或通过网络对 JVM 仍然可用。因此,RTTI 和反射的真正区别在于,使用 RTTI 时,编译器在编译时会打开并检查 `.class` 文件。换句话说,你可以用“正常”的方式调用一个对象的所有方法。通过反射,`.class` 文件在编译时不可用;它由运行时环境打开并检查。 +重要的是要意识到反射并没有什么神奇的地方。当你使用反射与未知类型的对象交互时,JVM 将查看该对象,并看到它属于特定的类(就像普通的 RTTI)。在对其执行任何操作之前,必须加载 `Class` 对象。因此,该特定类型的 `.class` 文件仍然必须对 JVM 可用,无论是在本地机器上或在网络上。因此,RTTI 和反射的真正区别在于,使用 RTTI 时,编译器在编译时会打开并检查 `.class` 文件。换句话说,你可以用“正常”的方式调用一个对象的所有方法。通过反射,`.class` 文件在编译时不可用;它由运行时环境打开并检查。 ### 类方法提取器 通常,你不会直接使用反射工具,但它们可以帮助你创建更多的动态代码。反射是用来支持其他 Java 特性的,例如对象序列化(参见[附录:对象序列化](https://lingcoder.github.io/OnJava8/#/book/Appendix-Object-Serialization))。但是,有时动态提取有关类的信息很有用。 -考虑一个类方法提取器。查看类定义的源代码或 JDK 文档,只显示*在该类定义中*定义或重写的方法。但是,可能还有几十个来自基类的可用方法。找到它们既单调又费时[^1]。幸运的是,反射提供了一种方法,可以简单地编写一个工具类自动地向你展示所有的接口: +考虑一个类方法提取器。当我们在查看类定义的源代码或 JDK 文档的时候,它只显示*在该类定义中*定义或重写的方法。但是,可能还有几十个来自基类的可用方法。找到它们既繁琐又费时[^1]。幸运的是,反射提供了一种方法,可以简单地编写一个工具类自动地向你展示所有的接口: ```java // typeinfo/ShowMethods.java @@ -1380,24 +1382,19 @@ public class ShowMethods { Constructor[] ctors = c.getConstructors(); if (args.length == 1) { for (Method method : methods) - System.out.println( - p.matcher( - method.toString()).replaceAll("")); + System.out.println(p.matcher(method.toString()).replaceAll("")); for (Constructor ctor : ctors) - System.out.println( - p.matcher(ctor.toString()).replaceAll("")); + System.out.println(p.matcher(ctor.toString()).replaceAll("")); lines = methods.length + ctors.length; } else { for (Method method : methods) if (method.toString().contains(args[1])) { - System.out.println(p.matcher( - method.toString()).replaceAll("")); + System.out.println(p.matcher(method.toString()).replaceAll("")); lines++; } for (Constructor ctor : ctors) if (ctor.toString().contains(args[1])) { - System.out.println(p.matcher( - ctor.toString()).replaceAll("")); + System.out.println(p.matcher(ctor.toString()).replaceAll("")); lines++; } } @@ -1413,10 +1410,8 @@ public class ShowMethods { ``` public static void main(String[]) public final void wait() throws InterruptedException -public final void wait(long,int) throws -InterruptedException -public final native void wait(long) throws -InterruptedException +public final void wait(long,int) throws InterruptedException +public final native void wait(long) throws InterruptedException public boolean equals(Object) public String toString() public native int hashCode() @@ -1426,7 +1421,7 @@ public final native void notifyAll() public ShowMethods() ``` -`Class` 方法 `getmethods()` 和 `getconstructors()` 分别返回 `Method` 数组和 `Constructor` 数组。这些类中的每一个都有进一步的方法来解析它们所表示的方法的名称、参数和返回值。但你也可以像这里所做的那样,使用 `toString()`,生成带有整个方法签名的 `String`。代码的其余部分提取命令行信息,确定特定签名是否与目标 `String`(使用 `indexOf()`)匹配,并使用正则表达式(在 [Strings](#ch021.xhtml#strings) 一章中介绍)删除名称限定符。 +`Class` 方法 `getmethods()` 和 `getconstructors()` 分别返回 `Method` 数组和 `Constructor` 数组。这些类中的每一个都有进一步的方法来解析它们所表示的方法的名称、参数和返回值。但你也可以像这里所做的那样,使用 `toString()`,生成带有整个方法签名的 `String`。余下代码则提取命令行信息,使用`contains()`(其内在使用 `indexOf()`)确定特定方法签名是否与入参 `String`匹配,并使用正则表达式(在 [Strings](#ch021.xhtml#strings) 一章中介绍)删除名称限定符。 编译时无法知道 `Class.forName()` 生成的结果,因此所有方法签名信息都是在运行时提取的。如果你研究 JDK 反射文档,你将看到有足够的支持来实际设置和对编译时完全未知的对象进行方法调用(本书后面有这样的例子)。虽然最初你可能认为你永远都不需要这样做,但是反射的全部价值可能会令人惊讶。 @@ -1436,17 +1431,17 @@ public ShowMethods() java ShowMethods ShowMethods ``` -输出包含一个 `public` 无参数构造函数,即使未定义构造函数。你看到的构造函数是由编译器自动合成的。如果将 `ShowMethods` 设置为非 `public` 类(即只有包级访问权),则合成的无参数构造函数将不再显示在输出中。自动为合成的无参数构造函数授予与类相同的访问权。 +输出包含有一个 `public` 无参构造函数,即使未定义构造函数。你看到的构造函数是由编译器自动合成的。如果将 `ShowMethods` 设置为非 `public` 类(即变成包级访问权),那么输出中就没有合成的无参构造函数。这是因为合成的无参构造函数也变成了包级访问,是自动被赋予了和类相同的访问权限。 尝试运行 `java ShowMethods java.lang.String`,并附加一个 `char`、`int`、`String` 等参数。 -编程时,当你不记得某个类是否有特定的方法,并且不想在 JDK 文档中搜索索引或类层次结构时,或者如果你不知道该类是否可以对 `Color` 对象执行任何操作时,该工具能节省不少时间。 +编程时该工具能节省不少时间——当你不记得某个类是否有特定的方法,并且不想在 JDK 文档中搜索索引或类层次结构时;或者如果你不知道该类是否可以对某个对象,例如 `Color` 对象执行任何操作时。 ## 动态代理 -*代理*是基本的设计模式之一。一个对象封装真实对象,代替其提供其他或不同的操作---这些操作通常涉及到与“真实”对象的通信,因此代理通常充当中间对象。这是一个简单的示例,显示代理的结构: +*代理 Proxy* 是基本的设计模式之一。它是你插入一个对象来代替“真实”对象,以提供额外的或不同的操作——这些操作通常涉及到与“真实”对象的通信,所以代理通常作为一个中间人。这里有一个简单的示例展示代理的结构: ```java // typeinfo/SimpleProxyDemo.java @@ -1515,11 +1510,11 @@ somethingElse bonobo ``` 因为 `consumer()` 接受 `Interface`,所以它不知道获得的是 `RealObject` 还是 `SimpleProxy`,因为两者都实现了 `Interface`。 -但是,在客户端和 `RealObject` 之间插入的 `SimpleProxy` 执行操作,然后在 `RealObject` 上调用相同的方法。 +但是插入在客户端和 `RealObject` 之间的 `SimpleProxy` 会执行操作,然后在 `RealObject` 上调用相同的方法。 -当你希望将额外的操作与“真实对象”做分离时,代理可能会有所帮助,尤其是当你想要轻松地启用额外的操作时,反之亦然(设计模式就是封装变更---所以你必须改变一些东西以证明模式的合理性)。例如,如果你想跟踪对 `RealObject` 中方法的调用,或衡量此类调用的开销,该怎么办?你不想这部分代码耦合到你的程序中,而代理能使你可以很轻松地添加或删除它。 +当你希望将【额外的操作】与【“真实对象”】分离时,尤其是当你想要轻松地启用和停止【额外的操作】时,代理可能会有所帮助。(设计模式的要点是封装变更——所以你必须改变一些东西以证明模式的合理性)。例如,如果你想跟踪对 `RealObject` 中方法的调用,或测量这种调用的开销,该怎么办?你不想这部分代码耦合到你的程序中,而代理能使你可以很轻松地添加或删除它。 -Java 的*动态代理*更进一步,不仅动态创建代理对象而且动态处理对代理方法的调用。在动态代理上进行的所有调用都被重定向到单个*调用处理程序*,该处理程序负责发现调用的内容并决定如何处理。这是 `SimpleProxyDemo.java` 使用动态代理重写的例子: +Java 的*动态代理 dynamic proxy* 更进一步,不仅动态创建代理对象,而且动态处理对代理方法的调用。在动态代理上进行的所有调用都被重定向到单个*调用处理程序* *invocation handler*,该处理程序负责发现调用的内容并决定如何处理。这里是 `SimpleProxyDemo.java` 使用动态代理理念重写的例子: ```java // typeinfo/SimpleDynamicProxy.java @@ -1534,9 +1529,7 @@ class DynamicProxyHandler implements InvocationHandler { } @Override - public Object - invoke(Object proxy, Method method, Object[] args) - throws Throwable { + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println( "**** proxy: " + proxy.getClass() + ", method: " + method + ", args: " + args); @@ -1571,21 +1564,24 @@ class SimpleDynamicProxy { ``` doSomething somethingElse bonobo -**** proxy: class $Proxy0, method: public abstract void -Interface.doSomething(), args: null +**** proxy: class typeinfo.$Proxy0, method: public abstract void typeinfo.Interface.doSomething(), args: null doSomething -**** proxy: class $Proxy0, method: public abstract void -Interface.somethingElse(java.lang.String), args: -[Ljava.lang.Object;@6bc7c054 +**** proxy: class typeinfo.$Proxy0, method: public abstract void typeinfo.Interface.somethingElse(java.lang.String), args: [Ljava.lang.Object;@3b764bce bonobo somethingElse bonobo ``` -可以通过调用静态方法 `Proxy.newProxyInstance()` 来创建动态代理,该方法需要一个类加载器(通常可以从已加载的对象中获取),希望代理实现的接口列表(不是类或抽象类),以及接口 `InvocationHandler` 的一个实现。动态代理会将所有调用重定向到调用处理程序,因此通常为调用处理程序的构造函数提供对“真实”对象的引用,以便一旦执行中介任务便可以转发请求。 +可以通过调用静态方法 `Proxy.newProxyInstance()` 来创建动态代理,该方法的三个参数: + +- 1.一个类加载器(通常可以从已加载的对象中获取); +- 2.希望代理实现的接口列表(不是类或抽象类); +- 3.接口 `InvocationHandler` 的一个实现。 + +动态代理会将所有调用重定向到调用处理程序 `InvocationHandler`,因此,调用处理程序的构造函数通常被赋予对“真实”对象的引用,以便一旦执行中介任务就可以转发请求。 -`invoke()` 方法被传递给代理对象,以防万一你必须区分请求的来源---但是在很多情况下都无需关心。但是,在 `invoke()` 内的代理上调用方法时要小心,因为接口的调用是通过代理重定向的。 +`invoke()` 方法会被交给代理对象,以防你必须区分请求来自哪里——但是在很多情况下不会在意。然而,在调用 `invoke()` 中的代理方法时要小心,因为通过接口的调用会通过代理对象重定向。 -通常执行代理操作,然后使用 `Method.invoke()` 将请求转发给被代理对象,并携带必要的参数。这在一开始看起来是有限制的,好像你只能执行一般的操作。但是,可以过滤某些方法调用,同时传递其他方法调用: +一般来说,你执行代理操作,然后使用`Method.invoke()`将请求转发给代理对象,并传递必要的参数。这最初可能看起来很有局限性,好像你只能执行一般的操作。然而,你可以对某些方法调用进行过滤,而将其他方法传递过去: ```java // typeinfo/SelectingMethods.java @@ -1601,12 +1597,9 @@ class MethodSelector implements InvocationHandler { } @Override - public Object - invoke(Object proxy, Method method, Object[] args) - throws Throwable { + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (method.getName().equals("interesting")) - System.out.println( - "Proxy detected the interesting method"); + System.out.println("Proxy detected the interesting method"); return method.invoke(proxied, args); } } @@ -1680,7 +1673,7 @@ boring3 虽然 `Optional` 是 Java 8 为了支持流式编程才引入的,但其实它是一个通用的工具。为了证明这点,在本节中,我们会把它用在普通的类中。因为涉及一些运行时检测,所以把这一小节放在了本章。 -实际上,在所有地方都使用 `Optional` 是没有意义的,有时候检查一下是不是 `null` 也挺好的,或者有时我们可以合理地假设不会出现 `null`,甚至有时候检查 `NullPointException` 异常也是可以接受的。`Optional` 最有用武之地的是在那些“更接近数据”的地方,在问题空间中代表实体的对象上。举个简单的例子,很多系统中都有 `Person` 类型,代码中有些情况下你可能没有一个实际的 `Person` 对象(或者可能有,但是你还没用关于那个人的所有信息)。这时,在传统方法下,你会用到一个 `null` 引用,并且在使用的时候测试它是不是 `null`。而现在,我们可以使用 `Optional`: +在实践中,并非所有地方使用 `Optional` 都有意义的,有时检查一下 `null` 是可接受的,有时可以合理地假设不会出现 `null`,甚至有时通过 `NullPointException` 来检测异常也是可以接受的。`Optional` 最有用武之地的是在那些“更接近数据”的地方,用对象来代表问题空间中的实体。举个简单的例子,很多系统中都有 `Person` 类型,在代码中有些情况下,你可能没有一个实际的人(也许你有,但你还没有这个人的所有信息)。此时,在传统方法下,你会使用一个 `null` 引用,并且在使用的时候测试它是不是 `null`。而现在,我们可以使用 `Optional`: ```java // typeinfo/Person.java @@ -1746,11 +1739,11 @@ Bob Smith Bob Smith 11 Degree Lane, Frostbite Falls, MN ``` -`Person` 的设计有时候又叫“数据传输对象(DTO,data-transfer object)”。注意,所有字段都是 `public` 和 `final` 的,所以没有 `getter` 和 `setter` 方法。也就是说,`Person` 是不可变的,你只能通过构造器给它赋值,之后就只能读而不能修改它的值(字符串本身就是不可变的,因此你无法修改字符串的内容,也无法给它的字段重新赋值)。如果你想修改一个 `Person`,你只能用一个新的 `Person` 对象来替换它。`empty` 字段在对象创建的时候被赋值,用于快速判断这个 `Person` 对象是不是空对象。 +`Person` 的设计有时候又叫“数据传输对象(DTO,data-transfer object)”。注意,所有字段都是 `public` 和 `final` 的,所以没有 `getter` 和 `setter` 方法。也就是说,`Person` 是不可变的,你只能通过构造器给它赋值,之后就只能读而不能修改它的值(字符串本身就是不可变的,因此你无法修改字符串的内容,也无法给它的字段重新赋值)。如果你想修改一个 `Person`,你只能用一个新的 `Person` 对象来替换它。`empty` 字段在对象创建的时候被赋值,用于快速判断这个 `Person` 对象是否代表一个空对象。 -如果想使用 `Person`,就必须使用 `Optional` 接口才能访问它的 `String` 字段,这样就不会意外触发 `NullPointException` 了。 +无论是谁如果想使用 `Person`,就必须使用 `Optional` 接口才能访问它的 `String` 字段,这样就不会意外触发 `NullPointException` 了。 -现在假设你已经因你惊人的理念而获得了一大笔风险投资,现在你要招兵买马了,但是在虚位以待时,你可以将 `Person Optional` 对象放在每个 `Position` 上: +现在假设你已经为你的 "惊人创意 "获得了一大笔风险资金。您已经准备好增补职员,但是在您等待职位被填补的时候,你可以将 `Person Optional` 对象放在每个 `Position` 上: ```java // typeinfo/Position.java @@ -1828,9 +1821,9 @@ caught EmptyTitleException `Person` 字段的限制又不太一样:如果你把它的值设为 `null`,程序会自动把将它赋值成一个空的 `Person` 对象。先前我们也用过类似的方法把字段转换成 `Optional`,但这里我们是在返回结果的时候使用 `orElse(new Person())` 插入一个空的 `Person` 对象替代了 `null`。 -在 `Position` 里边,我们没有创建一个表示“空”的标志位或者方法,因为 `person` 字段的 `Person` 对象为空,就表示这个 `Position` 是个空缺位置。之后,你可能会发现你必须添加一个显式的表示“空位”的方法,但是正如 YAGNI[^2] (You Aren't Going to Need It,你永远不需要它)所言,在初稿时“实现尽最大可能的简单”,直到程序在某些方面要求你为其添加一些额外的特性,而不是假设这是必要的。 +在 `Position` 里边,我们没有创建一个表示“空”的标志位或者方法,因为 `person` 字段中的,空的 `Person` 对象就表示这个 `Position` 是个空缺位置。在以后,你可能会发现你必须添加一个显式的表示“空位”的方法,但是正如 YAGNI[^2] (You Aren't Going to Need It,你永远不需要它)所言,在初稿时“实现尽最大可能的简单”,直到程序在某些方面要求你为其添加一些额外的特性,而不是假设这是必要的。 -请注意,虽然你清楚你使用了 `Optional`,可以免受 `NullPointerExceptions` 的困扰,但是 `Staff` 类却对此毫不知情。 +请注意,`Staff` 类可以完全忽略 `Optional` 的存在(解耦),尽管你清楚你使用了 `Optional`,保护你免受 `NullPointerExceptions` 的困扰。 ```java // typeinfo/Staff.java @@ -1918,7 +1911,7 @@ package onjava; public interface Null {} ``` -如果你用接口取代具体类,那么就可以使用 `DynamicProxy` 来自动地创建 `Null` 对象。假设我们有一个 `Robot` 接口,它定义了一个名字、一个模型和一个描述 `Robot` 行为能力的 `List`: +如果你用接口取代具体类,那么就可以使用 `DynamicProxy` 来自动地创建 `Null` 对象。假设我们有一个 `Robot` 接口,它定义了一个名字、一个型号和一个描述 `Robot` 的行为能力列表 `List`: ```java // typeinfo/Robot.java @@ -2027,7 +2020,7 @@ Slusher can clear the roof Slusher clearing roof ``` -假设存在许多不同类型的 `Robot`,我们想让每种 `Robot` 都创建一个 `Null` 对象来执行一些特殊的操作——在本例中,即提供 `Null` 对象所代表 `Robot` 的确切类型信息。这些信息是通过动态代理捕获的: +假设存在许多不同类型的 `Robot`,我们希望每个 `Null` 为每个 `Robot` 类型做一些特别的事情——在本例中,即提供 `Null` 对象所代表 `Robot` 的确切类型信息。这些信息是通过动态代理捕获的: ```java // typeinfo/NullRobot.java @@ -2039,8 +2032,7 @@ import java.util.stream.*; import onjava.*; -class NullRobotProxyHandler - implements InvocationHandler { +class NullRobotProxyHandler implements InvocationHandler { private String nullName; private Robot proxied = new NRobot(); @@ -2074,8 +2066,7 @@ class NullRobotProxyHandler } public class NullRobot { - public static Robot - newNullRobot(Class type) { + public static Robot newNullRobot(Class type) { return (Robot) Proxy.newProxyInstance( NullRobot.class.getClassLoader(), new Class[] { Null.class, Robot.class }, @@ -2109,13 +2100,77 @@ Robot model: SnowRemovalRobot NullRobot 无论何时,如果你需要一个空 `Robot` 对象,只需要调用 `newNullRobot()`,并传递需要代理的 `Robot` 的类型。这个代理满足了 `Robot` 和 `Null` 接口的需要,并提供了它所代理的类型的确切名字。 -### Mock 对象和桩 +### 模拟对象和桩对象 + +**模拟对象 (Mock)** 和 **桩对象 (Stub)** 在逻辑上都是 `Optional` 的变体。他们都是最终程序中所使用的“实际”对象的代理。不过,模拟对象和桩都假装是真实的对象,提供真实的信息,而不是像 `Optional` 那样,去隐藏【包含潜在 `null` 值的对象】。 + +模拟对象和桩,它们都是为了同一个目标而出现的,代替依赖部分,让原先的“整合测试”简化为“单元测试”。而它们之间的区别在于程度不同: + +- 桩只是返回桩数据,它通常是重量级的,并且经常在多个测试中被复用。桩可以根据它们被调用的方式,通过配置进行修改。因此,桩是一种复杂对象,它可以做很多事。Stubs一般用来stub out那些难于创建或操纵的对象。一个经典的例子就是一个database connection。因此,一般的stub都被发现于系统边界,或者围绕着系统中复杂的对象群。为了建立一个stub,你建立了一个接口的另一种实现,利用简单的数据替换了真实的方法。总结为一句就是: **stub给被调用者( 方法/模块)制造假的返回值,以便不影响调用者的测试,用于提供测试的条件。** +- 模拟对象往往是轻量级的,且用于`自我测试(self-testing)`——每个测试用例都能够自行判断其后置条件(postcondition)是否得到满足,并依次来反馈测试的执行是否成功。通常很多模拟对象是为了处理各种测试情况而创建的。如果你需要做很多事,通常会创建很多小而简单的 模拟对象。总结为一句就是: **mock给被测试方法模拟一个预期的效果**。 + +Here we can begin to see the difference between mocks and stubs. If we were writing a test for this mailing behavior, we might write a simple stub like this. + +```java +public interface MailService { + public void send (Message msg); +} +public class MailServiceStub implements MailService { + private List messages = new ArrayList(); + public void send (Message msg) { + messages.add(msg); + } + public int numberSent() { + return messages.size(); + } +} +``` + +We can then use state verification on the stub like this. + +```java +class OrderStateTester... + public void testOrderSendsMailIfUnfilled() { + Order order = new Order(TALISKER, 51); + MailServiceStub mailer = new MailServiceStub(); + order.setMailer(mailer); + order.fill(warehouse); + assertEquals(1, mailer.numberSent()); + } +``` + +Of course this is a very simple test - only that a message has been sent. We've not tested it was sent to the right person, or with the right contents, but it will do to illustrate the point. + +Using mocks this test would look quite different. + +```java +class OrderInteractionTester... + public void testOrderSendsMailIfUnfilled() { + Order order = new Order(TALISKER, 51); + Mock warehouse = mock(Warehouse.class); + Mock mailer = mock(MailService.class); + order.setMailer((MailService) mailer.proxy()); + + mailer.expects(once()).method("send"); + warehouse.expects(once()).method("hasInventory") + .withAnyArguments() + .will(returnValue(false)); + + order.fill((Warehouse) warehouse.proxy()); + } +} +``` + +In both cases I'm using a test double instead of the real mail service. There is a difference in that the stub uses state verification while the mock uses behavior verification. + +In order to use state verification on the stub, I need to make some extra methods on the stub to help with verification. As a result the stub implements `MailService` but adds extra test methods. + +Mock objects always use behavior verification, a stub can go either way. Meszaros refers to stubs that use behavior verification as a Test Spy. The difference is in how exactly the double runs and verifies and I'll leave that for you to explore on your own.[Mocks Aren't Stubs] -**Mock 对象**和 **桩(Stub)**在逻辑上都是 `Optional` 的变体。他们都是最终程序中所使用的“实际”对象的代理。不过,Mock 对象和桩都是假扮成那些可以传递实际信息的实际对象,而不是像 `Optional` 那样把包含潜在 `null` 值的对象隐藏。 -Mock 对象和桩之间的的差别在于程度不同。Mock 对象往往是轻量级的,且用于自测试。通常,为了处理各种不同的测试场景,我们会创建出很多 Mock 对象。而桩只是返回桩数据,它通常是重量级的,并且经常在多个测试中被复用。桩可以根据它们被调用的方式,通过配置进行修改。因此,桩是一种复杂对象,它可以做很多事情。至于 Mock 对象,如果你要做很多事,通常会创建大量又小又简单的 Mock 对象。 + ## 接口和类型 `interface` 关键字的一个重要目标就是允许程序员隔离组件,进而降低耦合度。使用接口可以实现这一目标,但是通过类型信息,这种耦合性还是会传播出去——接口并不是对解耦的一种无懈可击的保障。比如我们先写一个接口: @@ -2167,9 +2222,9 @@ B 通过使用 RTTI,我们发现 `a` 是用 `B` 实现的。通过将其转型为 `B`,我们可以调用不在 `A` 中的方法。 -这样的操作完全是合情合理的,但是你也许并不想让客户端开发者这么做,因为这给了他们一个机会,使得他们的代码与你的代码的耦合度超过了你的预期。也就是说,你可能认为 `interface` 关键字正在保护你,但其实并没有。另外,在本例中使用 `B` 来实现 `A` 这种情况是有公开案例可查的[^3]。 +这样的操作完全是合情合理的,但是你也许并不想让客户端开发者这么做,因为这给了他们一个机会,使得他们的代码与你的代码的耦合度超过了你的预期。也就是说,你可能认为 `interface` 关键字正保护着你,然而并没有。另外,在本例中使用 `B` 来实现 `A` 这种情况是有公开案例可查的[^3]。 -一种解决方案是直接声明,如果开发者决定使用实际的类而不是接口,他们需要自己对自己负责。这在很多情况下都是可行的,但“可能”还不够,你或许希望能有一些更严格的控制方式。 +一种解决方案是直接声明,如果开发者决定使用实际的类而不是接口,他们需要自己对自己负责。 在许多情况下,这可能是合理的,但如果这种“可能”还不足够的话,你会用更严格的控制方式。 最简单的方式是让实现类只具有包访问权限,这样在包外部的客户端就看不到它了: @@ -2473,7 +2528,7 @@ RTTI 允许通过匿名类的引用来获取类型信息。初学者极易误用 然而使用多态机制的方法调用,要求我们拥有基类定义的控制权。因为在你扩展程序的时候,可能会发现基类并未包含我们想要的方法。如果基类来自别人的库,这时 RTTI 便是一种解决之道:可继承一个新类,然后添加你需要的方法。在代码的其它地方,可以检查你自己特定的类型,并调用你自己的方法。这样做不会破坏多态性以及程序的扩展能力,因为这样添加一个新的类并不需要修改程序中的 `switch` 语句。但如果想在程序中增加具有新特性的代码,你就必须使用 RTTI 来检查这个特定的类型。 -如果只是为了方便某个特定的类,就将某个特性放进基类里边,这将使得从那个基类派生出的所有其它子类都带有这些可能毫无意义的东西。这会导致接口更加不清晰,因为我们必须覆盖从基类继承而来的所有抽象方法,事情就变得很麻烦。举个例子,现在有一个表示乐器 `Instrument` 的类层次结构。假设我们想清理管弦乐队中某些乐器残留的口水,一种办法是在基类 `Instrument` 中放入 `clearSpitValve()` 方法。但这样做会导致类结构混乱,因为这意味着打击乐器 `Percussion`、弦乐器 `Stringed` 和电子乐器 `Electronic` 也需要清理口水。在这个例子中,RTTI 可以提供一种更合理的解决方案。可以将 `clearSpitValve()` 放在某个合适的类中,在这个例子中是管乐器 `Wind`。不过,在这里你可能会发现还有更好的解决方法,就是将 `prepareInstrument()` 放在基类中,但是初次面对这个问题的读者可能想不到还有这样的解决方案,而误认为必须使用 RTTI。 +如果只是为了方便某个特定的类,就将某个特性放进基类里边,这将使得从那个基类派生出的所有其它子类都带有这些可能毫无意义的东西。这会导致接口更加不清晰,因为我们必须重写从基类继承而来的所有抽象方法,事情就变得很麻烦。举个例子,现在有一个表示乐器 `Instrument` 的类层次结构。假设我们想清理管弦乐队中某些乐器残留的口水,一种办法是在基类 `Instrument` 中放入 `clearSpitValve()` 方法。但这样做会导致类结构混乱,因为这意味着打击乐器 `Percussion`、弦乐器 `Stringed` 和电子乐器 `Electronic` 也需要清理口水。在这个例子中,RTTI 可以提供一种更合理的解决方案。可以将 `clearSpitValve()` 放在某个合适的类中,在这个例子中是管乐器 `Wind`。不过,在这里你可能会发现还有更好的解决方法,就是将 `prepareInstrument()` 放在基类中,但是初次面对这个问题的读者可能想不到还有这样的解决方案,而误认为必须使用 RTTI。 最后一点,RTTI 有时候也能解决效率问题。假设你的代码运用了多态,但是为了实现多态,导致其中某个对象的效率非常低。这时候,你就可以挑出那个类,使用 RTTI 为它编写一段特别的代码以提高效率。然而必须注意的是,不要太早地关注程序的效率问题,这是个诱人的陷阱。最好先让程序能跑起来,然后再去看看程序能不能跑得更快,下一步才是去解决效率问题(比如使用 Profiler)[^5]。 diff --git a/docs/book/20-Generics.md b/docs/book/20-Generics.md index d93b82ed..767ac344 100644 --- a/docs/book/20-Generics.md +++ b/docs/book/20-Generics.md @@ -6,9 +6,9 @@ > 普通的类和方法只能使用特定的类型:基本数据类型或类类型。如果编写的代码需要应用于多种类型,这种严苛的限制对代码的束缚就会很大。 -多态是一种面向对象思想的泛化机制。你可以将方法的参数类型设为基类,这样的方法就可以接受任何派生类作为参数,包括暂时还不存在的类。这样的方法更通用,应用范围更广。在类内部也是如此,在任何使用特定类型的地方,基类意味着更大的灵活性。除了 `final` 类(或只提供私有构造函数的类)任何类型都可被扩展,所以大部分时候这种灵活性是自带的。 +多态是一种面向对象思想的泛化工具。你可以将方法的参数类型设为基类,这样的方法就可以接受任何派生类作为参数,包括暂时还不存在的类。这样的方法更通用,应用范围更广。在类内部也是如此,在任何使用特定类型的地方,基类意味着更大的灵活性。除了 `final` 类(或只提供私有构造函数的类)之外的任何类型都可被扩展,所以大部分时候这种灵活性是自带的。 -拘泥于单一的继承体系太过局限,因为只有继承体系中的对象才能适用基类作为参数的方法中。如果方法以接口而不是类作为参数,限制就宽松多了,只要实现了接口就可以。这给予调用方一种选项,通过调整现有的类来实现接口,满足方法参数要求。接口可以突破继承体系的限制。 +单一的继承层次限制性太强,因为你必须从该层次结构中继承,才能产生一个适配你的方法参数的对象。如果一个方法参数是一个接口而不是一个类,那么限制就会放宽,包括任何实现了接口的东西。这就给了客户程序员一个选择,将接口与现有的类结合起来实现——也就是说,可以将现有的类改编成适配你的方法。接口可以跨越类的继承层次结构,只要你可以选择实现这些接口来适配。 即便是接口也还是有诸多限制。一旦指定了接口,它就要求你的代码必须使用特定的接口。而我们希望编写更通用的代码,能够适用“非特定的类型”,而不是一个具体的接口或类。 @@ -126,7 +126,7 @@ public class Diamond { 有时一个方法需要能返回多个对象。而 **return** 语句只能返回单个对象,解决方法就是创建一个对象,用它打包想要返回的多个对象。当然,可以在每次需要的时候,专门创建一个类来完成这样的工作。但是有了泛型,我们就可以一劳永逸。同时,还获得了编译时的类型安全。 -这个概念称为*元组*,它是将一组对象直接打包存储于单一对象中。可以从该对象读取其中的元素,但不允许向其中存储新对象(这个概念也称为 *数据传输对象* 或 *信使* )。 +这个概念称为 *元组* *tuple*,它是将一组对象包裹在一起,形成一个对象。 允许对象的接收者读取元素,但不允许将新元素放入其中。(这个概念也称为 *数据传输对象* *Data Transfer Object* 或 *信使* *Messenger*)。 通常,元组可以具有任意长度,元组中的对象可以是不同类型的。不过,我们希望能够为每个对象指明类型,并且从元组中读取出来时,能够得到正确的类型。要处理不同长度的问题,我们需要创建多个不同的元组。下面是一个可以存储两个对象的元组: @@ -149,7 +149,7 @@ public class Tuple2 { 构造函数传入要存储的对象。这个元组隐式地保持了其中元素的次序。 -初次阅读上面的代码时,你可能认为这违反了 Java 编程的封装原则。`a1` 和 `a2` 应该声明为 **private**,然后提供 `getFirst()` 和 `getSecond()` 取值方法才对呀?考虑下这样做能提供的“安全性”是什么:元组的使用程序可以读取 `a1` 和 `a2` 然后对它们执行任何操作,但无法对 `a1` 和 `a2` 重新赋值。例子中的 `final` 可以实现同样的效果,并且更为简洁明了。 +初次阅读上面的代码时,你可能认为这违反了 Java 编程封装的安全性原则。`a1` 和 `a2` 应该声明为 **private**,然后提供 `getFirst()` 和 `getSecond()` 取值方法才对呀?考虑下这样做提供的所谓“安全性”实际是什么:元组的使用程序可以读取 `a1` 和 `a2` 然后对它们执行任何操作,但无法对 `a1` 和 `a2` 重新赋值。而例子中的 `final` 就可以实现同样的效果,并且更为简洁明了。 另一种设计思路是允许元组的用户给 `a1` 和 `a2` 重新赋值。然而,采用上例中的形式无疑更加安全,如果用户想存储不同的元素,就会强制他们创建新的 `Tuple2` 对象。 @@ -292,7 +292,7 @@ public class LinkedStack { } } - private Node top = new Node<>(); // 栈顶 + private Node top = new Node<>(); // 末端标识 public void push(T item) { top = new Node<>(item, top); @@ -529,15 +529,19 @@ public class Fibonacci implements Supplier { 如果还想更进一步,编写一个实现了 `Iterable` 的 `Fibnoacci` 生成器。我们的一个选择是重写这个类,令其实现 `Iterable` 接口。不过,你并不是总能拥有源代码的控制权,并且,除非必须这么做,否则,我们也不愿意重写一个类。而且我们还有另一种选择,就是创建一个 *适配器* (Adapter) 来实现所需的接口,我们在前面介绍过这个设计模式。 -有多种方法可以实现适配器。例如,可以通过继承来创建适配器类: +有多种方法可以实现适配器。例如,可以通过继承来创建适配器类 : + +``` +【类适配器模式】新定义一个适配器类,它实现了需求对应的接口,并继承现有的业务类, +通过在接口方法中显式地调用父类的方法的方式,达到适应新需求同时又复用现有方法代码的目的 +``` ```java // generics/IterableFibonacci.java // Adapt the Fibonacci class to make it Iterable import java.util.*; -public class IterableFibonacci -extends Fibonacci implements Iterable { +public class IterableFibonacci extends Fibonacci implements Iterable { private int n; public IterableFibonacci(int count) { n = count; } @@ -579,7 +583,7 @@ extends Fibonacci implements Iterable { 到目前为止,我们已经研究了参数化整个类。其实还可以参数化类中的方法。类本身可能是泛型的,也可能不是,不过这与它的方法是否是泛型的并没有什么关系。 -泛型方法独立于类而改变方法。作为准则,请“尽可能”使用泛型方法。通常将单个方法泛型化要比将整个类泛型化更清晰易懂。 +泛型方法独立于类而改变方法。它的使用准则是:只要条件允许,应该随时随地使用泛型方法。通常将单个方法泛型化要比将整个类泛型化更清晰易懂。 如果方法是 **static** 的,则无法访问该类的泛型类型参数,因此,如果使用了泛型类型参数,则它必须是泛型方法。 @@ -617,7 +621,7 @@ GenericMethods 对于泛型类,必须在实例化该类时指定类型参数。使用泛型方法时,通常不需要指定参数类型,因为编译器会找出这些类型。 这称为 *类型参数推断*。因此,对 `f()` 的调用看起来像普通的方法调用,并且 `f()` 看起来像被重载了无数次一样。它甚至会接受 **GenericMethods** 类型的参数。 -如果使用基本类型调用 `f()` ,自动装箱就开始起作用,自动将基本类型包装在它们对应的包装类型中。 +如果调用 `f()` 使用了基本类型 ,自动装箱就开始起作用,自动将基本类型包装在它们对应的包装类型中。 ### 变长参数和泛型方法 @@ -659,12 +663,35 @@ S, T, U, V, W, X, Y, Z] 此处显示的 `makeList()` 方法产生的功能与标准库的 `java.util.Arrays.asList()` 方法相同。 -`@SafeVarargs` 注解保证我们不会对变长参数列表进行任何修改,这是正确的,因为我们只从中读取。如果没有此注解,编译器将无法知道这些并会发出警告。 +`@SafeVarargs` 注解保证我们不会对变长参数列表进行任何修改,这是正确的,因为我们只从中读取。如果没有此注解,编译器就无法知道,并发出警告。 + +```java +// Mixing generics and varargs can violate type safety! +static void dangerous(List... stringLists) { + List intList = List.of(42); + Object[] objects = stringLists; + objects[0] = intList; // Heap pollution + String s = stringLists[0].get(0); // ClassCastException +} +//当参数化类型的变量引用不属于该类型的对象时,会发生堆污染(Heap pollution ) +``` + +本质上,SafeVarargs注释代表了该方法作者的一个承诺,它是类型安全的(the SafeVarargs annotation constitutes a promise by the author of a method that it is typesafe)。作为对此承诺的交换,编译器同意不会警告用户——调用该方法可能是不安全的。 + +除非方法实际上是安全的,否则不要使用`@SafeVarargs`注释方法,这点至关重要。所以确保这一点【方法是安全的】需要什么呢?回想一下,在调用方法时会创建一个泛型数组,用来保存可变参数。如果方法没有将任何内容存储到数组中(这会覆盖参数)并且不允许对数组的引用进行转义(这会使不受信任的代码访问数组),那么它就是安全的。换句话说,如果可变参数数组仅用于从调用者向方法传递可变数量的参数——毕竟这是可变参数的目的——那么该方法就是安全的。 + +决定何时使用SafeVarargs注释的规则很简单:**在每个方法上使用`@SafeVarargs`,使用泛型或参数化类型的可变参数,** 这样其用户就不用承担不必要和令人困惑的编译器警告的负担。这意味着你永远不应该编写像dangerous或toArray这样的不安全的可变参数方法。每次编译器在你控制的方法中警告你可能存在来自泛型可变参数的堆污染时,请检查该方法是否安全。提醒一下,如果符合以下条件,泛型可变参数方法是安全的: + +  1、它不会在可变参数数组中存储任何内容。 +  2、它不会使数组(或克隆出来的数组)对不受信任的代码可见。 + +  请注意,`@SafeVarargs`注释仅对无法覆盖的方法是合法的,因为无法保证每个可能的重写方法都是安全的。在Java 8中,注释仅对静态方法和final的实例方法合法; 在Java 9中,它在private实例方法上也是合法的。 + ### 一个泛型的 Supplier -这是一个为任意具有无参构造方法的类生成 **Supplier** 的类。为了减少键入,它还包括一个用于生成 **BasicSupplier** 的泛型方法: +这是一个类,为任何具有无参构造方法的类生成 **Supplier** 。为了减少键入,它还包括一个用于生成 **BasicSupplier** 的泛型方法: ```java // onjava/BasicSupplier.java @@ -767,18 +794,15 @@ public class Tuple { return new Tuple2<>(a, b); } - public static Tuple3 - tuple(A a, B b, C c) { + public static Tuple3 tuple(A a, B b, C c) { return new Tuple3<>(a, b, c); } - public static Tuple4 - tuple(A a, B b, C c, D d) { + public static Tuple4 tuple(A a, B b, C c, D d) { return new Tuple4<>(a, b, c, d); } - public static - Tuple5 tuple(A a, B b, C c, D d, E e) { + public static Tuple5 tuple(A a, B b, C c, D d, E e) { return new Tuple5<>(a, b, c, d, e); } } @@ -810,14 +834,11 @@ public class TupleTest2 { } static Tuple4 h() { - return tuple( - new Vehicle(), new Amphibian(), "hi", 47); + return tuple(new Vehicle(), new Amphibian(), "hi", 47); } - static Tuple5 k() { - return tuple(new Vehicle(), new Amphibian(), - "hi", 47, 11.1); + static Tuple5 k() { + return tuple(new Vehicle(), new Amphibian(), "hi", 47, 11.1); } public static void main(String[] args) { @@ -860,16 +881,14 @@ public class Sets { return result; } - public static - Set intersection(Set a, Set b) { + public static Set intersection(Set a, Set b) { Set result = new HashSet<>(a); result.retainAll(b); return result; } // Subtract subset from superset: - public static Set - difference(Set superset, Set subset) { + public static Set difference(Set superset, Set subset) { Set result = new HashSet<>(superset); result.removeAll(subset); return result; @@ -884,7 +903,7 @@ public class Sets { 前三个方法通过将第一个参数的引用复制到新的 **HashSet** 对象中来复制第一个参数,因此不会直接修改参数集合。因此,返回值是一个新的 **Set** 对象。 -这四种方法代表数学集合操作: `union()` 返回一个包含两个参数并集的 **Set** , `intersection()` 返回一个包含两个参数集合交集的 **Set** , `difference()` 从 **superset** 中减去 **subset** 的元素 ,而 `complement()` 返回所有不在交集中的元素的 **Set**。作为显示这些方法效果的简单示例的一部分,下面是一个包含不同水彩名称的 **enum** : +这四种方法代表数学集合操作: `union()` 返回一个包含两个参数并集的 **Set** , `intersection()` 返回一个包含两个参数集合交集的 **Set** , `difference()` 从 **superset** 中减去 **subset** 后剩余的元素 (差集),而 `complement()` 返回的 **Set** 包含所有不在交集中的元素(交集的补集)。作为显示这些方法效果的简单示例的一部分,下面是一个包含不同水彩名称的 **enum** : ```java // generics/watercolors/Watercolors.java @@ -1106,8 +1125,7 @@ import onjava.Tuple4; import java.util.ArrayList; -public class TupleList - extends ArrayList> { +public class TupleList extends ArrayList> { public static void main(String[] args) { TupleList tl = new TupleList<>(); @@ -1237,7 +1255,7 @@ public class Store extends ArrayList { ## 泛型擦除 -当你开始更深入地钻研泛型时,会发现有大量的东西初看起来是没有意义的。例如,尽管可以说 `ArrayList.class`,但不能说成 `ArrayList.class`。考虑下面的情况: +当你开始更深入地钻研泛型时,会发现有大量的东西初看起来是没有意义的。例如,虽然可以说 `ArrayList.class`,但不能说成 `ArrayList.class`。考虑以下情况: ```java // generics/ErasedTypeEquivalence.java @@ -1299,9 +1317,9 @@ public class LostInformation { 残酷的现实是: -在泛型代码内部,无法获取任何有关泛型参数类型的信息。 +> 在泛型代码内部,无法获取任何有关泛型参数类型的信息。 -因此,你可以知道如类型参数标识符和泛型边界这些信息,但无法得知实际的类型参数从而用来创建特定的实例。如果你曾是 C++ 程序员,那么这个事实会让你很沮丧,在使用 Java 泛型工作时,它是必须处理的最基本的问题。 +因此,你可以知道如类型参数标识符和泛型边界这些信息,但无法得知实际的类型参数从而用来创建特定的实例。如果你曾是 C++ 程序员,那么这个事实会让你很沮丧,在使用 Java 泛型工作时,这一事实是必须处理的最基本的问题。 Java 泛型是使用擦除实现的。这意味着当你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。因此,`List` 和 `List` 在运行时实际上是相同的类型。它们都被擦除成原生类型 `List`。 @@ -1399,9 +1417,9 @@ public class Manipulator2 { 边界 `` 声明 T 必须是 HasF 类型或其子类。如果情况确实如此,就可以安全地在 **obj** 上调用 `f()` 方法。 -我们说泛型类型参数会擦除到它的第一个边界(可能有多个边界,稍后你将看到)。我们还提到了类型参数的擦除。编译器实际上会把类型参数替换为它的擦除,就像上面的示例,**T** 擦除到了 **HasF**,就像在类的声明中用 **HasF** 替换了 **T** 一样。 +我们说泛型类型参数会擦除到它的第一个边界(可能有多个边界,稍后你将看到)【如果泛型参数是有界的, 那么编译之后会替换成边界, 否则的话会替换成`Object`】。我们还提到了类型参数的擦除。编译器实际上会把类型参数替换为它的擦除,因此在上述例子中,**T** 擦除到了 **HasF**,就像在类的声明中用 **HasF** 替换了 **T** 一样。 -你可能正确地观察到了泛型在 **Manipulator2.java** 中没有贡献任何事。你可以很轻松地自己去执行擦除,生成没有泛型的类: +你可能正确地观察到:泛型在 **Manipulator2.java** 中没有任何贡献。你可以很轻松地自己执行擦除,生成没有泛型的类: ```java // generics/Manipulator3.java @@ -1447,15 +1465,31 @@ public class ReturnGenericType { 如果 Java 1.0 就含有泛型的话,那么这个特性就不会使用擦除来实现——它会使用具体化,保持参数类型为第一类实体,因此你就能在类型参数上执行基于类型的语言操作和反射操作。本章稍后你会看到,擦除减少了泛型的泛化性。泛型在 Java 中仍然是有用的,只是不如它们本来设想的那么有用,而原因就是擦除。 -在基于擦除的实现中,泛型类型被当作第二类类型处理,即不能在某些重要的上下文使用泛型类型。泛型类型只有在静态类型检测期间才出现,在此之后,程序中的所有泛型类型都将被擦除,替换为它们的非泛型上界。例如, `List` 这样的类型注解会被擦除为 **List**,普通的类型变量在未指定边界的情况下会被擦除为 **Object**。 +第一类对象(First-class Object)在1960年由Christopher Strachey发明,原来称之为一等公民(First-class citizen),意思是指函数可以作为电脑中的一等公民。英文中也称之为First-class entity或First-class value。 + + +> +> 闲话:很多资料把 first-class object 翻译成 “第一类对象”,我觉得还是翻译成 “一等对象” 比较好,因为它明显借用了英语中 “一等公民” first-class citizen 的说法。 + + + +第一类对象不一定是指面向对象程序设计中所指的对象,而是指程序中的所有实体(比如:变量、函数、队列、字典等等)。一般第一类对象具有一下特征: + +- 可以被存入变量或其他结构 +- 可以被作为参数传递给其他方法/函数 +- 可以被作为方法/函数的返回值 +- 可以在执行期被创建,而无需在设计期全部写出 +- 有固定身份 + +在基于擦除的实现中,泛型类型被当作第二类对象类型处理,即不能在某些重要的上下文使用泛型类型。泛型类型只有在静态类型检测期间才出现,在此之后,程序中的所有泛型类型都将被擦除,替换为它们的非泛型上界。例如, `List` 这样的类型注解会被擦除为 **List**,普通的类型变量在未指定边界的情况下会被擦除为 **Object**。 擦除的核心动机是你可以在泛化的客户端上使用非泛型的类库,反之亦然。这经常被称为“迁移兼容性”。在理想情况下,所有事物将在指定的某天被泛化。在现实中,即使程序员只编写泛型代码,他们也必须处理 Java 5 之前编写的非泛型类库。这些类库的作者可能从没想过要泛化他们的代码,或许他们可能刚刚开始接触泛型。 因此 Java 泛型不仅必须支持向后兼容性——现有的代码和类文件仍然合法,继续保持之前的含义——而且还必须支持迁移兼容性,使得类库能按照它们自己的步调变为泛型,当某个类库变为泛型时,不会破坏依赖于它的代码和应用。在确定了这个目标后,Java 设计者们和从事此问题相关工作的各个团队决策认为擦除是唯一可行的解决方案。擦除使得这种向泛型的迁移成为可能,允许非泛型的代码和泛型代码共存。 -例如,假设一个应用使用了两个类库 **X** 和 **Y**,**Y** 使用了类库 **Z**。随着 Java 5 的出现,这个应用和这些类库的创建者最终可能希望迁移到泛型上。但是当进行迁移时,它们有着不同的动机和限制。为了实现迁移兼容性,每个类库与应用必须与其他所有的部分是否使用泛型无关。因此,它们不能探测其他类库是否使用了泛型。因此,某个特定的类库使用了泛型这样的证据必须被”擦除“。 +例如,假设一个应用使用了两个类库 **X** 和 **Y**,**Y** 使用了类库 **Z**。随着 Java 5 的出现,这个应用和这些类库的创建者最终可能希望迁移到泛型上。但是当进行迁移时,它们有着不同的动机和限制。为了实现迁移兼容性,每个类库与应用必须与其他所有的部分是否使用泛型无关。因此,它们不能探测其他类库是否使用了泛型。因此,某个特定的类库使用了泛型的证据必须被”擦除“。 -如果没有某种类型的迁移途径,所有已经构建了很长时间的类库就需要与希望迁移到 Java 泛型上的开发者们说再见了。类库毫无争议是编程语言的一部分,对生产效率有着极大的影响,所以这种代码无法接受。擦除是否是最佳的或唯一的迁移途径,还待时间来证明。 +如果没有某种类型的迁移途径,所有已经构建了很长时间的类库就需要与选择迁移到 Java 泛型上的开发者们说再见了。类库可以说是编程语言中对生产力影响最大的部分,所以这不是一个可以接受的代价。擦除是否是最好的或唯一的迁移路径,只有时间才能证明。 ### 擦除的问题 @@ -1509,7 +1543,7 @@ public class ErasureAndInteritance { public static void main(String[] args) { Derived2 d2 = new Derived2(); Object obj = d2.get(); - d2.set(obj); // Warning here! + d2.set(obj); // Warning here! -> Unchecked call to 'set(T)' as a member of raw type 'GenericBase' } } ``` @@ -1524,13 +1558,13 @@ public class ErasureAndInteritance { 这个注解放置在产生警告的方法上,而不是整个类上。当你要关闭警告时,最好尽可能地“聚焦”,这样就不会因为过于宽泛地关闭警告,而导致意外地遮蔽掉真正的问题。 -可以推断,**Derived3** 产生的错误意味着编译器期望得到一个原生基类。 +可以推断,**Derived3** 产生的错误意味着编译器期望得到一个原生基类( No wildcard expected )。 当你希望将类型参数不仅仅当作 Object 处理时,就需要付出额外努力来管理边界,并且与在 C++、Ada 和 Eiffel 这样的语言中获得参数化类型相比,你需要付出多得多的努力来获得少得多的回报。这并不是说,对于大多数的编程问题而言,这些语言通常都会比 Java 更得心应手,只是说它们的参数化类型机制相比 Java 更灵活、更强大。 ### 边界处的动作 -因为擦除,我发现了泛型最令人困惑的方面是可以表示没有任何意义的事物。例如: +因为擦除,我发现了泛型最令人困惑的方面是——可以表示没有任何意义的事物。例如: ```java // generics/ArrayMaker.java @@ -1561,7 +1595,7 @@ public class ArrayMaker { */ ``` -即使 **kind** 被存储为 `Class`,擦除也意味着它实际被存储为没有任何参数的 **Class**。因此,当你在使用它时,例如创建数组,`Array.newInstance()` 实际上并未拥有 **kind** 所蕴含的类型信息。所以它不会产生具体的结果,因而必须转型,这会产生一条令你无法满意的警告。 +即使 **kind** 被存储为 `Class`,擦除也意味着它实际被存储为没有任何参数的 **Class**。因此,当你在使用它时,例如创建数组,`Array.newInstance()` 实际上并未拥有 **kind** 所蕴含的类型信息。所以它不会产生特定类型的结果,因而必须转型,这会产生一条碍眼的警告。 注意,对于在泛型中创建数组,使用 `Array.newInstance()` 是推荐的方式。 @@ -1622,7 +1656,7 @@ public class FilledList extends ArrayList { 即使编译器无法得知 `add()` 中的 **T** 的任何信息,但它仍可以在编译期确保你放入 **FilledList** 中的对象是 **T** 类型。因此,即使擦除移除了方法或类中的实际类型的信息,编译器仍可以确保方法或类中使用的类型的内部一致性。 -因为擦除移除了方法体中的类型信息,所以在运行时的问题就是*边界*:即对象进入和离开方法的地点。这些正是编译器在编译期执行类型检查并插入转型代码的地点。 +因为擦除移除了方法体中的类型信息,所以在运行时重要的是*边界*:即对象进入和离开方法的地点。这些点正是编译器在编译期执行类型检查并插入转型代码的地方。 考虑如下这段非泛型示例: @@ -1677,7 +1711,7 @@ public static void main(java.lang.String[]); 22: return ``` -`set()` 和 `get()` 方法存储和产生值,转型在调用 `get()` 时接受检查。 +`set()` 和 `get()` 方法分别存储和产生值,转型在调用 `get()` 时接受检查。 现在将泛型融入上例代码中: @@ -1732,9 +1766,9 @@ public static void main(java.lang.String[]); 22: return ``` -所产生的字节码是相同的。对进入 `set()` 的类型进行检查是不需要的,因为这将由编译器执行。而对 `get()` 返回的值进行转型仍然是需要的,只不过不需要你来操作,它由编译器自动插入,这样你就不用编写(阅读)杂乱的代码。 +所产生的字节码是相同的。对进入 `set()` 的类型进行检查是不需要的,因为这将由编译器执行。而对 `get()` 返回的值进行转型仍然是需要的,只不过不需要你来操作,它由编译器自动插入,这样你就不用编写和阅读杂乱的代码。 -`get()` 和 `set()` 产生了相同的字节码,这就告诉我们泛型的所有动作都发生在边界处——对入参的编译器检查和对返回值的转型。这有助于澄清对擦除的困惑,记住:“边界就是动作发生的地方”。 +`get()` 和 `set()` 产生了相同的字节码,这就告诉我们泛型的所有动作都发生在边界——对入参进行额外的编译期检查,以及对返回值插入的转型。记住:“边界就是动作发生的地方”,这有助于解除对擦除的疑惑。 @@ -1764,7 +1798,7 @@ public class Erased { } ``` -有时,我们可以对这些问题进行编程,但是有时必须通过引入类型标签来补偿擦除。这意味着为所需的类型显式传递一个 **Class** 对象,以在类型表达式中使用它。 +有时,我们可以对这些问题进行编程,但是有时必须通过引入**类型标签**来补偿擦除。这意味着为所需的类型显式传递一个 **Class** 对象,以在类型表达式中使用它。 例如,由于擦除了类型信息,因此在上一个程序中尝试使用 **instanceof** 将会失败。类型标签可以使用动态 `isInstance()` : @@ -1834,7 +1868,7 @@ int main() { } ``` -Java 中的解决方案是传入一个工厂对象,并使用该对象创建新实例。方便的工厂对象只是 **Class** 对象,因此,如果使用类型标记,则可以使用 `newInstance()` 创建该类型的新对象: +Java 中的解决方案是传入一个工厂对象,并使用该对象创建新实例。方便的工厂对象只是 **Class** 对象,因此,如果使用类型标记/**类型标签**,则可以使用 `newInstance()` 创建该类型的新对象: ```java // generics/InstantiateGenericType.java @@ -1886,7 +1920,7 @@ java.lang.InstantiationException: java.lang.Integer */ ``` -这样可以编译,但对于 `ClassAsFactory` 会失败,这是因为 **Integer** 没有无参构造函数。由于错误不是在编译时捕获的,因此语言创建者不赞成这种方法。他们建议使用显式工厂(**Supplier**)并约束类型,以便只有实现该工厂的类可以这样创建对象。这是创建工厂的两种不同方法: +这样可以编译,但对于 `ClassAsFactory` 会失败,这是因为 **Integer** 没有无参构造函数。由于错误不是在编译时捕获的,因此语言创建者不赞成这种方法。他们建议使用明确的工厂(**Supplier**)并约束类型,以便只有实现该工厂的类可以这样创建对象。下面是创建工厂的 2 种不同方法: ```java // generics/FactoryConstraint.java @@ -1969,7 +2003,7 @@ public class FactoryConstraint { */ ``` -**IntegerFactory** 本身就是通过实现 `Supplier` 的工厂。 **Widget** 包含一个内部类,它是一个工厂。还要注意,**Fudge** 并没有做任何类似于工厂的操作,并且传递 `Fudge::new` 仍然会产生工厂行为,因为编译器将对函数方法 `::new` 的调用转换为对 `get()` 的调用。 +**IntegerFactory** 本身就是通过实现 `Supplier` 的工厂。 **Widget** 包含一个内部类,它是一个工厂。还要注意,**Fudge** 并没有做任何类似于工厂的操作,而传递 `Fudge::new` 仍然会产生工厂行为,因为编译器将对函数方法 `::new` 的调用转换为对 `get()` 的调用。 另一种方法是模板方法设计模式。在以下示例中,`create()` 是模板方法,在子类中被重写以生成该类型的对象: @@ -2018,7 +2052,7 @@ X ### 泛型数组 -正如在 **Erased.java** 中所看到的,我们无法创建泛型数组。通用解决方案是在试图创建泛型数组的时候使用 **ArrayList** : +正如在 **Erased.java** 中所看到的,我们无法创建泛型数组( `new T[SIZE] `)。通用解决方案是在试图创建泛型数组的时候使用 **ArrayList** : ```java // generics/ListOfGenerics.java @@ -2071,7 +2105,7 @@ public class ArrayOfGeneric { System.out.println(e.getMessage()); } // Runtime type is the raw (erased) type: - gia = (Generic[]) new Generic[SIZE]; + gia = (Generic[]) new Generic[SIZE]; //唯一方法 System.out.println(gia.getClass().getSimpleName()); gia[0] = new Generic<>(); //- gia[1] = new Object(); // Compile-time error @@ -2083,9 +2117,32 @@ public class ArrayOfGeneric { [Ljava.lang.Object; cannot be cast to [LGeneric; Generic[] */ +//String型数组的类型是用“[Ljava.lang.String”,数组类型的表示: +// a. " [ "表示一维数组, "[[ "二维数组 +// b. "L"代表这个数组是引用数据类型的数组. "java.lang.String"是数组元素的类型,标识这个数组是什么类型的数组.基本数据类型的每种类型都有自已对应的标识符,如int[] 的类型是"[I"表示它是int型的数组 ``` -问题在于数组会跟踪其实际类型,而该类型是在创建数组时建立的。因此,即使 `gia` 被强制转换为 `Generic[]` ,该信息也仅在编译时存在(并且没有 **@SuppressWarnings** 注解,将会收到有关该强制转换的警告)。在运行时,它仍然是一个 **Object** 数组,这会引起问题。成功创建泛型类型的数组的唯一方法是创建一个已擦除类型的新数组,并将其强制转换。 +问题在于数组会跟踪其实际类型,而该类型是在创建数组时建立的。因此,即使 `gia` 被强转为 `Generic[]` ,该信息也仅在编译时存在(并且没有 **@SuppressWarnings** 注解,将会收到有关该强制转换的警告)。在运行时,它仍然是一个 **Object** 数组,这会引起问题。成功创建泛型类型的数组的唯一方法:创建一个已擦除类型的新数组,并将其强制转换。 + +```java +try { + Object obj = new Object(); + String str = (String)obj; //如果一个[Ljava.lang.Object 类型的实例并不指向[Ljava.lang.String类型的对象,强转肯定报错 +} catch (Exception e) { + System.err.println("not OK! "+e); +} + +try { + String str = new String(); + Object obj = str; + String str2 = (String)obj; + System.err.println("OK!"); +} catch (Exception e) { + System.err.println(e); +} +//not OK! java.lang.ClassCastException: java.lang.Object cannot be cast to java.lang.String +//OK! +``` 让我们看一个更复杂的示例。考虑一个包装数组的简单泛型包装器: @@ -2130,9 +2187,9 @@ public class GenericArray { */ ``` -和以前一样,我们不能说 `T[] array = new T[sz]` ,所以我们创建了一个 **Object** 数组并将其强制转换。 +和之前一样,我们不能说 `T[] array = new T[sz]` ,所以我们创建了一个 **Object** 数组并将其强制转换。 -`rep()` 方法返回一个 `T[]` ,在主方法中它应该是 `gai` 的 `Integer[]`,但是如果调用它并尝试将结果转换为 `Integer[]` 引用,则会得到 **ClassCastException** ,这再次是因为实际的运行时类型为 `Object[]` 。 +`rep()` 方法返回一个 `T[]` ,在主方法中它应该是 `gai` 的 `Integer[]`,但是如果调用它并尝试将结果转换为 `Integer[]` 引用,则会得到 **ClassCastException** ,这又是因为实际的运行时类型为 `Object[]` 。 如果再注释掉 **@SuppressWarnings** 注解后编译 **GenericArray.java** ,则编译器会产生警告: @@ -2146,10 +2203,17 @@ Recompile with -Xlint:unchecked for details. 但是要真正确定,请使用 `-Xlint:unchecked` 进行编译: ```java -GenericArray.java:7: warning: [unchecked] unchecked cast array = (T[])new Object[sz]; ^ required: T[] found: Object[] where T is a type-variable: T extends Object declared in class GenericArray 1 warning +GenericArray.java:7: warning: [unchecked] unchecked cast + array = (T[])new Object[sz]; + ^ +required: T[] +found: Object[] +where T is a type-variable: + T extends Object declared in class GenericArray +1 warning ``` -确实是在抱怨那个强制转换。由于警告会变成噪音,因此,一旦我们确认预期会出现特定警告,我们可以做的最好的办法就是使用 **@SuppressWarnings** 将其关闭。这样,当警告确实出现时,我们将进行实际调查。 +确实是在抱怨那个强制转换。由于警告会变成噪音,因此,一旦我们确认预期会出现特定警告,最好的办法就是使用 **@SuppressWarnings** 将其关闭。这样,当警告确实出现时,我们将进行实际调查。 由于擦除,数组的运行时类型只能是 `Object[]` 。 如果我们立即将其转换为 `T[]` ,则在编译时会丢失数组的实际类型,并且编译器可能会错过一些潜在的错误检查。因此,最好在集合中使用 `Object[]` ,并在使用数组元素时向 **T** 添加强制类型转换。让我们来看看在 **GenericArray.java** 示例中会是怎么样的: @@ -2193,13 +2257,12 @@ public class GenericArray2 { } } /* Output: -0 1 2 3 4 5 6 7 8 9 -java.lang.ClassCastException: [Ljava.lang.Object; -cannot be cast to [Ljava.lang.Integer; +0 1 2 3 4 5 6 7 8 9 +java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer; */ ``` -最初,看起来并没有太大不同,只是转换的位置移动了。没有 **@SuppressWarnings** 注解,仍然会收到“unchecked”警告。但是,内部表示现在是 `Object[]` 而不是 `T[]` 。 调用 `get()` 时,它将对象强制转换为 **T** ,实际上这是正确的类型,因此很安全。但是,如果调用 `rep()` ,它将再次尝试将 `Object[]` 强制转换为 `T[]` ,但仍然不正确,并在编译时生成警告,并在运行时生成异常。因此,无法破坏基础数组的类型,该基础数组只能是 `Object[]` 。在内部将数组视为 `Object[]` 而不是 `T[]` 的优点是,我们不太可能会忘记数组的运行时类型并意外地引入了bug,尽管大多数(也许是全部)此类错误会在运行时被迅速检测到。 +最初,看起来并没有太大不同,只是转换的位置移动了。没有 **@SuppressWarnings** 注解,仍然会收到“unchecked”警告。但是,内部表示现在是 `Object[]` 而不是 `T[]` 。 调用 `get()` 时,它将对象强制转换为 **T** ,实际上这是正确的类型,因此很安全。但是,如果调用 `rep()` ,它将再次尝试将 `Object[]` 强制转换为 `T[]` ,但仍然不正确,并在编译时生成警告,并在运行时生成异常。因此,无法破坏基础数组的类型,该基础数组只能是 `Object[]` 。在内部将数组视为 `Object[]` 而不是 `T[]` 的优点是,我们不太可能会遗忘数组的运行时类型并意外地引入bug,而且这些bug大多数(也许是全部)会在运行时被迅速检测到。 对于新代码,请传入类型标记。在这种情况下,**GenericArray** 如下所示: @@ -2262,15 +2325,15 @@ Note: Recompile with -Xlint:unchecked for details. Neal Gafter(Java 5 的主要开发人员之一)在他的博客中[^2]指出,他在重写 Java 库时是很随意、马虎的,我们不应该像他那样做。Neal 还指出,他在不破坏现有接口的情况下无法修复某些 Java 库代码。因此,即使在 Java 库源代码中出现了一些习惯用法,它们也不一定是正确的做法。当查看库代码时,我们不能认为这就是要在自己代码中必须遵循的示例。 -请注意,在 Java 文献中推荐使用类型标记技术,例如 Gilad Bracha 的论文《Generics in the Java Programming Language》[^3],他指出:“例如,这种用法已广泛用于新的 API 中以处理注解。” 我发现此技术在人们对于舒适度的看法方面存在一些不一致之处;有些人强烈喜欢本章前面介绍的工厂方法。 +请注意,在 Java 文献中推荐使用类型标记技术,例如 Gilad Bracha 的论文《Generics in the Java Programming Language》[^3],他指出:“这种用法已广泛用于新的 API 中,例如用来处理注解。” 我发现此技术在人们对于舒适度的看法方面存在一些不一致之处;有些人强烈喜欢本章前面介绍的工厂方法。 ## 边界 -*边界*(bounds)在本章的前面进行了简要介绍。边界允许我们对泛型使用的参数类型施加约束。尽管这可以强制执行有关应用了泛型类型的规则,但潜在的更重要的效果是我们可以在绑定的类型中调用方法。 +*边界*(bounds)在本章的前面进行了简要介绍。边界允许我们对泛型使用的参数类型施加约束。这可以强制执行适用于某泛型的规则,而更重要的潜在效果是我们可以在绑定的类型中调用方法。 -由于擦除会删除类型信息,因此唯一可用于无限制泛型参数的方法是那些 **Object** 可用的方法。但是,如果将该参数限制为某类型的子集,则可以调用该子集中的方法。为了应用约束,Java 泛型使用了 `extends` 关键字。 +由于擦除会删除类型信息,所以对于一个无约束的泛型参数,你只能调用那些 **Object** 可用的方法。但是,如果将该参数限制为某类型的子集,则可以调用该子集中的方法。为了施加约束,Java 泛型重用了 `extends` 关键字。 重要的是要理解,当用于限定泛型类型时,`extends` 的含义与通常的意义截然不同。此示例展示边界的基础应用: @@ -2305,7 +2368,7 @@ class Coord { // This fails. Class must be first, then interfaces: // class WithColorCoord { -// Multiple bounds: +// Multiple bounds: 多个边界,类放在前,接口放在后 class WithColorCoord { T item; @@ -2339,7 +2402,7 @@ interface Weight { } // As with inheritance, you can have only one -// concrete class but multiple interfaces: +// concrete class but multiple interfaces: 一个实体类 和两个接口 class Solid { T item; @@ -2372,8 +2435,7 @@ class Solid { } } -class Bounded - extends Coord implements HasColor, Weight { +class Bounded extends Coord implements HasColor, Weight { @Override public java.awt.Color getColor() { return null; @@ -2396,7 +2458,7 @@ public class BasicBounds { } ``` -你可能会观察到 **BasicBounds.java** 中似乎包含一些冗余,它们可以通过继承来消除。在这里,每个继承级别还添加了边界约束: +你可能会观察到 **BasicBounds.java** 中似乎包含一些冗余,它们可以通过继承来消除(如下)。在这里每个继承级别还添加了边界约束: ```java // generics/InheritBounds.java @@ -2413,8 +2475,7 @@ class HoldItem { } } -class WithColor2 - extends HoldItem { +class WithColor2 extends HoldItem { WithColor2(T item) { super(item); } @@ -2424,8 +2485,7 @@ class WithColor2 } } -class WithColorCoord2 - extends WithColor2 { +class WithColorCoord2 extends WithColor2 { WithColorCoord2(T item) { super(item); } @@ -2443,8 +2503,7 @@ class WithColorCoord2 } } -class Solid2 - extends WithColorCoord2 { +class Solid2 extends WithColorCoord2 { Solid2(T item) { super(item); } @@ -2502,8 +2561,7 @@ class SuperHero { } } -class SuperSleuth - extends SuperHero { +class SuperSleuth extends SuperHero { SuperSleuth(POWER power) { super(power); } @@ -2513,9 +2571,7 @@ class SuperSleuth } } -class -CanineHero - extends SuperHero { +class CanineHero extends SuperHero { CanineHero(POWER power) { super(power); } @@ -2529,8 +2585,7 @@ CanineHero } } -class SuperHearSmell - implements SuperHearing, SuperSmell { +class SuperHearSmell implements SuperHearing, SuperSmell { @Override public void hearSubtleNoises() { } @@ -2579,7 +2634,7 @@ public class EpicBattle { 你已经在 [集合](book/12-Collections.md) 章节中看到了一些简单示例使用了通配符——在泛型参数表达式中的问号,在 [类型信息](book/19-Type-Information.md) 一章中这种示例更多。本节将更深入地探讨这个特性。 -我们的起始示例要展示数组的一种特殊行为:你可以将派生类的数组赋值给基类的引用: +我们从一个示例开始,它展示了数组的一种特殊行为:你可以将派生类的数组赋值给基类的引用: ```java // generics/CovariantArrays.java @@ -2620,11 +2675,11 @@ java.lang.ArrayStoreException: Orange `main()` 中的第一行创建了 **Apple** 数组,并赋值给一个 **Fruit** 数组引用。这是有意义的,因为 **Apple** 也是一种 **Fruit**,因此 **Apple** 数组应该也是一个 **Fruit** 数组。 -但是,如果实际的数组类型是 **Apple[]**,你可以在其中放置 **Apple** 或 **Apple** 的子类型,这在编译期和运行时都可以工作。但是你也可以在数组中放置 **Fruit** 对象。这对编译器来说是有意义的,因为它有一个 **Fruit[]** 引用——它有什么理由不允许将 **Fruit** 对象或任何从 **Fruit** 继承出来的对象(比如 **Orange**),放置到这个数组中呢?因此在编译期,这是允许的。然而,运行时的数组机制知道它处理的是 **Apple[]**,因此会在向数组中放置异构类型时抛出异常。 +但是,如果实际的数组类型是 **Apple[]**,你可以在其中放置 **Apple** 或 **Apple** 的子类型,这在编译期和运行时都可以工作。但是你也可以在数组中放置 **Fruit** 对象。这对编译器来说是有意义的,因为它有一个 **Fruit[]** 引用——它有什么理由不允许将 **Fruit** 对象或任何从 **Fruit** 继承出来的对象(比如 **Orange**),放置到这个数组中呢?因此在编译期,这是允许的。然而,【运行时的数组机制】知道它处理的是 **Apple[]**,因此【运行时的数组机制】会在放置异构(foreign)类型到数组时抛出异常。 -向上转型用在这里不合适。你真正在做的是将一个数组赋值给另一个数组。数组的行为是持有其他对象,这里只是因为我们能够向上转型而已,所以很明显,数组对象可以保留有关它们包含的对象类型的规则。看起来就像数组对它们持有的对象是有意识的,因此在编译期检查和运行时检查之间,你不能滥用它们。 +“向上转型”在这里不是恰当的描述。你真正在做的是将一个数组赋值给另一个数组。数组的行为是持有其他对象,但因为我们能够向上转型,所以很明显,数组对象可以保留有关它们所包含的对象类型的规则。这就好像数组是有意识地知道它们所容纳的(对象),因此在编译期检查和运行时检查都不能滥用数组。 -数组的这种赋值并不是那么可怕,因为在运行时你可以发现插入了错误的类型。但是泛型的主要目标之一是将这种错误检测移到编译期。所以当我们试图使用泛型集合代替数组时,会发生什么呢? +数组的这种赋值并不是那么可怕,因为在运行时你可以发现插入了错误的类型。但是泛型的主要目标之一是将这种错误检测提前到编译期。所以当我们试图使用泛型集合代替数组时,会发生什么呢? ```java // generics/NonCovariantGenerics.java @@ -2638,9 +2693,9 @@ public class NonCovariantGenerics { } ``` -尽管你在首次阅读这段代码时会认为“不能将一个 **Apple** 集合赋值给一个 **Fruit** 集合”。记住,泛型不仅仅是关于集合,它真正要表达的是“不能把一个涉及 **Apple** 的泛型赋值给一个涉及 **Fruit** 的泛型”。如果像在数组中的情况一样,编译器对代码的了解足够多,可以确定所涉及到的集合,那么它可能会留下一些余地。但是它不知道任何有关这方面的信息,因此它拒绝向上转型。然而实际上这也不是向上转型—— **Apple** 的 **List** 不是 **Fruit** 的 **List**。**Apple** 的 **List** 将持有 **Apple** 和 **Apple** 的子类型,**Fruit** 的 **List** 将持有任何类型的 **Fruit**。是的,这包括 **Apple**,但是它不是一个 **Apple** 的 **List**,它仍然是 **Fruit** 的 **List**。**Apple** 的 **List** 在类型上不等价于 **Fruit** 的 **List**,即使 **Apple** 是一种 **Fruit** 类型。 +尽管你在首次阅读这段代码时会认为“不能将一个 **Apple** 集合赋值给一个 **Fruit** 集合”。记住,泛型不仅仅是关于集合,它真正要表达的是“不能把一个 **Apple** 的泛型赋值给一个 **Fruit** 的泛型”。如果和在数组中的情况一样,编译器对代码的了解足够多,可以确定所涉及到的集合,那么它可能会留下一些余地。但是它不知道任何有关这方面的信息,因此它不允许向上转型。然而实际上这也不是向上转型—— **Apple** 的 **List** 不是 **Fruit** 的 **List**。**Apple** 的 **List** 将持有 **Apple** 和 **Apple** 的子类型,**Fruit** 的 **List** 将持有任何类型的 **Fruit**。是的,这包括 **Apple**,但是它不是一个 **Apple** 的 **List**,它仍然是 **Fruit** 的 **List**。**Apple** 的 **List** 在类型上不等价于 **Fruit** 的 **List**,即使 **Apple** 是一种 **Fruit** 类型。 -真正的问题是我们在讨论的集合类型,而不是集合持有对象的类型。与数组不同,泛型没有内建的协变类型。这是因为数组是完全在语言中定义的,因此可以具有编译期和运行时的内建检查,但是在使用泛型时,编译器和运行时系统不知道你想用类型做什么,以及应该采用什么规则。 +真正的问题是我们在讨论的集合类型,而不是集合持有对象的类型。与数组不同,泛型没有内建的协变类型。这是因为数组是完全在java语言中定义的,因此可以具有编译期和运行时的内建检查,但是在使用泛型时,编译器和运行时系统不知道你想用类型做什么,以及应该采用什么规则。(Java中,数组是协变的,泛型是不变的,``实现了泛型的协变 **covariance**,``实现了泛型的逆变**contravariance**) 但是,有时你想在两个类型间建立某种向上转型关系。通配符可以产生这种关系。 @@ -2666,17 +2721,22 @@ public class GenericsAndCovariance { } ``` -**flist** 的类型现在是 `List`,你可以读作“一个具有任何从 **Fruit** 继承的类型的列表”。然而,这实际上并不意味着这个 **List** 将持有任何类型的 **Fruit**。通配符引用的是明确的类型,因此它意味着“某种 **flist** 引用没有指定的具体类型”。因此这个被赋值的 **List** 必须持有诸如 **Fruit** 或 **Apple** 这样的指定类型,但是为了向上转型为 **flist**,这个类型是什么没人在意。 +**flist** 的类型现在是 `List`,你可以读作“一个具有任何继承自 **Fruit** 类型的列表”。然而,这实际上并不意味着这个 **List** 将持有任何类型的 **Fruit**。通配符引用的是明确的类型,因此它意味着“一些 **flist** 引用没有指定特定类型”。因此这个被赋值的 **List** 必须持有诸如 **Fruit** 或 **Apple** 这样的指定类型,但是为了向上转型为 **flist**,并不关心这个具体的类型是什么。(编译器只知道 **flist** 是**Fruit**某个子类的List,但并不知道这个子类具体是什么类,只好阻止向其中加入任何子类。为了类型安全,不能往使用了`? extends`的数据结构里写入任何的值。) -**List** 必须持有一种具体的 **Fruit** 或 **Fruit** 的子类型,但是如果你不关心具体的类型是什么,那么你能对这样的 **List** 做什么呢?如果不知道 **List** 中持有的对象是什么类型,你怎能保证安全地向其中添加对象呢?就像在 **CovariantArrays.java** 中向上转型一样,你不能,除非编译器而不是运行时系统可以阻止这种操作的发生。你很快就会发现这个问题。 +**List** 必须持有一种具体的 **Fruit** 或 **Fruit** 的子类型,但是如果你不关心具体的类型是什么,那么你能对这样的 **List** 做什么呢?如果不知道 **List** 中持有的对象是什么类型,你怎能保证安全地向其中添加对象呢?就和 **CovariantArrays.java** 中的 向上转型的数组一样,你不能这样做,只不过是现在是编译器阻止了这种情况的发生,而不是运行时系统——你会更早发现问题。 -你可能认为事情开始变得有点走极端了,因为现在你甚至不能向刚刚声明过将持有 **Apple** 对象的 **List** 中放入一个 **Apple** 对象。是的,但编译器并不知道这一点。`List` 可能合法地指向一个 `List`。一旦执行这种类型的向上转型,你就丢失了向其中传递任何对象的能力,甚至传递 **Object** 也不行。 +你可能认为事情变得更糟了,因为现在你甚至不能向刚刚声明过将持有 **Apple** 对象的 **List** 中放入一个 **Apple** 对象。的确如此,但编译器并不知道这一点(编译能通过)。`List` 可以合法地指向一个 `List`。而一旦你做了这种“向上转型”,你就失去了传入任何对象的能力,甚至传递 **Object** 都不行。 -另一方面,如果你调用了一个返回 **Fruit** 的方法,则是安全的,因为你知道这个 **List** 中的任何对象至少具有 **Fruit** 类型,因此编译器允许这么做。 +另一方面,如果你调用一个返回 **Fruit** 类型的方法则是安全的,因为你知道这个 **List** 中的任何对象至少具有 **Fruit** 类型,因此编译器允许这么做。 + +- 总结PECS原则如下: + - 如果要从集合中读取类型T的数据,并且不能写入,可以使用 ? extends 通配符;(Producer Extends) + - 如果要从集合中写入类型T的数据,并且不需要读取,可以使用 ? super 通配符;(Consumer Super) + - 如果既要存又要取,那么就不要使用任何通配符。 ### 编译器有多聪明 -现在你可能会猜想自己不能去调用任何接受参数的方法,但是考虑下面的代码: +现在你可能会认为你不能去调用任何有入参的方法,但是考虑下面的代码: ```java // generics/CompilerIntelligence.java @@ -2697,9 +2757,9 @@ public class CompilerIntelligence { 这里对 `contains()` 和 `indexOf()` 的调用接受 **Apple** 对象作为参数,执行没问题。这是否意味着编译器实际上会检查代码,以查看是否有某个特定的方法修改了它的对象? -通过查看 **ArrayList** 的文档,我们发现编译器没有那么聪明。尽管 `add()` 接受一个泛型参数类型的参数,但 `contains()` 和 `indexOf()` 接受的参数类型是 **Object**。因此当你指定一个 `ArrayList` 时,`add()` 的参数就变成了"**? extends Fruit**"。从这个描述中,编译器无法得知这里需要 **Fruit** 的哪个具体子类型,因此它不会接受任何类型的 **Fruit**。如果你先把 **Apple** 向上转型为 **Fruit**,也没有关系——编译器仅仅会拒绝调用像 `add()` 这样参数列表中涉及通配符的方法。 +通过查看 **ArrayList** 的文档,我们发现编译器没有那么聪明。 `add()` 接受一个泛型参数类型的参数,而 `contains()` 和 `indexOf()` 接受的参数类型是 **Object**。因此当你指定一个 `ArrayList` 时,`add()` 的参数就变成了"**? extends Fruit**"。从这个描述来看,编译器无法知道那里需要 **Fruit** 的哪个具体子类型,因此它不会接受任何类型的 **Fruit**。如果你先把 **Apple** 向上转型为 **Fruit**,也没有关系——编译器仅仅会拒绝调用诸如像 `add()` 这样参数列表中涉及通配符的方法。(当我们使用?号通配符的时候:**就只能调对象与类型无关的方法,不能调用对象与类型有关的方法。** 在上面的List集合,我是不能使用add()方法的。**因为add()方法是把对象丢进集合中,而现在我是不知道对象的类型是什么。**) -`contains()` 和 `indexOf()` 的参数类型是 **Object**,不涉及通配符,所以编译器允许调用它们。这意味着将由泛型类的设计者来决定哪些调用是“安全的”,并使用 **Object** 类作为它们的参数类型。为了禁止对类型中使用了通配符的方法调用,需要在参数列表中使用类型参数。 +`contains()` 和 `indexOf()` 的参数类型是 **Object**,不涉及通配符,所以编译器允许调用它们。这意味着将由泛型类的设计者来决定哪些调用是“安全的”,并使用 **Object** 类作为它们的参数类型。要在类型使用通配符时禁止调用,请使用参数列表中的类型参数。 下面展示一个简单的 **Holder** 类: @@ -2758,7 +2818,7 @@ false */ ``` -**Holder** 有一个接受 **T** 类型对象的 `set()` 方法,一个返回 T 对象的 `get()` 方法和一个接受 Object 对象的 `equals()` 方法。正如你所见,如果创建了一个 `Holder`,就不能将其向上转型为 `Holder`,但是可以向上转型为 `Holder`。如果调用 `get()`,只能返回一个 **Fruit**——这就是在给定“任何扩展自 **Fruit** 的对象”这一边界后,它所能知道的一切了。如果你知道更多的信息,就可以将其转型到某种具体的 **Fruit** 而不会导致任何警告,但是存在得到 **ClassCastException** 的风险。`set()` 方法不能工作在 **Apple** 和 **Fruit** 上,因为 `set()` 的参数也是"**? extends Fruit**",意味着它可以是任何事物,编译器无法验证“任何事物”的类型安全性。 +**Holder** 有一个接受 **T** 类型对象的 `set()` 方法,一个返回 T 对象的 `get()` 方法和一个接受 Object 对象的 `equals()` 方法。正如你所见,如果创建了一个 `Holder`,就不能将其向上转型为 `Holder`,但是可以向上转型为 `Holder`。如果调用 `get()`,只能返回一个 **Fruit**——这已是在给定“任何扩展自 **Fruit** 的对象”这一边界后,它所能知道的一切了。如果你知道更多的信息,就可以将其转型到某种具体的 **Fruit** 而不会导致任何警告,但是存在得到 **ClassCastException** 的风险。`set()` 方法不能工作在 **Apple** 和 **Fruit** 上,因为 `set()` 的参数也是"**? extends Fruit**",意味着它可以是任何事物,编译器无法验证“任何事物”的类型安全性。 但是,`equals()` 方法可以正常工作,因为它接受的参数是 **Object** 而不是 **T** 类型。因此,编译器只关注传递进来和要返回的对象类型。它不会分析代码,以查看是否执行了任何实际的写入和读取操作。 @@ -2766,7 +2826,7 @@ Java 7 引入了 **java.util.Objects** 库,使创建 `equals()` 和 `hashCode( ### 逆变 -还可以走另外一条路,即使用超类型通配符。这里,可以声明通配符是由某个特定类的任何基类来界定的,方法是指定 `<?super MyClass>` ,或者甚至使用类型参数: `<?super T>`(尽管你不能对泛型参数给出一个超类型边界;即不能声明 `` )。这使得你可以安全地传递一个类型对象到泛型类型中。因此,有了超类型通配符,就可以向 **Collection** 写入了: +还可以走另外一条路——使用超类型通配符 (*supertype wildcards*)。这里,可以声明通配符是由某个特定类的任何基类来限制的,通过指定 `<?super MyClass>` ,或者甚至使用类型参数: `<?super T>`(尽管你不能对泛型参数给出一个超类型边界;即不能声明 `` )。这使得你可以安全地传递一个类型对象到泛型类型中。因此,有了超类型通配符,就可以向 **Collection** 写入了: ```java // generics/SuperTypeWildcards.java @@ -2780,8 +2840,9 @@ public class SuperTypeWildcards { } ``` -参数 **apples** 是 **Apple** 的某种基类型的 **List**,这样你就知道向其中添加 **Apple** 或 **Apple** 的子类型是安全的。但是因为 **Apple** 是下界,所以你知道向这样的 **List** 中添加 **Fruit** 是不安全的,因为这将使这个 **List** 敞开口子,从而可以向其中添加非 **Apple** 类型的对象,而这是违反静态类型安全的。 -下面的示例复习了一下逆变和通配符的的使用: +参数 **apples** 是 **Apple** 的某种基类型的 **List**;这样你就知道向其中添加 **Apple** 或 **Apple** 的子类型是安全的。但是因为 **Apple** 是下界,所以你知道将 **Fruit** 添加到这样的 **List** 中是不安全的——将使这个 **List** 敞开口子,导致可以向其中添加非 **Apple** 类型的对象(只是**Apple** 的某种基类,这种基类不一定就是 **Fruit**),而这是违反静态类型安全的。(因为下界规定了元素的最小粒度的下限,实际上是放松了容器元素的类型控制。既然元素是 **Apple** 的基类,所以放比 **Apple** 粒度小的如 Jonathan、Apple 都行, 但往外取时, 只有所有类的基类 **Object** 对象才能装下——但是这样的话,元素的类型信息全部丢失。) + +下面的示例复习了一下协变和通配符的的使用: ```java // generics/GenericReading.java @@ -2838,10 +2899,37 @@ public class GenericReading { } ``` -`readExact()` 方法使用了精确的类型。如果使用这个没有任何通配符的精确类型,就可以向 **List** 中写入和读取这个精确类型。另外,对于返回值,静态的泛型方法 `readExact()` 可以有效地“适应”每个方法调用,并能够从 `List` 中返回一个 **Apple** ,从 `List` 中返回一个 **Fruit** ,就像在 `f1()` 中看到的那样。因此,如果可以摆脱静态泛型方法,那么在读取时就不需要协变类型了。 -然而对于泛型类来说,当你创建这个类的实例时,就要为这个类确定参数。就像在 `f2()` 中看到的,**fruitReader** 实例可以从 `List` 中读取一个 **Fruit** ,因为这就是它的确切类型。但是 `List` 也应该产生 **Fruit** 对象,而 **fruitReader** 不允许这么做。 +`readExact()` 方法使用了精确的类型。如果使用这个没有任何通配符的精确类型,就可以向 **List** 中写入和读取这个精确类型。另外,对于返回值,静态的泛型方法 `readExact()` 可以有效地“适应”每个方法调用,并能够从 `List` 中返回一个 **Apple** ,从 `List` 中返回一个 **Fruit** ,就像在 `f1()` 中看到的那样。因此,在你可用静态泛型方法的时候,如果只用作读取,不需要协变类型(参与)。 + +然而对于泛型类来说,当你创建这个类的实例时,就要为这个类确定参数。就像在 `f2()` 中看到的,**fruitReader** 实例可以从 `List` 中读取一个 **Fruit** ,因为这就是它的确切类型。但在 `List` 也应该能产生 **Fruit** 对象时, **fruitReader** 却不允许(容器之间没有继承关系)。 + 为了修正这个问题,`CovariantReader.readCovariant()` 方法将接受 `List<?extends T>` ,因此,从这个列表中读取一个 **T** 是安全的(你知道在这个列表中的所有对象至少是一个 **T** ,并且可能是从 T 导出的某种对象)。在 `f3()` 中,你可以看到现在可以从 `List` 中读取 **Fruit** 了。 +```java +//第一段 +public void testAdd(List list){ + list.add(new Animal("animal")); + list.add(new Bird("bird")); + list.add(new Cat("cat")); +} +//第二段 +List list = new ArrayList<>(); +list.add(new Animal("animal")); +list.add(new Bird("bird")); +list.add(new Cat("cat")); +/* +我们知道List、List的子类型。先假设传入的参数为为List,则第一段代码的三个“add”操作都是可行的;可如果是List呢??则只有第二个“add”可以执行;再假设传入的是List(Tiger是想象出来的,可认为是Cat的子类),则三个“add”操作都不能执行。 + +现在反过来说,给testAdd传入不同的参数,三个“add”操作都可能引发类型不兼容问题,而传入的参数是未知的,所以java为了保护其类型一致,禁止向List添加任意对象,不过却可以添加null,即list.add(null)是可行的。 + +有了上面谈到的基础,再来理解第二段代码就不难了,因为List的类型“? extends Animal”无法确定,可以是Animal,Bird或者Cat等,所以为了保护其类型的一致性,也是不能往list添加任意对象的,不过却可以添加null。 + +总结如下:不能往List 添加任意对象,除了null。 +*/ +``` + + + ### 无界通配符 无界通配符 `` 看起来意味着“任何事物”,因此使用无界通配符好像等价于使用原生类型。事实上,编译器初看起来是支持这种判断的: @@ -2909,7 +2997,8 @@ public class UnboundedWildcards1 { ``` 有很多情况都和你在这里看到的情况类似,即编译器很少关心使用的是原生类型还是 `` 。在这些情况中,`` 可以被认为是一种装饰,但是它仍旧是很有价值的,因为,实际上它是在声明:“我是想用 Java 的泛型来编写这段代码,我在这里并不是要用原生类型,但是在当前这种情况下,泛型参数可以持有任何类型。” -第二个示例展示了无界通配符的一个重要应用。当你在处理多个泛型参数时,有时允许一个参数可以是任何类型,同时为其他参数确定某种特定类型的这种能力会显得很重要: + +第二个示例展示了无界通配符的一个重要应用。当你在处理多个泛型参数时,有时允许一个参数可以是任何类型,同时为其他参数确定特定类型: ```java // generics/UnboundedWildcards2.java @@ -2956,8 +3045,10 @@ public class UnboundedWildcards2 { } ``` -但是,当你拥有的全都是无界通配符时,就像在 `Map` 中看到的那样,编译器看起来就无法将其与原生 **Map** 区分开了。另外, **UnboundedWildcards1.java** 展示了编译器处理 `List` 和 `List` 是不同的。 -令人困惑的是,编译器并非总是关注像 `List` 和 `List` 之间的这种差异,因此它们看起来就像是相同的事物。事实上,因为泛型参数擦除到它的第一个边界,因此 `List` 看起来等价于 `List` ,而 **List** 实际上也是 `List` ——除非这些语句都不为真。**List** 实际上表示“持有任何 **Object** 类型的原生 **List** ”,而 `List` 表示“具有某种特定类型的非原生 **List** ,只是我们不知道类型是什么。” +但是,当你用的全是无界通配符时,就像在 `Map` 中看到的那样,编译器看起来就无法将其与原生 **Map** 【*raw* *type*(原生类型)】区分开了。而且, **UnboundedWildcards1.java** 也展示了编译器处理 `List` 和 `List` 是不同的。 + +令人困惑的是,编译器并不总是关注像 `List` 和 `List` 之间的这种差异,所以它们看起来就像是相同的。事实上,因为泛型参数擦除到它的第一个边界,因此 `List` 看起来等价于 `List` ,而 **List** 实际上也是 `List` ——只是这两种说法都不完全正确。**List** 实际上表示“持有任何 **Object** 类型的原生 **List** ”,而 `List` 表示“具有某种特定类型的非原生 **List** ,只是我们不知道类型是什么。” + 编译器何时才会关注原生类型和涉及无界通配符的类型之间的差异呢?下面的示例使用了前面定义的 `Holder` 类,它包含接受 **Holder** 作为参数的各种方法,但是它们具有不同的形式:作为原生类型,具有具体的类型参数以及具有无界通配符参数: ```java @@ -3248,21 +3339,28 @@ public class Wildcards { ``` 在 `rawArgs()` 中,编译器知道 `Holder` 是一个泛型类型,因此即使它在这里被表示成一个原生类型,编译器仍旧知道向 `set()` 传递一个 **Object** 是不安全的。由于它是原生类型,你可以将任何类型的对象传递给 `set()` ,而这个对象将被向上转型为 **Object** 。因此无论何时,只要使用了原生类型,都会放弃编译期检查。对 `get()` 的调用说明了相同的问题:没有任何 **T** 类型的对象,因此结果只能是一个 **Object**。 + 人们很自然地会开始考虑原生 `Holder` 与 `Holder` 是大致相同的事物。但是 `unboundedArg()` 强调它们是不同的——它揭示了相同的问题,但是它将这些问题作为错误而不是警告报告,因为原生 **Holder** 将持有任何类型的组合,而 `Holder` 将持有具有某种具体类型的同构集合,因此不能只是向其中传递 **Object** 。 + 在 `exact1()` 和 `exact2()` 中,你可以看到使用了确切的泛型参数——没有任何通配符。你将看到,`exact2()`与 `exact1()` 具有不同的限制,因为它有额外的参数。 -在 `wildSubtype()` 中,在 **Holder** 类型上的限制被放松为包括持有任何扩展自 **T** 的对象的 **Holder** 。这还是意味着如果 T 是 **Fruit** ,那么 `holder` 可以是 `Holder` ,这是合法的。为了防止将 **Orange** 放置到 `Holder` 中,对 `set()` 的调用(或者对任何接受这个类型参数为参数的方法的调用)都是不允许的。但是,你仍旧知道任何来自 `Holder<?extends Fruit>` 的对象至少是 **Fruit** ,因此 `get()` (或者任何将产生具有这个类型参数的返回值的方法)都是允许的。 -`wildSupertype()` 展示了超类型通配符,这个方法展示了与 `wildSubtype()` 相反的行为:`holder` 可以是持有任何 T 的基类型的容器。因此, `set()` 可以接受 **T** ,因为任何可以工作于基类的对象都可以多态地作用于导出类(这里就是 **T** )。但是,尝试着调用 `get()` 是没有用的,因为由 `holder` 持有的类型可以是任何超类型,因此唯一安全的类型就是 **Object** 。 -这个示例还展示了对于在 `unbounded()` 中使用无界通配符能够做什么不能做什么所做出的限制:因为你没有 **T**,所以你不能将 `set()` 或 `get()` 作用于 **T** 上。 + +在 `wildSubtype()` 中,在 **Holder** 类型上的限制被放松为包括持有任何扩展自 **T** 的对象的 **Holder** 。这还是意味着如果 T 是 **Fruit** ,那么 该方法的参数`holder`的类型 可以是 `Holder` ,这是合法的。为了防止将 **Orange** 放置到 `Holder` 中,对 `set()` 的调用(或者对任何接受这个类型参数为参数的方法的调用)都是不允许的。但是,你仍旧知道任何来自 `Holder<?extends Fruit>` 的对象至少是 **Fruit** ,因此 `get()` (或者任何将产生具有这个类型参数的返回值的方法)都是允许的。 + +`wildSupertype()` 中展示了超类型通配符,这个方法展示了与 `wildSubtype()` 相反的行为:`holder` 是一个集合,它持有的任何类型都是 T 的基类。因此, `set()` 可以接受 **T** ,因为任何与基类一起工作的东西都会多态地与派生类一起工作(这里就是 **T** )。但是,尝试着调用 `get()` 是没有用的,因为由 `holder` 持有的类型可以是任何基类,因此唯一安全的类型就是 **Object** 。 + +这个例子也显示了在 `unboundedArg()` 中,对无界参数能做什么不能做什么的限制:因为你没有 **T**,所以你不能将 `set()` 或 `get()`一个 **T** 。 在 `main()` 方法中你看到了某些方法在接受某些类型的参数时没有报错和警告。为了迁移兼容性,`rawArgs()` 将接受所有 **Holder** 的不同变体,而不会产生警告。`unboundedArg()` 方法也可以接受相同的所有类型,尽管如前所述,它在方法体内部处理这些类型的方式并不相同。 -如果向接受“确切”泛型类型(没有通配符)的方法传递一个原生 **Holder** 引用,就会得到一个警告,因为确切的参数期望得到在原生类型中并不存在的信息。如果向 `exact1()` 传递一个无界引用,就不会有任何可以确定返回类型的类型信息。 -可以看到,`exact2()` 具有最多的限制,因为它希望精确地得到一个 `Holder` ,以及一个具有类型 **T** 的参数,正由于此,它将产生错误或警告,除非提供确切的参数。有时,这样做很好,但是如果它过于受限,那么就可以使用通配符,这取决于是否想要从泛型参数中返回类型确定的返回值(就像在 `wildSubtype()` 中看到的那样),或者是否想要向泛型参数传递类型确定的参数(就像在 `wildSupertype()` 中看到的那样)。 -因此,使用确切类型来替代通配符类型的好处是,可以用泛型参数来做更多的事,但是使用通配符使得你必须接受范围更宽的参数化类型作为参数。因此,必须逐个情况地权衡利弊,找到更适合你的需求的方法。 +如果将一个原生 **Holder** 引用传递至一个接受“精确”泛型类型(没有通配符)的方法,就会得到警告,因为确切的参数期望得到在原生类型中并不存在的信息。如果向 `exact1()` 传递一个无界引用,就不会有任何可以确定返回类型的类型信息。 + +可以看到,`exact2()` 有最多的约束,因为它希望精确地得到一个 `Holder` ,以及一个具有类型 **T** 的参数,正由于此,它将产生错误或警告,除非提供精确的参数。有时,这样做很好,但如果觉得它过于受限,就可以使用通配符,这取决于是否想要从泛型参数中返回类型精确类型的返回值(就像在 `wildSubtype()` 中看到那样),或者是否想要将精确类型的参数传递到泛型参数(就像在 `wildSupertype()` 中看到那样)。 + +因此,使用精确泛型类型(T)来替代通配符类型的好处是,可以用泛型参数来做更多的事。但是通配符可以接受更广泛的参数化类型作为参数。您必须根据具体情况决定哪种取舍更适合您的需求。 ### 捕获转换 -有一种特殊情况需要使用 `` 而不是原生类型。如果向一个使用 `` 的方法传递原生类型,那么对编译器来说,可能会推断出实际的类型参数,使得这个方法可以回转并调用另一个使用这个确切类型的方法。下面的示例演示了这种技术,它被称为捕获转换,因为未指定的通配符类型被捕获,并被转换为确切类型。这里,有关警告的注释只有在 `@SuppressWarnings` 注解被移除之后才能起作用: +有一种情况,特别需要使用 `` 而不是原生类型。如果你把一个原生类型传递给一个使用 `` 的方法,编译器就有可能会推断出实际的类型参数,这样该方法就可以回转并调用另一个使用这个确切类型的方法。下面的示例演示了这种技术,它被称为捕获转换(*capture* *conversion*),因为未指定的通配符类型被捕获并转换为确切类型。这里,有关警告的注释只有在 `@SuppressWarnings` 注解被移除后才生效: ```java // generics/CaptureConversion.java @@ -3326,8 +3424,8 @@ Double */ ``` -`f1()` 中的类型参数都是确切的,没有通配符或边界。在 `f2()` 中,**Holder** 参数是一个无界通配符,因此它看起来是未知的。但是,在 `f2()` 中调用了 `f1()`,而 `f1()` 需要一个已知参数。这里所发生的是:在调用 `f2()` 的过程中捕获了参数类型,并在调用 `f1()` 时使用了这种类型。 -你可能想知道这项技术是否可以用于写入,但是这要求在传递 `Holder` 时同时传递一个具体类型。捕获转换只有在这样的情况下可以工作:即在方法内部,你需要使用确切的类型。注意,不能从 `f2()` 中返回 **T**,因为 **T** 对于 `f2()` 来说是未知的。捕获转换十分有趣,但是非常受限。 +`f1()` 中的类型参数都是确切的,没有通配符或边界。在 `f2()` 中,**Holder** 参数是一个无界通配符,因此它看起来是未知的。但是,在 `f2()` 中调用了 `f1()`,而 `f1()` 需要一个已知参数。这里所发生的是:参数类型在调用 `f2()` 的过程中被捕获,并在调用 `f1()` 中使用。 +你可能想知道这项技术是否可以用于写入,但这样会要求在传递 `Holder` 时同时传递一个具体类型。捕获转换只适用于这样的情况:即在方法内部,你需要使用确切的类型。注意,不能从 `f2()` 中返回 **T**,因为 **T** 对于 `f2()` 来说是未知的。捕获转换很有趣,但是非常受限。 @@ -3443,13 +3541,13 @@ class Employee implements Payable {} class Hourly extends Employee implements Payable {} ``` -**Hourly** 不能编译,因为擦除会将 `Payable` 和 `Payable` 简化为相同的类 **Payable**,这样,上面的代码就意味着在重复两次地实现相同的接口。十分有趣的是,如果从 **Payable** 的两种用法中都移除掉泛型参数(就像编译器在擦除阶段所做的那样)这段代码就可以编译。 +**Hourly** 不能编译,因为擦除会将 `Payable` 和 `Payable` 简化为相同的类 **Payable**,这样,上面的代码就意味着在重复两次地实现相同的接口。十分有趣的是,如果从 **Payable** 的两种用法中都移除掉泛型参数(就像编译器在擦除阶段所做的那样),这段代码就可以编译。 在使用某些更基本的 Java 接口,例如 `Comparable` 时,这个问题可能会变得十分令人恼火,就像你在本节稍后看到的那样。 ### 转型和警告 -使用带有泛型类型参数的转型或 **instanceof** 不会有任何效果。下面的集合在内部将各个值存储为 **Object**,并在获取这些值时,再将它们转型回 **T**: +对泛型类型参数使用转型或 **instanceof** 不会有任何效果。下面的集合在内部将各个值存储为 **Object**,并在获取这些值时,再将它们转型回 **T**: ```java // generics/GenericCast.java @@ -3618,7 +3716,7 @@ public class ComparablePet implements Comparable { } ``` -尝试缩小 **ComparablePet** 子类的比较类型是有意义的。例如,**Cat** 类可以与其他的 **Cat** 比较: +尝试缩小 **ComparablePet** 子类的比较类型是有意义的。例如,**Cat** 类应该限制为只能与其他的 **Cat** 比较: ```java // generics/HijackedInterface.java @@ -3668,13 +3766,13 @@ class Gecko extends ComparablePet { class SelfBounded> { // ... ``` -这就像两面镜子彼此照向对方所引起的目眩效果一样,是一种无限反射。**SelfBounded** 类接受泛型参数 **T**,而 **T** 由一个边界类限定,这个边界就是拥有 **T** 作为其参数的 **SelfBounded**。 +这就像两面镜子彼此照向对方所引起的目眩效果一样,是一种无限反射。**SelfBounded** 类接受泛型参数 **T**,而 **T** 被边界约束,这个边界就是 **SelfBounded** ,**SelfBounded** 又拥有 **T** 作为其参数。 当你首次看到它时,很难去解析它,它强调的是当 **extends** 关键字用于边界与用来创建子类明显是不同的。 ### 古怪的循环泛型 -为了理解自限定类型的含义,我们从这个惯用法的一个简单版本入手,它没有自限定的边界。 +为了理解自限定类型的含义,我们从这个惯用法的一个简化版本入手:它没有自限定的边界。 不能直接继承一个泛型参数,但是,可以继承在其自己的定义中使用这个泛型参数的类。也就是说,可以声明: @@ -3687,8 +3785,8 @@ public class CuriouslyRecurringGeneric extends GenericType {} ``` -这可以按照 Jim Coplien 在 C++ 中的*古怪的循环模版模式*的命名方式,称为古怪的循环泛型(CRG)。“古怪的循环”是指类相当古怪地出现在它自己的基类中这一事实。 -为了理解其含义,努力大声说:“我在创建一个新类,它继承自一个泛型类型,这个泛型类型接受我的类的名字作为其参数。”当给出导出类的名字时,这个泛型基类能够实现什么呢?好吧,Java 中的泛型关乎参数和返回类型,因此它能够产生使用导出类作为其参数和返回类型的基类。它还能将导出类型用作其域类型,尽管这些将被擦除为 **Object** 的类型。下面是表示了这种情况的一个泛型类: +这可以按照 Jim Coplien 在 C++ 中的*古怪的循环模版模式*的命名方式,称为古怪的循环泛型(CRG *curiously recurring generics*)。“古怪的循环”是指类相当古怪地出现在它自己的基类中这一事实。 +为了理解其含义,试着大声跟读:“我在创建一个新的类,它继承自一个泛型类型,这个泛型类型以我的这个新类名作为其参数。”当给予派生类名时,泛型基类能够实现什么呢?好吧,Java 中的泛型关于参数和返回类型的,所以它可以产生一个使用派生类型作为参数和返回类型的基类。它还能将派生类型用作其字段,尽管这些字段的类型被擦除了,变为 **Object** 的类型。下面的一个泛型类表示了这种情况: ```java // generics/BasicHolder.java @@ -3703,7 +3801,7 @@ public class BasicHolder { } ``` -这是一个普通的泛型类型,它的一些方法将接受和产生具有其参数类型的对象,还有一个方法在其存储的域上执行操作(尽管只是在这个域上执行 **Object** 操作)。 +这是一个普通的泛型类型,其方法都接受和生成参数类型的对象,还有一个方法`f()`会对存储的字段进行操作(尽管只是对该字段进行 **Object** 类操作)。 我们可以在一个古怪的循环泛型中使用 **BasicHolder**: ```java @@ -3724,7 +3822,7 @@ Subtype */ ``` -注意,这里有些东西很重要:新类 **Subtype** 接受的参数和返回的值具有 **Subtype** 类型而不仅仅是基类 **BasicHolder** 类型。这就是 CRG 的本质:基类用导出类替代其参数。这意味着泛型基类变成了一种其所有导出类的公共功能的模版,但是这些功能对于其所有参数和返回值,将使用导出类型。也就是说,在所产生的类中将使用确切类型而不是基类型。因此,在**Subtype** 中,传递给 `set()` 的参数和从 `get()` 返回的类型都是确切的 **Subtype**。 +注意,这里有些东西很重要:新类 **Subtype** 接受的参数和返回的值具有 **Subtype** 类型而不仅仅是基类 **BasicHolder** 类型。这就是 CRG 的本质:基类用派生类替代其参数。这意味着泛型基类变成了所有派生类的公共功能的一种模版,但这些功能对于其所有参数和返回值,将使用派生类型。也就是说,在由此产生的类中将使用精确类型而不是基类类型。因此,在**Subtype** 中,传递给 `set()` 的参数和从 `get()` 返回的类型都是确切的 **Subtype**。 ### 自限定 @@ -3753,7 +3851,7 @@ Other */ ``` -限定将采取额外的步骤,强制泛型当作其自身的边界参数来使用。观察所产生的类可以如何使用以及不可以如何使用: +自限定将采取额外的步骤,强制泛型当作其自身的边界参数来使用。观察由此产生的类可以如何使用以及不可以如何使用: ```java // generics/SelfBounding.java @@ -3804,10 +3902,10 @@ public class SelfBounding { class A extends SelfBounded{} ``` -这会强制要求将正在定义的类当作参数传递给基类。 +这将迫使你把正在定义的类作为参数传递给基类。 -自限定的参数有何意义呢?它可以保证类型参数必须与正在被定义的类相同。正如你在 B 类的定义中所看到的,还可以从使用了另一个 **SelfBounded** 参数的 **SelfBounded** 中导出,尽管在 **A** 类看到的用法看起来是主要的用法。对定义 **E** 的尝试说明不能使用不是 **SelfBounded** 的类型参数。 -遗憾的是, **F** 可以编译,不会有任何警告,因此自限定惯用法不是可强制执行的。如果它确实很重要,可以要求一个外部工具来确保不会使用原生类型来替代参数化类型。 +自限定的参数有何附带意义呢?——它可以保证类型参数必须与正在定义的类相同。如同你在 B 类的定义中所看到的,还可以从使用另一个 **SelfBounded** 参数的 **SelfBounded** 中派生,尽管在 **A** 类看到的用法看起来是主要的用法。定义 **E** 的尝试表明了,不能使用非**SelfBounded** 的类型参数。 +遗憾的是, **F** 可以编译,不会有任何警告,因此自限定惯用法不是可强制执行的。如果这真的很重要,可以用一个外部工具来确保原始类型不被使用来代替参数化类型。 注意,可以移除自限定这个限制,这样所有的类仍旧是可以编译的,但是 **E** 也会因此而变得可编译: ```java @@ -3837,7 +3935,7 @@ class D2 {} class E2 extends NotSelfBounded {} ``` -因此很明显,自限定限制只能强制作用于继承关系。如果使用自限定,就应该了解这个类所用的类型参数将与使用这个参数的类具有相同的基类型。这会强制要求使用这个类的每个人都要遵循这种形式。 +显然,自限定约束的作用只是为了强制继承关系。如果使用了自限定,说明类使用的类型参数与使用该类型参数的类的类型是一样的。它迫使任何使用该类的人都要遵循这种形式。 还可以将自限定用于泛型方法: ```java @@ -3857,13 +3955,13 @@ public class SelfBoundingMethods { } ``` -这可以防止这个方法被应用于除上述形式的自限定参数之外的任何事物上。 +这样可以防止这个方法被应用于除上述形式的自限定参数之外的其它地方。 ### 参数协变 自限定类型的价值在于它们可以产生*协变参数类型*——方法参数类型会随子类而变化。 -尽管自限定类型还可以产生与子类类型相同的返回类型,但是这并不十分重要,因为*协变返回类型*是在 Java 5 引入: +尽管自限定类型还可以产生与子类类型相同的返回类型,但是这并不十分重要,因为*协变返回类型*在 Java 5 引入了(如下): ```java // generics/CovariantReturnTypes.java @@ -3888,9 +3986,9 @@ public class CovariantReturnTypes { } ``` -**DerivedGetter** 中的 `get()` 方法覆盖了 **OrdinaryGetter** 中的 `get()` ,并返回了一个从 `OrdinaryGetter.get()` 的返回类型中导出的类型。尽管这是完全合乎逻辑的事情(导出类方法应该能够返回比它覆盖的基类方法更具体的类型)但是这在早先的 Java 版本中是不合法的。 +**DerivedGetter** 中的 `get()` 方法覆盖了 **OrdinaryGetter** 中的 `get()` ,并返回了一个从 `OrdinaryGetter.get()` 的返回类型中派生的类型。尽管这是完全合乎逻辑的事情(派生类方法应该能够返回比它重写的基类方法更具体的类型)但是这在早先的 Java 版本中是不合法的。 -自限定泛型事实上将产生确切的导出类型作为其返回值,就像在 `get()` 中所看到的一样: +自限定泛型事实上将产生确切的派生类型作为其返回值,就像这里 所看到的`get()` 一样: ```java // generics/GenericsAndReturnTypes.java @@ -3909,7 +4007,7 @@ public class GenericsAndReturnTypes { } ``` -注意,这段代码不能编译,除非是使用囊括了协变返回类型的 Java 5。 +注意,这需要使用囊括了协变返回类型的 Java 5,否则这段代码不能编译。 然而,在非泛型代码中,参数类型不能随子类型发生变化: @@ -3944,8 +4042,8 @@ OrdinarySetter.set(Base) */ ``` -`set(derived)` 和 `set(base)` 都是合法的,因此 `DerivedSetter.set()` 没有覆盖 `OrdinarySetter.set()` ,而是重载了这个方法。从输出中可以看到,在 **DerivedSetter** 中有两个方法,因此基类版本仍旧是可用的,因此可以证明它被重载过。 -但是,在使用自限定类型时,在导出类中只有一个方法,并且这个方法接受导出类型而不是基类型为参数: +`set(derived)` 和 `set(base)` 都是合法的,因此 `DerivedSetter.set()` 没有重写 `OrdinarySetter.set()` ,而是重载了这个方法。从输出中可以看到,在 **DerivedSetter** 中有两个方法,因此基类版本仍旧是可用的,因此可以证明它被重载过。 +但是,在使用自限定类型时,派生类中只有一个方法,并且这个方法接受派生类型而不是基类型为参数: ```java // generics/SelfBoundingAndCovariantArguments.java @@ -3957,8 +4055,7 @@ interface SelfBoundSetter> { interface Setter extends SelfBoundSetter {} public class SelfBoundingAndCovariantArguments { - void - testA(Setter s1, Setter s2, SelfBoundSetter sbs) { + void testA(Setter s1, Setter s2, SelfBoundSetter sbs) { s1.set(s2); //- s1.set(sbs); // error: method set in interface SelfBoundSetter @@ -3977,7 +4074,7 @@ public class SelfBoundingAndCovariantArguments { } ``` -编译器不能识别将基类型当作参数传递给 `set()` 的尝试,因为没有任何方法具有这样的签名。实际上,这个参数已经被覆盖。 +编译器不能识别将基类型当作参数传递给 `set()` 的尝试,因为没有任何方法具有这样的签名。这个参数实际上已经被重写。 如果不使用自限定类型,普通的继承机制就会介入,而你将能够重载,就像在非泛型的情况下一样: ```java @@ -4016,9 +4113,9 @@ GenericSetter.set(Base) ## 动态类型安全 -因为可以向 Java 5 之前的代码传递泛型集合,所以旧式代码仍旧有可能会破坏你的集合。Java 5 的 **java.util.Collections** 中有一组便利工具,可以解决在这种情况下的类型检查问题,它们是:静态方法 `checkedCollection()` 、`checkedList()`、 `checkedMap()` 、 `checkedSet()` 、`checkedSortedMap()`和 `checkedSortedSet()`。这些方法每一个都会将你希望动态检查的集合当作第一个参数接受,并将你希望强制要求的类型作为第二个参数接受。 +因为可以向 Java 5 之前的代码传递泛型集合,所以旧式代码仍旧有可能会破坏你的集合。Java 5 的 **java.util.Collections** 中有一组便利工具,可以解决在这种情况下的类型检查问题,它们是:静态方法 `checkedCollection()` 、`checkedList()`、 `checkedMap()` 、 `checkedSet()` 、`checkedSortedMap()`和 `checkedSortedSet()`。所有这些方法都会将你希望动态检查的集合当作第一个参数,并将你希望强制要求的类型作为第二个参数。 -受检查的集合在你试图插入类型不正确的对象时抛出 **ClassCastException** ,这与泛型之前的(原生)集合形成了对比,对于后者来说,当你将对象从集合中取出时,才会通知你出现了问题。在后一种情况中,你知道存在问题,但是不知道罪魁祸首在哪里,如果使用受检查的集合,就可以发现谁在试图插入不良对象。 +受检查的集合在你试图插入类型不正确的对象时抛出 **ClassCastException** ,而不是之前泛型(原生)集合——当你将对象从集合中取出时,才会通知你出现了问题——在种情况中,你知道存在问题,但是不知道罪魁祸首在哪里。如果使用受检查的集合,就可以发现谁在试图插入不良对象。 让我们用受检查的集合来看看“将猫插入到狗列表中”这个问题。这里,`oldStyleMethod()` 表示遗留代码,因为它接受的是原生的 **List** ,而 **@SuppressWarnings(“unchecked”)** 注解对于压制所产生的警告是必需的: ```java @@ -4057,14 +4154,14 @@ with element type class typeinfo.pets.Dog */ ``` -运行这个程序时,你会发现插入一个 **Cat** 对于 **dogs1** 来说没有任何问题,而 **dogs2** 立即会在这个错误类型的插入操作上抛出一个异常。还可以看到,将导出类型的对象放置到将要检查基类型的受检查容器中是没有问题的。 +运行这个程序时,你会发现插入一个 **Cat** 对于 **dogs1** 来说没有任何问题,而 **dogs2** 立即会在这个错误类型的插入操作上抛出一个异常。还可以看到,将派生类型的对象放置到将要检查基类型的受检查容器中是没有问题的【` pets.add(new Cat());`】。 ## 泛型异常 -由于擦除的原因,**catch** 语句不能捕获泛型类型的异常,因为在编译期和运行时都必须知道异常的确切类型。泛型类也不能直接或间接继承自 **Throwable**(这将进一步阻止你去定义不能捕获的泛型异常)。 -但是,类型参数可能会在一个方法的 **throws** 子句中用到。这使得你可以编写随检查型异常类型变化的泛型代码: +由于擦除的原因,**catch** 语句不能捕获泛型类型的异常,因为在编译期和运行时都必须知道异常的确切类型。泛型类也不能直接或间接地继承 **Throwable**(这将进一步阻止你试图定义不能捕获的泛型异常)。 +但是,类型参数可以在方法声明的 **throws** 子句中使用。这意味着你可以编写随【被检查的异常】的类型而变化的泛型代码: ```java // generics/ThrowGenericException.java @@ -4075,8 +4172,7 @@ interface Processor { void process(List resultCollector) throws E; } -class ProcessRunner -extends ArrayList> { +class ProcessRunner extends ArrayList> { List processAll() throws E { List resultCollector = new ArrayList<>(); for(Processor processor : this) @@ -4087,12 +4183,10 @@ extends ArrayList> { class Failure1 extends Exception {} -class Processor1 -implements Processor { +class Processor1 implements Processor { static int count = 3; @Override - public void process(List resultCollector) - throws Failure1 { + public void process(List resultCollector) throws Failure1 { if(count-- > 1) resultCollector.add("Hep!"); else @@ -4104,12 +4198,10 @@ implements Processor { class Failure2 extends Exception {} -class Processor2 -implements Processor { +class Processor2 implements Processor { static int count = 2; @Override - public void process(List resultCollector) - throws Failure2 { + public void process(List resultCollector) throws Failure2 { if(count-- == 0) resultCollector.add(47); else { @@ -4149,15 +4241,15 @@ Failure2 */ ``` -**Processor** 执行 `process()` 方法,并且可能会抛出具有类型 **E** 的异常。`process()` 的结果存储在 `ListresultCollector` 中(这被称为*收集参数*)。**ProcessRunner** 有一个 `processAll()` 方法,它会在所持有的每个 **Process** 对象执行,并返回 **resultCollector** 。 -如果不能参数化所抛出的异常,那么由于检查型异常的缘故,将不能编写出这种泛化的代码。 +**Processor** 执行 `process()` 方法,并且可能会抛出具有类型 **E** 的异常。`process()` 的结果存储在 `ListresultCollector` 中(这被称为*收集参数* *collecting* *parameter*)。**ProcessRunner** 有一个 `processAll()` 方法,它执行它所持有的每个 **Process** 对象,并返回 **resultCollector** 。 +如果不能参数化所抛出的异常(`E extends Exception`),那么由于【被检查的异常】的缘故,将不能编写出这种通用的代码。 ## 混型 -术语*混型*随时间的推移好像拥有了无数的含义,但是其最基本的概念是混合多个类的能力,以产生一个可以表示混型中所有类型的类。这往往是你最后的手段,它将使组装多个类变得简单易行。 -混型的价值之一是它们可以将特性和行为一致地应用于多个类之上。如果想在混型类中修改某些东西,作为一种意外的好处,这些修改将会应用于混型所应用的所有类型之上。正由于此,混型有一点*面向切面编程* (AOP) 的味道,而切面经常被建议用来解决混型问题。 +*混型* **mixin** 这一术语已经被赋予了多种含义,但其最基本的概念是将多个类的功能混合起来,由此产生一个类,可以代表所有类型的混型 **mixins** 。这往往是你最后才做的事情——方便了你轻松组装类。 +混型的价值之一是它们可以将特性和行为一致地应用于多个类之上。另外,如果在混型类中修改了某些东西,这些修改将会应用在所有使用混型的类上面。正因为这样,混型有一点*面向切面编程* (AOP) 的味道,而切面经常被建议用来解决混型问题。 ### C++ 中的混型 @@ -4218,11 +4310,11 @@ test string 2 1452987605 2 TimeStamped> mixin1,mixin2; ``` -遗憾的是,Java 泛型不允许这样。擦除会忘记基类类型,因此 +遗憾的是,Java 泛型不允许这样。擦除会忘记基类类型,因此: > 泛型类不能直接继承自一个泛型参数 -这突显了许多我在 Java 语言设计决策(以及与这些功能一起发布)中遇到的一大问题:处理一件事很有希望,但是当您实际尝试做一些有趣的事情时,您会发现自己做不到。 +这突显了我在许多 Java 语言设计决策(以及与这些特性一起推广)中遇到的一大问题:有很多承诺,但是当您真正尝试做一些有趣的事情时,您会发现做不到。 ### 与接口混合 @@ -4266,11 +4358,9 @@ class BasicImp implements Basic { public String get() { return value; } } -class Mixin extends BasicImp -implements TimeStamped, SerialNumbered { +class Mixin extends BasicImp implements TimeStamped, SerialNumbered { private TimeStamped timeStamp = new TimeStampedImp(); - private SerialNumbered serialNumber = - new SerialNumberedImp(); + private SerialNumbered serialNumber = new SerialNumberedImp(); @Override public long getStamp() { return timeStamp.getStamp(); @@ -4298,13 +4388,13 @@ test string 2 1494331663027 2 */ ``` -**Mixin** 类基本上是在使用*委托*,因此每个混入类型都要求在 **Mixin** 中有一个相应的域,而你必须在 **Mixin** 中编写所有必需的方法,将方法调用转发给恰当的对象。这个示例使用了非常简单的类,但是当使用更复杂的混型时,代码数量会急速增加。[^4] +上面的 **Mixin** 类基本上是在使用*委托* *delegation*,因此每个混入类型都要求在 **Mixin** 中有一个相应的字段,而你必须在 **Mixin** 中编写所有必需的方法,将方法调用转发给恰当的对象。这个示例使用了一些简单的类,但是当使用更复杂的混型时,代码数量会急速增加。[^4] ### 使用装饰器模式 当你观察混型的使用方式时,就会发现混型概念好像与*装饰器*设计模式关系很近。装饰器经常用于满足各种可能的组合,而直接子类化会产生过多的类,因此是不实际的。 -装饰器模式使用分层对象来动态透明地向单个对象中添加责任。装饰器指定包装在最初的对象周围的所有对象都具有相同的基本接口。某些事物是可装饰的,可以通过将其他类包装在这个可装饰对象的四周,来将功能分层。这使得对装饰器的使用是透明的——无论对象是否被装饰,你都拥有一个可以向对象发送的公共消息集。装饰类也可以添加新方法,但是正如你所见,这将是受限的。 -装饰器是通过使用组合和形式化结构(可装饰物/装饰器层次结构)来实现的,而混型是基于继承的。因此可以将基于参数化类型的混型当作是一种泛型装饰器机制,这种机制不需要装饰器设计模式的继承结构。 +装饰器模式使用分层对象来动态透明地向单个对象中添加责任。该模式规定,包装在最初的对象周围的所有对象都具有相同的基本接口。某些事物是可装饰的,可以通过将其他类包装在这个可装饰对象的四周,来将功能分层。这使得对装饰器的使用是透明的——无论对象是否被装饰,你都拥有一个可以向对象发送的公共消息集。装饰类也可以添加新方法,但是正如你所见,这将是受限的。 +装饰器是通过使用组合和形式化结构(可装饰物/装饰器层次结构)来实现的,而混型是基于继承的。可以将基于参数化类型的混型当作是一种泛型装饰器机制,这种机制不需要装饰器设计模式的继承结构。 前面的示例可以被改写为使用装饰器: ```java @@ -4752,9 +4842,9 @@ Pretending to sit 反射提供了一些有用的可能性,但是它将所有的类型检查都转移到了运行时,因此在许多情况下并不是我们所希望的。如果能够实现编译期类型检查,这通常会更符合要求。但是有可能实现编译期类型检查和潜在类型机制吗? -让我们看一个说明这个问题的示例。假设想要创建一个 `apply()` 方法,它能够将任何方法应用于某个序列中的所有对象。这种情况下使用接口不适合,因为你想要将任何方法应用于一个对象集合,而接口不可能描述任何方法。如何用 Java 来实现这个需求呢? +让我们来看一个探讨这个问题的例子。假设想要创建一个 `apply()` 方法——它能够将任何方法应用于序列中的每个对象。这种情况下使用接口不适合,因为你想要将任何方法应用于一个对象集合,而接口不可能描述任何方法。如何用 Java 来实现这个需求呢? -最初,我们可以用反射来解决这个问题,由于有了 Java 的可变参数,这种方式被证明是相当优雅的: +最初,我们可以用反射来解决这个问题,由于用了 Java 5 的可变参数,发现这种方式是相当优雅的: ```java // generics/Apply.java @@ -4780,7 +4870,7 @@ public class Apply { 在 **Apply.java** 中,异常被转换为 **RuntimeException** ,因为没有多少办法可以从这种异常中恢复——在这种情况下,它们实际上代表着程序员的错误。 -为什么我们不只使用 Java 8 方法参考(稍后显示)而不是反射方法 **f** ? 注意,`invoke()` 和 `apply()` 的优点是它们可以接受任意数量的参数。 在某些情况下,灵活性可能至关重要。 +为什么我们不能单单使用 Java 8 方法引用(稍后展示)来代替反射方法 **f** ? 请注意`invoke()` 和 `apply()` 的优点是它们可以接受任意数量的参数。 在某些情况下,灵活性可能至关重要。 为了测试 **Apply** ,我们首先创建一个 **Shape** 类: @@ -4953,7 +5043,7 @@ Square 11 rotate 我们使用 `peek()` 当做对 `rotate()` 的调用,因为 `peek()` 执行一个操作(此处是出于副作用),并在未更改的情况下传递对象。 -注意,使用 **FilledList** 和 **shapeQ** 调用 `forEach()` 比 `Apply.apply()` 代码整洁得多。 在代码简单性和可读性方面,结果比以前的方法好得多。 并且,现在也不可能从 `main()` 引发异常。 +注意,使用 **FilledList** 和 **shapeQ** 调用 `forEach()` 比 `Apply.apply()` 代码整洁得多。 在代码简单性和可读性方面,结果比以前的方法好得多。 并且,现在不再从 `main()` 引发运行时异常。 @@ -5066,7 +5156,7 @@ public class Suppliers { `create()` 为你创建一个新的 **Collection** 子类型,而 `fill()` 的第一个版本将元素放入 **Collection** 的现有子类型中。 请注意,还会返回传入的容器的确切类型,因此不会丢失类型信息。[^6] -前两种方法一般都受约束,只能与 **Collection** 子类型一起使用。`fill()` 的第二个版本适用于任何类型的 **holder** 。 它需要一个附加参数:未绑定方法引用 `adder. fill()` ,使用辅助潜在类型来使其与任何具有添加元素方法的 **holder** 类型一起使用。因为此未绑定方法 **adder** 必须带有一个参数(要添加到 **holder** 的元素),所以 **adder** 必须是 `BiConsumer ` ,其中 **H** 是要绑定到的 **holder** 对象的类型,而 **A** 是要被添加的绑定元素类型。 对 `accept()` 的调用将使用参数 a 调用对象 **holder** 上的未绑定方法 **holder**。 +前两种方法一般都受约束,只能与 **Collection** 子类型一起使用。第二个版本的`fill()` 适用于任何类型的 **holder** 。 它需要一个附加参数:未绑定方法引用 `adder. fill()` ,使用辅助潜在类型来使其与任何具有添加元素方法的 **holder** 类型一起使用。因为此未绑定方法 **adder** 必须带有一个参数(要添加到 **holder** 的元素),所以 **adder** 必须是 `BiConsumer ` ,其中 **H** 是要绑定到的 **holder** 对象的类型,而 **A** 是要被添加的绑定元素类型。 对 `accept()` 的调用将使用参数 a 调用对象 **holder** 上的未绑定方法 **holder**。 在一个稍作模拟的测试中对 **Suppliers** 工具程序进行了测试,该仿真还使用了本章前面定义的 **RandomList** : @@ -5142,7 +5232,7 @@ Teller 4 serves Customer 12 */ ``` -可以看到 `create()` 生成一个新的 **Collection** 对象,而 `fill()` 添加到现有 **Collection** 中。第二个版本`fill()` 显示,它不仅与无关的新类型 **Bank** 一起使用,还能与 **List** 一起使用。因此,从技术上讲,`fill()` 的第一个版本在技术上不是必需的,但在使用 **Collection** 时提供了较短的语法。 +可以看到 `create()` 生成一个新的 **Collection** 对象,而 `fill()` 添加到现有 **Collection** 中。第二个版本`fill()` 显示,它不仅与无关的新类型 **Bank** 一起使用,还能与 **List** 一起使用。因此,从技术上讲,`fill()` 的第一个版本不是必需的,但使用 **Collection** 提供了较短的语法。 diff --git a/docs/book/21-Arrays.md b/docs/book/21-Arrays.md index 979170cf..bb4b8c0c 100755 --- a/docs/book/21-Arrays.md +++ b/docs/book/21-Arrays.md @@ -19,7 +19,7 @@ 将数组和其他类型的集合区分开来的原因有三:效率,类型,保存基本数据类型的能力。在 Java 中,使用数组存储和随机访问对象引用序列是非常高效的。数组是简单的线性序列,这使得对元素的访问变得非常快。然而这种高速也是有代价的,代价就是数组对象的大小是固定的,且在该数组的生存期内不能更改。 -速度通常并不是问题,如果有问题,你保存和检索对象的方式也很少是罪魁祸首。你应该总是从 **ArrayList** (来自 [集合](book/12-Collections.md ))开始,它将数组封装起来。必要时,它会自动分配更多的数组空间,创建新数组,并将旧数组中的引用移动到新数组。这种灵活性需要开销,所以一个 **ArrayList** 的效率不如数组。在极少的情况下效率会成为问题,所以这种时候你可以直接使用数组。 +速度通常并不是问题,如果有问题,你保存和检索对象的方式也很少是罪魁祸首。你应该总是从 **ArrayList** (来自 [集合](book/12-Collections.md ))开始,它将数组封装起来,在内部使用和管理数组。必要时,它会自动分配更多的数组空间,创建新数组,并将旧数组中的引用移动到新数组。这种灵活性需要开销,所以一个 **ArrayList** 的效率不如数组。当效率成为了问题的罕见情况下,可以直接使用数组。 数组和集合(Collections)都不能滥用。不管你使用数组还是集合,如果你越界,你都会得到一个 **RuntimeException** 的异常提醒,这表明你的程序中存在错误。 @@ -745,7 +745,7 @@ a9: [Hello, Hello, Hello, World, World, Hello] * **void setAll(int[] a, IntUnaryOperator gen)** * **void setAll(long[] a, IntToLongFunction gen)** -* **void setAll(double[] a, IntToDoubleFunctiongen)** +* **void setAll(double[] a, IntToDoubleFunction gen)** * ** void setAll(T[] a, IntFunction gen)** 除了 **int** , **long** , **double** 有特殊的版本,其他的一切都由泛型版本处理。生成器不是 **Supplier** 因为它们不带参数,并且必须将 **int** 数组索引作为参数。 @@ -1477,7 +1477,7 @@ public interface Rand { ``` -对于除了 **int** 、 **long** 和 **double** 之外的所有基本类型元素生成器,只生成数组,而不是 Count 中看到的完整操作集。这只是一个设计选择,因为本书不需要额外的功能。 +对于除了 **int** 、 **long** 和 **double** 之外的所有基本类型元素生成器,只生成数组(没对数组内元素初始化,用临近的类型强制转换获得),而不是 Count 中看到的完整操作集。这只是一个设计选择,因为本书不需要额外的功能。 下面是对所有 **Rand** 工具的测试: @@ -2115,10 +2115,17 @@ public class ArrayCopying { } } } -/* Output: a1: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14] a1: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] - a2:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]a2:[0, 1, 2, 3, 4, 5, 6]a2:[ - 0, 1, 2, 3, 4, 5, 6, 0, 0, 0, 0, 0]a4:[4, 5, 6, 7, 8, 9, 10, 11][Sub0, Sub1, Sub2, Sub3, Sub4, Sub5, Sub6][ - Sub0, Sub1, Sub2, Sub3, Sub4, Sub5, Sub6]java.lang.ArrayStoreException */ +/* Output: +a1: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14] +a1: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] +a2: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14] +a2: [0, 1, 2, 3, 4, 5, 6] +a2: [0, 1, 2, 3, 4, 5, 6, 0, 0, 0, 0, 0] +a4: [4, 5, 6, 7, 8, 9, 10, 11] +[Sub0, Sub1, Sub2, Sub3, Sub4, Sub5, Sub6] +[Sub0, Sub1, Sub2, Sub3, Sub4, Sub5, Sub6] +java.lang.ArrayStoreException +*/ ``` @@ -2144,7 +2151,7 @@ public class ArrayCopying { **数组** 提供了 **equals()** 来比较一维数组,以及 **deepEquals()** 来比较多维数组。对于所有原生类型和对象,这些方法都是重载的。 -数组相等的含义:数组必须有相同数量的元素,并且每个元素必须与另一个数组中的对应元素相等,对每个元素使用 **equals()**(对于原生类型,使用原生类型的包装类的 **equals()** 方法;例如,int的Integer.equals()。 +数组相等的含义:数组必须有相同数量的元素,并且每个元素必须与另一个数组中的对应元素相等,对每个元素使用 **equals()** ( 对于原生类型,使用原生类型的包装类的 **equals()** 方法;例如,int的Integer.equals()。) ```JAVA // arrays/ComparingArrays.java @@ -2551,11 +2558,11 @@ Location of 635 is 2, a[2] is 635 在while循环中,随机值作为搜索项生成,直到在数组中找到其中一个为止。 -如果找到了搜索项,**Arrays.binarySearch()** 将生成一个大于或等于零的值。否则,它将产生一个负值,表示如果手动维护已排序的数组,则应该插入元素的位置。产生的值是 -(插入点) - 1 。插入点是大于键的第一个元素的索引,如果数组中的所有元素都小于指定的键,则是 **a.size()** 。 +如果找到了搜索项(搜索键 r),**Arrays.binarySearch()** 将生成一个大于或等于零的值。否则,它会产生一个负值,代表如果你是手工维护排序数组的话,该元素应该被插入的位置。产生的值是 - (插入点) - 1 。插入点是大于搜索键 r 的第一个元素的索引,如果数组中的所有元素都小于搜索键 r,则是 **a.size()** 。 -如果数组包含重复的元素,则无法保证找到其中的那些重复项。搜索算法不是为了支持重复的元素,而是为了容忍它们。如果需要没有重复元素的排序列表,可以使用 **TreeSet** (用于维持排序顺序)或 **LinkedHashSet** (用于维持插入顺序)。这些类自动为您处理所有的细节。只有在出现性能瓶颈的情况下,才应该使用手工维护的数组替换这些类中的一个。 +如果数组包含重复的元素,则无法保证找到其中的那些重复项。搜索算法不是为了支持重复的元素,而是为了容忍它们。如果需要元素不重复的排序列表,可以使用 **TreeSet** (用于维持排序)或 **LinkedHashSet** (用于维持插入顺序)。这些类自动为您处理所有的细节。只有在出现性能瓶颈的情况下,才应该使用手工维护的数组替换这些类。 -如果使用比较器(原语数组不允许使用比较器进行排序)对对象数组进行排序,那么在执行 **binarySearch()** (使用重载版本的binarySearch())时必须包含相同的比较器。例如,可以修改 **StringSorting.java** 来执行搜索: +如果使用比较器(原语数组不允许使用比较器进行排序)对对象数组进行排序,那么在执行 **binarySearch()** ( 使用重载版本的binarySearch() ) 时必须包含相同的比较器。例如,可以修改 **StringSorting.java** 来执行搜索: ```JAVA // arrays/AlphabeticSearch.java @@ -2604,11 +2611,11 @@ import static onjava.ArrayShow.*; public class ParallelPrefix1 { public static void main(String[] args) { int[] nums = new Count.Pint().array(10); - show(nums); - System.out.println(Arrays.stream(nums).reduce(Integer::sum).getAsInt()); + show(nums);//[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + System.out.println(Arrays.stream(nums).reduce(Integer::sum).getAsInt());//45 Arrays.parallelPrefix(nums, Integer::sum); - show(nums); - System.out.println(Arrays.stream(new Count.Pint().array(6)).reduce(Integer::sum).getAsInt()); + show(nums);//[0, 1, 3, 6, 10, 15, 21, 28, 36, 45] + System.out.println(Arrays.stream(new Count.Pint().array(6)).reduce(Integer::sum).getAsInt());//15 } } /* Output: diff --git a/docs/book/22-Enumerations.md b/docs/book/22-Enumerations.md index 818641a9..d9e8889d 100644 --- a/docs/book/22-Enumerations.md +++ b/docs/book/22-Enumerations.md @@ -342,7 +342,7 @@ final class Explore extends java.lang.Enum { 答案是,values() 是由编译器添加的 static 方法。可以看出,在创建 Explore 的过程中,编译器还为其添加了 valueOf() 方法。这可能有点令人迷惑,Enum 类不是已经有 valueOf() 方法了吗。 -不过 Enum 中的 valueOf() 方法需要两个参数,而这个新增的方法只需一个参数。由于这里使用的 Set 只存储方法的名字,而不考虑方法的签名,所以在调用 Explore.removeAll(Enum) 之后,就只剩下[values] 了。 +不过 Enum 中的 valueOf() 方法需要两个参数,而这个新增的方法只需一个参数。由于这里存储用的 Set 只存方法名,而不是方法签名,所以在调用 Explore.removeAll(Enum) 之后,就只剩下[values] 了。 从最后的输出中可以看到,编译器将 Explore 标记为 final 类,所以无法继承自 enum,其中还有一个 static 的初始化子句,稍后我们将学习如何重定义该句。