diff --git a/README.md b/README.md index bb65c47..358234f 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ - [ ] [第十九章 类型信息](docs/book/19-Type-Information.md) - [ ] [第二十章 泛型](docs/book/20-Generics.md) - [ ] [第二十一章 数组](docs/book/21-Arrays.md) -- [ ] [第二十二章 枚举](docs/book/22-Enumerations.md) +- [x] [第二十二章 枚举](docs/book/22-Enumerations.md) - [x] [第二十三章 注解](docs/book/23-Annotations.md) - [ ] [第二十四章 并发编程](docs/book/24-Concurrent-Programming.md) - [ ] [第二十五章 设计模式](docs/book/25-Patterns.md) @@ -53,8 +53,8 @@ - [ ] [附录:标准IO](docs/book/Appendix-Standard-IO.md) - [x] [附录:新IO](docs/book/Appendix-New-IO.md) - [ ] [附录:理解equals和hashCode方法](docs/book/Appendix-Understanding-equals-and-hashCode.md) -- [ ] [附录:集合主题](docs/book/Appendix-Collection-Topics.md) -- [ ] [附录:并发底层原理](docs/book/Appendix-Low-Level-Concurrency.md) +- [x] [附录:集合主题](docs/book/Appendix-Collection-Topics.md) +- [x] [附录:并发底层原理](docs/book/Appendix-Low-Level-Concurrency.md) - [x] [附录:数据压缩](docs/book/Appendix-Data-Compression.md) - [ ] [附录:对象序列化](docs/book/Appendix-Object-Serialization.md) - [ ] [附录:静态语言类型检查](docs/book/Appendix-Benefits-and-Costs-of-Static-Type-Checking.md) @@ -90,9 +90,9 @@ ## 贡献者 -* 主译:[LingCoder](https://github.com/LingCoder),[sjsdfg](https://github.com/sjsdfg) -* 参译:[xiangflight](https://github.com/xiangflight),[1326670425](https://github.com/1326670425),[LortSir](https://github.com/LortSir) -* 校对:[LingCoder](https://github.com/LingCoder),[xiangflight](https://github.com/xiangflight),[nickChenyx](https://github.com/nickChenyx) +* 主译:[LingCoder](https://github.com/LingCoder),[sjsdfg](https://github.com/sjsdfg),[xiangflight](https://github.com/xiangflight) +* 参译:[Langdon-Chen](https://github.com/Langdon-Chen),[1326670425](https://github.com/1326670425),[LortSir](https://github.com/LortSir) +* 校对:[LingCoder](https://github.com/LingCoder),[jason31520](https://github.com/jason31520),[xiangflight](https://github.com/xiangflight),[nickChenyx](https://github.com/nickChenyx) ## 翻译说明 diff --git a/SUMMARY.md b/SUMMARY.md index 9e2ee3c..eea7c55 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -364,12 +364,12 @@ * [调整HashMap](docs/book/Appendix-Understanding-equals-and-hashCode.md#调整HashMap) * [附录:集合主题](docs/book/Appendix-Collection-Topics.md) * [示例数据](docs/book/Appendix-Collection-Topics.md#示例数据) - * [List表现](docs/book/Appendix-Collection-Topics.md#List表现) - * [Set表现](docs/book/Appendix-Collection-Topics.md#Set表现) + * [List行为](docs/book/Appendix-Collection-Topics.md#List行为) + * [Set行为](docs/book/Appendix-Collection-Topics.md#Set行为) * [在Map中使用函数式操作](docs/book/Appendix-Collection-Topics.md#在Map中使用函数式操作) - * [选择Map的部分](docs/book/Appendix-Collection-Topics.md#选择Map的部分) - * [集合的fill方法](docs/book/Appendix-Collection-Topics.md#集合的fill方法) - * [使用Flyweight自定义集合和Map](docs/book/Appendix-Collection-Topics.md#使用Flyweight自定义集合和Map) + * [选择Map片段](docs/book/Appendix-Collection-Topics.md#选择Map片段) + * [填充集合](docs/book/Appendix-Collection-Topics.md#填充集合) + * [使用享元(Flyweight)自定义Collection和Map](docs/book/Appendix-Collection-Topics.md#使用享元(Flyweight)自定义Collection和Map) * [集合功能](docs/book/Appendix-Collection-Topics.md#集合功能) * [可选操作](docs/book/Appendix-Collection-Topics.md#可选操作) * [Set和存储顺序](docs/book/Appendix-Collection-Topics.md#Set和存储顺序) @@ -377,7 +377,7 @@ * [理解Map](docs/book/Appendix-Collection-Topics.md#理解Map) * [集合工具类](docs/book/Appendix-Collection-Topics.md#集合工具类) * [持有引用](docs/book/Appendix-Collection-Topics.md#持有引用) - * [避免旧式类库](docs/book/Appendix-Collection-Topics.md#避免旧式类库) + * [Java 1.0 / 1.1 的集合类](docs/book/Appendix-Collection-Topics.md#避免旧式类库) * [本章小结](docs/book/Appendix-Collection-Topics.md#本章小结) * [附录:并发底层原理](docs/book/Appendix-Low-Level-Concurrency.md) * [线程](docs/book/Appendix-Low-Level-Concurrency.md#线程) @@ -385,7 +385,7 @@ * [资源共享](docs/book/Appendix-Low-Level-Concurrency.md#资源共享) * [volatile关键字](docs/book/Appendix-Low-Level-Concurrency.md#volatile关键字) * [原子性](docs/book/Appendix-Low-Level-Concurrency.md#原子性) - * [关键部分](docs/book/Appendix-Low-Level-Concurrency.md#关键部分) + * [临界区](docs/book/Appendix-Low-Level-Concurrency.md#临界区) * [库组件](docs/book/Appendix-Low-Level-Concurrency.md#库组件) * [本章小结](docs/book/Appendix-Low-Level-Concurrency.md#本章小结) * [附录:数据压缩](docs/book/Appendix-Data-Compression.md) diff --git a/docs/README.md b/docs/README.md index d5dc4e5..328b1fe 100644 --- a/docs/README.md +++ b/docs/README.md @@ -38,8 +38,12 @@ 本项目基于 MIT 协议开源。 +## 友情链接 + +Effective Java 第 3 版: https://github.com/sjsdfg/effective-java-3rd-chinese + ## 联系方式 - E-mail : -
\ No newline at end of file +
diff --git a/docs/book/01-What-is-an-Object.md b/docs/book/01-What-is-an-Object.md index 61a5a55..ace67d3 100644 --- a/docs/book/01-What-is-an-Object.md +++ b/docs/book/01-What-is-an-Object.md @@ -104,7 +104,7 @@ Java 有三个显式关键字来设置类中的访问权限:`public`(公开 ![UML-example](../images/1545758268350.png) -上图中实心菱形指向“ **Car** ”表示 **组合** 的关系;如果是 **聚合** 关系,可以使用空心菱形。 +上图中实心三角形指向“ **Car** ”表示 **组合** 的关系;如果是 **聚合** 关系,可以使用空心三角形。 (**译者注**:组合和聚合都属于关联关系的一种,只是额外具有整体-部分的意义。至于是聚合还是组合,需要根据实际的业务需求来判断。可能相同超类和子类,在不同的业务场景,关联关系会发生变化。只看代码是无法区分聚合和组合的,具体是哪一种关系,只能从语义级别来区分。聚合关系中,整件不会拥有部件的生命周期,所以整件删除时,部件不会被删除。再者,多个整件可以共享同一个部件。组合关系中,整件拥有部件的生命周期,所以整件删除时,部件一定会跟着删除。而且,多个整件不可以同时共享同一个部件。这个区别可以用来区分某个关联关系到底是组合还是聚合。两个类生命周期不同步,则是聚合关系,生命周期同步就是组合关系。) @@ -116,17 +116,17 @@ Java 有三个显式关键字来设置类中的访问权限:`public`(公开 “继承”给面向对象编程带来极大的便利。它在概念上允许我们将各式各样的数据和功能封装到一起,这样便可恰当表达“问题空间”的概念,而不用受制于必须使用底层机器语言。 -通过使用 `class` 关键字,这些概念形成了编程语言中的基本单元。遗憾的是,这么做还是有很多麻烦:在创建了一个类之后,即使另一个新类与其具有相似的功能,你还是得重新创建一个新类。但我们若能利用现成的数据类型,对其进行“克隆”,再根据情况进行添加和修改,情况就显得理想多了。“继承”正是针对这个目标而设计的。但继承并不完全等价于克隆。在继承过程中,若原始类(正式名称叫作基础类、超类或父类)发生了变化,修改过的“克隆”类(正式名称叫作继承类或者子类)也会反映出这种变化。 +通过使用 `class` 关键字,这些概念形成了编程语言中的基本单元。遗憾的是,这么做还是有很多麻烦:在创建了一个类之后,即使另一个新类与其具有相似的功能,你还是得重新创建一个新类。但我们若能利用现成的数据类型,对其进行“克隆”,再根据情况进行添加和修改,情况就显得理想多了。“继承”正是针对这个目标而设计的。但继承并不完全等价于克隆。在继承过程中,若原始类(正式名称叫作基类、超类或父类)发生了变化,修改过的“克隆”类(正式名称叫作继承类或者子类)也会反映出这种变化。 ![Inheritance-example](../images/1545763399825.png) -这个图中的箭头从派生类指向基类。正如你将看到的,通常有多个派生类。类型不仅仅描述一组对象的约束,它还涉及其他类型。两种类型可以具有共同的特征和行为,但是一种类型可能包含比另一种类型更多的特征,并且还可以处理更多的消息(或者以不同的方式处理它们)。继承通过基本类型和派生类型的概念来表达这种相似性。基本类型包含派生自它的类型之间共享的所有特征和行为。创建基本类型以表示思想的核心。从基类型中派生出其他类型来表示实现该核心的不同方式。 +这个图中的箭头从派生类指向基类。正如你将看到的,通常有多个派生类。类型不仅仅描述一组对象的约束,它还涉及其他类型。两种类型可以具有共同的特征和行为,但是一种类型可能包含比另一种类型更多的特征,并且还可以处理更多的消息(或者以不同的方式处理它们)。继承通过基类和派生类的概念来表达这种相似性。基类包含派生自它的类型之间共享的所有特征和行为。创建基类以表示思想的核心。从基类中派生出其他类型来表示实现该核心的不同方式。 ![1545764724202](../images/1545764724202.png) -例如,垃圾回收机对垃圾进行分类。基本类型是“垃圾”。每块垃圾都有重量、价值等特性,它们可以被切碎、熔化或分解。在此基础上,可以通过添加额外的特性(瓶子有颜色,钢罐有磁性)或行为(铝罐可以被压碎)衍生出更具体的垃圾类型。此外,一些行为可以不同(纸张的价值取决于它的类型和状态)。使用继承,你将构建一个类型层次结构,来表示你试图解决的某种类型的问题。第二个例子是常见的“形状”例子,可能用于计算机辅助设计系统或游戏模拟。基本类型是“形状”,每个形状都有大小、颜色、位置等等。每个形状可以绘制、擦除、移动、着色等。由此,可以衍生出(继承出)具体类型的形状——圆形、正方形、三角形等等——每个形状可以具有附加的特征和行为。 +例如,垃圾回收机对垃圾进行分类。基类是“垃圾”。每块垃圾都有重量、价值等特性,它们可以被切碎、熔化或分解。在此基础上,可以通过添加额外的特性(瓶子有颜色,钢罐有磁性)或行为(铝罐可以被压碎)派生出更具体的垃圾类型。此外,一些行为可以不同(纸张的价值取决于它的类型和状态)。使用继承,你将构建一个类型层次结构,来表示你试图解决的某种类型的问题。第二个例子是常见的“形状”例子,可能用于计算机辅助设计系统或游戏模拟。基类是“形状”,每个形状都有大小、颜色、位置等等。每个形状可以绘制、擦除、移动、着色等。由此,可以派生出(继承出)具体类型的形状——圆形、正方形、三角形等等——每个形状可以具有附加的特征和行为。 ![1545764780795](../images/1545764780795.png) @@ -157,7 +157,7 @@ Java 有三个显式关键字来设置类中的访问权限:`public`(公开 这样的代码不会受添加的新类型影响,并且添加新类型是扩展面向对象程序以处理新情况的常用方法。 例如,你可以通过通用的“形状”基类派生出新的“五角形”形状的子类,而不需要修改通用"形状"基类的方法。通过派生新的子类来扩展设计的这种能力是封装变化的基本方法之一。 -这种能力改善了我们的设计,且减少了软件的维护代价。如果我们把派生的对象类型统一看成是它本身的基础类型(“圆”当作“形状”,“自行车”当作“车”,“鸬鹚”当作“鸟”等等),编译器(compiler)在编译时期就无法准确地知道什么“形状”被擦除,哪一种“车”在行驶,或者是哪种“鸟”在飞行。这就是关键所在:当程序接收这种消息时,程序员并不想知道哪段代码会被执行。“绘图”的方法可以平等地应用到每种可能的“形状”上,形状会依据自身的具体类型执行恰当的代码。 +这种能力改善了我们的设计,且减少了软件的维护代价。如果我们把派生的对象类型统一看成是它本身的基类(“圆”当作“形状”,“自行车”当作“车”,“鸬鹚”当作“鸟”等等),编译器(compiler)在编译时期就无法准确地知道什么“形状”被擦除,哪一种“车”在行驶,或者是哪种“鸟”在飞行。这就是关键所在:当程序接收这种消息时,程序员并不想知道哪段代码会被执行。“绘图”的方法可以平等地应用到每种可能的“形状”上,形状会依据自身的具体类型执行恰当的代码。 如果不需要知道执行了哪部分代码,那我们就能添加一个新的不同执行方式的子类而不需要更改调用它的方法。那么编译器在不确定该执行哪部分代码时是怎么做的呢?举个例子,下图的 **BirdController** 对象和通用 **Bird** 对象中,**BirdController** 不知道 **Bird** 的确切类型却还能一起工作。从 **BirdController** 的角度来看,这是很方便的,因为它不需要编写特别的代码来确定 **Bird** 对象的确切类型或行为。那么,在调用 **move()** 方法时是如何保证发生正确的行为(鹅走路、飞或游泳、企鹅走路或游泳)的呢? @@ -213,19 +213,19 @@ void doSomething(Shape shape) { shape.draw(); ``` -我们可以看到程序并未这样表达:“如果你是一个 Circle ,就这样做;如果你是一个 Square,就那样做…...”。若那样编写代码,就需检查 Shape 所有可能的类型,如圆、矩形等等。这显然是非常麻烦的,而且每次添加了一种新的 Shape 类型后,都要相应地进行修改。在这里,我们只需说:“你是一种几何形状,我知道你能删掉 `erase()` 和绘制 `draw()` 你自己,去做吧,注意细节。” +我们可以看到程序并未这样表达:“如果你是一个 Circle ,就这样做;如果你是一个 Square,就那样做…...”。若那样编写代码,就需检查 Shape 所有可能的类型,如圆、矩形等等。这显然是非常麻烦的,而且每次添加了一种新的 Shape 类型后,都要相应地进行修改。在这里,我们只需说:“你是一种几何形状,我知道你能删掉 `erase()` 和绘制 `draw()`,你自己去做吧,注意细节。” 尽管我们没作出任何特殊指示,程序的操作也是完全正确和恰当的。我们知道,为 Circle 调用`draw()` 时执行的代码与为一个 Square 或 Line 调用 `draw()` 时执行的代码是不同的。但在将 `draw()` 信息发给一个匿名 Shape 时,根据 Shape 句柄当时连接的实际类型,会相应地采取正确的操作。这非常神奇,因为当 Java 编译器为 `doSomething()` 编译代码时,它并不知道自己要操作的准确类型是什么。 -尽管我们确实可以保证最终会为 Shape 调用 `erase()`、 `draw()`,但并不能确定特定的 Circle,Square 或者 Line 调用什么。最后,程序执行的操作却依然是正确的,这是怎么做到的呢? +尽管我们确实可以保证最终会为 Shape 调用 `erase()` 和 `draw()`,但并不能确定特定的 Circle,Square 或者 Line 调用什么。最后,程序执行的操作却依然是正确的,这是怎么做到的呢? 发送消息给对象时,如果程序不知道接收的具体类型是什么,但最终执行是正确的,这就是对象的“多态性”(Polymorphism)。面向对象的程序设计语言是通过“动态绑定”的方式来实现对象的多态性的。编译器和运行时系统会负责对所有细节的控制;我们只需知道要做什么,以及如何利用多态性来更好地设计程序。 ## 单继承结构 -自从 C++ 引入以来,一个OOP 问题变得尤为突出:是否所有的类都应该默认从一个基类继承呢?这个答案在 Java 中是肯定的(实际上,除 C++ 以外的几乎所有OOP语言中也是这样)。在 Java 中,这个最终的基类的名字就是 `Object`。 +自从 C++ 引入以来,一个OOP 问题变得尤为突出:是否所有的类都应该默认从一个基类继承呢?这个答案在 Java 中是肯定的(实际上,除 C++ 以外的几乎所有OOP语言中也是这样)。在 Java 中,这个最终基类的名字就是 `Object`。 -Java 的单继承结构有很多好处。由于所有对象都具有一个公共接口,因此它们最终都属于同一个基本类型。相反的,对于 C++ 所使用的多继承的方案则是不保证所有的对象都属于同一个基类。从向后兼容的角度看,多继承的方案更符合 C 的模型,而且受限较少。 +Java 的单继承结构有很多好处。由于所有对象都具有一个公共接口,因此它们最终都属于同一个基类。相反的,对于 C++ 所使用的多继承的方案则是不保证所有的对象都属于同一个基类。从向后兼容的角度看,多继承的方案更符合 C 的模型,而且受限较少。 对于完全面向对象编程,我们必须要构建自己的层次结构,以提供与其他 OOP 语言同样的便利。我们经常会使用到新的类库和不兼容的接口。为了整合它们而花费大气力(有可能还要用上多继承)以获得 C++ 样的“灵活性”值得吗?如果从零开始,Java 这样的替代方案会是更好的选择。 @@ -244,7 +244,7 @@ Java 的单继承结构有很多好处。由于所有对象都具有一个公共 还好,一般优秀的 OOP 语言都会将“集合”作为其基础包。在 C++ 中,“集合”是其标准库的一部分,通常被称为 STL(Standard Template Library,标准模板库)。SmallTalk 有一套非常完整的集合库。同样,Java 的标准库中也提供许多现成的集合类。 -在一些库中,一两个集合泛型集合就能满足我们所有的需求了,而在其他(Java)中,不同类型的集合对应不同的需求:常见的有 List,常用于保存序列;Map,也称为关联数组,常用于将对象与其他对象关联);Set,只能保存非重复的值;其他还包括如队列(Queue)、树(Tree)、堆(Stack)等等。从设计的角度来看,我们真正想要的是一个能够解决某个问题的集合。如果一种集合就满足所有需求,那么我们就不需要剩下的了。之所以选择集合有以下两个原因: +在一些库中,一两个泛型集合就能满足我们所有的需求了,而在其他一些类库(Java)中,不同类型的集合对应不同的需求:常见的有 List,常用于保存序列;Map,也称为关联数组,常用于将对象与其他对象关联);Set,只能保存非重复的值;其他还包括如队列(Queue)、树(Tree)、栈(Stack)等等。从设计的角度来看,我们真正想要的是一个能够解决某个问题的集合。如果一种集合就满足所有需求,那么我们就不需要剩下的了。之所以选择集合有以下两个原因: 1. 集合可以提供不同类型的接口和外部行为。堆栈、队列的应用场景和集合、列表不同,它们中的一种提供的解决方案可能比其他灵活得多。 diff --git a/docs/book/03-Objects-Everywhere.md b/docs/book/03-Objects-Everywhere.md index ee8f7dd..bd96286 100644 --- a/docs/book/03-Objects-Everywhere.md +++ b/docs/book/03-Objects-Everywhere.md @@ -100,7 +100,7 @@ char c = ch; -### 高精数值的存储 +### 高精度数值 在 Java 中有两种类型的数据可用于高精度的计算。它们是 `BigInteger` 和 `BigDecimal`。尽管它们大致可以划归为“包装类型”,但是它们并没有对应的基本类型。 @@ -417,7 +417,7 @@ import java.util.*; 2. 创建一个与此类的任何对象无关的方法。也就是说,即使没有创建对象,也能调用该方法。 -**static** 关键字(从 C++ 采用)就符合上述两点要求。当我们说某个事物是静态时,就意味着该字段或方法不依赖于任何特定的对象实例 。 即使我们从未创建过该类的对象,也可以调用其静态方法或访问静态字段。相反,对于普通的非静态字段和方法,我们必须要先创建一个对象并使用该对象来访问字段或方法,因为非静态字段和方法必须与特定对象关联 [^6] 。 +**static** 关键字(从 C++ 采用)就符合上述两点要求。当我们说某个事物是静态时,就意味着该字段或方法不依赖于任何特定的对象实例 。 即使我们从未创建过该类的对象,也可以调用其静态方法或访问其静态字段。相反,对于普通的非静态字段和方法,我们必须要先创建一个对象并使用该对象来访问字段或方法,因为非静态字段和方法必须与特定对象关联 [^6] 。 一些面向对象的语言使用类数据(class data)和类方法(class method),表示静态数据和方法只是作为类,而不是类的某个特定对象而存在的。有时 Java 文献也使用这些术语。 @@ -436,7 +436,7 @@ StaticTest st1 = new StaticTest(); StaticTest st2 = new StaticTest(); ``` -`st1.i` 和 `st2.i` 指向同一块存储空间,因此它们的值都是 47。引用静态变量有两种方法。在前面的示例中,我们通过一个对象来去定位它,例如 `st2.i`。我们也可以通过类名直接引用它,这种方式对于非静态成员则不行: +`st1.i` 和 `st2.i` 指向同一块存储空间,因此它们的值都是 47。引用静态变量有两种方法。在前面的示例中,我们通过一个对象来定位它,例如 `st2.i`。我们也可以通过类名直接引用它,这种方式对于非静态成员则不可行: ```java StaticTest.i++; @@ -444,7 +444,7 @@ StaticTest.i++; `++` 运算符将会使变量结果 + 1。此时 `st1.i` 和 `st2.i` 的值都变成了 48。 -使用类名直接引用静态变量是首选方法,因为它强调了变量的静态属性。类似的逻辑也适用于静态方法。我们可以通过对象引用静态方法,就像使用任何方法一样,也可以通过特殊的语法方式 `Classname.method()` 来直接调用静态属性或方法 [^7]。 代码示例: +使用类名直接引用静态变量是首选方法,因为它强调了变量的静态属性。类似的逻辑也适用于静态方法。我们可以通过对象引用静态方法,就像使用任何方法一样,也可以通过特殊的语法方式 `Classname.method()` 来直接调用静态属性或方法 [^7]。 代码示例: ```java class Incrementable { @@ -454,7 +454,7 @@ class Incrementable { } ``` -上例中,`Incrementable` 的 `increment()` 方法通过 `++` 运算符将静态数据 `i` 加1。我们依然可以先实例化对象再调用该方法。 代码示例: +上例中,`Incrementable` 的 `increment()` 方法通过 `++` 运算符将静态数据 `i` 加 1。我们依然可以先实例化对象再调用该方法。 代码示例: ```java Incrementable sf = new Incrementable(); @@ -489,17 +489,17 @@ public class HelloDate { ``` -在这本书中,所有代码示例的第一行都是注释行,其中包含文件的路径信息(比如本章的目录名是 **objects**),后跟文件名。我的工具可以根据这些信息自动提取和测试书籍的代码,你也可以通过参考第一行注释轻松地在 Github 库中找到对应的代码示例。 +在本书中,所有代码示例的第一行都是注释行,其中包含文件的路径信息(比如本章的目录名是 **objects**),后跟文件名。我的工具可以根据这些信息自动提取和测试书籍的代码,你也可以通过参考第一行注释轻松地在 Github 库中找到对应的代码示例。 如果你想在代码中使用一些额外的类库,那么就必须在程序文件的开始处使用 **import** 关键字来导入它们。之所以说是额外的,因为有一些类库已经默认自动导入到每个文件里了。例如:`java.lang` 包。 现在打开你的浏览器在 [Oracle](https://www.oracle.com/) 上查看文档。如果你还没有从 [Oracle](https://www.oracle.com/) 网站上下载 JDK 文档,那现在就去 [^8] 。查看包列表,你会看到 Java 附带的所有不同的类库。 -选择 `java.lang` ,你会看到该库中所有类的列表。由于 `java.lang` 隐式包含在每个 Java代码文件中,因此这些类是自动可用的。`java.lang` 类库中没有 **Date** 类,所以我们必须导入其他的类库(即 Date 所在的类库)。如果你不清楚某个类所在的类库或者想查看类库中所有的类,那么可以在 Java 文档中选择 “Tree” 查看。 +选择 `java.lang`,你会看到该库中所有类的列表。由于 `java.lang` 隐式包含在每个 Java 代码文件中,因此这些类是自动可用的。`java.lang` 类库中没有 **Date** 类,所以我们必须导入其他的类库(即 Date 所在的类库)。如果你不清楚某个类所在的类库或者想查看类库中所有的类,那么可以在 Java 文档中选择 “Tree” 查看。 现在,我们可以找到 Java 附带的每个类。使用浏览器的“查找”功能查找 **Date**,搜索结果中将会列出 **java.util.Date**,我们就知道了 **Date** 在 **util** 库中,所以必须导入 **java.util.*** 才能使用 **Date**。 -如果你在文档中选择 **java.lang**,然后选择 **System**,你会看到 **System** 类中有几个字段,如果你选择了 **out**,你会发现它是一个静态的 **PrintStream** 对象。 所以,即使我们不使用 **new** 创建, **out** 对象就已经存在并可以使用。 **out** 对象可以执行的操作取决于它的类型: **PrintStream** ,其在文档中是一个超链接,如果单击该链接,我们将可以看到 **PrintStream** 对应的方法列表(更多详情,将在本书后面介绍)。 现在我们重点说的是 **println()** 这个方法。 它的作用是 “将信息输出到控制台,并以换行符结束”。既然如此,我们可以这样编码来输出信息到控制台。 代码示例: +如果你在文档中选择 **java.lang**,然后选择 **System**,你会看到 **System** 类中有几个字段,如果你选择了 **out**,你会发现它是一个静态的 **PrintStream** 对象。 所以,即使我们不使用 **new** 创建, **out** 对象就已经存在并可以使用。 **out** 对象可以执行的操作取决于它的类型: **PrintStream** ,其在文档中是一个超链接,如果单击该链接,我们将可以看到 **PrintStream** 对应的方法列表(更多详情,将在本书后面介绍)。 现在我们重点说的是 **println()** 这个方法。 它的作用是 “将信息输出到控制台,并以换行符结束”。既然如此,我们可以这样编码来输出信息到控制台。 代码示例: ```java System.out.println("A String of things"); @@ -641,4 +641,4 @@ class AllTheColorsOfTheRainbow { [^10]: 为了保持本书的代码排版紧凑,我并没完全遵守规范,但我尽量会做到符合 Java 标准。 -
\ No newline at end of file +
diff --git a/docs/book/04-Operators.md b/docs/book/04-Operators.md index ebf1958..05ed80d 100644 --- a/docs/book/04-Operators.md +++ b/docs/book/04-Operators.md @@ -1472,7 +1472,7 @@ public class AllOps { } ``` -**注意** :**boolean** 类型的的运算是受限的。你能为其赋值 `true` 或 `false`,也可测试它的值是否是 `true` 或 `false`。但你不能对其作加减等其他运算。 +**注意** :**boolean** 类型的运算是受限的。你能为其赋值 `true` 或 `false`,也可测试它的值是否是 `true` 或 `false`。但你不能对其作加减等其他运算。 在 **char**,**byte** 和 **short** 类型中,我们可以看到算术运算符的“类型转换”效果。我们必须要显式强制类型转换才能将结果重新赋值为原始类型。对于 **int** 类型的运算则不用转换,因为默认就是 **int** 型。虽然我们不用再停下来思考这一切是否安全,但是两个大的 int 型整数相乘时,结果有可能超出 **int** 型的范围,这种情况下结果会发生溢出。下面的代码示例: diff --git a/docs/book/05-Control-Flow.md b/docs/book/05-Control-Flow.md index f9674a6..9f5676e 100644 --- a/docs/book/05-Control-Flow.md +++ b/docs/book/05-Control-Flow.md @@ -7,7 +7,7 @@ Java 使用了 C 的所有执行控制语句,因此对于熟悉 C/C++ 编程的人来说,这部分内容轻车熟路。大多数面向过程编程语言都有共通的某种控制语句。在 Java 中,涉及的关键字包括 **if-else,while,do-while,for,return,break** 和选择语句 **switch**。 Java 并不支持备受诟病的 **goto**(尽管它在某些特殊场景中依然是最行之有效的方法)。 尽管如此,在 Java 中我们仍旧可以进行类似的逻辑跳转,但较之典型的 **goto** 用法限制更多。 -## true和flase +## true 和 false 所有的条件语句都利用条件表达式的“真”或“假”来决定执行路径。举例: `a == b`。它利用了条件表达式 `==` 来比较 `a` 与 `b` 的值是否相等。 该表达式返回 `true` 或 `false`。代码示例: @@ -28,7 +28,7 @@ public class TrueFalse { true false ``` -通过上一章的学习,我们知道任何关系运算符都可以产生条件语句。 **注意**:在 Java 中使用数值作为布尔值是非法的,即便这种操作在 C/C++ 中是被允许的(在这些语言中,“真”为非零,而“假”是零)。如果想在布尔测试中使用一个非布尔值,那么首先需要使用条件表达式来产生 **boolean** 类型的结果,例如 `if(a! = 0)`。 +通过上一章的学习,我们知道任何关系运算符都可以产生条件语句。 **注意**:在 Java 中使用数值作为布尔值是非法的,即便这种操作在 C/C++ 中是被允许的(在这些语言中,“真”为非零,而“假”是零)。如果想在布尔测试中使用一个非布尔值,那么首先需要使用条件表达式来产生 **boolean** 类型的结果,例如 `if(a != 0)`。 ## if-else @@ -235,7 +235,7 @@ i = 4 j = 8 上例中 **int** 类型声明包含了 `i` 和 `j`。实际上,在初始化部分我们可以定义任意数量的同类型变量。**注意**:在 Java 中,仅允许 **for** 循环在控制表达式中定义变量。 我们不能将此方法与其他的循环语句和选择语句中一起使用。同时,我们可以看到:无论在初始化还是在步进部分,语句都是顺序执行的。 -## for-in语法 +## for-in 语法 Java 5 引入了更为简洁的“增强版 **for** 循环”语法来操纵数组和集合。(更多细节,可参考 [数组](./21-Arrays.md) 和 [集合](./12-Collections.md) 章节内容)。大部分文档也称其为 **for-each** 语法,但因为了不与 Java 8 新添的 `forEach()` 产生混淆,因此我称之为 **for-in** 循环。 (Python 已有类似的先例,如:**for x in sequence**)。**注意**:你可能会在其他地方看到不同叫法。 @@ -355,7 +355,7 @@ public class ForInInt { 在 Java 中有几个关键字代表无条件分支,这意味无需任何测试即可发生。这些关键字包括 **return**,**break**,**continue** 和跳转到带标签语句的方法,类似于其他语言中的 **goto**。 -**return** 关键字有两方面的作用:1.指定一个方法返回值(在方法返回类型非 **void** 的情况下);2.退出当前方法,并返回作用 1 中值。我们可以利用 `return` 的这些特点来改写上例 `IfElse.java` 文件中的 `test()` 方法。代码示例: +**return** 关键字有两方面的作用:1.指定一个方法返回值 (在方法返回类型非 **void** 的情况下);2.退出当前方法,并返回作用 1 中值。我们可以利用 `return` 的这些特点来改写上例 `IfElse.java` 文件中的 `test()` 方法。代码示例: ```java // control/TestWithReturn.java @@ -389,7 +389,7 @@ public class TestWithReturn { 如果在方法签名中定义了返回值类型为 **void**,那么在代码执行结束时会有一个隐式的 **return**。 也就是说我们不用在总是在方法中显式地包含 **return** 语句。 **注意**:如果你的方法声明的返回值类型为非 **void** 类型,那么则必须确保每个代码路径都返回一个值。 -## break和continue +## break 和 continue 在任何迭代语句的主体内,都可以使用 **break** 和 **continue** 来控制循环的流程。 其中,**break** 表示跳出当前循环体。而 **continue** 表示停止本次循环,开始下一次循环。 @@ -442,7 +442,7 @@ public class BreakAndContinue { **[3]** 无限 **while** 循环。循环内的 **break** 语句可中止循环。**注意**,**continue** 语句可将控制权移回循环的顶部,而不会执行 **continue** 之后的任何操作。 因此,只有当 `i` 的值可被 10 整除时才会输出。在输出中,显示值 0,因为 `0%9` 产生 0。还有一种无限循环的形式: `for(;;)`。 在编译器看来,它与 `while(true)` 无异,使用哪种完全取决于你的编程品味。 -## 臭名昭著的goto +## 臭名昭著的 goto [**goto** 关键字](https://en.wikipedia.org/wiki/Goto) 很早就在程序设计语言中出现。事实上,**goto** 起源于[汇编](https://en.wikipedia.org/wiki/Assembly_language)(assembly language)语言中的程序控制:“若条件 A 成立,则跳到这里;否则跳到那里”。如果你读过由由编译器编译后的代码,你会发现在其程序控制中充斥了大量的跳转。较之汇编产生的代码直接运行在硬件 CPU 中,Java 也会产生自己的“汇编代码”(字节码),只不过它是运行在 Java 虚拟机里的(Java Virtual Machine)。 @@ -485,7 +485,7 @@ outer-iteration { ```java // control/LabeledFor.java -// 搭配“标签break”的for循环中使用 break 和 continue +// 搭配“标签 break”的 for 循环中使用 break 和 continue public class LabeledFor { public static void main(String[] args) { @@ -523,7 +523,7 @@ public class LabeledFor { } } } - // 在此处无法break或continue标签 + // 在此处无法 break 或 continue 标签 } } ``` @@ -624,7 +624,7 @@ break outer 大家要记住的重点是:在 Java 里需要使用标签的唯一理由就是因为有循环嵌套存在,而且想从多层嵌套中 **break** 或 **continue**。 -**break** 和 **continue** 标签在编码中的使用频率相对较低(此前的语言中很少使用或没有先例),所以我们很少在代码里看到它们。 +**break** 和 **continue** 标签在编码中的使用频率相对较低 (此前的语言中很少使用或没有先例),所以我们很少在代码里看到它们。 在 *Dijkstra* 的 **“Goto 有害”** 论文中,他最反对的就是标签,而非 **goto**。他观察到 BUG 的数量似乎随着程序中标签的数量而增加[^2]。标签和 **goto** 使得程序难以分析。但是,Java 标签不会造成这方面的问题,因为它们的应用场景受到限制,无法用于以临时方式传输控制。由此也引出了一个有趣的情形:对语言能力的限制,反而使它这项特性更加有价值。 @@ -649,7 +649,7 @@ switch(integral-selector) { 在上面的定义中,大家会注意到每个 **case** 均以一个 **break** 结尾。这样可使执行流程跳转至 **switch** 主体的末尾。这是构建 **switch** 语句的一种传统方式,但 **break** 是可选的。若省略 **break,** 会继续执行后面的 **case** 语句的代码,直到遇到一个 **break** 为止。通常我们不想出现这种情况,但对有经验的程序员来说,也许能够善加利用。注意最后的 **default** 语句没有 **break**,因为执行流程已到了 **break** 的跳转目的地。当然,如果考虑到编程风格方面的原因,完全可以在 **default** 语句的末尾放置一个 **break**,尽管它并没有任何实际的作用。 -**switch** 语句是一种实现多路选择的干净利落的一种方式(比如从一系列执行路径中挑选一个)。但它要求使用一个选择因子,并且必须是 **int** 或 **char** 那样的整数值。例如,假若将一个字串或者浮点数作为选择因子使用,那么它们在 switch 语句里是不会工作的。对于非整数类型(Java 7 以上版本中的 String 型除外),则必须使用一系列 **if** 语句。 在[下一章的结尾](./06-Housekeeping.md#枚举类型)中,我们将会了解到**枚举类型**被用来搭配 **switch** 工作,并优雅地解决了这种限制。 +**switch** 语句是一种实现多路选择的干净利落的一种方式(比如从一系列执行路径中挑选一个)。但它要求使用一个选择因子,并且必须是 **int** 或 **char** 那样的整数值。例如,假若将一个字串或者浮点数作为选择因子使用,那么它们在 switch 语句里是不会工作的。对于非整数类型(Java 7 以上版本中的 String 型除外),则必须使用一系列 **if** 语句。 在[下一章的结尾](./06-Housekeeping.md#枚举类型) 中,我们将会了解到**枚举类型**被用来搭配 **switch** 工作,并优雅地解决了这种限制。 下面这个例子可随机生成字母,并判断它们是元音还是辅音字母: @@ -713,7 +713,7 @@ int c = rand.nextInt(26) + 'a'; -## switch字符串 +## switch 字符串 Java 7 增加了在字符串上 **switch** 的用法。 下例展示了从一组 **String** 中选择可能值的传统方法,以及新式方法: @@ -818,10 +818,10 @@ java RandomBounds upper 本章总结了我们对大多数编程语言中出现的基本特性的探索:计算,运算符优先级,类型转换,选择和迭代。 现在让我们准备好,开始步入面向对象和函数式编程的世界吧。 下一章的内容涵盖了 Java 编程中的重要问题:对象的[初始化和清理](./06-Housekeeping.md)。紧接着,还会介绍[封装](./07-Implementation-Hiding.md)(implementation hiding)的核心概念。 -[^1]: 在早期的语言中,许多决策都是基于让编译器设计者的体验更好。 但在现代语言设计中,许多决策都是为了提高语言使用者的体验,尽管有时会有妥协 —— 这通常会让语言设计者后悔。 +[^1]: 在早期的语言中,许多决策都是基于让编译器设计者的体验更好。 但在现代语言设计中,许多决策都是为了提高语言使用者的体验,尽管有时会有妥协 —— 这通常会让语言设计者后悔。 -[^2]: **注意**,此处观点似乎难以让人信服,很可能只是一个因认知偏差而造成的[因果关系谬误](https://en.wikipedia.org/wiki/Correlation_does_not_imply_causation)的例子。 +[^2]: **注意**,此处观点似乎难以让人信服,很可能只是一个因认知偏差而造成的[因果关系谬误](https://en.wikipedia.org/wiki/Correlation_does_not_imply_causation) 的例子。 -
\ No newline at end of file +
diff --git a/docs/book/06-Housekeeping.md b/docs/book/06-Housekeeping.md index 4a3d263..b589ee4 100644 --- a/docs/book/06-Housekeeping.md +++ b/docs/book/06-Housekeeping.md @@ -712,7 +712,7 @@ Error: checked out 如前文所述,这里讨论的 Java 虚拟机中,内存分配以较大的"块"为单位。如果对象较大,它会占用单独的块。严格来说,"停止-复制"要求在释放旧对象之前,必须先将所有存活对象从旧堆复制到新堆,这导致了大量的内存复制行为。有了块,垃圾回收器就可以把对象复制到废弃的块。每个块都有年代数来记录自己是否存活。通常,如果块在某处被引用,其年代数加 1,垃圾回收器会对上次回收动作之后新分配的块进行整理。这对处理大量短命的临时对象很有帮助。垃圾回收器会定期进行完整的清理动作——大型对象仍然不会复制(只是年代数会增加),含有小型对象的那些块则被复制并整理。Java 虚拟机会监视,如果所有对象都很稳定,垃圾回收的效率降低的话,就切换到"标记-清扫"方式。同样,Java 虚拟机会跟踪"标记-清扫"的效果,如果堆空间出现很多碎片,就会切换回"停止-复制"方式。这就是"自适应"的由来,你可以给它个啰嗦的称呼:"自适应的、分代的、停止-复制、标记-清扫"式的垃圾回收器。 -Java 虚拟机中有许多附加技术用来提升速度。尤其是与加载器操作有关的,被称为"即时"(Just-In-Time, JIT)编译器的技术。这种技术可以把程全部或部分翻译成本地机器码,所以不需要 JVM 来进行翻译,因此运行得更快。当需要装载某个类(通常是创建该类的第一个对象)时,编译器会先找到其 **.class** 文件,然后将该类的字节码装入内存。你可以让即时编译器编译所有代码,但这种做法有两个缺点:一是这种加载动作贯穿整个程序生命周期内,累加起来需要花更多时间;二是会增加可执行代码的长度(字节码要比即时编译器展开后的本地机器码小很多),这会导致页面调度,从而一定降低程序速度。另一种做法称为*惰性评估*,意味着即时编译器只有在必要的时候才编译代码。这样,从未被执行的代码也许就压根不会被 JIT 编译。新版 JDK 中的 Java HotSpot 技术就采用了类似的做法,代码每被执行一次就优化一些,所以执行的次数越多,它的速度就越快。 +Java 虚拟机中有许多附加技术用来提升速度。尤其是与加载器操作有关的,被称为"即时"(Just-In-Time, JIT)编译器的技术。这种技术可以把程序全部或部分翻译成本地机器码,所以不需要 JVM 来进行翻译,因此运行得更快。当需要装载某个类(通常是创建该类的第一个对象)时,编译器会先找到其 **.class** 文件,然后将该类的字节码装入内存。你可以让即时编译器编译所有代码,但这种做法有两个缺点:一是这种加载动作贯穿整个程序生命周期内,累加起来需要花更多时间;二是会增加可执行代码的长度(字节码要比即时编译器展开后的本地机器码小很多),这会导致页面调度,从而一定降低程序速度。另一种做法称为*惰性评估*,意味着即时编译器只有在必要的时候才编译代码。这样,从未被执行的代码也许就压根不会被 JIT 编译。新版 JDK 中的 Java HotSpot 技术就采用了类似的做法,代码每被执行一次就优化一些,所以执行的次数越多,它的速度就越快。 @@ -1054,7 +1054,7 @@ f3(1) 概括一下创建对象的过程,假设有个名为 **Dog** 的类: -1. 即使没有显式地使用 **static** 关键字,构造器实际上也是静态方法。所以,当首次创建 **Dog** 类型的对象时,或是首次访问 **Dog** 类的静态方法或属性,Java 解释器必须在类路径中查找,以定位 **Dog.class**。 +1. 即使没有显式地使用 **static** 关键字,构造器实际上也是静态方法。所以,当首次创建 **Dog** 类型的对象或是首次访问 **Dog** 类的静态方法或属性时,Java 解释器必须在类路径中查找,以定位 **Dog.class**。 2. 当加载完 **Dog.class** 后(后面会学到,这将创建一个 **Class** 对象),有关静态初始化的所有动作都会执行。因此,静态初始化只会在首次加载 **Class** 对象时初始化一次。 3. 当用 `new Dog()` 创建对象时,首先会在堆上为 **Dog** 对象分配足够的存储空间。 4. 分配的存储空间首先会被清零,即会将 **Dog** 对象中的所有基本类型数据设置为默认值(数字会被置为 0,布尔型和字符型也相同),引用被置为 **null**。 @@ -1812,4 +1812,4 @@ This burrito is maybe too hot. -
\ No newline at end of file +
diff --git a/docs/book/08-Reuse.md b/docs/book/08-Reuse.md index 04af7f9..99e6768 100644 --- a/docs/book/08-Reuse.md +++ b/docs/book/08-Reuse.md @@ -210,13 +210,13 @@ Cleanser dilute() apply() scrub() 这演示了一些特性。首先,在 **Cleanser** 的 `append()` 方法中,使用 `+=` 操作符将字符串连接到 **s**,这是 Java 设计人员“重载”来处理字符串的操作符之一 (还有 + )。 -第二,**Cleanser** 和 **Detergent** 都包含一个 `main()` 方法。你可以为每个类创建一个 `main()` ; 这允许对每个类进行简单的测试。当你完成测试时,不需要删除 `main()`; 你可以将其留在以后的测试中。即使程序中有很多类都有 `main()` 方法,惟一运行的只有在命令行上调用的 `main()`。这里,当你使用 **java Detergent** 时候,就调用了 `Detergent.main()`。但是你也可以使用 **java Cleanser** 来调用 `Cleanser.main()`,即使 **Cleanser** 不是一个公共类。即使类具只有包访问权,也可以访问 `public main()`。 +第二,**Cleanser** 和 **Detergent** 都包含一个 `main()` 方法。你可以为每个类创建一个 `main()` ; 这允许对每个类进行简单的测试。当你完成测试时,不需要删除 `main()`; 你可以将其留在以后的测试中。即使程序中有很多类都有 `main()` 方法,惟一运行的只有在命令行上调用的 `main()`。这里,当你使用 **java Detergent** 时候,就调用了 `Detergent.main()`。但是你也可以使用 **java Cleanser** 来调用 `Cleanser.main()`,即使 **Cleanser** 不是一个公共类。即使类只具有包访问权,也可以访问 `public main()`。 在这里,`Detergent.main()` 显式地调用 `Cleanser.main()`,从命令行传递相同的参数(当然,你可以传递任何字符串数组)。 **Cleanser** 中的所有方法都是公开的。请记住,如果不使用任何访问修饰符,则成员默认为包访问权限,这只允许包内成员访问。因此,如果没有访问修饰符,那么包内的任何人都可以使用这些方法。例如,**Detergent** 就没有问题。但是,如果其他包中的类继承 **Cleanser**,则该类只能访问 **Cleanser** 的公共成员。因此,为了允许继承,一般规则是所有字段为私有,所有方法为公共。(受保护成员也允许派生类访问;你以后会知道的。)在特定的情况下,你必须进行调整,但这是一个有用的指南。 -**Cleanser** 的接口中有一组方法: `append()`、`dilute()`、`apply()`、`scrub()` 和 `toString()`。因为 **Detergent** 是从 **Cleanser** 派生的(通过 **extends** 关键字),所以它会在其接口中自动获取所有这些方法,即使你没有在 **Detergent** 中看到所有这些方法的显式定义。你那么,可以把继承看作是复用类。如在 `scrub()` 中所见,可以使用基类中定义的方法并修改它。在这里,你可以在新类中调用基类的该方法。但是在 `scrub()` 内部,不能简单地调用 `scrub()`,因为这会产生递归调用。为了解决这个问题,Java的 **super** 关键字引用了当前类继承的“超类”(基类)。因此表达式 `super.scrub()` 调用方法 `scrub()` 的基类版本。 +**Cleanser** 的接口中有一组方法: `append()`、`dilute()`、`apply()`、`scrub()` 和 `toString()`。因为 **Detergent** 是从 **Cleanser** 派生的(通过 **extends** 关键字),所以它会在其接口中自动获取所有这些方法,即使你没有在 **Detergent** 中看到所有这些方法的显式定义。那么,可以把继承看作是复用类。如在 `scrub()` 中所见,可以使用基类中定义的方法并修改它。在这里,你可以在新类中调用基类的该方法。但是在 `scrub()` 内部,不能简单地调用 `scrub()`,因为这会产生递归调用。为了解决这个问题,Java的 **super** 关键字引用了当前类继承的“超类”(基类)。因此表达式 `super.scrub()` 调用方法 `scrub()` 的基类版本。 继承时,你不受限于使用基类的方法。你还可以像向类添加任何方法一样向派生类添加新方法:只需定义它。方法 `foam()` 就是一个例子。`Detergent.main()` 中可以看到,对于 **Detergent** 对象,你可以调用 **Cleanser** 和 **Detergent** 中可用的所有方法 (如 `foam()` )。 @@ -507,7 +507,7 @@ PlaceSetting constructor ### 保证适当的清理 -Java 没有 C++ 中析构函数的概念,析构函数是在对象被销毁时自动调用的方法。原因可能是,在Java中,通常是忘掉而不是销毁对象,从而允许垃圾收集器根据需要回收内存。通常这是可以的,但是有时你的类可能在其生命周期中执行一些需要清理的活动。初始化和清理章节提到,你不能知道垃圾收集器何时会被调用,甚至它是否会被调用。因此,如果你想为类清理一些东西,你必须显式地编写一个特殊的方法来完成它,并确保客户端程序员知道他们必须调用这个方法。最重要的是——正如在"异常"章节中描述的——你必须通过在 **finally **子句中放置此类清理来防止异常。 +Java 没有 C++ 中析构函数的概念,析构函数是在对象被销毁时自动调用的方法。原因可能是,在Java中,通常是忘掉而不是销毁对象,从而允许垃圾收集器根据需要回收内存。通常这是可以的,但是有时你的类可能在其生命周期中执行一些需要清理的活动。初始化和清理章节提到,你无法知道垃圾收集器何时会被调用,甚至它是否会被调用。因此,如果你想为类清理一些东西,必须显式地编写一个特殊的方法来完成它,并确保客户端程序员知道他们必须调用这个方法。最重要的是——正如在"异常"章节中描述的——你必须通过在 **finally **子句中放置此类清理来防止异常。 请考虑一个在屏幕上绘制图片的计算机辅助设计系统的例子: @@ -898,7 +898,7 @@ public class Wind extends Instrument { 一个被 **static** 和 **final** 同时修饰的属性只会占用一段不能改变的存储空间。 -当用 **final** 修饰对象引用而非基本类型时,其含义会有一点令人困惑。对于基本类型,**final** 使数值恒定不变,而对于对象引用,**final** 使引用恒定不变。一旦引用被初始化指向了某个对象,它就不能改为指向其他对象。但是,对象本身是可以修改的,Java 没有提供使任何对象恒定不变的方法。(你可以自己编写类达到使对象恒定不变的效果)这一限制同样适用数组,数组也是对象。 +当用 **final** 修饰对象引用而非基本类型时,其含义会有一点令人困惑。对于基本类型,**final** 使数值恒定不变,而对于对象引用,**final** 使引用恒定不变。一旦引用被初始化指向了某个对象,它就不能改为指向其他对象。但是,对象本身是可以修改的,Java 没有提供将任意对象设为常量的方法。(你可以自己编写类达到使对象恒定不变的效果)这一限制同样适用数组,数组也是对象。 下面例子展示了 **final** 属性的使用: @@ -1062,7 +1062,7 @@ public class FinalArguments { 过去建议使用 **final** 方法的第二个原因是效率。在早期的 Java 实现中,如果将一个方法指明为 **final**,就是同意编译器把对该方法的调用转化为内嵌调用。当编译器遇到 **final** 方法的调用时,就会很小心地跳过普通的插入代码以执行方法的调用机制(将参数压栈,跳至方法代码处执行,然后跳回并清理栈中的参数,最终处理返回值),而用方法体内实际代码的副本替代方法调用。这消除了方法调用的开销。但是如果一个方法很大代码膨胀,你也许就看不到内嵌带来的性能提升,因为内嵌调用带来的性能提高被花费在方法里的时间抵消了。 -在最近的 Java 版本中,虚拟机可以探测到这些情况(尤其是 *hotspot* 技术),并优化去掉这些效率反而降低的内嵌调用方法。有很长一段时间,使用 **final** 来提高效率都被阻止。你应该让编译器和 JVM 处理效率问题,只有在防止方法ß覆写时才使用 **final**。 +在最近的 Java 版本中,虚拟机可以探测到这些情况(尤其是 *hotspot* 技术),并优化去掉这些效率反而降低的内嵌调用方法。有很长一段时间,使用 **final** 来提高效率都被阻止。你应该让编译器和 JVM 处理性能问题,只有在为了明确禁止覆写方法时才使用 **final**。 ### final 和 private diff --git a/docs/book/09-Polymorphism.md b/docs/book/09-Polymorphism.md index aa1b55c..4f56929 100644 --- a/docs/book/09-Polymorphism.md +++ b/docs/book/09-Polymorphism.md @@ -153,7 +153,7 @@ Brass.play() MIDDLE_C ## 转机 -运行程序后会看到 **Music.java** 的难点。**Wind.play()** 的输出结果正是我们期望的,但它看起来似乎不能产生我们所期望的结果。观察 `tune()` 方法: +运行程序后会看到 **Music.java** 的难点。**Wind.play()** 的输出结果正是我们期望的,然而它看起来似乎不应该得出这样的结果。观察 `tune()` 方法: ```java public static void tune(Instrument i) { @@ -194,7 +194,7 @@ Shape s = new Circle(); 这会创建一个 **Circle** 对象,引用被赋值给 **Shape** 类型的变量 s,这看似错误(将一种类型赋值给另一种类型),然而是没问题的,因此从继承上可认为圆(Circle)就是一个形状(Shape)。因此编译器认可了赋值语句,没有报错。 -假设你调用了一个基类方法(在派生类中都被覆写): +假设你调用了一个基类方法(在各个派生类中都被覆写): ```java s.draw() @@ -202,7 +202,7 @@ s.draw() 你可能再次认为 **Shape** 的 `draw()` 方法被调用,因为 s 是一个 **Shape** 引用——编译器怎么可能知道要做其他的事呢?然而,由于后期绑定(多态)被调用的是 **Circle** 的 `draw()` 方法,这是正确的。 -下面的例子稍微有些不同。首先让我们创建一个可复用的 **Shape** 类库,基类 **Shape** 中建立了所有继承自 **Shape** 的类的共同接口——所有的形状都可以被绘画和擦除: +下面的例子稍微有些不同。首先让我们创建一个可复用的 **Shape** 类库,基类 **Shape** 为它的所有子类建立了公共接口——所有的形状都可以被绘画和擦除: ```java // polymorphism/shape/Shape.java @@ -329,7 +329,7 @@ Circle.draw() ### 可扩展性 -现在让我们回头看音乐乐器的例子。由于多态机制,你可以向系统中添加任意多的新类型,而不需要修改 `tune()` 方法。在一个设计良好的面向对象程序中,许多方法将会遵循 `tune()` 的模型,只与基类接口通信。这样的程序时可扩展的,因为可以从通用的基类派生出新的数据类型,从而添加新的功能。那些操纵基类接口的方法不需要改动就可以应用于新类。 +现在让我们回头看音乐乐器的例子。由于多态机制,你可以向系统中添加任意多的新类型,而不需要修改 `tune()` 方法。在一个设计良好的面向对象程序中,许多方法将会遵循 `tune()` 的模型,只与基类接口通信。这样的程序是可扩展的,因为可以从通用的基类派生出新的数据类型,从而添加新的功能。那些操纵基类接口的方法不需要改动就可以应用于新类。 考虑一下乐器的例子,如果在基类中添加更多的方法,并加入一些新类,将会发生什么呢: @@ -1164,7 +1164,7 @@ SadActor ![类图](../images/1562406479787.png) -这被称作纯粹的“is - a"关系,因为类的接口已经确定了它是什么。继承可以确保任何派生类都拥有基类的接口,绝对不会少。如果按图上这么做,派生类的接口就与基类一样多,也不会多。 +这被称作纯粹的“is - a"关系,因为类的接口已经确定了它是什么。继承可以确保任何派生类都拥有基类的接口,绝对不会少。如果按图上这么做,派生类将只拥有基类的接口。 纯粹的替代意味着派生类可以完美地替代基类,当使用它们时,完全不需要知道这些子类的信息。也就是说,基类可以接收任意发送给派生类的消息,因为它们具有完全相同的接口。只需将派生类向上转型,不要关注对象的具体类型。所有一切都可以通过多态处理。 @@ -1252,4 +1252,4 @@ RTTI 不仅仅包括简单的转型。例如,它还提供了一种方法,使 -
\ No newline at end of file +
diff --git a/docs/book/10-Interfaces.md b/docs/book/10-Interfaces.md index aff455e..f2a4f21 100644 --- a/docs/book/10-Interfaces.md +++ b/docs/book/10-Interfaces.md @@ -760,7 +760,7 @@ Woodwind.play() MIDDLE_C ## 抽象类和接口 -尤其是在 Java 8 引入 **default** 方法之后,区分何时抽象类还是接口是最好的选择变得更加令人困惑。下表做了明确的区分: +尤其是在 Java 8 引入 **default** 方法之后,选择用抽象类还是用接口变得更加令人困惑。下表做了明确的区分: | 特性 | 接口 | 抽象类 | | :------------------: | :--------------------------------------------------------: | :--------------------------------------: | @@ -1076,7 +1076,7 @@ Waveform 0 ![类图](../images/1562999314238.png) -在一个派生类中,你不能要求必须有一个是抽象的活“具体的”(没有任何抽象方法)的基类。如果确实要继承一个非接口的类,那么只能继承一个,其余的基元素必须都是接口。需要将所有的接口名称置于 **implements** 关键字之后且用逗号分隔。可以有任意多个接口,并可以向上转型为每个接口,因为每个接口都是独立的类型。下例展示了一个组合了多接口的具体类产生了新类: +派生类并不要求必须继承自抽象的或“具体的”(没有任何抽象方法)的基类。如果继承一个非接口的类,那么只能继承一个类,其余的基元素必须都是接口。需要将所有的接口名称置于 **implements** 关键字之后且用逗号分隔。可以有任意多个接口,并可以向上转型为每个接口,因为每个接口都是独立的类型。下例展示了一个由多个接口组合而成的具体类产生的新类: ```java // interfaces/Adventure.java @@ -1299,7 +1299,7 @@ import java.nio.*; import java.util.*; public class RandomStrings implements Readable { - private statdic Random rand = new Random(47); + private static Random rand = new Random(47); private static final char[] CAPITALS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray(); private static final char[] LOWERS = "abcdefghijklmnopqrstuvwxyz".toCharArray(); private static final char[] VOWELS = "aeiou".toCharArray(); @@ -1815,4 +1815,4 @@ Chess move 3 -
\ No newline at end of file +
diff --git a/docs/book/12-Collections.md b/docs/book/12-Collections.md index 4bd425a..199300a 100644 --- a/docs/book/12-Collections.md +++ b/docs/book/12-Collections.md @@ -22,9 +22,9 @@ MyType aReference; ## 泛型和类型安全的集合 -使用 Java 5 之前的集合的一个主要问题是编译器允许你向集合中插入不正确的类型。例如,考虑一个 **Apple** 对象的集合,这里使用最基本最可靠的 **ArrayList** 。现在,可以把 **ArrayList** 看作“可以自动扩充自身尺寸的数组”来看待。使用 **ArrayList** 相当简单:创建一个实例,用 **add()** 插入对象;然后用 **get()** 来访问这些对象,此时需要使用索引,就像数组那样,但是不需要方括号。[^2] **ArrayList** 还有一个 **size()** 方法,来说明集合中包含了多少个元素,所以不会不小心因数组越界而引发错误(通过抛出*运行时异常*,[异常]()章节介绍了异常)。 +使用 Java 5 之前的集合的一个主要问题是编译器允许你向集合中插入不正确的类型。例如,考虑一个 **Apple** 对象的集合,这里使用最基本最可靠的 **ArrayList** 。现在,可以把 **ArrayList** 看作“可以自动扩充自身尺寸的数组”来看待。使用 **ArrayList** 相当简单:创建一个实例,用 `add()` 插入对象;然后用 `get()` 来访问这些对象,此时需要使用索引,就像数组那样,但是不需要方括号。[^2] **ArrayList** 还有一个 `size()` 方法,来说明集合中包含了多少个元素,所以不会不小心因数组越界而引发错误(通过抛出*运行时异常*,[异常]()章节介绍了异常)。 -在本例中, **Apple** 和 **Orange** 都被放到了集合中,然后将它们取出。正常情况下,Java编译器会给出警告,因为这个示例没有使用泛型。在这里,使用特定的注解来抑制警告信息。注解以“@”符号开头,可以带参数。这里的 **@SuppressWarning** 注解及其参数表示只抑制“unchecked”类型的警告([注解]()章节将介绍更多有关注解的信息): +在本例中, **Apple** 和 **Orange** 都被放到了集合中,然后将它们取出。正常情况下,Java编译器会给出警告,因为这个示例没有使用泛型。在这里,使用特定的注解来抑制警告信息。注解以“@”符号开头,可以带参数。这里的 `@SuppressWarning` 注解及其参数表示只抑制“unchecked”类型的警告([注解]()章节将介绍更多有关注解的信息): ```java // collections/ApplesAndOrangesWithoutGenerics.java @@ -64,11 +64,11 @@ ndOrangesWithoutGenerics.java:23) */ ``` -**Apple** 和 **Orange** 是截然不同的,它们除了都是 **Object** 之外没有任何共同点(如果一个类没有显式地声明继承自哪个类,那么它就自动继承自 **Object**)。因为 **ArrayList** 保存的是 **Object** ,所以不仅可以通过 **ArrayList** 的 **add()** 方法将 **Apple** 对象放入这个集合,而且可以放入 **Orange** 对象,这无论在编译期还是运行时都不会有问题。当使用 **ArrayList** 的 **get()** 方法来取出你认为是 **Apple** 的对象时,得到的只是 **Object** 引用,必须将其转型为 **Apple**。然后需要将整个表达式用括号括起来,以便在调用 **Apple** 的 **id()** 方法之前,强制执行转型。否则,将会产生语法错误。 +**Apple** 和 **Orange** 是截然不同的,它们除了都是 **Object** 之外没有任何共同点(如果一个类没有显式地声明继承自哪个类,那么它就自动继承自 **Object**)。因为 **ArrayList** 保存的是 **Object** ,所以不仅可以通过 **ArrayList** 的 `add()` 方法将 **Apple** 对象放入这个集合,而且可以放入 **Orange** 对象,这无论在编译期还是运行时都不会有问题。当使用 **ArrayList** 的 `get()` 方法来取出你认为是 **Apple** 的对象时,得到的只是 **Object** 引用,必须将其转型为 **Apple**。然后需要将整个表达式用括号括起来,以便在调用 **Apple** 的 `id()` 方法之前,强制执行转型。否则,将会产生语法错误。 在运行时,当尝试将 **Orange** 对象转为 **Apple** 时,会出现输出中显示的错误。 -在[泛型]()章节中,你将了解到使用 Java 泛型来创建类可能很复杂。但是,使用预先定义的泛型类却相当简单。例如,要定义一个用于保存 **Apple** 对象的 **ArrayList** ,只需要使用 **ArrayList** 来代替 **ArrayList** 。尖括号括起来的是*类型参数*(可能会有多个),它指定了这个集合实例可以保存的类型。 +在[泛型]()章节中,你将了解到使用 Java 泛型来创建类可能很复杂。但是,使用预先定义的泛型类却相当简单。例如,要定义一个用于保存 **Apple** 对象的 **ArrayList** ,只需要使用 **ArrayList\** 来代替 **ArrayList** 。尖括号括起来的是*类型参数*(可能会有多个),它指定了这个集合实例可以保存的类型。 通过使用泛型,就可以在编译期防止将错误类型的对象放置到集合中。[^3]下面还是这个示例,但是使用了泛型: ```java @@ -94,7 +94,7 @@ public class ApplesAndOrangesWithGenerics { */ ``` -在 **apples** 定义的右侧,可以看到 **new ArrayList<>()** 。这有时被称为“菱形语法”(diamond syntax)。在 Java 7 之前,必须要在两端都进行类型声明,如下所示: +在 **apples** 定义的右侧,可以看到 `new ArrayList<>()` 。这有时被称为“菱形语法”(diamond syntax)。在 Java 7 之前,必须要在两端都进行类型声明,如下所示: ```java ArrayList apples = new ArrayList(); @@ -104,7 +104,7 @@ ArrayList apples = new ArrayList(); 有了 **ArrayList** 声明中的类型指定,编译器会阻止将 **Orange** 放入 **apples** ,因此,这会成为一个编译期错误而不是运行时错误。 -使用泛型,从 **List** 中获取元素不需要强制类型转换。因为 **List** 知道它持有什么类型,因此当调用 **get()** 时,它会替你执行转型。这样,不仅可以知道编译器将会检查放入集合中的对象的类型,而且在使用集合中的对象时,可以使用更加清晰的语法。 +使用泛型,从 **List** 中获取元素不需要强制类型转换。因为 **List** 知道它持有什么类型,因此当调用 `get()` 时,它会替你执行转型。这样,不仅可以知道编译器将会检查放入集合中的对象的类型,而且在使用集合中的对象时,可以使用更加清晰的语法。 当指定了某个类型为泛型参数时,并不仅限于只能将确切类型的对象放入集合中。向上转型也可以像作用于其他类型一样作用于泛型: ```java @@ -137,7 +137,7 @@ Braeburn@4e25154f 因此,可以将 **Apple** 的子类型添加到被指定为保存 **Apple** 对象的集合中。 -程序的输出是从 **Object** 默认的 **toString()** 方法产生的,该方法打印类名,后边跟着对象的散列码的无符号十六进制表示(这个散列码是通过 **hashCode()** 方法产生的)。将在[附录:理解equals和hashCode方法]()中了解有关散列码的内容。 +程序的输出是从 **Object** 默认的 `toString()` 方法产生的,该方法打印类名,后边跟着对象的散列码的无符号十六进制表示(这个散列码是通过 `hashCode()` 方法产生的)。将在[附录:理解equals和hashCode方法]()中了解有关散列码的内容。 ## 基本概念 @@ -145,7 +145,7 @@ Braeburn@4e25154f Java集合类库采用“持有对象”(holding objects)的思想,并将其分为两个不同的概念,表示为类库的基本接口: 1. **集合(Collection)** :一个独立元素的序列,这些元素都服从一条或多条规则。**List** 必须以插入的顺序保存元素, **Set** 不能包含重复元素, **Queue** 按照*排队规则*来确定对象产生的顺序(通常与它们被插入的顺序相同)。 -2. **映射(Map)** : 一组成对的“键值对”对象,允许使用键来查找值。 **ArrayList** 使用数字来查找对象,因此在某种意义上讲,它是将数字和对象关联在一起。 **map**允许我们使用一个对象来查找另一个对象,它也被称作*关联数组*(associative array),因为它将对象和其它对象关联在一起;或者称作*字典*(dictionary),因为可以使用一个键对象来查找值对象,就像在字典中使用单词查找定义一样。 **Map**s是强大的编程工具。 +2. **映射(Map)** : 一组成对的“键值对”对象,允许使用键来查找值。 **ArrayList** 使用数字来查找对象,因此在某种意义上讲,它是将数字和对象关联在一起。 **map** 允许我们使用一个对象来查找另一个对象,它也被称作*关联数组*(associative array),因为它将对象和其它对象关联在一起;或者称作*字典*(dictionary),因为可以使用一个键对象来查找值对象,就像在字典中使用单词查找定义一样。 **Map** 是强大的编程工具。 尽管并非总是可行,但在理想情况下,你编写的大部分代码都在与这些接口打交道,并且唯一需要指定所使用的精确类型的地方就是在创建的时候。因此,可以像下面这样创建一个 **List** : @@ -182,9 +182,9 @@ public class SimpleCollection { */ ``` -这个例子仅适用 **Collection** 方法,所以任何继承自 **Collection** 的类的对象都可以使用。但是 **ArrayList** 是最基本的序列类型。 +这个例子仅使用了 **Collection** 中的方法(即 `add()` ),所以使用任何继承自 **Collection** 的类的对象都可以正常工作。但是 **ArrayList** 是最基本的序列类型。 -**add()** 方法的名称就表明它是在 **Collection** 中添加一个新元素。但是,文档中非常详细地叙述到 **add()** “要确保这个 **Collection** 包含指定的元素。”这是因为考虑到了 **Set** 的含义,因为在 **Set**中,只有当元素不存在时才会添加元素。在使用 **ArrayList** ,或任何其他类型的 **List** 时,**add()** 总是表示“把它放进去”,因为 **List** 不关心是否存在重复元素。 +`add()` 方法的名称就表明它是在 **Collection** 中添加一个新元素。但是,文档中非常详细地叙述到 `add()` “要确保这个 **Collection** 包含指定的元素。”这是因为考虑到了 **Set** 的含义,因为在 **Set**中,只有当元素不存在时才会添加元素。在使用 **ArrayList** ,或任何其他类型的 **List** 时,`add()` 总是表示“把它放进去”,因为 **List** 不关心是否存在重复元素。 可以使用 *for-in* 语法来遍历所有的 **Collection** ,就像这里所展示的那样。在本章的后续部分,还将学习到一个更灵活的概念,*迭代器*。 @@ -194,7 +194,7 @@ public class SimpleCollection { 在 **java.util** 包中的 **Arrays** 和 **Collections** 类中都有很多实用的方法,可以在一个 **Collection** 中添加一组元素。 -**Arrays.asList()** 方法接受一个数组或是逗号分隔的元素列表(使用可变参数),并将其转换为 **List** 对象。 **Collections.addAll()** 方法接受一个 **Collection** 对象,以及一个数组或是一个逗号分隔的列表,将其中元素添加到 **Collection** 中。下边的示例展示了这两个方法,以及更通用的 **addAll()** 方法,所有 **Collection** 类型都包含该方法: +`Arrays.asList()` 方法接受一个数组或是逗号分隔的元素列表(使用可变参数),并将其转换为 **List** 对象。 `Collections.addAll()` 方法接受一个 **Collection** 对象,以及一个数组或是一个逗号分隔的列表,将其中元素添加到 **Collection** 中。下边的示例展示了这两个方法,以及更通用的 `addAll()` 方法,所有 **Collection** 类型都包含该方法: ```java // collections/AddingGroups.java @@ -220,11 +220,11 @@ public class AddingGroups { } ``` -**Collection** 的构造器可以接受另一个 **Collection**,用它来将自身初始化。因此,可以使用 **Arrays.asList()** 来为这个构造器产生输入。但是, **Collections.addAll()** 运行得更快,而且很容易构建一个不包含元素的 **Collection** ,然后调用 **Collections.addAll()** ,因此这是首选方式。 +**Collection** 的构造器可以接受另一个 **Collection**,用它来将自身初始化。因此,可以使用 `Arrays.asList()` 来为这个构造器产生输入。但是, `Collections.addAll()` 运行得更快,而且很容易构建一个不包含元素的 **Collection** ,然后调用 `Collections.addAll()` ,因此这是首选方式。 -**Collection.addAll()** 方法只能接受另一个 **Collection** 作为参数,因此它没有 **Arrays.asList()** 或 **Collections.addAll()** 灵活。这两个方法都使用可变参数列表。 +`Collection.addAll()` 方法只能接受另一个 **Collection** 作为参数,因此它没有 `Arrays.asList()` 或 `Collections.addAll()` 灵活。这两个方法都使用可变参数列表。 -也可以直接使用 **Arrays.asList()** 的输出作为一个 **List** ,但是这里的底层实现是数组,没法调整大小。如果尝试在这个 **List** 上调用 **add()** 或 **delete()**,由于这两个方法会尝试修改数组大小,所以会在运行时得到“Unsupported Operation(不支持的操作)”错误: +也可以直接使用 `Arrays.asList()` 的输出作为一个 **List** ,但是这里的底层实现是数组,没法调整大小。如果尝试在这个 **List** 上调用 `add()` 或 `delete()`,由于这两个方法会尝试修改数组大小,所以会在运行时得到“Unsupported Operation(不支持的操作)”错误: ```java // collections/AsListInference.java @@ -260,12 +260,12 @@ public class AsListInference { } ``` -在 **snow4** 中,注意 **Arrays.asList()** 中间的“hint”,告诉编译器 **Arrays.asList()** 生成的结果 **List** 类型的实际目标类型是什么。这称为*显式类型参数说明*(explicit type argument specification)。 +在 **snow4** 中,注意 `Arrays.asList()` 中间的“暗示”(即 `` ),告诉编译器 `Arrays.asList()` 生成的结果 **List** 类型的实际目标类型是什么。这称为*显式类型参数说明*(explicit type argument specification)。 ## 集合的打印 -必须使用 **Arrays.toString()** 来生成数组的可打印形式。但是打印集合无需任何帮助。下面是一个例子,这个例子中也介绍了基本的Java集合: +必须使用 `Arrays.toString()` 来生成数组的可打印形式。但是打印集合无需任何帮助。下面是一个例子,这个例子中也介绍了基本的Java集合: ```java // collections/PrintingCollections.java // Collections print themselves automatically @@ -310,31 +310,31 @@ public class PrintingCollections { */ ``` -这显示了Java集合库中的两个主要类型。它们的区别在于集合中的每个“槽”(slot)保存的元素个数。 **Collection** 类型在每个槽中只能保存一个元素。此类集合包括: **List**, 它以特定的顺序保存一组元素; **Set** ,其中元素不允许重复; **Queue** ,只能在集合一端插入对象,并从另一端移除对象(就本例而言,这只是查看序列的另一种方式,因此并没有显示它)。 **Map** 在每个槽中存放了两个元素,即*键*和与之关联的*值*。 +这显示了Java集合库中的两个主要类型。它们的区别在于集合中的每个“槽”(slot)保存的元素个数。 **Collection** 类型在每个槽中只能保存一个元素。此类集合包括: **List** ,它以特定的顺序保存一组元素; **Set** ,其中元素不允许重复; **Queue** ,只能在集合一端插入对象,并从另一端移除对象(就本例而言,这只是查看序列的另一种方式,因此并没有显示它)。 **Map** 在每个槽中存放了两个元素,即*键*和与之关联的*值*。 -默认的打印行为,(使用集合几桶的toString()方法)即可生成可读性很好的结果。 **Collection** 打印出的内容用方括号括住,每个元素由逗号分隔。 **Map** 则由大括号括住,每个键和值用等号连接(键在左侧,值在右侧)。 +默认的打印行为,使用集合提供的 `toString()` 方法即可生成可读性很好的结果。 **Collection** 打印出的内容用方括号括住,每个元素由逗号分隔。 **Map** 则由大括号括住,每个键和值用等号连接(键在左侧,值在右侧)。 -第一个 **fill()** 方法适用于所有类型的 **Collection** ,这些类型都实现了 **add()** 方法以添加新元素。 +第一个 `fill()` 方法适用于所有类型的 **Collection** ,这些类型都实现了 `add()` 方法以添加新元素。 **ArrayList** 和 **LinkedList** 都是 **List** 的类型,从输出中可以看出,它们都按插入顺序保存元素。两者之间的区别不仅在于执行某些类型的操作时的性能,而且 **LinkedList** 包含的操作多于 **ArrayList** 。本章后面将对这些内容进行更全面的探讨。 -**HashSet** , **TreeSet** 和 **LinkedHashSet** 是 **Set** 的类型。从输出中可以看到, **Set** 仅保存每个相同项中的一个,并且不同的 **Set** 实现存储元素的方式也不同。 **HashSet** 使用相当复杂的方法存储元素,这在[附录:集合主题]()中进行了探讨。现在只需要知道,这种技术是检索元素的最快方法,因此,存储顺序看似荒谬(通常只关心某事物是否是 **Set** 的成员,而存储顺序并不重要)。如果存储顺序很重要,则可以使用**TreeSet** ,它将按比较结果的升序保存对象)或 **LinkedHashSet** ,它按照被添加的先后顺序保存对象。 +**HashSet** , **TreeSet** 和 **LinkedHashSet** 是 **Set** 的类型。从输出中可以看到, **Set** 仅保存每个相同项中的一个,并且不同的 **Set** 实现存储元素的方式也不同。 **HashSet** 使用相当复杂的方法存储元素,这在[附录:集合主题]()中进行了探讨。现在只需要知道,这种技术是检索元素的最快方法,因此,存储顺序看上去没有什么意义(通常只关心某事物是否是 **Set** 的成员,而存储顺序并不重要)。如果存储顺序很重要,则可以使用 **TreeSet** ,它将按比较结果的升序保存对象)或 **LinkedHashSet** ,它按照被添加的先后顺序保存对象。 -**Map** (也称为*关联数组*)使用*键*来查找对象,就像一个简单的数据库。所关联的对象称为*值*。 假设有一个 **Map** 将美国州名与它们的首府联系在一起,如果想要俄亥俄州(Ohio)的首府,可以用“Ohio”作为键来查找,几乎就像使用数组下标一样。 这是由于这种行为,对于每个键, **Map** 只接受一次。 - -**Map.put(key, value)** 添加一个所想要添加的值并将它与一个键(用来查找值)相关联。 **Map.get(key)** 生成与该键相关联的值。上面的示例仅添加键值对,并没有执行查找。这将在稍后展示。 +**Map** (也称为*关联数组*)使用*键*来查找对象,就像一个简单的数据库。所关联的对象称为*值*。 假设有一个 **Map** 将美国州名与它们的首府联系在一起,如果想要俄亥俄州(Ohio)的首府,可以用“Ohio”作为键来查找,几乎就像使用数组下标一样。正是由于这种行为,对于每个键, **Map** 只存储一次。 +`Map.put(key, value)` 添加一个所想要添加的值并将它与一个键(用来查找值)相关联。 `Map.get(key)` 生成与该键相关联的值。上面的示例仅添加键值对,并没有执行查找。这将在稍后展示。 请注意,这里没有指定(或考虑) **Map** 的大小,因为它会自动调整大小。 此外, **Map** 还知道如何打印自己,它会显示相关联的键和值。 -本例使用了 **Map** 的三种基本风格: **HashMap** , **TreeMap**和 **LinkedHashMap** 。 +本例使用了 **Map** 的三种基本风格: **HashMap** , **TreeMap** 和 **LinkedHashMap** 。 键和值保存在 **HashMap** 中的顺序不是插入顺序,因为 **HashMap** 实现使用了非常快速的算法来控制顺序。 **TreeMap** 通过比较结果的升序来保存键, **LinkedHashMap** 在保持 **HashMap** 查找速度的同时按键的插入顺序保存键。 + ## 列表List -**List**s承诺以特定顺序保存元素。 **List** 接口在 **Collection** 的基础上添加了许多方法,允许在 **List** 的中间插入和删除元素。 +**List**s承诺将元素保存在特定的序列中。 **List** 接口在 **Collection** 的基础上添加了许多方法,允许在 **List** 的中间插入和删除元素。 有两种类型的 **List** : @@ -344,7 +344,7 @@ public class PrintingCollections { 下面的示例导入 **typeinfo.pets** ,超前使用了[类型信息]()一章中的类库。这个类库包含了 **Pet** 类层次结构,以及用于随机生成 **Pet** 对象的一些工具类。此时不需要了解完整的详细信息,只需要知道两点: 1. 有一个 **Pet** 类,以及 **Pet** 的各种子类型。 -2. 静态的 **Pets.arrayList()** 方法返回一个填充了随机选取的 **Pet** 对象的 **ArrayList**: +2. 静态的 `Pets.arrayList()` 方法返回一个填充了随机选取的 **Pet** 对象的 **ArrayList**: ```java // collections/ListFeatures.java @@ -439,44 +439,46 @@ sub: [Mouse, Pug] */ ``` -打印行都编了号,因此输出可以与源代码相关。 第1行输出展示了原始的由 **Pet** 组成的 **List** 。 与数组不同, **List** 可以在创建后添加或删除元素,并自行调整大小。这正是它的重要价值:一种可修改的序列。在第2行输出中可以看到添加一个 **Hamster** 的结果,该对象将被追加到列表的末尾。 +打印行都编了号,因此可从输出追溯到源代码。 第 1 行输出展示了原始的由 **Pet** 组成的 **List** 。 与数组不同, **List** 可以在创建后添加或删除元素,并自行调整大小。这正是它的重要价值:一种可修改的序列。在第 2 行输出中可以看到添加一个 **Hamster** 的结果,该对象将被追加到列表的末尾。 -可以使用 **contains()** 方法确定对象是否在列表中。如果要删除一个对象,可以将该对象的引用传递给 **remove()** 方法。同样,如果有一个对象的引用,可以使用 **indexOf()** 在 **List** 中找到该对象所在位置的下标号,如第4行输出所示中所示。 +可以使用 `contains()` 方法确定对象是否在列表中。如果要删除一个对象,可以将该对象的引用传递给 `remove()` 方法。同样,如果有一个对象的引用,可以使用 `indexOf()` 在 **List** 中找到该对象所在位置的下标号,如第 4 行输出所示中所示。 -当确定元素是否是属于某个 **List** ,寻找某个元素的索引,以及通过引用从 **List** 中删除元素时,都会用到 **equals()** 方法(根类 **Object** 的一个方法)。每个 **Pet** 被定义为一个唯一的对象,所以即使列表中已经有两个 **Cymrics** ,如果再创建一个新的 **Cymric** 对象并将其传递给 **indexOf()** 方法,结果仍为 **-1** (表示未找到),并且尝试调用 **remove()** 方法来删除这个对象将返回 **false** 。对于其他类, **equals()** 的定义可能有所不同。例如,如果两个 **String** 的内容相同,则这两个 **String** 相等。因此,为了防止出现意外,请务必注意 **List** 行为会根据 **equals()** 行为而发生变化。 +当确定元素是否是属于某个 **List** ,寻找某个元素的索引,以及通过引用从 **List** 中删除元素时,都会用到 `equals()` 方法(根类 **Object** 的一个方法)。每个 **Pet** 被定义为一个唯一的对象,所以即使列表中已经有两个 **Cymrics** ,如果再创建一个新的 **Cymric** 对象并将其传递给 `indexOf()` 方法,结果仍为 **-1** (表示未找到),并且尝试调用 `remove()` 方法来删除这个对象将返回 **false** 。对于其他类, `equals()` 的定义可能有所不同。例如,如果两个 **String** 的内容相同,则这两个 **String** 相等。因此,为了防止出现意外,请务必注意 **List** 行为会根据 `equals()` 行为而发生变化。 -第7、8行输出展示了删除与 **List** 中的对象完全匹配的对象是成功的。 +第 7、8 行输出展示了删除与 **List** 中的对象完全匹配的对象是成功的。 -可以在 **List** 的中间插入一个元素,就像在第9行输出和它之前的代码那样。但这会带来一个问题:对于 **LinkedList** ,在列表中间插入和删除都是廉价操作(在本例中,除了对列表中间进行的真正的随机访问),但对于 **ArrayList** ,这可是代价高昂的操作。这是否意味着永远不应该在 **ArrayList** 的中间插入元素,并最好是转换为 **LinkedList** ?不,它只是意味着你应该意识到这个问题,如果你开始在某个 **ArrayList** 中间进执行很多插入操作,并且程序开始变慢,那么你应该看看你的 **List** 实现有可能就是罪魁祸首(发现此类瓶颈的最佳方式是使用仿真器)。优化是一个很棘手的问题,最好的策略就是置之不顾,直到发现必须要去担心它了(尽管去理解这些问题总是一个很好的主意)。 +可以在 **List** 的中间插入一个元素,就像在第 9 行输出和它之前的代码那样。但这会带来一个问题:对于 **LinkedList** ,在列表中间插入和删除都是廉价操作(在本例中,除了对列表中间进行的真正的随机访问),但对于 **ArrayList** ,这可是代价高昂的操作。这是否意味着永远不应该在 **ArrayList** 的中间插入元素,并最好是转换为 **LinkedList** ?不,它只是意味着你应该意识到这个问题,如果你开始在某个 **ArrayList** 中间执行很多插入操作,并且程序开始变慢,那么你应该看看你的 **List** 实现有可能就是罪魁祸首(发现此类瓶颈的最佳方式是使用分析器 profiler)。优化是一个很棘手的问题,最好的策略就是置之不顾,直到发现必须要去担心它了(尽管去理解这些问题总是一个很好的主意)。 -**subList()** 方法可以轻松地从更大的列表中创建切片,当将切片结果传递给原来这个较大的列表的 **containsAll()** 方法时,很自然地会得到 **true**。请注意,顺序并不重要,在第11、12行输出中可以看到,在 **sub** 上调用直观命名的 **Collections.sort()** 和 **Collections.shuffle()** 方法,不会影响 **containsAll()** 的结果。 **subList()** 所产生的列表的幕后支持就是原始列表。因此,对所返回列表的更改都将会反映在原始列表中,反之亦然。 +`subList()` 方法可以轻松地从更大的列表中创建切片,当将切片结果传递给原来这个较大的列表的 `containsAll()` 方法时,很自然地会得到 **true**。请注意,顺序并不重要,在第 11、12 行输出中可以看到,在 **sub** 上调用直观命名的 `Collections.sort()` 和 `Collections.shuffle()` 方法,不会影响 `containsAll()` 的结果。 `subList()` 所产生的列表的幕后支持就是原始列表。因此,对所返回列表的更改都将会反映在原始列表中,反之亦然。 -**retainAll()** 方法实际上是一个“集合交集”操作,在本例中,它保留了同时在 **copy** 和 **sub** 中的所有元素。请再次注意,所产生的结果行为依赖于 **equals()** 方法。 +`retainAll()` 方法实际上是一个“集合交集”操作,在本例中,它保留了同时在 **copy** 和 **sub** 中的所有元素。请再次注意,所产生的结果行为依赖于 `equals()` 方法。 -第14行输出展示了使用索引号来删除元素的结果,与通过对象引用来删除元素相比,它显得更加直观,因为在使用索引时,不必担心 **equals()** 的行为。 +第 14 行输出展示了使用索引号来删除元素的结果,与通过对象引用来删除元素相比,它显得更加直观,因为在使用索引时,不必担心 `equals()` 的行为。 -**removeAll()** 方法也是基于 **equals()** 方法运行的。 顾名思义,它会从 **List** 中删除在参数 **List** 中的所有元素。 +`removeAll()` 方法也是基于 `equals()` 方法运行的。 顾名思义,它会从 **List** 中删除在参数 **List** 中的所有元素。 -**set()** 方法的命名显得很不合时宜,因为它与 **Set** 类存在潜在的冲突。在这里使用“replace”可能更适合,因为它的功能是用第二个参数替换索引处的元素(第一个参数)。 +`set()` 方法的命名显得很不合时宜,因为它与 **Set** 类存在潜在的冲突。在这里使用“replace”可能更适合,因为它的功能是用第二个参数替换索引处的元素(第一个参数)。 -第17行输出表明,对于 **List** ,有一个重载的 **addAll()** 方法可以将新列表插入到原始列表的中间,而不仅仅只能用 **Collection** 的 **addAll()** 方法将追加到列表末尾。 +第 17 行输出表明,对于 **List** ,有一个重载的 `addAll()` 方法可以将新列表插入到原始列表的中间,而不仅仅只能用 **Collection** 的 `addAll()` 方法将追加到列表末尾。 -第18-20行输出展示了 **isEmpty()** 和 **clear()** 方法的效果。 +第 18 - 20 行输出展示了 `isEmpty()` 和 `clear()` 方法的效果。 -第22、23行输出展示了如何使用 **toArray()** 方法将任意的 **Collection** 转换为数组。这是一个重载方法,其无参版本返回一个 **Object** 数组,但是如果将目标类型的数组传递给这个重载版本,那么它会生成一个指定类型的数组(假设它通过了类型检查)。如果参数数组太小而无法容纳 **List** 中的所有元素(就像本例一样),则 **toArray()** 会创建一个具有合适尺寸的新数组。 **Pet** 对象有一个 **id()** 方法,可以在所产生的数组中的对象上调用这个方法。 +第 22、23 行输出展示了如何使用 `toArray()` 方法将任意的 **Collection** 转换为数组。这是一个重载方法,其无参版本返回一个 **Object** 数组,但是如果将目标类型的数组传递给这个重载版本,那么它会生成一个指定类型的数组(假设它通过了类型检查)。如果参数数组太小而无法容纳 **List** 中的所有元素(就像本例一样),则 `toArray()` 会创建一个具有合适尺寸的新数组。 **Pet** 对象有一个 `id()` 方法,可以在所产生的数组中的对象上调用这个方法。 + ## 迭代器Iterators -在任何集合中,都必须有某种方式可以插入元素并再次获取它们。毕竟,保存事物是集合最基本的工作。对于 **List** , **add()** 是插入元素的一种方式, **get()** 是获取元素的一种方式。 +在任何集合中,都必须有某种方式可以插入元素并再次获取它们。毕竟,保存事物是集合最基本的工作。对于 **List** , `add()` 是插入元素的一种方式, `get()` 是获取元素的一种方式。 如果从更高层次的角度考虑,会发现这里有个缺点:要使用集合,必须对集合的确切类型编程。这一开始可能看起来不是很糟糕,但是考虑下面的情况:如果原本是对 **List** 编码的,但是后来发现如果能够将相同的代码应用于 **Set** 会更方便,此时应该怎么做?或者假设想从一开始就编写一段通用代码,它不知道或不关心它正在使用什么类型的集合,因此它可以用于不同类型的集合,那么如何才能不重写代码就可以应用于不同类型的集合? -*迭代器*(也是一种设计模式)的概念实现了这种抽象。迭代器是一个对象,它在一个序列中移动并选择该序列中的每个对象,而客户端程序员不知道或不关心该序列的底层结构。另外,迭代器通常被称为*轻量级对象*(lightweight object):创建它的代价小。因此,经常可以看到一些对迭代器有些奇怪的约束。例如,Java的 **Iterator** 只能单向移动。这个 **Iterator** 只能用来: -1. 使用 **iterator()** 方法要求集合返回一个 **Iterator**。 **Iterator** 将准备好返回序列中的第一个元素。 -2. 使用 **next()** 方法获得序列中的下一个元素。 -3. 使用 **hasNext()** 方法检查序列中是否还有元素。 -4. 使用 **remove()** 方法将迭代器最近返回的那个元素删除。 +*迭代器*(也是一种设计模式)的概念实现了这种抽象。迭代器是一个对象,它在一个序列中移动并选择该序列中的每个对象,而客户端程序员不知道或不关心该序列的底层结构。另外,迭代器通常被称为*轻量级对象*(lightweight object):创建它的代价小。因此,经常可以看到一些对迭代器有些奇怪的约束。例如,Java 的 **Iterator** 只能单向移动。这个 **Iterator** 只能用来: + +1. 使用 `iterator()` 方法要求集合返回一个 **Iterator**。 **Iterator** 将准备好返回序列中的第一个元素。 +2. 使用 `next()` 方法获得序列中的下一个元素。 +3. 使用 `hasNext()` 方法检查序列中是否还有元素。 +4. 使用 `remove()` 方法将迭代器最近返回的那个元素删除。 为了观察它的工作方式,这里再次使用[类型信息]()章节中的 **Pet** 工具: @@ -516,15 +518,15 @@ public class SimpleIteration { */ ``` -有了 **Iterator** ,就不必再为集合中元素的数量操心了。这是由 **hasNext()** 和 **next()** 关心的事情。 +有了 **Iterator** ,就不必再为集合中元素的数量操心了。这是由 `hasNext()` 和 `next()` 关心的事情。 如果只是想向前遍历 **List** ,并不打算修改 **List** 对象本身,那么使用 *for-in* 语法更加简洁。 -**Iterator** 还可以删除由 **next()** 生成的最后一个元素,这意味着在调用 **remove()** 之前必须先调用 **next()** 。[^4] +**Iterator** 还可以删除由 `next()` 生成的最后一个元素,这意味着在调用 `remove()` 之前必须先调用 `next()` 。[^4] 在集合中的每个对象上执行操作,这种思想十分强大,并且贯穿于本书。 -现在考虑创建一个 **display()** 方法,它不必知晓集合的确切类型: +现在考虑创建一个 `display()` 方法,它不必知晓集合的确切类型: ```java // collections/CrossCollectionIteration.java @@ -562,7 +564,7 @@ public class CrossCollectionIteration { */ ``` -**display()** 方法不包含任何有关它所遍历的序列的类型信息。这也展示了 **Iterator** 的真正威力:能够将遍历序列的操作与该序列的底层结构分离。出于这个原因,我们有时会说:迭代器统一了对集合的访问方式。 +`display()` 方法不包含任何有关它所遍历的序列的类型信息。这也展示了 **Iterator** 的真正威力:能够将遍历序列的操作与该序列的底层结构分离。出于这个原因,我们有时会说:迭代器统一了对集合的访问方式。 我们可以使用 **Iterable** 接口生成上一个示例的更简洁版本,该接口描述了“可以产生 **Iterator** 的任何东西”: @@ -603,12 +605,12 @@ public class CrossCollectionIteration2 { */ ``` -这里所有的类都是 **Iterable** ,所以现在对 **display()** 的调用显然更简单。 +这里所有的类都是 **Iterable** ,所以现在对 `display()` 的调用显然更简单。 ### ListIterator -**ListIterator** 是一个更强大的 **Iterator**子类型,它只能由各种 **List** 类生成。虽然 **Iterator** 只能向前移动,但 **ListIterator** 可以双向移动。它还可以生成相对于迭代器在列表中指向的当前位置的后一个和前一个元素的索引,并且可以使用 **set()** 方法替换它访问过的最后一个元素。可以通过调用 **listIterator()** 方法来生成指向 **List** 开头处的 **ListIterator** ,还可以通过调用** listIterator(n)** 创建一个一开始就指向列表索引号为 **n** 的元素处的 **ListIterator** 。 下面的示例演示了所有这些能力: +**ListIterator** 是一个更强大的 **Iterator** 子类型,它只能由各种 **List** 类生成。 **Iterator** 只能向前移动,而 **ListIterator** 可以双向移动。它还可以生成相对于迭代器在列表中指向的当前位置的后一个和前一个元素的索引,并且可以使用 `set()` 方法替换它访问过的最近一个元素。可以通过调用 `listIterator()` 方法来生成指向 **List** 开头处的 **ListIterator** ,还可以通过调用 `listIterator(n)` 创建一个一开始就指向列表索引号为 **n** 的元素处的 **ListIterator** 。 下面的示例演示了所有这些能力: ```java // collections/ListIteration.java @@ -647,20 +649,21 @@ EgyptianMau] */ ``` -**Pets.get()** 方法用来从位置3开始替换 **List** 中的所有Pet对象。 +`Pets.get()` 方法用来从位置 3 开始替换 **List** 中的所有 Pet 对象。 + ## 链表LinkedList -**LinkedList** 也像 **ArrayList** 一样实现了基本的 **List** 接口,但它在 **List** 中间执行插入和删除操作时,比 **ArrayList** 更高效。但在随机访问操作效率方面却要逊色一些。 +**LinkedList** 也像 **ArrayList** 一样实现了基本的 **List** 接口,但它在 **List** 中间执行插入和删除操作时比 **ArrayList** 更高效。然而,它在随机访问操作效率方面却要逊色一些。 -**LinkedList还添加了一些方法,使其可以被用作栈、队列或双端队列(deque)** 。在这些方法中,有些彼此之间可能只是名称有些差异,或者只存在些许差异,以使得这些名字在特定用法的上下文环境中更加适用(特别是在 **Queue** 中)。例如: +**LinkedList 还添加了一些方法,使其可以被用作栈、队列或双端队列(deque)** 。在这些方法中,有些彼此之间可能只是名称有些差异,或者只存在些许差异,以使得这些名字在特定用法的上下文环境中更加适用(特别是在 **Queue** 中)。例如: -- **getFirst()** 和 **element()** 是相同的,它们都返回列表的头部(第一个元素)而并不删除它,如果 **List** 为空,则抛出 **NoSuchElementException** 异常。 **peek()** 方法与这两个方法只是稍有差异,它在列表为空时返回 **null** 。 -- **removeFirst()** 和 **remove()** 也是相同的,它们删除并返回列表的头部元素,并在列表为空时抛出 **NoSuchElementException** 异常。 **poll()** 稍有差异,它在列表为空时返回 **null** 。 -- **addFirst()** 在列表的开头插入一个元素。 -- **offer()** 与 **add()** 和 **addLast()** 相同。 它们都在列表的尾部(末尾)添加一个元素。 -- **removeLast()** 删除并返回列表的最后一个元素。 +- `getFirst()` 和 `element()` 是相同的,它们都返回列表的头部(第一个元素)而并不删除它,如果 **List** 为空,则抛出 **NoSuchElementException** 异常。 `peek()` 方法与这两个方法只是稍有差异,它在列表为空时返回 **null** 。 +- `removeFirst()` 和 `remove()` 也是相同的,它们删除并返回列表的头部元素,并在列表为空时抛出 **NoSuchElementException** 异常。 `poll()` 稍有差异,它在列表为空时返回 **null** 。 +- `addFirst()` 在列表的开头插入一个元素。 +- `offer()` 与 `add()` 和 `addLast()` 相同。 它们都在列表的尾部(末尾)添加一个元素。 +- `removeLast()` 删除并返回列表的最后一个元素。 下面的示例展示了这些功能之间基本的相似性和差异性。它并不是重复执行 **ListFeatures.java** 中所示的行为: @@ -718,14 +721,15 @@ pets.removeLast(): Hamster */ ``` -**Pets.list()** 的结果被传递给 **LinkedList** 的构造器,以便使用它来填充 **LinkedList** 。如果查看 **Queue** 接口就会发现,它在 **LinkedList** 的基础上添加了 **element()** , **offer()** , **peek()** , **poll()** 和 **remove()** 方法,以使其可以成为一个 **Queue** 的实现。 **Queue** 的完整示例将在本章稍后给出。 +`Pets.list()` 的结果被传递给 **LinkedList** 的构造器,以便使用它来填充 **LinkedList** 。如果查看 **Queue** 接口就会发现,它在 **LinkedList** 的基础上添加了 `element()` , `offer()` , `peek()` , `poll()` 和 `remove()` 方法,以使其可以成为一个 **Queue** 的实现。 **Queue** 的完整示例将在本章稍后给出。 + ## 堆栈Stack 堆栈是“后进先出”(LIFO)集合。它有时被称为*叠加栈*(pushdown stack),因为最后“压入”(push)栈的元素,第一个被“弹出”(pop)栈。经常用来类比栈的事物是带有弹簧支架的自助餐厅托盘。最后装入的托盘总是最先拿出来使用的。 -Java 1.0中附带了一个 **Stack** 类,结果设计得很糟糕(为了向后兼容,我们永远坚持Java中的旧设计错误)。Java 6添加了 **ArrayDeque** ,其中包含直接实现堆栈功能的方法: +Java 1.0 中附带了一个 **Stack** 类,结果设计得很糟糕(为了向后兼容,我们永远坚持 Java 中的旧设计错误)。Java 6 添加了 **ArrayDeque** ,其中包含直接实现堆栈功能的方法: ```java // collections/StackTest.java @@ -767,7 +771,7 @@ public class Stack { } ``` -这里引入了使用泛型的类定义的最简单的可能示例。类名称后面的 **** 告诉编译器这是一个参数化类型,而其中的类型参数,即在类被使用时将会被实际类型替换的参数,就是 **T** 。基本上,这个类是在声明“我们在定义一个可以持有 **T** 类型对象的 **Stack** 。” **Stack** 是使用 **ArrayDeque** 实现的,而 **ArrayDeque** 也被告知它将持有 **T** 类型对象。注意, **push()** 接受类型为 **T** 的对象,而 **peek()** 和 **pop()** 返回类型为 **T** 的对象。 **peek()** 方法将返回栈顶元素,但并不将其从栈顶删除,而 **pop()** 删除并返回顶部元素。 +这里引入了使用泛型的类定义的最简单的可能示例。类名称后面的 **\** 告诉编译器这是一个参数化类型,而其中的类型参数 **T** 会在使用类时被实际类型替换。基本上,这个类是在声明“我们在定义一个可以持有 **T** 类型对象的 **Stack** 。” **Stack** 是使用 **ArrayDeque** 实现的,而 **ArrayDeque** 也被告知它将持有 **T** 类型对象。注意, `push()` 接受类型为 **T** 的对象,而 `peek()` 和 `pop()` 返回类型为 **T** 的对象。 `peek()` 方法将返回栈顶元素,但并不将其从栈顶删除,而 `pop()` 删除并返回顶部元素。 如果只需要栈的行为,那么使用继承是不合适的,因为这将产生一个具有 **ArrayDeque** 的其它所有方法的类(在[附录:集合主题]()中将会看到, **Java 1.0** 设计者在创建 **java.util.Stack** 时,就犯了这个错误)。使用组合,可以选择要公开的方法以及如何命名它们。 @@ -830,9 +834,9 @@ import onjava.Stack; ## 集合Set -**Set** 不保存重复的元素。 如果试图将相同对象的多个实例添加到 **Set** 中,那么它会阻止这种行为。 **Set** 最常见的用途是测试归属性,可以很轻松地询问某个对象是否在一个 **Set** 中。因此,查找通常是 **Set** 最重要的操作,因此通常会选择 **HashSet** 实现,该实现针对快速查找进行了优化。 +**Set** 不保存重复的元素。 如果试图将相同对象的多个实例添加到 **Set** 中,那么它会阻止这种重复行为。 **Set** 最常见的用途是测试归属性,可以很轻松地询问某个对象是否在一个 **Set** 中。因此,查找通常是 **Set** 最重要的操作,因此通常会选择 **HashSet** 实现,该实现针对快速查找进行了优化。 -**Set** 具有与 **Collection** 相同的接口,因此没有任何额外的功能,不像前面两种不同类型的 **List** 那样。实际上, **Set** 就是一个 **Collection** ,只是行为不同。(这是继承和多态思想的典型应用:表现不同的行为。)**Set** 根据对象的“值”确定归属性,更复杂的问题将在[附录:集合主题]()中介绍。 +**Set** 具有与 **Collection** 相同的接口,因此没有任何额外的功能,不像前面两种不同类型的 **List** 那样。实际上, **Set** 就是一个 **Collection** ,只是行为不同。(这是继承和多态思想的典型应用:表现不同的行为。)**Set** 根据对象的“值”确定归属性,更复杂的内容将在[附录:集合主题]()中介绍。 下面是使用存放 **Integer** 对象的 **HashSet** 的示例: @@ -855,9 +859,9 @@ public class SetOfInteger { */ ``` -在0到29之间的10000个随机整数被添加到 **Set** 中,因此可以想象每个值都重复了很多次。但是从结果中可以看到,每一个数只有一个实例出现在结果中。 +在 0 到 29 之间的 10000 个随机整数被添加到 **Set** 中,因此可以想象每个值都重复了很多次。但是从结果中可以看到,每一个数只有一个实例出现在结果中。 -早期Java版本中的 **HashSet** 产生的输出没有可辨别的顺序。这是因为出于对速度的追求, **HashSet** 使用了散列,请参阅[附录:集合主题]()一章。由 **HashSet** 维护的顺序与 **TreeSet** 或 **LinkedHashSet** 不同,因为它们的实现具有不同的元素存储方式。 **TreeSet** 将元素存储在红-黑树数据结构中,而 **HashSet** 使用散列函数。 **LinkedHashSet** 因为查询速度的原因也使用了散列,但是看起来使用了链表来维护元素的插入顺序。显然,哈希算法已经更改,现在 **Integer** 按顺序排序。 但是,您不应该依赖此行为: +早期 Java 版本中的 **HashSet** 产生的输出没有可辨别的顺序。这是因为出于对速度的追求, **HashSet** 使用了散列,请参阅[附录:集合主题]()一章。由 **HashSet** 维护的顺序与 **TreeSet** 或 **LinkedHashSet** 不同,因为它们的实现具有不同的元素存储方式。 **TreeSet** 将元素存储在红-黑树数据结构中,而 **HashSet** 使用散列函数。 **LinkedHashSet** 因为查询速度的原因也使用了散列,但是看起来使用了链表来维护元素的插入顺序。看起来散列算法好像已经改变了,现在 **Integer** 按顺序排序。但是,您不应该依赖此行为: ```java // collections/SetOfString.java @@ -911,7 +915,7 @@ public class SortedSetOfString { */ ``` -最常见的操作之一是使用 **contains()** 测试成员归属性,但也有一些其它操作,可能会让你想起在小学学过的文氏图(译者注:用圆表示集与集之间关系的图): +最常见的操作之一是使用 `contains()` 测试成员归属性,但也有一些其它操作,这可能会让你想起在小学学过的维恩图(译者注:利用图形的交合表示多个集合之间的逻辑关系): ```java // collections/SetOperations.java @@ -953,9 +957,9 @@ Z] */ ``` -这些方法名都是自解释的,JDK文档中还有一些其它的方法。 +这些方法名都是自解释的,JDK 文档中还有一些其它的方法。 -能够产生每个元素都唯一的列表是相当有用的功能。例如,假设想要列出上面的 **SetOperations.java** 文件中的所有单词,通过使用本书后面介绍的 **java.nio.file.Files.readAllLines()** 方法,可以打开一个文件,并将其作为一个 **List\** 读取,每个 **String** 都是输入文件中的一行: +能够产生每个元素都唯一的列表是相当有用的功能。例如,假设想要列出上面的 **SetOperations.java** 文件中的所有单词,通过使用本书后面介绍的 `java.nio.file.Files.readAllLines()` 方法,可以打开一个文件,并将其作为一个 **List\** 读取,每个 **String** 都是输入文件中的一行: ```java // collections/UniqueWords.java @@ -986,7 +990,7 @@ void] */ ``` -我们逐步浏览文件中的每一行,并使用 **String.split()** 将其分解为单词,这里使用正则表达式 **\\\ W +** ,这意味着它会依据一个或多个(即 **+** )非单词字母来拆分字符串(正则表达式将在[字符串]()章节介绍)。每个结果单词都会添加到 **Set words** 中。因为它是 **TreeSet** ,所以对结果进行排序。这里,排序是按*字典顺序*(lexicographically)完成的,因此大写和小写字母位于不同的组中。如果想按*字母顺序*(alphabetically)对其进行排序,可以向 **TreeSet** 构造器传入 **String.CASE_INSENSITIVE_ORDER** 比较器(比较器是一个建立排序顺序的对象): +我们逐步浏览文件中的每一行,并使用 `String.split()` 将其分解为单词,这里使用正则表达式 **\\\ W +** ,这意味着它会依据一个或多个(即 **+** )非单词字母来拆分字符串(正则表达式将在[字符串]()章节介绍)。每个结果单词都会添加到 **Set words** 中。因为它是 **TreeSet** ,所以对结果进行排序。这里,排序是按*字典顺序*(lexicographically)完成的,因此大写和小写字母位于不同的组中。如果想按*字母顺序*(alphabetically)对其进行排序,可以向 **TreeSet** 构造器传入 **String.CASE_INSENSITIVE_ORDER** 比较器(比较器是一个建立排序顺序的对象): ```java // collections/UniqueWordsAlphabetic.java @@ -1023,7 +1027,7 @@ String, System, to, true, util, void, X, Y, Z] ## 映射Map -将对象映射到其他对象的能力是解决编程问题的有效方法。例如,考虑一个程序,它被用来检查Java的 **Random** 类的随机性。理想情况下, **Random** 会产生完美的数字分布,但为了测试这一点,则需要生成大量的随机数,并计算落在各种范围内的数字个数。 **Map** 可以很容易地解决这个问题。在本例中,键是 **Random** 生成的数字,而值是该数字出现的次数: +将对象映射到其他对象的能力是解决编程问题的有效方法。例如,考虑一个程序,它被用来检查 Java 的 **Random** 类的随机性。理想情况下, **Random** 会产生完美的数字分布,但为了测试这一点,则需要生成大量的随机数,并计算落在各种范围内的数字个数。 **Map** 可以很容易地解决这个问题。在本例中,键是 **Random** 生成的数字,而值是该数字出现的次数: ```java // collections/Statistics.java @@ -1053,9 +1057,9 @@ public class Statistics { */ ``` -- **[1]** 自动包装机制将随机生成的 **int** 转换为可以与 **HashMap** 一起使用的 **Integer** 引用(不能使用基本类型的集合)。如果键不在集合中,则 **get()** 返回 **null** (这意味着这是第一次找到该数字)。否则, **get()** 会为键生成与之关联的 **Integer** 值,然后该值被递增(自动包装机制再次简化了表达式,但实际上确实发生了对 **Integer** 的装箱和拆箱)。 +- **[1]** 自动包装机制将随机生成的 **int** 转换为可以与 **HashMap** 一起使用的 **Integer** 引用(不能使用基本类型的集合)。如果键不在集合中,则 `get()` 返回 **null** (这意味着这是第一次找到该数字)。否则, `get()` 会为键生成与之关联的 **Integer** 值,然后该值被递增(自动包装机制再次简化了表达式,但实际上确实发生了对 **Integer** 的装箱和拆箱)。 -接下来的示例将使用一个 **String** 描述来查找 **Pet** 对象。它还展示了通过使用 **containsKey()**和 **containsValue()** 方法去测试一个 **Map** ,以查看它是否包含某个键或某个值: +接下来的示例将使用一个 **String** 描述来查找 **Pet** 对象。它还展示了通过使用 `containsKey()` 和 `containsValue()` 方法去测试一个 **Map** ,以查看它是否包含某个键或某个值: ```java // collections/PetMap.java @@ -1084,7 +1088,7 @@ true */ ``` -**Map** 与数组和其他的 **Collection** 一样,可以轻松地扩展到多个维度,只需要创建一个值为 **Map** 的 **Map**(这些 **Map** 的值可以是其他集合,甚至是其他 **Map**)。因此,能够很容易地将集合组合起来以快速生成强大的数据结构。例如,假设你正在追踪有多个宠物的人,只需要一个 **Map\\>** 即可: +**Map** 与数组和其他的 **Collection** 一样,可以轻松地扩展到多个维度,只需要创建一个值为 **Map** 的 **Map**(这些 **Map** 的值可以是其他集合,甚至是其他 **Map**)。因此,能够很容易地将集合组合起来以快速生成强大的数据结构。例如,假设你正在追踪有多个宠物的人,只需要一个 **Map\>** 即可: ```java @@ -1152,9 +1156,10 @@ Person Luke has: */ ``` -**Map** 可以返回由其键组成的 **Set** ,由其值组成的 **Collection** ,或者其键值对的 **Set** 。 **keySet()** 方法生成由在 **petPeople** 中的所有键组成的 **Set** ,它在*for-in*语句中被用来遍历该 **Map** 。 +**Map** 可以返回由其键组成的 **Set** ,由其值组成的 **Collection** ,或者其键值对的 **Set** 。 `keySet()` 方法生成由在 **petPeople** 中的所有键组成的 **Set** ,它在 *for-in* 语句中被用来遍历该 **Map** 。 + ## 队列Queue 队列是一个典型的“先进先出”(FIFO)集合。 即从集合的一端放入事物,再从另一端去获取它们,事物放入集合的顺序和被取出的顺序是相同的。队列通常被当做一种可靠的将对象从程序的某个区域传输到另一个区域的途径。队列在[并发编程]()中尤为重要,因为它们可以安全地将对象从一个任务传输到另一个任务。 @@ -1190,9 +1195,9 @@ B r o n t o s a u r u s */ ``` -**offer()** 是与 **Queue** 相关的方法之一,它在允许的情况下,在队列的尾部插入一个元素,或者返回 **false** 。 **peek()** 和 **element()** 都返回队头元素而不删除它,但是如果队列为空,则 **element()** 抛出 **NoSuchElementException** ,而 **peek()** 返回 **null** 。 **poll()** 和 **remove()** 都删除并返回队头元素,但如果队列为空,**poll()** 返回 **null** ,而 **remove()** 抛出 **NoSuchElementException** 。 +`offer()` 是与 **Queue** 相关的方法之一,它在允许的情况下,在队列的尾部插入一个元素,或者返回 **false** 。 `peek()` 和 `element()` 都返回队头元素而不删除它,但是如果队列为空,则 `element()` 抛出 **NoSuchElementException** ,而 `peek()` 返回 **null** 。 `poll()` 和 `remove()`* 都删除并返回队头元素,但如果队列为空,`poll()` 返回 **null** ,而 `remove()` 抛出 **NoSuchElementException** 。 -自动包装机制会自动将 **nextInt()** 的 **int** 结果转换为 **queue** 所需的 **Integer** 对象,并将 **char c** 转换为 **qc** 所需的 **Character** 对象。 **Queue** 接口窄化了对 **LinkedList** 方法的访问权限,因此只有适当的方法才能使用,因此能够访问到的 **LinkedList** 的方法会变少(这里实际上可以将 **Queue** 强制转换回 **LinkedList** ,但至少我们不鼓励这样做)。 +自动包装机制会自动将 `nextInt()` 的 **int** 结果转换为 **queue** 所需的 **Integer** 对象,并将 **char c** 转换为 **qc** 所需的 **Character** 对象。 **Queue** 接口窄化了对 **LinkedList** 方法的访问权限,因此只有适当的方法才能使用,因此能够访问到的 **LinkedList** 的方法会变少(这里实际上可以将 **Queue** 强制转换回 **LinkedList** ,但至少我们不鼓励这样做)。 与 **Queue** 相关的方法提供了完整而独立的功能。 也就是说,对于 **Queue** 所继承的 **Collection** ,在不需要使用它的任何方法的情况下,就可以拥有一个可用的 **Queue** 。 @@ -1201,9 +1206,9 @@ B r o n t o s a u r u s 先进先出(FIFO)描述了最典型的*队列规则*(queuing discipline)。队列规则是指在给定队列中的一组元素的情况下,确定下一个弹出队列的元素的规则。先进先出声明的是下一个弹出的元素应该是等待时间最长的元素。 -优先级队列声明下一个弹出的元素是最需要的元素(具有最高的优先级)。例如,在机场,当飞机临近起飞时,这架飞机的乘客可以在办理登机手续时排到队头。如果构建了一个消息传递系统,某些消息比其他消息更重要,应该尽快处理,而不管它们何时到达。在Java 5中添加了 **PriorityQueue** ,以便自动实现这种行为。 +优先级队列声明下一个弹出的元素是最需要的元素(具有最高的优先级)。例如,在机场,当飞机临近起飞时,这架飞机的乘客可以在办理登机手续时排到队头。如果构建了一个消息传递系统,某些消息比其他消息更重要,应该尽快处理,而不管它们何时到达。在Java 5 中添加了 **PriorityQueue** ,以便自动实现这种行为。 -当在 **PriorityQueue** 上调用 **offer()** 方法来插入一个对象时,该对象会在队列中被排序。[^5]默认的排序使用队列中对象的*自然顺序*(natural order),但是可以通过提供自己的 **Comparator** 来修改这个顺序。 **PriorityQueue** 确保在调用**peek()** , **poll()** 或 **remove()** 方法时,获得的元素将是队列中优先级最高的元素。 +当在 **PriorityQueue** 上调用 `offer()` 方法来插入一个对象时,该对象会在队列中被排序。[^5]默认的排序使用队列中对象的*自然顺序*(natural order),但是可以通过提供自己的 **Comparator** 来修改这个顺序。 **PriorityQueue** 确保在调用 `peek()` , `poll()` 或 `remove()` 方法时,获得的元素将是队列中优先级最高的元素。 让 **PriorityQueue** 与 **Integer** , **String** 和 **Character** 这样的内置类型一起工作易如反掌。在下面的示例中,第一组值与前一个示例中的随机值相同,可以看到它们从 **PriorityQueue** 中弹出的顺序与前一个示例不同: @@ -1260,7 +1265,7 @@ C B A A */ ``` -**PriorityQueue** 是允许重复的,最小的值具有最高的优先级(如果是 **String** ,空格也可以算作值,并且比字母的优先级高)。为了展示如何通过提供自己的 **Comparator** 对象来改变顺序,第三个对 **PriorityQueue\** 构造器的调用,和第二个对 **PriorityQueue\** 的调用使用了由 **Collections.reverseOrder()** (Java SE5中新添加的)产生的反序的 **Comparator** 。 +**PriorityQueue** 是允许重复的,最小的值具有最高的优先级(如果是 **String** ,空格也可以算作值,并且比字母的优先级高)。为了展示如何通过提供自己的 **Comparator** 对象来改变顺序,第三个对 **PriorityQueue\** 构造器的调用,和第二个对 **PriorityQueue\** 的调用使用了由 `Collections.reverseOrder()` (Java 5 中新添加的)产生的反序的 **Comparator** 。 最后一部分添加了一个 **HashSet** 来消除重复的 **Character**。 @@ -1271,7 +1276,7 @@ C B A A **Collection** 是所有序列集合共有的根接口。它可能会被认为是一种“附属接口”(incidental interface),即因为要表示其他若干个接口的共性而出现的接口。此外,**java.util.AbstractCollection** 类提供了 **Collection** 的默认实现,使得你可以创建 **AbstractCollection** 的子类型,而其中没有不必要的代码重复。 -使用接口描述的一个理由是它可以使我们创建更通用的代码。通过针对接口而非具体实现来编写代码,我们的代码可以应用于更多类型的对象。[^6]因此,如果所编写的方法接受一个 **Collection** ,那么该方法可以应用于任何实现了 **Collection** 的类——这也就使得一个新类可以选择去实现 **Collection** 接口,以便该方法可以使用它。标准C++类库中的的集合并没有共同的基类——集合之间的所有共性都是通过迭代器实现的。在Java中,遵循C++的方式看起来似乎很明智,即用迭代器而不是 **Collection** 来表示集合之间的共性。但是,这两种方法绑定在了一起,因为实现 **Collection** 就意味着需要提供 **iterator()** 方法: +使用接口描述的一个理由是它可以使我们创建更通用的代码。通过针对接口而非具体实现来编写代码,我们的代码可以应用于更多类型的对象。[^6]因此,如果所编写的方法接受一个 **Collection** ,那么该方法可以应用于任何实现了 **Collection** 的类——这也就使得一个新类可以选择去实现 **Collection** 接口,以便该方法可以使用它。标准 C++ 类库中的的集合并没有共同的基类——集合之间的所有共性都是通过迭代器实现的。在 Java 中,遵循 C++ 的方式看起来似乎很明智,即用迭代器而不是 **Collection** 来表示集合之间的共性。但是,这两种方法绑定在了一起,因为实现 **Collection** 就意味着需要提供 `iterator()` 方法: ```java // collections/InterfaceVsIterator.java @@ -1328,11 +1333,11 @@ Britney=Pug, Sam=Cymric, Spot=Pug, Fluffy=Manx} */ ``` -两个版本的 **display()** 方法都可以使用 **Map** 或 **Collection** 的子类型来工作。 而且**Collection** 接口和 **Iterator** 都将 **display()** 方法与低层集合的特定实现解耦。 +两个版本的 `display()` 方法都可以使用 **Map** 或 **Collection** 的子类型来工作。 而且**Collection** 接口和 **Iterator** 都将 `display()` 方法与低层集合的特定实现解耦。 -在本例中,这两种方式都可以奏效。事实上, **Collection** 要更方便一点,因为它是 **Iterable** 类型,因此在 **display(Collection)** 的实现中可以使用*for-in*构造,这使得代码更加清晰。 +在本例中,这两种方式都可以奏效。事实上, **Collection** 要更方便一点,因为它是 **Iterable** 类型,因此在 `display(Collection)` 的实现中可以使用 *for-in* 构造,这使得代码更加清晰。 -当需要实现一个不是 **Collection** 的外部类时,由于让它去实现 **Collection** 接口可能非常困难或麻烦,因此使用 **Iterator** 就会变得非常吸引人。例如,如果我们通过继承一个持有 **Pet** 对象的类来创建一个 **Collection** 的实现,那么我们必须实现 **Collection** 所有的方法,即使我们不在 **display()** 方法中使用它们,也必须这样做。虽然这可以通过继承 **AbstractCollection** 而很容易地实现,但是无论如何还是要被强制去实现 **iterator()** 和 **size()** 方法,这些方法 **AbstractCollection** 没有实现,但是 **AbstractCollection** 中的其它方法会用到: +当需要实现一个不是 **Collection** 的外部类时,由于让它去实现 **Collection** 接口可能非常困难或麻烦,因此使用 **Iterator** 就会变得非常吸引人。例如,如果我们通过继承一个持有 **Pet** 对象的类来创建一个 **Collection** 的实现,那么我们必须实现 **Collection** 所有的方法,即使我们不在 `display()` 方法中使用它们,也必须这样做。虽然这可以通过继承 **AbstractCollection** 而很容易地实现,但是无论如何还是要被强制去实现 `iterator()` 和 `size()` 方法,这些方法 **AbstractCollection** 没有实现,但是 **AbstractCollection** 中的其它方法会用到: ```java // collections/CollectionSequence.java @@ -1374,11 +1379,11 @@ extends AbstractCollection { */ ``` -**remove()** 方法是一个“可选操作”,在[附录:集合主题]()中详细介绍。 这里可以不必实现它,如果你调用它,它将抛出异常。 +`remove()` 方法是一个“可选操作”,在[附录:集合主题]()中详细介绍。 这里可以不必实现它,如果你调用它,它将抛出异常。 -- **[1]** 你可能会认为,因为 **iterator()** 返回 **Iterator\** ,匿名内部类定义可以使用菱形语法,Java可以推断出类型。但这不起作用,类型推断仍然非常有限。 +- **[1]** 你可能会认为,因为 `iterator()` 返回 **Iterator\** ,匿名内部类定义可以使用菱形语法,Java可以推断出类型。但这不起作用,类型推断仍然非常有限。 -这个例子表明,如果实现了 **Collection** ,就必须实现 **iterator()** ,并且只拿实现 **iterator()** 与继承 **AbstractCollection** 相比,花费的代价只有略微减少。但是,如果类已经继承了其他的类,那么就不能继承再 **AbstractCollection** 了。 在这种情况下,要实现 **Collection** ,就必须实现该接口中的所有方法。 此时,继承并提供创建迭代器的能力要容易得多: +这个例子表明,如果实现了 **Collection** ,就必须实现 `iterator()` ,并且只拿实现 `iterator()` 与继承 **AbstractCollection** 相比,花费的代价只有略微减少。但是,如果类已经继承了其他的类,那么就不能继承再 **AbstractCollection** 了。在这种情况下,要实现 **Collection** ,就必须实现该接口中的所有方法。此时,继承并提供创建迭代器的能力要容易得多: ```java // collections/NonCollectionSequence.java @@ -1422,7 +1427,7 @@ public class NonCollectionSequence extends PetSequence { ## for-in和迭代器 -到目前为止,*for-in*语法主要用于数组,但它也适用于任何 **Collection** 对象。实际上在使用 **ArrayList** 时,已经看到了一些使用它的示例,下面是一个更通用的证明: +到目前为止,*for-in* 语法主要用于数组,但它也适用于任何 **Collection** 对象。实际上在使用 **ArrayList** 时,已经看到了一些使用它的示例,下面是一个更通用的证明: ```java // collections/ForInCollections.java @@ -1443,9 +1448,9 @@ public class ForInCollections { */ ``` -由于 **cs** 是一个 **Collection** ,因此该代码展示了使用*for-in*是所有 **Collection** 对象的特征。 +由于 **cs** 是一个 **Collection** ,因此该代码展示了使用 *for-in* 是所有 **Collection** 对象的特征。 -这样做的原因是Java SE5引入了一个名为 **Iterable** 的接口,该接口包含一个能够生成 **Iterator** 的 **iterator()** 方法。*for-in*使用此 **Iterable** 接口来遍历序列。因此,如果创建了任何实现了 **Iterable** 的类,都可以将它用于*for-in*语句中: +这样做的原因是 Java 5 引入了一个名为 **Iterable** 的接口,该接口包含一个能够生成 **Iterator** 的 `iterator()` 方法。*for-in* 使用此 **Iterable** 接口来遍历序列。因此,如果创建了任何实现了 **Iterable** 的类,都可以将它用于 *for-in* 语句中: ```java // collections/IterableClass.java @@ -1482,9 +1487,9 @@ And that is how we know the Earth to be banana-shaped. */ ``` -**iterator()** 返回的是实现了 **Iterator\** 的匿名内部类的实例,该匿名内部类可以遍历数组中的每个单词。在 **main()** 中,可以看到 **IterableClass** 确实可以用于*for-in*语句。 +`iterator()` 返回的是实现了 **Iterator\** 的匿名内部类的实例,该匿名内部类可以遍历数组中的每个单词。在主方法中,可以看到 **IterableClass** 确实可以用于 *for-in* 语句。 -在Java SE5中,许多类都是 **Iterable** ,主要包括所有的 **Collection** 类(但不包括各种 **Maps** )。 例如,下面的代码可以显示所有的操作系统环境变量: +在 Java 5 中,许多类都是 **Iterable** ,主要包括所有的 **Collection** 类(但不包括各种 **Maps** )。 例如,下面的代码可以显示所有的操作系统环境变量: ```java // collections/EnvironmentVariables.java @@ -1501,9 +1506,9 @@ public class EnvironmentVariables { } ``` -**System.getenv()** [^7]返回一个 **Map** , **entrySet()** 产生一个由 **Map.Entry** 的元素构成的 **Set** ,并且这个 **Set** 是一个 **Iterable** ,因此它可以用于*for-in*循环。 +`System.getenv()` [^7]返回一个 **Map** , `entrySet()` 产生一个由 **Map.Entry** 的元素构成的 **Set** ,并且这个 **Set** 是一个 **Iterable** ,因此它可以用于 *for-in* 循环。 -*for-in*语句适用于数组或其它任何 **Iterable** ,但这并不意味着数组肯定也是个 **Iterable** ,也不会发生任何自动装箱: +*for-in* 语句适用于数组或其它任何 **Iterable** ,但这并不意味着数组肯定也是个 **Iterable** ,也不会发生任何自动装箱: ```java // collections/ArrayIsNotIterable.java @@ -1533,10 +1538,10 @@ public class ArrayIsNotIterable { ### 适配器方法惯用法 -如果现在有一个 **Iterable** 类,你想要添加一种或多种在*for-in*语句中使用这个类的方法,应该怎么做呢? 例如,假设你希望可以选择是在正向还是反向遍历一个单词列表。如果直接继承这个类,并覆盖 **iterator()** 方法,则只能替换现有的方法,而不能实现选择。 +如果现在有一个 **Iterable** 类,你想要添加一种或多种在 *for-in* 语句中使用这个类的方法,应该怎么做呢? 例如,假设你希望可以选择是在正向还是反向遍历一个单词列表。如果直接继承这个类,并覆盖 `iterator()` 方法,则只能替换现有的方法,而不能实现选择。 -一种解决方案是所谓*适配器方法*(Adapter Method)的惯用法。“适配器”部分来自于设计模式,因为必须要提供特定的接口来满足*for-in*语句。如果已经有一个接口并且需要另一个接口时,则编写适配器就可以解决这个问题。 -在这里,若希望在默认的正向迭代器的基础上,添加产生反向迭代器的能力,因此不能使用覆盖,相反,而是添加了一个能够生成 **Iterable** 对象的方法,该对象可以用于*for-in*语句。这使得我们可以提供多种使用*for-in*语句的方式: +一种解决方案是所谓*适配器方法*(Adapter Method)的惯用法。“适配器”部分来自于设计模式,因为必须要提供特定的接口来满足 *for-in* 语句。如果已经有一个接口并且需要另一个接口时,则编写适配器就可以解决这个问题。 +在这里,若希望在默认的正向迭代器的基础上,添加产生反向迭代器的能力,因此不能使用覆盖,相反,而是添加了一个能够生成 **Iterable** 对象的方法,该对象可以用于 *for-in* 语句。这使得我们可以提供多种使用 *for-in* 语句的方式: ```java // collections/AdapterMethodIdiom.java @@ -1586,7 +1591,7 @@ be to not or be To */ ``` -在 **main()** 中,如果直接将 **ral** 对象放在*for-in*语句中,则会得到(默认的)正向迭代器。但是如果在该对象上调用 **reversed()** 方法,它会产生不同的行为。 +在主方法中,如果直接将 **ral** 对象放在 *for-in* 语句中,则会得到(默认的)正向迭代器。但是如果在该对象上调用 `reversed()` 方法,它会产生不同的行为。 通过使用这种方式,可以在 **IterableClass.java** 示例中添加两种适配器方法: @@ -1643,9 +1648,9 @@ And that is how we know the Earth to be banana-shaped. */ ``` -注意,第二个方法 **random()** 没有创建它自己的 **Iterator** ,而是直接返回被打乱的 **List** 中的 **Iterator** 。 +注意,第二个方法 `random()` 没有创建它自己的 **Iterator** ,而是直接返回被打乱的 **List** 中的 **Iterator** 。 -从输出中可以看到, **Collections.shuffle()** 方法不会影响到原始数组,而只是打乱了 **shuffled** 中的引用。之所以这样,是因为 **randomized()** 方法用一个 **ArrayList** 将 **Arrays.asList()** 的结果包装了起来。如果这个由 **Arrays.asList()** 生成的 **List** 被直接打乱,那么它将修改底层数组,如下所示: +从输出中可以看到, `Collections.shuffle()` 方法不会影响到原始数组,而只是打乱了 **shuffled** 中的引用。之所以这样,是因为 `randomized()` 方法用一个 **ArrayList** 将 `Arrays.asList()` 的结果包装了起来。如果这个由 `Arrays.asList()` 生成的 **List** 被直接打乱,那么它将修改底层数组,如下所示: ```java // collections/ModifyingArraysAsList.java @@ -1679,16 +1684,16 @@ array: [9, 1, 6, 3, 7, 2, 5, 10, 4, 8] */ ``` -在第一种情况下, **Arrays.asList()** 的输出被传递给了 **ArrayList** 的构造器,这将创建一个引用 **ia** 的元素的 **ArrayList** ,因此打乱这些引用不会修改该数组。但是,如果直接使用 **Arrays.asList(ia)** 的结果,这种打乱就会修改 **ia** 的顺序。重要的是要注意 **Arrays.asList()** 生成一个 **List** 对象,该对象使用底层数组作为其物理实现。如果执行的会修改这个 **List** ,并且不希望修改原始数组,那么就应该在另一个集合中创建一个副本。 +在第一种情况下, `Arrays.asList()` 的输出被传递给了 **ArrayList** 的构造器,这将创建一个引用 **ia** 的元素的 **ArrayList** ,因此打乱这些引用不会修改该数组。但是,如果直接使用 `Arrays.asList(ia)` 的结果,这种打乱就会修改 **ia** 的顺序。重要的是要注意 `Arrays.asList()` 生成一个 **List** 对象,该对象使用底层数组作为其物理实现。如果执行的操作会修改这个 **List** ,并且不希望修改原始数组,那么就应该在另一个集合中创建一个副本。 ## 本章小结 -Java提供了许多保存对象的方法: +Java 提供了许多保存对象的方法: 1. 数组将数字索引与对象相关联。它保存类型明确的对象,因此在查找对象时不必对结果做类型转换。它可以是多维的,可以保存基本类型的数据。虽然可以在运行时创建数组,但是一旦创建数组,就无法更改数组的大小。 -2. **Collection** 保存单一的元素,而 **Map** 包含相关联的键值对。使用Java泛型,可以指定集合中保存的对象的类型,因此不能将错误类型的对象放入集合中,并且在从集合中获取元素时,不必进行类型转换。各种 **Collection** 和各种 **Map** 都可以在你向其中添加更多的元素时,自动调整其尺寸大小。集合不能保存基本类型,但自动装箱机制会负责执行基本类型和集合中保存的包装类型之间的双向转换。 +2. **Collection** 保存单一的元素,而 **Map** 包含相关联的键值对。使用 Java 泛型,可以指定集合中保存的对象的类型,因此不能将错误类型的对象放入集合中,并且在从集合中获取元素时,不必进行类型转换。各种 **Collection** 和各种 **Map** 都可以在你向其中添加更多的元素时,自动调整其尺寸大小。集合不能保存基本类型,但自动装箱机制会负责执行基本类型和集合中保存的包装类型之间的双向转换。 3. 像数组一样, **List** 也将数字索引与对象相关联,因此,数组和 **List** 都是有序集合。 @@ -1698,14 +1703,15 @@ Java提供了许多保存对象的方法: 6. **Map** 是一种将对象(而非数字)与对象相关联的设计。 **HashMap** 专为快速访问而设计,而 **TreeMap** 保持键始终处于排序状态,所以没有 **HashMap** 快。 **LinkedHashMap** 按插入顺序保存其元素,但使用散列提供快速访问的能力。 -7. **Set** 不接受重复元素。 **HashSet** 提供最快的查询速度,而 **TreeSet** 保持元素处于排序状态。 **LinkedHashSets** 按插入顺序保存其元素,但使用散列提供快速访问的能力。 +7. **Set** 不接受重复元素。 **HashSet** 提供最快的查询速度,而 **TreeSet** 保持元素处于排序状态。 **LinkedHashSet** 按插入顺序保存其元素,但使用散列提供快速访问的能力。 -8. 不要在新代码中使用遗留类 **Vector** , **Hashtable** 和 **Stack** 。 +8. 不要在新代码中使用遗留类 **Vector** ,**Hashtable** 和 **Stack** 。 -浏览一下Java集合的简图(不包含抽象类或遗留组件)会很有帮助。这里仅包括在一般情况下会碰到的接口和类。 +浏览一下Java集合的简图(不包含抽象类或遗留组件)会很有帮助。这里仅包括在一般情况下会碰到的接口和类。(译者注:下图为原著PDF中的截图,可能由于未知原因存在问题。这里可参考译者绘制版[^8]) ![simple collection taxonomy](../images/simple-collection-taxonomy.png) +### 简单集合分类 可以看到,实际上只有四个基本的集合组件: **Map** , **List** , **Set** 和 **Queue** ,它们各有两到三个实现版本(**Queue** 的 **java.util.concurrent** 实现未包含在此图中)。最常使用的集合用黑色粗线线框表示。 @@ -1784,27 +1790,31 @@ Serializable] */ ``` -除 **TreeSet** 之外的所有 **Set** 都具有与 **Collection** 完全相同的接口。**List** 和 **Collection** 存在着明显的不同,尽管 **List** 所要求的方法都在 **Collection** 中。另一方面,在 **Queue** 接口中的方法是独立的,在创建具有 **Queue** 功能的实现时,不需要使用 **Collection** 方法。最后, **Map** 和 **Collection** 之间唯一的交集是Map可以使用 **entrySet()** 和 **values()** 方法来产生 **Collection** 。 +除 **TreeSet** 之外的所有 **Set** 都具有与 **Collection** 完全相同的接口。**List** 和 **Collection** 存在着明显的不同,尽管 **List** 所要求的方法都在 **Collection** 中。另一方面,在 **Queue** 接口中的方法是独立的,在创建具有 **Queue** 功能的实现时,不需要使用 **Collection** 方法。最后, **Map** 和 **Collection** 之间唯一的交集是 **Map** 可以使用 `entrySet()` 和 `values()` 方法来产生 **Collection** 。 请注意,标记接口 **java.util.RandomAccess** 附加到了 **ArrayList** 上,但不附加到 **LinkedList** 上。这为根据特定 **List** 动态改变其行为的算法提供了信息。 从面向对象的继承层次结构来看,这种组织结构确实有些奇怪。但是,当了解了 **java.util** 中更多的有关集合的内容后(特别是在[附录:集合主题]()中的内容),就会发现出了继承结构有点奇怪外,还有更多的问题。集合类库一直以来都是设计难题——解决这些问题涉及到要去满足经常彼此之间互为牵制的各方面需求。所以要做好准备,在各处做出妥协。 -尽管存在这些问题,但Java集合仍是在日常工作中使用的基本工具,它可以使程序更简洁、更强大、更有效。你可能需要一段时间才能熟悉集合类库的某些方面,但我想你很快就会找到自己的路子,来获得和使用这个类库中的类。 +尽管存在这些问题,但 Java 集合仍是在日常工作中使用的基本工具,它可以使程序更简洁、更强大、更有效。你可能需要一段时间才能熟悉集合类库的某些方面,但我想你很快就会找到自己的路子,来获得和使用这个类库中的类。 -[^1]: 许多语言,例如Perl,Python和Ruby,都有集合的本地支持。 +[^1]: 许多语言,例如 Perl ,Python 和 Ruby ,都有集合的本地支持。 [^2]: 这里是操作符重载的用武之地,C++和C#的集合类都使用操作符重载生成了更简洁的语法。 [^3]: 在[泛型]()章节的末尾,有个关于这个问题是否很严重的讨论。但是,[泛型]()章节还将展示Java泛型远不止是类型安全的集合这么简单。 -[^4]: **remove()** 是一个所谓的“可选”方法(还有一些其它的这种方法),这意味着并非所有的 **Iterator** 实现都必须实现该方法。这个问题将在[附录:集合主题]()中介绍。但是,标准Java库集合实现了 **remove()** ,因此在[附录:集合主题]()章节之前,都不必担心这个问题。 +[^4]: `remove()` 是一个所谓的“可选”方法(还有一些其它的这种方法),这意味着并非所有的 **Iterator** 实现都必须实现该方法。这个问题将在[附录:集合主题]()中介绍。但是,标准 Java 库集合实现了 `remove()` ,因此在[附录:集合主题]()章节之前,都不必担心这个问题。 [^5]: 这实际上依赖于具体实现。优先级队列算法通常会按插入顺序排序(维护一个*堆*),但它们也可以在删除时选择最重要的元素。 如果对象的优先级在它在队列中等待时可以修改,那么算法的选择就显得很重要了。 [^6]: 有些人提倡这样一种自动创建机制,即对一个类中所有可能的方法组合都自动创建一个接口,有时候对于单个的类都是如此。 我相信接口的意义不应该仅限于方法组合的机械地复制,因此我在创建接口之前,总是要先看到增加接口带来的价值。 -[^7]: 这在Java SE5之前是不可用的,因为该方法被认为与操作系统的耦合度过紧,因此违反“一次编写,处处运行”的原则。现在提供它这一事实表明,Java的设计者们更加务实了。 +[^7]: 这在 Java 5 之前是不可用的,因为该方法被认为与操作系统的耦合度过紧,因此违反“一次编写,处处运行”的原则。现在却提供它,这一事实表明, Java 的设计者们更加务实了。 + +[^8]: 下面是译者绘制的 Java 集合框架简图,黄色为接口,绿色为抽象类,蓝色为具体类。虚线箭头表示实现关系,实线箭头表示继承关系。 +![collection](../images/collection.png) +![map](../images/map.png) diff --git a/docs/book/13-Functional-Programming.md b/docs/book/13-Functional-Programming.md index 4ed2ab9..7b9473e 100644 --- a/docs/book/13-Functional-Programming.md +++ b/docs/book/13-Functional-Programming.md @@ -7,11 +7,11 @@ > 函数式编程语言操纵代码片段就像操作数据一样容易。 虽然 Java 不是函数式语言,但 Java 8 Lambda 表达式和方法引用 (Method References) 允许你以函数式编程。 -在计算机时代早期,内存是稀缺和昂贵的。几乎每个人都用汇编语言编程。人们对编译器有所了解,但仅仅想到编译生成的代码肯定会比手工编码多了很多字节。 +在计算机时代早期,内存是稀缺和昂贵的。几乎每个人都用汇编语言编程。人们对编译器有所了解,但仅仅想到编译生成的代码肯定会比手工编码多很多字节。 -通常,只是为了使程序适合有限的内存,程序员通过修改内存中的代码来保存代码空间,以便在程序执行时执行不同的操作。这种技术被称为**自修改代码** (self-modifying code)。只要程序足够小,少数人可以维护所有棘手和神秘的汇编代码,你就可以让它运行起来。 +通常,只是为了使程序适合有限的内存,程序员通过修改内存中的代码来节省代码空间,以便在程序执行时执行不同的操作。这种技术被称为**自修改代码** (self-modifying code)。只要程序足够小,少数人可以维护所有棘手和神秘的汇编代码,你就可以让它运行起来。 -随着内存和处理器变得更便宜、更快。C 语言出现并被大多数汇编程序员认为更“高级”。人们发现使用 C 可以显著提高生产力。同时,创建自修改代码仍然不难。 +随着内存和处理器变得更便宜、更快。C 语言出现并被大多数汇编程序员认为更“高级”。人们发现使用 C 可以显著提高生产力。同时,使用 C 创建自修改代码仍然不难。 随着硬件越来越便宜,程序的规模和复杂性都在增长。这一切只是让程序工作变得困难。我们想方设法使代码更加一致和易懂。使用纯粹的自修改代码造成的结果就是:我们很难确定程序在做什么。它也难以测试:除非你想一点点测试输出,代码转换和修改等等过程? @@ -25,7 +25,7 @@ OO(object oriented,面向对象)是抽象数据,FP(functional programm 纯粹的函数式语言在安全性方面更进一步。它强加了额外的约束,即所有数据必须是不可变的:设置一次,永不改变。将值传递给函数,该函数然后生成新值但从不修改自身外部的任何东西(包括其参数或该函数范围之外的元素)。当强制执行此操作时,你知道任何错误都不是由所谓的副作用引起的,因为该函数仅创建并返回结果,而不是其他任何错误。 -更好的是,“不可变对象和无副作用”范例解决了并发编程中最基本和最棘手的问题之一(当程序的某些部分同时在多个处理器上运行时)。这是可变共享状态的问题,这意味着代码的不同部分(在不同的处理器上运行)可以尝试同时修改同一块内存(谁赢了?没人知道)。如果函数永远不会修改现有值但只生成新值,则不会对内存产生争用,这是纯函数式语言的定义。 因此,经常提出纯函数式语言作为并行编程的解决方案(还有其他可行的解决方案)。 +更好的是,“不可变对象和无副作用”范式解决了并发编程中最基本和最棘手的问题之一(当程序的某些部分同时在多个处理器上运行时)。这是可变共享状态的问题,这意味着代码的不同部分(在不同的处理器上运行)可以尝试同时修改同一块内存(谁赢了?没人知道)。如果函数永远不会修改现有值但只生成新值,则不会对内存产生争用,这是纯函数式语言的定义。 因此,经常提出纯函数式语言作为并行编程的解决方案(还有其他可行的解决方案)。 需要提醒大家的是,函数式语言背后有很多动机,这意味着描述它们可能会有些混淆。它通常取决于各种观点:“为并行编程”,“代码可靠性”和“代码创建和库复用”。[^1] 同时,函数式编程的参数能帮助程序员创建更快更健壮的代码 —— 部分仍然只是假设。虽然已有一些好的范例[^2],但我们还不能证明纯函数式语言就是解决编程问题的最佳方法。 @@ -102,13 +102,13 @@ Hello Hello there Hello there ``` -**Strategy** 接口提供了单一 `approach()` 方法来承载函数式功能。通过实现不同的 **Strategy** 对象,我们可以创建不同的行为。 +**Strategy** 接口提供了单一的 `approach()` 方法来承载函数式功能。通过创建不同的 **Strategy** 对象,我们可以创建不同的行为。 传统上,我们通过创建一个实现 **Strategy** 接口的类来实现此行为,比如在 **Soft**。 - **[1]** 在 **Strategize** 中,**Soft** 作为默认策略,在初始化构造函数中赋值的。 -- **[2]** 一种略显冗长且更自发的方法是创建一个**匿名内部类**。即使这样,仍有相当数量的冗余代码。你总是要仔细观察:“哦,原来这样,这里使用了匿名内部类。” +- **[2]** 一种略显简短且更自发的方法是创建一个**匿名内部类**。即使这样,仍有相当数量的冗余代码。你总是要仔细观察:“哦,原来这样,这里使用了匿名内部类。” - **[3]** Java 8 的 Lambda 表达式。由箭头 `->` 分隔开参数和函数体,箭头左边是参数,箭头右侧是从 Lambda 返回的表达式,即函数体。这实现了与定义类、匿名内部类相同的效果,但代码少得多。 @@ -116,12 +116,12 @@ Hello there Hello there - **[5]** 在使用默认的 **Soft** **strategy** 之后,我们逐步遍历数组中的所有 **Strategy**,并使用 `changeStrategy()` 方法将每个 **Strategy** 放入 变量 `s` 中。 -- **[6]** 现在,每次调用 `communic()` 都会产生不同的行为,具体取决于此刻正在使用的策略**代码对象**。我们传递的是行为,而非仅数据。[^3] - -在 Java 8 之前,我们能够通过 **[1]** 和 **[2]** 的方式传递功能。然而,它的读写语法非常笨拙,并且我们别无选择。方法引用和 Lambda 表达式的出现让我们可以在需要时**传递功能**,而不是仅在必要才这么做。 +- **[6]** 现在,每次调用 `communicate()` 都会产生不同的行为,具体取决于此刻正在使用的策略**代码对象**。我们传递的是行为,而非仅数据。[^3] +在 Java 8 之前,我们能够通过 **[1]** 和 **[2]** 的方式传递功能。然而,这种语法的读写非常笨拙,并且我们别无选择。方法引用和 Lambda 表达式的出现让我们可以在需要时**传递功能**,而不是仅在必要才这么做。 + ## Lambda表达式 @@ -129,9 +129,9 @@ Lambda 表达式是使用**最小可能**语法编写的函数定义: 1. Lambda 表达式产生函数,而不是类。 在 JVM(Java Virtual Machine,Java 虚拟机)上,一切都是一个类,因此在幕后执行各种操作使 Lambda 看起来像函数 —— 但作为程序员,你可以高兴地假装它们“只是函数”。 -2. Lambda 语法尽可能多,这正是为了使 Lambda 易于编写和使用。 +2. Lambda 语法尽可能少,这正是为了使 Lambda 易于编写和使用。 -我们在 `Strategize.java` 中看到了一个 Lambda 表达式,但还有其他语法变体: +我们在 **Strategize.java** 中看到了一个 Lambda 表达式,但还有其他语法变体: ```java // functional/LambdaExpressions.java @@ -255,7 +255,7 @@ public class RecursiveFactorial { 这里,`fact` 是一个静态变量。 注意使用三元 **if-else**。 递归函数将一直调用自己,直到 `i == 0`。所有递归函数都有“停止条件”,否则无限递归并报异常。 -我们可以将 `Fibonacci` 序列改为递归 Lambda 表达式来实现,这次使用实例变量: +我们可以将 `Fibonacci` 序列改为使用递归 Lambda 表达式来实现,这次使用实例变量: ```java // functional/RecursiveFibonacci.java @@ -295,9 +295,10 @@ public class RecursiveFibonacci { 55 ``` -将 `Fibonacci` 序列对中的最后两个元素求和来产生下一个元素。 +将 `Fibonacci` 序列中的最后两个元素求和来产生下一个元素。 + ## 方法引用 @@ -366,7 +367,7 @@ Help! **[3]** `hello()` 也符合 `call()` 的签名。 -**[4]** 就像是 `help()`,静态内部类中的非静态方法。 +**[4]** `help()` 也符合,它是静态内部类中的非静态方法。 **[5]** `assist()` 是静态内部类中的静态方法。 @@ -585,7 +586,7 @@ Lambda 表达式包含类型推导(编译器会自动推导出类型信息, 为了解决这个问题,Java 8 引入了 `java.util.function` 包。它包含一组接口,这些接口是 Lambda 表达式和方法引用的目标类型。 每个接口只包含一个抽象方法,称为函数式方法。 -在编写接口时,可以使用 `@FunctionalInterface` 注解强制执行此“函数方法”模式: +在编写接口时,可以使用 `@FunctionalInterface` 注解强制执行此“函数式方法”模式: ```java // functional/FunctionalAnnotation.java @@ -627,9 +628,9 @@ public class FunctionalAnnotation { } ``` -`@FunctionalInterface` 注解是可选的; Java 将 `Functional` 和 `FunctionalNoAnn` 视为主方法中的函数接口。 `@FunctionalInterface` 的值在 `NotFunctional` 的定义中可见:接口中的如果有多个方法则会产生编译时错误消息。 +`@FunctionalInterface` 注解是可选的; Java 在 `main()` 中把 **Functional** 和 **FunctionalNoAnn** 都当作函数式接口。 `@FunctionalInterface` 的值在 `NotFunctional` 的定义中可见:接口中如果有多个方法则会产生编译时错误消息。 -仔细观察 `f` 和 `fna` 声明中会发生什么。 `Functional` 和 `FunctionalNoAnn` 定义接口,然而赋值的只是方法 `goodbye()`。首先,这只是一个方法而不是类;其次,它甚至都不是实现了该接口类的方法。Java 8 在这里添加了一点小魔法:如果将方法引用或 Lambda 表达式赋值给函数接口(以及适合的类型),Java 会适配你的赋值到目标接口。 编译器会自动包装方法引用或 Lambda 表达式到实现目标接口的类的实例中。 +仔细观察在定义 `f` 和 `fna` 时发生了什么。 `Functional` 和 `FunctionalNoAnn` 定义接口,然而被赋值的只是方法 `goodbye()`。首先,这只是一个方法而不是类;其次,它甚至都不是实现了该接口的类中的方法。Java 8 在这里添加了一点小魔法:如果将方法引用或 Lambda 表达式赋值给函数式接口(类型需要匹配),Java 会适配你的赋值到目标接口。 编译器会自动包装方法引用或 Lambda 表达式到实现目标接口的类的实例中。 尽管 `FunctionalAnnotation` 确实适合 `Functional` 模型,但 Java 不允许我们将 `FunctionalAnnotation` 像 `fac` 定义一样直接赋值给 `Functional`,因为它没有明确地实现 `Functional` 接口。 令人惊奇的是 ,Java 8 允许我们以简便的语法为接口赋值函数。 @@ -637,17 +638,17 @@ public class FunctionalAnnotation { 以下是基本命名准则: -1. 如果只处理对象而非基本类型,名称为 `Function`,`Consumer`,`Predicate` 等。参数类型通过泛型添加。 +1. 如果只处理对象而非基本类型,名称则为 `Function`,`Consumer`,`Predicate` 等。参数类型通过泛型添加。 -2. 如果是基本类型,则由名称的第一部分表示,如 `LongConsumer`,`DoubleFunction`,`IntPredicate` 等,但基本 `Supplier` 类型例外。 +2. 如果接收的参数是基本类型,则由名称的第一部分表示,如 `LongConsumer`,`DoubleFunction`,`IntPredicate` 等,但基本 `Supplier` 类型例外。 3. 如果返回值为基本类型,则用 `To` 表示,如 `ToLongFunction ` 和 `IntToLongFunction`。 4. 如果返回值类型与参数类型一致,则是一个运算符:单个参数使用 `UnaryOperator`,两个参数使用 `BinaryOperator`。 -5. 如果是 2 个参数且返回值为布尔值,则是一个谓词(Predicate)。 +5. 如果接收两个参数且返回值为布尔值,则是一个谓词(Predicate)。 -6. 如果是两个参数类型不同,则名称中有一个 `Bi`。 +6. 如果接收的两个参数类型不同,则名称中有一个 `Bi`。 下表描述了 `java.util.function` 中的目标类型(包括例外情况): @@ -668,15 +669,15 @@ public class FunctionalAnnotation { |2 参数类型不同|**Bi操作**
(不同方法名)|**`BiFunction`
`BiConsumer`
`BiPredicate`
`ToIntBiFunction`
`ToLongBiFunction`
`ToDoubleBiFunction`**| -  此表仅提供些常规方案。通过上表,你应该或多或少能自行推导出更多行的函数式接口。 +此表仅提供些常规方案。通过上表,你应该或多或少能自行推导出更多行的函数式接口。 -能看出来在创建 `java.util.function` 时,设计者做出了一些选择。 +可以看出,在创建 `java.util.function` 时,设计者们做出了一些选择。 -例如,为什么没有 `IntComparator`,`LongComparator` 和 `DoubleComparator` 呢?有 `BooleanSupplier` 却没有其他表示 **Boolean** 的接口;有通用的 `BiConsumer` 却没有用于 **int**,**long** 和 **double** 的 `BiConsumers` 变体(我对他们放弃的原因表示同情)。这是疏忽还是有人决定(他们是如何得出这个结论的)? +例如,为什么没有 `IntComparator`,`LongComparator` 和 `DoubleComparator` 呢?有 `BooleanSupplier` 却没有其他表示 **Boolean** 的接口;有通用的 `BiConsumer` 却没有用于 **int**,**long** 和 **double** 的 `BiConsumers` 变体(我对他们放弃的原因表示同情)。这些选择是疏忽还是有人认为其他组合的使用情况出现得很少(他们是如何得出这个结论的)? -你还可以看到基本类型给 Java 添加了多少复杂性。为了缓解效率问题,该语言的第一版中就包含了基本类型。现在,在语言的生命周期中,我们仍然受到语言设计选择不佳的影响。 +你还可以看到基本类型给 Java 添加了多少复杂性。为了缓和效率问题,该语言的第一版中就包含了基本类型。现在,在语言的生命周期中,我们仍然受到语言设计选择不佳的影响。 -下面枚举基于 Lambda 表达式的所有不同 Function 变体的示例: +下面枚举了基于 Lambda 表达式的所有不同 **Function** 变体的示例: ```java // functional/FunctionVariants.java @@ -744,7 +745,7 @@ public class FunctionVariants { } ``` -这些 Lambda 表达式尝试生成适合对应函数签名的最简代码。 在某些情况下,有必要进行强制类型转换,否则编译器会报截断错误。 +这些 Lambda 表达式尝试生成适合函数签名的最简代码。 在某些情况下,有必要进行强制类型转换,否则编译器会报截断错误。 主方法中的每个测试都显示了 `Function` 接口中不同类型的 `apply()` 方法。 每个都产生一个与其关联的 Lambda 表达式的调用。 @@ -787,7 +788,7 @@ someOtherName() 查看 `BiConsumer` 的文档,你会看到 `accept()` 方法。 实际上,如果我们将方法命名为 `accept()`,它就可以作为方法引用。 但是我们也可用不同的名称,比如 `someOtherName()`。只要参数类型、返回类型与 `BiConsumer` 的 `accept()` 相同即可。 -因此,在使用函数接口时,名称无关紧要——只要参数类型和返回类型相同。 Java 会将你的方法映射到接口方法。 要调用方法,可以调用接口的函数式方法(在本例中为 `accept()`),而不是你的方法名。 +因此,在使用函数接口时,名称无关紧要——只要参数类型和返回类型相同。 Java 会将你的方法映射到接口方法。 要调用方法,可以调用接口的函数式方法名(在本例中为 `accept()`),而不是你的方法名。 现在我们来看看所有基于类的函数式,应用于方法引用(即那些不涉及基本类型的函数)。下例我们创建了一个最简单的函数式签名。代码示例: @@ -869,7 +870,7 @@ public class TriFunctionTest { } ``` -这里我们测试方法引用和 Lambda 表达式。 +这里我们测试了方法引用和 Lambda 表达式。 ### 缺少基本类型的函数 @@ -903,7 +904,7 @@ public class BiConsumerPermutations { 1, 11 ``` -这里使用 `System.out.format()` 来显示。它类似于 `System.out.println()` 但提供了更多的显示选项。 这里,`%f` 表示我将 `n` 作为浮点值给出,`%d` 表示 `n` 是一个整数值。 我能够包含空格,并且它不会添加换行符,除非你输入 `%n` - 它也会接受传统 `\n` 换行符,但 `%n` 是自动跨平台的,这是使用的 `format()` 的另一个原因。 +这里使用 `System.out.format()` 来显示。它类似于 `System.out.println()` 但提供了更多的显示选项。 这里,`%f` 表示我将 `n` 作为浮点值给出,`%d` 表示 `n` 是一个整数值。 这其中可以包含空格,输入 `%n` 会换行 — 当然使用传统的 `\n` 也能换行,但 `%n` 是自动跨平台的,这是使用 `format()` 的另一个原因。 上例简单使用了包装类型,装箱和拆箱用于在基本类型之间来回转换。 我们也可以使用包装类型,如 `Function`,而不是预定义的基本类型。代码示例: @@ -939,7 +940,7 @@ public interface IntToDoubleFunction { ## 高阶函数 -这个名字听起来有点令人生畏,但是:[高阶函数](https://en.wikipedia.org/wiki/Higher-order_function)(Higher-order Function)只是一个消费或产生函数的函数。 +这个名字可能听起来令人生畏,但是:[高阶函数](https://en.wikipedia.org/wiki/Higher-order_function)(Higher-order Function)只是一个消费或产生函数的函数。 我们先来看看如何产生一个函数: @@ -969,9 +970,9 @@ yelling 这里,`produce()` 是高阶函数。 -**[1]** 使用继承,可以轻松地为你的专用接口创建别名。 +**[1]** 使用继承,可以轻松地为专用接口创建别名。 -**[2]** 使用 Lambda 表达式,在方法中轻松创建和返回一个函数。 +**[2]** 使用 Lambda 表达式,可以轻松地在方法中创建和返回一个函数。 要消费一个函数,消费函数需要在参数列表正确地描述函数类型。代码示例: @@ -993,7 +994,7 @@ public class ConsumeFunction { } ``` -有趣的是,根据消费函数生成新函数。代码示例: +当基于消费函数生成新函数时,事情就变得相当有趣了。代码示例如下: ```java // functional/TransformFunction.java @@ -1036,9 +1037,10 @@ O 在这里,`transform()` 生成一个与传入的函数具有相同签名的函数,但是你可以生成任何你想要的类型。 -这在 `Function` 接口中使用名为 `andThen()` 的默认方法,该方法专门用于操作函数。 顾名思义,在调用 `in` 函数之后调用 `toThen()`(还有 `compose()`,它在 `in` 函数之前应用新函数)。 要附加一个 `andThen()` 函数,我们只需将该函数作为参数传递。 `transform()` 产生的是一个新函数,它将 `in` 的动作与 `andThen()` 参数的动作结合起来。 +这里使用到了 `Function` 接口中名为 `andThen()` 的默认方法,该方法专门用于操作函数。 顾名思义,在调用 `in` 函数之后调用 `toThen()`(还有个 `compose()` 方法,它在 `in` 函数之前应用新函数)。 要附加一个 `andThen()` 函数,我们只需将该函数作为参数传递。 `transform()` 产生的是一个新函数,它将 `in` 的动作与 `andThen()` 参数的动作结合起来。 + ## 闭包 @@ -1046,9 +1048,7 @@ O **闭包**(Closure)一词总结了这些问题。 它非常重要,利用闭包可以轻松生成函数。 -考虑一个更复杂的 Lambda,它使用函数作用域之外的变量。 返回该函数会发生什么? 也就是说,当你调用函数时,它对那些 “外部 ”变量引用了什么? 如果语言不能自动解决这个问题,那将变得非常具有挑战性。 能够解决这个问题的语言被称为**支持闭包**,或者在词法上限定范围( 也使用术语变量捕获 )。Java 8 提供了有限但合理的闭包支持, - -我们将用一些简单的例子来研究它。 +考虑一个更复杂的 Lambda,它使用函数作用域之外的变量。 返回该函数会发生什么? 也就是说,当你调用函数时,它对那些 “外部 ”变量引用了什么? 如果语言不能自动解决这个问题,那将变得非常具有挑战性。 能够解决这个问题的语言被称为**支持闭包**,或者叫作在词法上限定范围( 也使用术语变量捕获 )。Java 8 提供了有限但合理的闭包支持,我们将用一些简单的例子来研究它。 首先,下例函数中,方法返回访问对象字段和方法参数。代码示例: @@ -1065,7 +1065,7 @@ public class Closure1 { } ``` -但是,仔细考虑一下,`i` 的这种用法并非是个大难题,因为对象很可能在你调用 `makeFun()` 之后就存在了——实际上,垃圾收集器几乎肯定会保留一个对象,并将现有的函数以这种方式绑定到该对象上[^5]。当然,如果你对同一个对象多次调用 `makeFun()` ,你最终会得到多个函数,它们都为 `i` 共享相同的存储空间: +但是,仔细考虑一下,`i` 的这种用法并非是个大难题,因为对象很可能在你调用 `makeFun()` 之后就存在了——实际上,垃圾收集器几乎肯定会保留一个对象,并将现有的函数以这种方式绑定到该对象上[^5]。当然,如果你对同一个对象多次调用 `makeFun()` ,你最终会得到多个函数,它们共享 `i` 的存储空间: ```java // functional/SharedStorage.java diff --git a/docs/book/21-Arrays.md b/docs/book/21-Arrays.md index a5abe15..9c40c66 100644 --- a/docs/book/21-Arrays.md +++ b/docs/book/21-Arrays.md @@ -4,13 +4,304 @@ # 第二十一章 数组 +> 在[初始化和清理](/book/06-Housekeeping.md)一章的最后,你已经学过如何定义和初始化一个数组。 + +简单来看,数组需要你去创建和初始化,你可以通过下标对数组元素进行访问,数组的大小不会改变。大多数时候你只需要知道这些,但有时候你必须在数组上进行更复杂的操作,你也可能需要在数组和更加灵活的 **集合** (Collection)之间做出评估。因此本章我们将对数组进行更加深入的分析。 + +**注意:** 随着Java Collection 和 Stream 类中高级功能的不断增加,日常编程中使用数组的需求也在变少,所以你暂且可以放心的略读甚至跳过这一章。但是,即使你自己避免使用数组,也总会有需要阅读别人数组代码的那一天。那时候,本章依然在这里等着你来翻阅。 + + + ## 数组特性 +明明还有很多其他的办法来保存对象,那么是什么令数组如此特别? + +将数组和其他类型的集合区分开来的原因有三:效率,类型,保存基本数据的能力。在Java中,使用数组存储和随机访问对象引用序列是非常高效的。数组是简单的线性序列,这使得对元素的访问变得非常快。然而这种高速也是有代价的,代价就是数组对象的大小是固定的,且在该数组的生存期内不能更改。 + +速度通常并不是问题,如果有问题,你保存和检索对象的方式也很少是罪魁祸首。你应该总是从 **ArrayList** (来自 [集合]( ))开始,它将数组封装起来。必要时,它会自动分配更多的数组空间,创建新数组,并将旧数组中的引用移动到新数组。这种灵活性需要开销,所以一个**ArrayList**的效率不如数组。在极少的情况下效率会成为问题,所以这种时候你可以直接使用数组。 + + +数组和集合(Collections)都不能滥用。不管你使用数组还是集合,如果你越界,你都会得到一个 **RuntimeException** 的异常提醒,这表明你的程序中存在错误。 + + +在泛型前,其他的集合类以一种宽泛的方式处理对象(就好像它们没有特定类型一样)。事实上,这些集合类把保存对象的类型默认为 **Object** ,也就Java中所有类的基类。而数组是优于 **预泛型** (pre-generic)集合类的,因为你创建一个数组就可以保存特定类型的数据。这意味着你获得了一个编译时的类型检查,而这可以防止你插入错误的数据类型,或者搞错你正在提取的数据类型。 + + +当然,不管在编译时还是运行时,Java都会阻止你犯向对象发送不正确消息的错误。然而不管怎样,使用数组都不会有更大的风险。比较好的地方在于,如果编译器报错,最终的用户更容易理解抛出异常的含义。 + + +一个数组可以保存基本数据类型,而一个预泛型的集合不可以。然而对于泛型而言,集合可以指定和检查他们保存对象的类型,而通过 **自动装箱** (autoboxing)机制,集合表现地就像它们可以保存基本数据类型一样,因为这种转换是自动的。 + +下面给出一例用于比较数组和泛型集合: + +```java +// arrays/CollectionComparison.java +// (c)2017 MindView LLC: see Copyright.txt +// We make no guarantees that this code is fit for any purpose. +// Visit http://OnJava8.com for more book information. +import java.util.*; +import onjava.*; +import static onjava.ArrayShow.*; + +class BerylliumSphere { + private static long counter; + private final long id = counter++; + @Override + public String toString() { + return "Sphere " + id; + } +} + +public class CollectionComparison { + public static void main(String[] args) { + BerylliumSphere[] spheres = + new BerylliumSphere[10]; + for(int i = 0; i < 5; i++) + spheres[i] = new BerylliumSphere(); + show(spheres); + System.out.println(spheres[4]); + + List sphereList = Suppliers.create( + ArrayList::new, BerylliumSphere::new, 5); + System.out.println(sphereList); + System.out.println(sphereList.get(4)); + + int[] integers = { 0, 1, 2, 3, 4, 5 }; + show(integers); + System.out.println(integers[4]); + + List intList = new ArrayList<>( + Arrays.asList(0, 1, 2, 3, 4, 5)); + intList.add(97); + System.out.println(intList); + System.out.println(intList.get(4)); + } +} +/* Output: +[Sphere 0, Sphere 1, Sphere 2, Sphere 3, Sphere 4, +null, null, null, null, null] +Sphere 4 +[Sphere 5, Sphere 6, Sphere 7, Sphere 8, Sphere 9] +Sphere 9 +[0, 1, 2, 3, 4, 5] +4 +[0, 1, 2, 3, 4, 5, 97] +4 +*/ +``` + +**Suppliers.create()** 方法在[泛型](/book/20-Generics.md)中被定义。上面两种保存对象的方式都是有类型检查的,唯一比较明显的区别就是数组使用[ ] 来随机存取元素,而一个List 使用诸如add()和get()等方法。数组和ArrayList之间的相似是设计者有意为之,所以在概念上,两者很容易切换。但是就像你在[集合](book/12-Collections.md)中看到的,集合的功能明显多于数组。随着Java自动装箱技术的出现,通过集合使用基本数据类型几乎和通过数组一样简单。数组唯一剩下的优势就是效率。然而,当你解决一个更加普遍的问题时,数组可能限制太多,这种情形下,您可以使用集合类。 + + +### 用于显示数组的实用程序 + +在本章中,我们处处都要显示数组。Java提供了 **array.toString()** 来将数组转换为可读字符串,然后可以在控制台上显示。然而这种方式视觉上噪音太大,所以我们创建一个小的库来完成这项工作。 + +```java +// onjava/ArrayShow.java +// (c)2017 MindView LLC: see Copyright.txt +// We make no guarantees that this code is fit for any purpose. +// Visit http://OnJava8.com for more book information. +package onjava; +import java.util.*; + +public interface ArrayShow { + static void show(Object[] a) { + System.out.println(Arrays.toString(a)); + } + static void show(boolean[] a) { + System.out.println(Arrays.toString(a)); + } + static void show(byte[] a) { + System.out.println(Arrays.toString(a)); + } + static void show(char[] a) { + System.out.println(Arrays.toString(a)); + } + static void show(short[] a) { + System.out.println(Arrays.toString(a)); + } + static void show(int[] a) { + System.out.println(Arrays.toString(a)); + } + static void show(long[] a) { + System.out.println(Arrays.toString(a)); + } + static void show(float[] a) { + System.out.println(Arrays.toString(a)); + } + static void show(double[] a) { + System.out.println(Arrays.toString(a)); + } + // Start with a description: + static void show(String info, Object[] a) { + System.out.print(info + ": "); + show(a); + } + static void show(String info, boolean[] a) { + System.out.print(info + ": "); + show(a); + } + static void show(String info, byte[] a) { + System.out.print(info + ": "); + show(a); + } + static void show(String info, char[] a) { + System.out.print(info + ": "); + show(a); + } + static void show(String info, short[] a) { + System.out.print(info + ": "); + show(a); + } + static void show(String info, int[] a) { + System.out.print(info + ": "); + show(a); + } + static void show(String info, long[] a) { + System.out.print(info + ": "); + show(a); + } + static void show(String info, float[] a) { + System.out.print(info + ": "); + show(a); + } + static void show(String info, double[] a) { + System.out.print(info + ": "); + show(a); + } +} + +``` + +第一个方法适用于对象数组,包括那些包装基本数据类型的数组。所有的方法重载对于不同的数据类型是必要的。 + +第二组重载方法可以让你显示带有信息 **字符串** 前缀的数组。 + +为了简单起见,你通常可以静态的导入它们。 + ## 一等对象 +不管你使用的什么类型的数组,数组中的数据集实际上都是对堆中真正对象的引用。数组是保存指向其他对象的引用的对象,数组可以隐式地地创建,作为数组初始化语法的一部分,也可以显式地创建,比如使用一个 **new** 关键字。数组对象的一部分(事实上,你唯一可以使用的方法)就是只读的 **length** 成员函数,它能告诉你数组对象中可以存储多少元素。**[ ]** 语法是你访问数组对象的唯一方式。 + +下面的例子总结了初始化数组的多种方式,并且展示了如何给不同的数组对象分配数组引用。同时也可以看出对象数组和基元数组在使用上是完全相同的。唯一的不同之处就是对象数组存储的是对象的引用,而基元数组则直接存储基本数据类型的值。 + +```java +// arrays/ArrayOptions.java +// Initialization & re-assignment of arrays +import java.util.*; +import static onjava.ArrayShow.*; + +public class ArrayOptions { + public static void main(String[] args) { + // Arrays of objects: + BerylliumSphere[] a; // Uninitialized local + BerylliumSphere[] b = new BerylliumSphere[5]; + + // The references inside the array are + // automatically initialized to null: + show("b", b); + BerylliumSphere[] c = new BerylliumSphere[4]; + for(int i = 0; i < c.length; i++) + if(c[i] == null) // Can test for null reference + c[i] = new BerylliumSphere(); + + // Aggregate initialization: + BerylliumSphere[] d = { + new BerylliumSphere(), + new BerylliumSphere(), + new BerylliumSphere() + }; + + // Dynamic aggregate initialization: + a = new BerylliumSphere[]{ + new BerylliumSphere(), new BerylliumSphere(), + }; + // (Trailing comma is optional) + + System.out.println("a.length = " + a.length); + System.out.println("b.length = " + b.length); + System.out.println("c.length = " + c.length); + System.out.println("d.length = " + d.length); + a = d; + System.out.println("a.length = " + a.length); + + // Arrays of primitives: + int[] e; // Null reference + int[] f = new int[5]; + + // The primitives inside the array are + // automatically initialized to zero: + show("f", f); + int[] g = new int[4]; + for(int i = 0; i < g.length; i++) + g[i] = i*i; + int[] h = { 11, 47, 93 }; + + // Compile error: variable e not initialized: + //- System.out.println("e.length = " + e.length); + System.out.println("f.length = " + f.length); + System.out.println("g.length = " + g.length); + System.out.println("h.length = " + h.length); + e = h; + System.out.println("e.length = " + e.length); + e = new int[]{ 1, 2 }; + System.out.println("e.length = " + e.length); + } +} +/* Output: +b: [null, null, null, null, null] +a.length = 2 +b.length = 5 +c.length = 4 +d.length = 3 +a.length = 3 +f: [0, 0, 0, 0, 0] +f.length = 5 +g.length = 4 +h.length = 3 +e.length = 3 +e.length = 2 +*/ + +``` + + + +数组 **a** 是一个未初始化的本地变量,编译器不会允许你使用这个引用直到你正确地对其进行初始化。数组 **b** 被初始化成一系列指向 **BerylliumSphere** 对象的引用,但是并没有真正的 **BerylliumSphere** 对象被存储在数组中。尽管你仍然可以获得这个数组的大小,因为 **b** 指向合法对象。这带来了一个小问题:你无法找出到底有多少元素存储在数组中,因为 **length** 只能告诉你数组可以存储多少元素;这就是说,数组对象的大小并不是真正存储在数组中对象的个数。然而,当你创建一个对象数组,其引用将自动初始化为 **null** ,因此你可以通过检查特定数组元素中的引用是否为 **null** 来判断其中是否有对象。基元数组也有类似的机制,比如自动将数值类型初始化为 **0** ,char型初始化为 **(char)0** ,布尔类型初始化为 **false** 。 + +在给数组中各元素分配 **BerylliumSphere** 对象后,数组 **c** 展示数组对象的创建。数组 **d** 展示了创建数组对象的聚合初始化语法(隐式地使用new在堆中创建对象,就像 **c** 一样)并且初始化成 **BeryliumSphere** 对象,这一切都在一条语句中完成。 + +下一个数组初始化可以被看做是一个“动态聚合初始化”。 **d** 使用的聚合初始化必须在 **d** 定义的点使用,但是使用第二种语法,你可以在任何地方创建和初始化数组对象。例如,假设 **hide()** 是一个需要使用一系列的 **BeryliumSphere**对象。你可以这样调用它: + +```Java +hide(d); +``` + +你也可以动态的创建你用作参数传递的数组: + +```Java +hide(new BerylliumSphere[]{ + new BerlliumSphere(), + new BerlliumSphere() +}); +``` + +很多情况下这种语法写代码更加方便。 + +表达式: + +```Java +a = d; +``` + +显示了你如何获取指向一个数组对象的引用并将其分配给另一个数组对象。就像你可以处理其他类型的对象引用。现在 **a** 和 **d** 都指向了堆中的同一个数组对象。 + +**ArrayOptions.java** 的第二部分展示了基元数组的语法就像对象数组一样,除了基元数组直接保存基本数据类型的值。 + ## 返回数组 diff --git a/docs/book/22-Enumerations.md b/docs/book/22-Enumerations.md index 1ef7d2d..309d9cf 100644 --- a/docs/book/22-Enumerations.md +++ b/docs/book/22-Enumerations.md @@ -1,60 +1,2109 @@ [TOC] + # 第二十二章 枚举 -> 关键字enum可以将一组具名的值的有限集合创建为一种新的类型,而这些具名的值可以作为常规的程序组件使用。这是一种非常有用的功能 - +> 关键字 enum 可以将一组具名的值的有限集合创建为一种新的类型,而这些具名的值可以作为常规的程序组件使用。这是一种非常有用的功能 +在[初始化和清理 ]() 这章结束的时候,我们已经简单地介绍了枚举的概念。现在,你对 Java 已经有了更深刻的理解,因此可以更深入地学习 Java 中的枚举了。你将在本章中看到,使用 enum 可以做很多有趣的事情,同时,我们也会深入其他的 Java 特性,例如泛型和反射。在这个过程中,我们还将学习一些设计模式。 -## 基本功能 +## 基本 enum 特性 + +我们已经在[初始化和清理 ]() 这章章看到,调用 enum 的 values() 方法,可以遍历 enum 实例 .values() 方法返回 enum 实例的数组,而且该数组中的元素严格保持其在 enum 中声明时的顺序,因此你可以在循环中使用 values() 返回的数组。 + +创建 enum 时,编译器会为你生成一个相关的类,这个类继承自 Java.lang.Enum。下面的例子演示了 Enum 提供的一些功能: + +```java +// enums/EnumClass.java +// Capabilities of the Enum class +enum Shrubbery { GROUND, CRAWLING, HANGING } +public class EnumClass { + public static void main(String[] args) { + for(Shrubbery s : Shrubbery.values()) { + System.out.println( + s + " ordinal: " + s.ordinal()); + System.out.print( + s.compareTo(Shrubbery.CRAWLING) + " "); + System.out.print( + s.equals(Shrubbery.CRAWLING) + " "); + System.out.println(s == Shrubbery.CRAWLING); + System.out.println(s.getDeclaringClass()); + System.out.println(s.name()); + System.out.println("********************"); + } +// Produce an enum value from a String name: + for(String s : + "HANGING CRAWLING GROUND".split(" ")) { + Shrubbery shrub = + Enum.valueOf(Shrubbery.class, s); + System.out.println(shrub); + } + } +} +``` + +输出: + +``` +GROUND ordinal: 0 +-1 false false +class Shrubbery +GROUND +******************** +CRAWLING ordinal: 1 +0 true true +class Shrubbery +CRAWLING +******************** +HANGING ordinal: 2 +1 false false +class Shrubbery +HANGING +******************** +HANGING +CRAWLING +GROUND +``` + +ordinal() 方法返回一个 int 值,这是每个 enum 实例在声明时的次序,从 0 开始。可以使用==来比较 enum 实例,编译器会自动为你提供 equals() 和 hashCode() 方法。Enum 类实现了 Comparable 接口,所以它具有 compareTo() 方法。同时,它还实现了 Serializable 接口。 + +如果在 enum 实例上调用 getDeclaringClass() 方法,我们就能知道其所属的 enum 类。 + +name() 方法返回 enum 实例声明时的名字,这与使用 toString() 方法效果相同。valueOf() 是在 Enum 中定义的 static 方法,它根据给定的名字返回相应的 enum 实例,如果不存在给定名字的实例,将会抛出异常。 + +### 将静态类型导入用于 enum + +先看一看 [初始化和清理 ]() 这章中 Burrito.java 的另一个版本: + +```java +// enums/SpicinessEnum.java +package enums; +public enum SpicinessEnum { + NOT, MILD, MEDIUM, HOT, FLAMING +} +// enums/Burrito2.java +// {java enums.Burrito2} +package enums; +import static enums.SpicinessEnum.*; +public class Burrito2 { + SpicinessEnum degree; + public Burrito2(SpicinessEnum degree) { + this.degree = degree; + } + @Override + public String toString() { + return "Burrito is "+ degree; + } + public static void main(String[] args) { + System.out.println(new Burrito2(NOT)); + System.out.println(new Burrito2(MEDIUM)); + System.out.println(new Burrito2(HOT)); + } +} +``` + +输出为: + +``` +Burrito is NOT +Burrito is MEDIUM +Burrito is HOT +``` + +使用 static import 能够将 enum 实例的标识符带入当前的命名空间,所以无需再用 enum 类型来修饰 enum 实例。这是一个好的想法吗?或者还是显式地修饰 enum 实例更好?这要看代码的复杂程度了。编译器可以确保你使用的是正确的类型,所以唯一需要担心的是,使用静态导入会不会导致你的代码令人难以理解。多数情况下,使用 static import 还是有好处的,不过,程序员还是应该对具体情况进行具体分析。 + +注意,在定义 enum 的同一个文件中,这种技巧无法使用,如果是在默认包中定义 enum,这种技巧也无法使用(在 Sun 内部对这一点显然也有不同意见)。 ## 方法添加 +除了不能继承自一个 enum 之外,我们基本上可以将 enum 看作一个常规的类。也就是说我们可以向 enum 中添加方法。enum 甚至可以有 main() 方法。 + +一般来说,我们希望每个枚举实例能够返回对自身的描述,而不仅仅只是默认的 toString() 实现,这只能返回枚举实例的名字。为此,你可以提供一个构造器,专门负责处理这个额外的信息,然后添加一个方法,返回这个描述信息。看一看下面的示例: + +```java +// enums/OzWitch.java +// The witches in the land of Oz +public enum OzWitch { + // Instances must be defined first, before methods: + WEST("Miss Gulch, aka the Wicked Witch of the West"), + NORTH("Glinda, the Good Witch of the North"), + EAST("Wicked Witch of the East, wearer of the Ruby " + + "Slippers, crushed by Dorothy's house"), + SOUTH("Good by inference, but missing"); + private String description; + // Constructor must be package or private access: + private OzWitch(String description) { + this.description = description; + } + public String getDescription() { return description; } + public static void main(String[] args) { + for(OzWitch witch : OzWitch.values()) + System.out.println( + witch + ": " + witch.getDescription()); + } +} +``` + +输出为: + +``` +WEST: Miss Gulch, aka the Wicked Witch of the West +NORTH: Glinda, the Good Witch of the North +EAST: Wicked Witch of the East, wearer of the Ruby +Slippers, crushed by Dorothy's house +SOUTH: Good by inference, but missing +``` + +注意,如果你打算定义自己的方法,那么必须在 enum 实例序列的最后添加一个分号。同时,Java 要求你必须先定义 enum 实例。如果在定义 enum 实例之前定义了任何方法或属性,那么在编译时就会得到错误信息。 + +enum 中的构造器与方法和普通的类没有区别,因为除了有少许限制之外,enum 就是一个普通的类。所以,我们可以使用 enum 做许多事情(虽然,我们一般只使用普通的枚举类型) + +在这个例子中,虽然我们有意识地将 enum 的构造器声明为 private,但对于它的可访问性而言,其实并没有什么变化,因为(即使不声明为 private)我们只能在 enum 定义的内部使用其构造器创建 enum 实例。一旦 enum 的定义结束,编译器就不允许我们再使用其构造器来创建任何实例了。 + +### 覆盖 enum 的方法 + +覆盖 toSring() 方法,给我们提供了另一种方式来为枚举实例生成不同的字符串描述信息。 +在下面的示例中,我们使用的就是实例的名字,不过我们希望改变其格式。覆盖 enum 的 toSring() 方法与覆盖一般类的方法没有区别: + +```java +// enums/SpaceShip.java +import java.util.stream.*; +public enum SpaceShip { + SCOUT, CARGO, TRANSPORT, + CRUISER, BATTLESHIP, MOTHERSHIP; + @Override + public String toString() { + String id = name(); + String lower = id.substring(1).toLowerCase(); + return id.charAt(0) + lower; + } + public static void main(String[] args) { + Stream.of(values()) + .forEach(System.out::println); + } +} +``` + +输出为: + +``` +Scout +Cargo +Transport +Cruiser +Battleship +Mothership +``` + +toString() 方法通过调用 name() 方法取得 SpaceShip 的名字,然后将其修改为只有首字母大写的格式。 + + -## switch语句 + +## switch 语句中的 enum + +在 switch 中使用 enum,是 enum 提供的一项非常便利的功能。一般来说,在 switch 中只能使用整数值,而枚举实例天生就具备整数值的次序,并且可以通过 ordinal() 方法取得其次序(显然编译器帮我们做了类似的工作),因此我们可以在 switch 语句中使用 enum。 + +虽然一般情况下我们必须使用 enum 类型来修饰一个 enum 实例,但是在 case 语句中却不必如此。下面的例子使用 enum 构造了一个小型状态机: + +```java +// enums/TrafficLight.java +// Enums in switch statements +// Define an enum type: +enum Signal { GREEN, YELLOW, RED, } + +public class TrafficLight { + Signal color = Signal.RED; + public void change() { + switch(color) { + // Note you don't have to say Signal.RED + // in the case statement: + case RED: color = Signal.GREEN; + break; + case GREEN: color = Signal.YELLOW; + break; + case YELLOW: color = Signal.RED; + break; + } + } + @Override + public String toString() { + return "The traffic light is " + color; + } + public static void main(String[] args) { + TrafficLight t = new TrafficLight(); + for(int i = 0; i < 7; i++) { + System.out.println(t); + t.change(); + } + } +} +``` + +输出为: + +``` +The traffic light is RED +The traffic light is GREEN +The traffic light is YELLOW +The traffic light is RED +The traffic light is GREEN +The traffic light is YELLOW +The traffic light is RED +``` + +编译器并没有抱怨 switch 中没有 default 语句,但这并不是因为每一个 Signal 都有对应的 case 语句。如果你注释掉其中的某个 case 语句,编译器同样不会抱怨什么。这意味着,你必须确保自己覆盖了所有的分支。但是,如果在 case 语句中调用 return,那么编译器就会抱怨缺少 default 语句了。这与是否覆盖了 enum 的所有实例无关。 + -## values方法 +## values 方法的神秘之处 + +前面已经提到,编译器为你创建的 enum 类都继承自 Enum 类。然而,如果你研究一下 Enum 类就会发现,它并没有 values() 方法。可我们明明已经用过该方法了,难道存在某种“隐藏的”方法吗?我们可以利用反射机制编写一个简单的程序,来查看其中的究竟: + +```java +// enums/Reflection.java +// Analyzing enums using reflection +import java.lang.reflect.*; +import java.util.*; +import onjava.*; +enum Explore { HERE, THERE } +public class Reflection { + public static + Set analyze(Class enumClass) { + System.out.println( + "_____ Analyzing " + enumClass + " _____"); + System.out.println("Interfaces:"); + for(Type t : enumClass.getGenericInterfaces()) + System.out.println(t); + System.out.println( + "Base: " + enumClass.getSuperclass()); + System.out.println("Methods: "); + Set methods = new TreeSet<>(); + for(Method m : enumClass.getMethods()) + methods.add(m.getName()); + System.out.println(methods); + return methods; + } + public static void main(String[] args) { + Set exploreMethods = + analyze(Explore.class); + Set enumMethods = analyze(Enum.class); + System.out.println( + "Explore.containsAll(Enum)? " + + exploreMethods.containsAll(enumMethods)); + System.out.print("Explore.removeAll(Enum): "); + exploreMethods.removeAll(enumMethods); + System.out.println(exploreMethods); +// Decompile the code for the enum: + OSExecute.command( + "javap -cp build/classes/main Explore"); + } +} +``` + +输出为: + +```java +_____ Analyzing class Explore _____ +Interfaces: +Base: class java.lang.Enum +Methods: +[compareTo, equals, getClass, getDeclaringClass, +hashCode, name, notify, notifyAll, ordinal, toString, +valueOf, values, wait] +_____ Analyzing class java.lang.Enum _____ +Interfaces: +java.lang.Comparable +interface java.io.Serializable +Base: class java.lang.Object +Methods: +[compareTo, equals, getClass, getDeclaringClass, +hashCode, name, notify, notifyAll, ordinal, toString, +valueOf, wait] +Explore.containsAll(Enum)? true +Explore.removeAll(Enum): [values] +Compiled from "Reflection.java" +final class Explore extends java.lang.Enum { + public static final Explore HERE; + public static final Explore THERE; + public static Explore[] values(); + public static Explore valueOf(java.lang.String); + static {}; +} +``` + +答案是,values() 是由编译器添加的 static 方法。可以看出,在创建 Explore 的过程中,编译器还为其添加了 valueOf() 方法。这可能有点令人迷惑,Enum 类不是已经有 valueOf() 方法了吗。 + +不过 Enum 中的 valueOf() 方法需要两个参数,而这个新增的方法只需一个参数。由于这里使用的 Set 只存储方法的名字,而不考虑方法的签名,所以在调用 Explore.removeAll(Enum) 之后,就只剩下[values] 了。 + +从最后的输出中可以看到,编译器将 Explore 标记为 final 类,所以无法继承自 enum,其中还有一个 static 的初始化子句,稍后我们将学习如何重定义该句。 + +由于擦除效应(在[泛型 ]() 章节中介绍过),反编译无法得到 Enum 的完整信息,所以它展示的 Explore 的父类只是一个原始的 Enum,而非事实上的 Enum\。 + +由于 values() 方法是由编译器插入到 enum 定义中的 static 方法,所以,如果你将 enum 实例向上转型为 Enum,那么 values() 方法就不可访问了。不过,在 Class 中有一个 getEnumConstants0 方法,所以即便 Enum 接口中没有 values0 方法,我们仍然可以通过 Class 对象取得所有 enum 实例。 + +```java +// enums/UpcastEnum.java +// No values() method if you upcast an enum +enum Search { HITHER, YON } +public class UpcastEnum { + public static void main(String[] args) { + Search[] vals = Search.values(); + Enum e = Search.HITHER; // Upcast +// e.values(); // No values() in Enum + for(Enum en : e.getClass().getEnumConstants()) + System.out.println(en); + } +} +``` + +输出为: + +``` +HITHER +YON +``` + +因为 getEnumConstants() 是 Class 上的方法,所以你甚至可以对不是枚举的类调用此方法: + +```java +// enums/NonEnum.java +public class NonEnum { + public static void main(String[] args) { + Class intClass = Integer.class; + try { + for(Object en : intClass.getEnumConstants()) + System.out.println(en); + } catch(Exception e) { + System.out.println("Expected: " + e); + } + } +} +``` + +输出为: + +```java +Expected: java.lang.NullPointerException +``` + +只不过,此时该方法返回 null,所以当你试图使用其返回的结果时会发生异常。 + ## 实现而非继承 +我们已经知道,所有的 enum 都继承自 Java.lang.Enum 类。由于 Java 不支持多重继承,所以你的 enum 不能再继承其他类: + +```java +enum NotPossible extends Pet { ... // Won't work +``` + +然而,在我们创建一个新的 enum 时,可以同时实现一个或多个接口: + +```java +// enums/cartoons/EnumImplementation.java +// An enum can implement an interface +// {java enums.cartoons.EnumImplementation} +package enums.cartoons; +import java.util.*; +import java.util.function.*; +enum CartoonCharacter + implements Supplier { + SLAPPY, SPANKY, PUNCHY, + SILLY, BOUNCY, NUTTY, BOB; + private Random rand = + new Random(47); + @Override + public CartoonCharacter get() { + return values()[rand.nextInt(values().length)]; + } +} +public class EnumImplementation { + public static void printNext(Supplier rg) { + System.out.print(rg.get() + ", "); + } + public static void main(String[] args) { +// Choose any instance: + CartoonCharacter cc = CartoonCharacter.BOB; + for(int i = 0; i < 10; i++) + printNext(cc); + } +} +``` + +输出为: + +``` +BOB, PUNCHY, BOB, SPANKY, NUTTY, PUNCHY, SLAPPY, NUTTY, +NUTTY, SLAPPY, +``` + +这个结果有点奇怪,不过你必须要有一个 enum 实例才能调用其上的方法。现在,在任何接受 Supplier 参数的方法中,例如 printNext(),都可以使用 CartoonCharacter。 + ## 随机选择 +就像你在 CartoonCharacter.get() 中看到的那样,本章中的很多示例都需要从 enum 实例中进行随机选择。我们可以利用泛型,从而使得这个工作更一般化,并将其加入到我们的工具库中。 + +```java +// onjava/Enums.java +package onjava; +import java.util.*; +public class Enums { + private static Random rand = new Random(47); + + public static > T random(Class ec) { + return random(ec.getEnumConstants()); + } + + public static T random(T[] values) { + return values[rand.nextInt(values.length)]; + } +} +``` + +古怪的语法\\> 表示 T 是一个 enum 实例。而将 Class\ 作为参数的话,我们就可以利用 Class 对象得到 enum 实例的数组了。重载后的 random() 方法只需使用 T[] 作为参数,因为它并不会调用 Enum 上的任何操作,它只需从数组中随机选择一个元素即可。这样,最终的返回类型正是 enum 的类型。 + +下面是 random() 方法的一个简单示例: + +```java +// enums/RandomTest.java +import onjava.*; +enum Activity { SITTING, LYING, STANDING, HOPPING, + RUNNING, DODGING, JUMPING, FALLING, FLYING } + +public class RandomTest { + public static void main(String[] args) { + for(int i = 0; i < 20; i++) + System.out.print( + Enums.random(Activity.class) + " "); + } +} +``` + +输出为: + +``` +STANDING FLYING RUNNING STANDING RUNNING STANDING LYING +DODGING SITTING RUNNING HOPPING HOPPING HOPPING RUNNING +STANDING LYING FALLING RUNNING FLYING LYING +``` + + -## 使用接口组织 +## 使用接口组织枚举 + +无法从 enum 继承子类有时很令人沮丧。这种需求有时源自我们希望扩展原 enum 中的元素,有时是因为我们希望使用子类将一个 enum 中的元素进行分组。 + +在一个接口的内部,创建实现该接口的枚举,以此将元素进行分组,可以达到将枚举元素分类组织的目的。举例来说,假设你想用 enum 来表示不同类别的食物,同时还希望每个 enum 元素仍然保持 Food 类型。那可以这样实现: + +```java +// enums/menu/Food.java +// Subcategorization of enums within interfaces +package enums.menu; +public interface Food { + enum Appetizer implements Food { + SALAD, SOUP, SPRING_ROLLS; + } + enum MainCourse implements Food { + LASAGNE, BURRITO, PAD_THAI, + LENTILS, HUMMOUS, VINDALOO; + } + enum Dessert implements Food { + TIRAMISU, GELATO, BLACK_FOREST_CAKE, + FRUIT, CREME_CARAMEL; + } + enum Coffee implements Food { + BLACK_COFFEE, DECAF_COFFEE, ESPRESSO, + LATTE, CAPPUCCINO, TEA, HERB_TEA; + } +} +``` + +对于 enum 而言,实现接口是使其子类化的唯一办法,所以嵌入在 Food 中的每个 enum 都实现了 Food 接口。现在,在下面的程序中,我们可以说“所有东西都是某种类型的 Food"。 + +```java +// enums/menu/TypeOfFood.java +// {java enums.menu.TypeOfFood} +package enums.menu; +import static enums.menu.Food.*; +public class TypeOfFood { + public static void main(String[] args) { + Food food = Appetizer.SALAD; + food = MainCourse.LASAGNE; + food = Dessert.GELATO; + food = Coffee.CAPPUCCINO; + } +} +``` + +如果 enum 类型实现了 Food 接口,那么我们就可以将其实例向上转型为 Food,所以上例中的所有东西都是 Food。 + +然而,当你需要与一大堆类型打交道时,接口就不如 enum 好用了。例如,如果你想创建一个“校举的枚举”,那么可以创建一个新的 enum,然后用其实例包装 Food 中的每一个 enum 类: + +```java +// enums/menu/Course.java +package enums.menu; +import onjava.*; +public enum Course { + APPETIZER(Food.Appetizer.class), + MAINCOURSE(Food.MainCourse.class), + DESSERT(Food.Dessert.class), + COFFEE(Food.Coffee.class); + private Food[] values; + private Course(Class kind) { + values = kind.getEnumConstants(); + } + public Food randomSelection() { + return Enums.random(values); + } +} +``` + +每一个 Course 的实例都将其对应的 Class 对象作为构造器的参数。通过 getEnumConstants0 方法,可以从该 Class 对象中取得某个 Food 子类的所有 enum 实例。这些实例在 randomSelection() 中被用到。因此,通过从每一个 Course 实例中随机地选择一个 Food,我们便能够生成一份菜单: + +```java +// enums/menu/Meal.java +// {java enums.menu.Meal} +package enums.menu; +public class Meal { + public static void main(String[] args) { + for(int i = 0; i < 5; i++) { + for(Course course : Course.values()) { + Food food = course.randomSelection(); + System.out.println(food); + } + System.out.println("***"); + } + } +} +``` + +输出为: + +``` +SPRING_ROLLS +VINDALOO +FRUIT +DECAF_COFFEE +*** +SOUP +VINDALOO +FRUIT +TEA +*** +SALAD +BURRITO +FRUIT +TEA +*** +SALAD +BURRITO +CREME_CARAMEL +LATTE +*** +SOUP +BURRITO +TIRAMISU +ESPRESSO +*** +``` + +在这个例子中,我们通过遍历每一个 Course 实例来获得“枚举的枚举”的值。稍后,在 VendingMachine.java 中,我们会看到另一种组织枚举实例的方式,但其也有一些其他的限制。 + +此外,还有一种更简洁的管理枚举的办法,就是将一个 enum 嵌套在另一个 enum 内。就像这样: + +```java +// enums/SecurityCategory.java +// More succinct subcategorization of enums +import onjava.*; +enum SecurityCategory { + STOCK(Security.Stock.class), + BOND(Security.Bond.class); + Security[] values; + SecurityCategory(Class kind) { + values = kind.getEnumConstants(); + } + interface Security { + enum Stock implements Security { + SHORT, LONG, MARGIN + } + enum Bond implements Security { + MUNICIPAL, JUNK + } + } + public Security randomSelection() { + return Enums.random(values); + } + public static void main(String[] args) { + for(int i = 0; i < 10; i++) { + SecurityCategory category = + Enums.random(SecurityCategory.class); + System.out.println(category + ": " + + category.randomSelection()); + } + } +} +``` + +输出为: + +``` +BOND: MUNICIPAL +BOND: MUNICIPAL +STOCK: MARGIN +STOCK: MARGIN +BOND: JUNK +STOCK: SHORT +STOCK: LONG +STOCK: LONG +BOND: MUNICIPAL +BOND: JUNK +``` + +Security 接口的作用是将其所包含的 enum 组合成一个公共类型,这一点是有必要的。然后,SecurityCategory 才能将 Security 中的 enum 作为其构造器的参数使用,以起到组织的效果。 + +如果我们将这种方式应用于 Food 的例子,结果应该这样: + +```java +// enums/menu/Meal2.java +// {java enums.menu.Meal2} +package enums.menu; +import onjava.*; +public enum Meal2 { + APPETIZER(Food.Appetizer.class), + MAINCOURSE(Food.MainCourse.class), + DESSERT(Food.Dessert.class), + COFFEE(Food.Coffee.class); + private Food[] values; + private Meal2(Class kind) { + values = kind.getEnumConstants(); + } + public interface Food { + enum Appetizer implements Food { + SALAD, SOUP, SPRING_ROLLS; + } + enum MainCourse implements Food { + LASAGNE, BURRITO, PAD_THAI, + LENTILS, HUMMOUS, VINDALOO; + } + enum Dessert implements Food { + TIRAMISU, GELATO, BLACK_FOREST_CAKE, + FRUIT, CREME_CARAMEL; + } + enum Coffee implements Food { + BLACK_COFFEE, DECAF_COFFEE, ESPRESSO, + LATTE, CAPPUCCINO, TEA, HERB_TEA; + } + } + public Food randomSelection() { + return Enums.random(values); + } + public static void main(String[] args) { + for(int i = 0; i < 5; i++) { + for(Meal2 meal : Meal2.values()) { + Food food = meal.randomSelection(); + System.out.println(food); + } + System.out.println("***"); + } + } +} +``` + +输出为: + +``` +SPRING_ROLLS +VINDALOO +FRUIT +DECAF_COFFEE +*** +SOUP +VINDALOO +FRUIT +TEA +*** +SALAD +BURRITO +FRUIT +TEA +*** +SALAD +BURRITO +CREME_CARAMEL +LATTE +*** +SOUP +BURRITO +TIRAMISU +ESPRESSO +*** +``` + +其实,这仅仅是重新组织了一下代码,不过多数情况下,这种方式使你的代码具有更清晰的结构。 -## 使用EnumSet替代Flags +## 使用 EnumSet 替代 Flags + +Set 是一种集合,只能向其中添加不重复的对象。当然,enum 也要求其成员都是唯一的,所以 enumi 看起来也具有集合的行为。不过,由于不能从 enum 中删除或添加元素,所以它只能算是不太有用的集合。Java SE5 引入 EnumSet,是为了通过 enum 创建一种替代品,以替代传统的基于 int 的“位标志”。这种标志可以用来表示某种“开/关”信息,不过,使用这种标志,我们最终操作的只是一些 bit,而不是这些 bit 想要表达的概念,因此很容易写出令人难以理解的代码。 + +EnumSet 的设计充分考虑到了速度因素,因为它必须与非常高效的 bit 标志相竞争(其操作与 HashSet 相比,非常地快),就其内部而言,它(可能)就是将一个 long 值作为比特向量,所以 EnumSet 非常快速高效。使用 EnumSet 的优点是,它在说明一个二进制位是否存在时,具有更好的表达能力,并且无需担心性能。 + +EnumSet 中的元素必须来自一个 enum。下面的 enum 表示在一座大楼中,警报传感器的安放位置: + +```java +// enums/AlarmPoints.java +package enums; +public enum AlarmPoints { + STAIR1, STAIR2, LOBBY, OFFICE1, OFFICE2, OFFICE3, + OFFICE4, BATHROOM, UTILITY, KITCHEN +} +``` + +然后,我们用 EnumSet 来跟踪报警器的状态: + +```java +// enums/EnumSets.java +// Operations on EnumSets +// {java enums.EnumSets} +package enums; +import java.util.*; +import static enums.AlarmPoints.*; +public class EnumSets { + public static void main(String[] args) { + EnumSet points = + EnumSet.noneOf(AlarmPoints.class); // Empty + points.add(BATHROOM); + System.out.println(points); + points.addAll( + EnumSet.of(STAIR1, STAIR2, KITCHEN)); + System.out.println(points); + points = EnumSet.allOf(AlarmPoints.class); + points.removeAll( + EnumSet.of(STAIR1, STAIR2, KITCHEN)); + System.out.println(points); + points.removeAll( + EnumSet.range(OFFICE1, OFFICE4)); + System.out.println(points); + points = EnumSet.complementOf(points); + System.out.println(points); + } +} +``` + +输出为: + +```java +[BATHROOM] +[STAIR1, STAIR2, BATHROOM, KITCHEN] +[LOBBY, OFFICE1, OFFICE2, OFFICE3, OFFICE4, BATHROOM, +UTILITY] +[LOBBY, BATHROOM, UTILITY] +[STAIR1, STAIR2, OFFICE1, OFFICE2, OFFICE3, OFFICE4, +KITCHEN] +``` + +使用 static import 可以简化 enum 常量的使用。EnumSet 的方法的名字都相当直观,你可以查阅 JDK 文档找到其完整详细的描述。如果仔细研究了 EnunSet 的文档,你还会发现 of() 方法被重载了很多次,不但为可变数量参数进行了重载,而且为接收 2 至 5 个显式的参数的情况都进行了重载。这也从侧面表现了 EnumSet 对性能的关注。因为,其实只使用单独的 of() 方法解决可变参数已经可以解决整个问题了,但是对比显式的参数,会有一点性能损失。采用现在这种设计,当你只使用 2 到 5 个参数调用 of() 方法时,你可以调用对应的重载过的方法(速度稍快一点),而当你使用一个参数或多过 5 个参数时,你调用的将是使用可变参数的 of() 方法。注意,如果你只使用一个参数,编译器并不会构造可变参数的数组,所以与调用只有一个参数的方法相比,也就不会有额外的性能损耗。 + +EnumSet 的基础是 long,一个 long 值有 64 位,而一个 enum 实例只需一位 bit 表示其是否存在。 +也就是说,在不超过一个 long 的表达能力的情况下,你的 EnumSet 可以应用于最多不超过 64 个元素的 enum。如果 enum 超过了 64 个元素会发生什么呢? + +```java +// enums/BigEnumSet.java +import java.util.*; +public class BigEnumSet { + enum Big { A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, + A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, + A20, A21, A22, A23, A24, A25, A26, A27, A28, A29, + A30, A31, A32, A33, A34, A35, A36, A37, A38, A39, + A40, A41, A42, A43, A44, A45, A46, A47, A48, A49, + A50, A51, A52, A53, A54, A55, A56, A57, A58, A59, + A60, A61, A62, A63, A64, A65, A66, A67, A68, A69, + A70, A71, A72, A73, A74, A75 } + public static void main(String[] args) { + EnumSet bigEnumSet = EnumSet.allOf(Big.class); + System.out.println(bigEnumSet); + } +} +``` + +输出为: + +```java +[A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, +A13, A14, A15, A16, A17, A18, A19, A20, A21, A22, A23, +A24, A25, A26, A27, A28, A29, A30, A31, A32, A33, A34, +A35, A36, A37, A38, A39, A40, A41, A42, A43, A44, A45, +A46, A47, A48, A49, A50, A51, A52, A53, A54, A55, A56, +A57, A58, A59, A60, A61, A62, A63, A64, A65, A66, A67, +A68, A69, A70, A71, A72, A73, A74, A75] +``` + +显然,EnumSet 可以应用于多过 64 个元素的 enum,所以我猜测,Enum 会在必要的时候增加一个 long。 -## 使用EnumMap +## 使用 EnumMap + +EnumMap 是一种特殊的 Map,它要求其中的键(key)必须来自一个 enum,由于 enum 本身的限制,所以 EnumMap 在内部可由数组实现。因此 EnumMap 的速度很快,我们可以放心地使用 enum 实例在 EnumMap 中进行查找操作。不过,我们只能将 enum 的实例作为键来调用 put() 可方法,其他操作与使用一般的 Map 差不多。 + +下面的例子演示了*命令设计模式*的用法。一般来说,命令模式首先需要一个只有单一方法的接口,然后从该接口实现具有各自不同的行为的多个子类。接下来,程序员就可以构造命令对象,并在需要的时候使用它们了: + +```java +// enums/EnumMaps.java +// Basics of EnumMaps +// {java enums.EnumMaps} +package enums; +import java.util.*; +import static enums.AlarmPoints.*; +interface Command { void action(); } +public class EnumMaps { + public static void main(String[] args) { + EnumMap em = + new EnumMap<>(AlarmPoints.class); + em.put(KITCHEN, + () -> System.out.println("Kitchen fire!")); + em.put(BATHROOM, + () -> System.out.println("Bathroom alert!")); + for(Map.Entry e: + em.entrySet()) { + System.out.print(e.getKey() + ": "); + e.getValue().action(); + } + try { // If there's no value for a particular key: + em.get(UTILITY).action(); + } catch(Exception e) { + System.out.println("Expected: " + e); + } + } +} +``` + +输出为: + +``` +BATHROOM: Bathroom alert! +KITCHEN: Kitchen fire! +Expected: java.lang.NullPointerException +``` + +与 EnumSet 一样,enum 实例定义时的次序决定了其在 EnumMap 中的顺序。 + +main0 方法的最后部分说明,enum 的每个实例作为一个键,总是存在的。但是,如果你没有为这个键调用 put() 方法来存人相应的值的话,其对应的值就是 null。 + +与常量相关的方法(constant-specific methods 将在下一节中介绍)相比,EnumMap 有一个优点,那 EnumMap 允许程序员改变值对象,而常量相关的方法在编译期就被固定了。稍后你会看到,在你有多种类型的 enum,而且它们之间存在互操作的情况下,我们可以用 EnumMap 实现多路分发(multiple dispatching)。 + ## 常量特定方法 +Java 的 enum 有一个非常有趣的特性,即它允许程序员为 enum 实例编写方法,从而为每个 enum 实例赋予各自不同的行为。要实现常量相关的方法,你需要为 enum 定义一个或多个 abstract 方法,然后为每个 enum 实例实现该抽象方法。参考下面的例子: + +```java +// enums/ConstantSpecificMethod.java +import java.util.*; +import java.text.*; +public enum ConstantSpecificMethod { + DATE_TIME { + @Override + String getInfo() { + return + DateFormat.getDateInstance() + .format(new Date()); + } + }, + CLASSPATH { + @Override + String getInfo() { + return System.getenv("CLASSPATH"); + } + }, + VERSION { + @Override + String getInfo() { + return System.getProperty("java.version"); + } + }; + abstract String getInfo(); + public static void main(String[] args) { + for(ConstantSpecificMethod csm : values()) + System.out.println(csm.getInfo()); + } +} +``` + +输出为: + +```java +May 9, 2017 +C:\Users\Bruce\Documents\GitHub\on- +java\ExtractedExamples\\gradle\wrapper\gradle- +wrapper.jar +1.8.0_112 +``` + +通过相应的 enum 实例,我们可以调用其上的方法。这通常也称为表驱动的代码(table-driven code,请注意它与前面提到的命令模式的相似之处)。 + +在面向对象的程序设计中,不同的行为与不同的类关联。而通过常量相关的方法,每个 enum 实例可以具备自己独特的行为,这似乎说明每个 enum 实例就像一个独特的类。在上面的例子中,enum 实例似乎被当作其“超类”ConstantSpecificMethod 来使用,在调用 getInfo() 方法时,体现出多态的行为。 + +然而,enum 实例与类的相似之处也仅限于此了。我们并不能真的将 enum 实例作为一个类型来使用: + +```java +// enums/NotClasses.java +// {javap -c LikeClasses} +enum LikeClasses { + WINKEN { + @Override + void behavior() { + System.out.println("Behavior1"); + } + }, + BLINKEN { + @Override + void behavior() { + System.out.println("Behavior2"); + } + }, + NOD { + @Override + void behavior() { + System.out.println("Behavior3"); + } + }; + abstract void behavior(); +} +public class NotClasses { + // void f1(LikeClasses.WINKEN instance) {} // Nope +} +``` + +输出为(前 12 行): + +``` +Compiled from "NotClasses.java" +abstract class LikeClasses extends +java.lang.Enum { +public static final LikeClasses WINKEN; +public static final LikeClasses BLINKEN; +public static final LikeClasses NOD; +public static LikeClasses[] values(); +Code: +0: getstatic #2 // Field +$VALUES:[LLikeClasses; +3: invokevirtual #3 // Method +"[LLikeClasses;".clone:()Ljava/lang/Object; +... +``` + +在方法 f1() 中,编译器不允许我们将一个 enum 实例当作 class 类型。如果我们分析一下编译器生成的代码,就知道这种行为也是很正常的。因为每个 enum 元素都是一个 LikeClasses 类型的 static final 实例。 + +同时,由于它们是 static 实例,无法访问外部类的非 static 元素或方法,所以对于内部的 enum 的实例而言,其行为与一般的内部类并不相同。 + +再看一个更有趣的关于洗车的例子。每个顾客在洗车时,都有一个选择菜单,每个选择对应一个不同的动作。可以将一个常量相关的方法关联到一个选择上,再使用一个 EnumSet 来保存客户的选择: + +```java +// enums/CarWash.java +import java.util.*; +public class CarWash { + public enum Cycle { + UNDERBODY { + @Override + void action() { + System.out.println("Spraying the underbody"); + } + }, + WHEELWASH { + @Override + void action() { + System.out.println("Washing the wheels"); + } + }, + PREWASH { + @Override + void action() { + System.out.println("Loosening the dirt"); + } + }, + BASIC { + @Override + void action() { + System.out.println("The basic wash"); + } + }, + HOTWAX { + @Override + void action() { + System.out.println("Applying hot wax"); + } + }, + RINSE { + @Override + void action() { + System.out.println("Rinsing"); + } + }, + BLOWDRY { + @Override + void action() { + System.out.println("Blowing dry"); + } + }; + abstract void action(); + } + EnumSet cycles = + EnumSet.of(Cycle.BASIC, Cycle.RINSE); + public void add(Cycle cycle) { + cycles.add(cycle); + } + public void washCar() { + for(Cycle c : cycles) + c.action(); + } + @Override + public String toString() { + return cycles.toString(); + } + public static void main(String[] args) { + CarWash wash = new CarWash(); + System.out.println(wash); + wash.washCar(); +// Order of addition is unimportant: + wash.add(Cycle.BLOWDRY); + wash.add(Cycle.BLOWDRY); // Duplicates ignored + wash.add(Cycle.RINSE); + wash.add(Cycle.HOTWAX); + System.out.println(wash); + wash.washCar(); + } +} +``` + +输出为: + +``` +[BASIC, RINSE] +The basic wash +Rinsing +[BASIC, HOTWAX, RINSE, BLOWDRY] +The basic wash +Applying hot wax +Rinsing +Blowing dry +``` + +与使用匿名内部类相比较,定义常量相关方法的语法更高效、简洁。 + +这个例子也展示了 EnumSet 了一些特性。因为它是一个集合,所以对于同一个元素而言,只能出现一次,因此对同一个参数重复地调用 add0 方法会被忽略掉(这是正确的行为,因为一个 bit 位开关只能“打开”一次),同样地,向 EnumSet 添加 enum 实例的顺序并不重要,因为其输出的次序决定于 enum 实例定义时的次序。 + +除了实现 abstract 方法以外,程序员是否可以覆盖常量相关的方法呢?答案是肯定的,参考下面的程序: + +```java +// enums/OverrideConstantSpecific.java +public enum OverrideConstantSpecific { + NUT, BOLT, + WASHER { + @Override + void f() { + System.out.println("Overridden method"); + } + }; + void f() { + System.out.println("default behavior"); + } + public static void main(String[] args) { + for(OverrideConstantSpecific ocs : values()) { + System.out.print(ocs + ": "); + ocs.f(); + } + } +} +``` + +输出为: + +``` +NUT: default behavior +BOLT: default behavior +WASHER: Overridden method +``` + +虽然 enum 有某些限制,但是一般而言,我们还是可以将其看作是类。 + +### 使用 enum 的职责链 + +在职责链(Chain of Responsibility)设计模式中,程序员以多种不同的方式来解决一个问题,然后将它们链接在一起。当一个请求到来时,它遍历这个链,直到链中的某个解决方案能够处理该请求。 + +通过常量相关的方法,我们可以很容易地实现一个简单的职责链。我们以一个邮局的模型为例。邮局需要以尽可能通用的方式来处理每一封邮件,并且要不断尝试处理邮件,直到该邮件最终被确定为一封死信。其中的每一次尝试可以看作为一个策略(也是一个设计模式),而完整的处理方式列表就是一个职责链。 + +我们先来描述一下邮件。邮件的每个关键特征都可以用 enum 来表示。程序将随机地生成 Mail 对象,如果要减小一封邮件的 GeneralDelivery 为 YES 的概率,那最简单的方法就是多创建几个不是 YES 的 enum 实例,所以 enum 的定义看起来有点古怪。 + +我们看到 Mail 中有一个 randomMail() 方法,它负责随机地创建用于测试的邮件。而 generator() 方法生成一个 Iterable 对象,该对象在你调用 next() 方法时,在其内部使用 randomMail() 来创建 Mail 对象。这样的结构使程序员可以通过调用 Mail.generator() 方法,很容易地构造出一个 foreach 循环: + +```java +// enums/PostOffice.java +// Modeling a post office +import java.util.*; +import onjava.*; +class Mail { + // The NO's reduce probability of random selection: + enum GeneralDelivery {YES,NO1,NO2,NO3,NO4,NO5} + enum Scannability {UNSCANNABLE,YES1,YES2,YES3,YES4} + enum Readability {ILLEGIBLE,YES1,YES2,YES3,YES4} + enum Address {INCORRECT,OK1,OK2,OK3,OK4,OK5,OK6} + enum ReturnAddress {MISSING,OK1,OK2,OK3,OK4,OK5} + GeneralDelivery generalDelivery; + Scannability scannability; + Readability readability; + Address address; + ReturnAddress returnAddress; + static long counter = 0; + long id = counter++; + @Override + public String toString() { return "Mail " + id; } + public String details() { + return toString() + + ", General Delivery: " + generalDelivery + + ", Address Scanability: " + scannability + + ", Address Readability: " + readability + + ", Address Address: " + address + + ", Return address: " + returnAddress; + } + // Generate test Mail: + public static Mail randomMail() { + Mail m = new Mail(); + m.generalDelivery = + Enums.random(GeneralDelivery.class); + m.scannability = + Enums.random(Scannability.class); + m.readability = + Enums.random(Readability.class); + m.address = Enums.random(Address.class); + m.returnAddress = + Enums.random(ReturnAddress.class); + return m; + } + public static + Iterable generator(final int count) { + return new Iterable() { + int n = count; + @Override + public Iterator iterator() { + return new Iterator() { + @Override + public boolean hasNext() { + return n-- > 0; + } + @Override + public Mail next() { + return randomMail(); + } + @Override + public void remove() { // Not implemented + throw new UnsupportedOperationException(); + } + }; + } + }; + } +} +public class PostOffice { + enum MailHandler { + GENERAL_DELIVERY { + @Override + boolean handle(Mail m) { + switch(m.generalDelivery) { + case YES: + System.out.println( + "Using general delivery for " + m); + return true; + default: return false; + } + } + }, + MACHINE_SCAN { + @Override + boolean handle(Mail m) { + switch(m.scannability) { + case UNSCANNABLE: return false; + default: + switch(m.address) { + case INCORRECT: return false; + default: + System.out.println( + "Delivering "+ m + " automatically"); + return true; + } + } + } + }, + VISUAL_INSPECTION { + @Override + boolean handle(Mail m) { + switch(m.readability) { + case ILLEGIBLE: return false; + default: + switch(m.address) { + case INCORRECT: return false; + default: + System.out.println( + "Delivering " + m + " normally"); + return true; + } + } + } + }, + RETURN_TO_SENDER { + @Override + boolean handle(Mail m) { + switch(m.returnAddress) { + case MISSING: return false; + default: + System.out.println( + "Returning " + m + " to sender"); + return true; + } + } + }; + abstract boolean handle(Mail m); + } + static void handle(Mail m) { + for(MailHandler handler : MailHandler.values()) + if(handler.handle(m)) + return; + System.out.println(m + " is a dead letter"); + } + public static void main(String[] args) { + for(Mail mail : Mail.generator(10)) { + System.out.println(mail.details()); + handle(mail); + System.out.println("*****"); + } + } +} +``` + +输出为: + +``` +Mail 0, General Delivery: NO2, Address Scanability: +UNSCANNABLE, Address Readability: YES3, Address +Address: OK1, Return address: OK1 +Delivering Mail 0 normally +***** +Mail 1, General Delivery: NO5, Address Scanability: +YES3, Address Readability: ILLEGIBLE, Address Address: +OK5, Return address: OK1 +Delivering Mail 1 automatically +***** +Mail 2, General Delivery: YES, Address Scanability: +YES3, Address Readability: YES1, Address Address: OK1, +Return address: OK5 +Using general delivery for Mail 2 +***** +Mail 3, General Delivery: NO4, Address Scanability: +YES3, Address Readability: YES1, Address Address: +INCORRECT, Return address: OK4 +Returning Mail 3 to sender +***** +Mail 4, General Delivery: NO4, Address Scanability: +UNSCANNABLE, Address Readability: YES1, Address +Address: INCORRECT, Return address: OK2 +Returning Mail 4 to sender +***** +Mail 5, General Delivery: NO3, Address Scanability: +YES1, Address Readability: ILLEGIBLE, Address Address: +OK4, Return address: OK2 +Delivering Mail 5 automatically +***** +Mail 6, General Delivery: YES, Address Scanability: +YES4, Address Readability: ILLEGIBLE, Address Address: +OK4, Return address: OK4 +Using general delivery for Mail 6 +***** +Mail 7, General Delivery: YES, Address Scanability: +YES3, Address Readability: YES4, Address Address: OK2, +Return address: MISSING +Using general delivery for Mail 7 +***** +Mail 8, General Delivery: NO3, Address Scanability: +YES1, Address Readability: YES3, Address Address: +INCORRECT, Return address: MISSING +Mail 8 is a dead letter +***** +Mail 9, General Delivery: NO1, Address Scanability: +UNSCANNABLE, Address Readability: YES2, Address +Address: OK1, Return address: OK4 +Delivering Mail 9 normally +***** +``` + +职责链由 enum MailHandler 实现,而 enum 定义的次序决定了各个解决策略在应用时的次序。对每一封邮件,都要按此顺序尝试每个解决策略,直到其中一个能够成功地处理该邮件,如果所有的策略都失败了,那么该邮件将被判定为一封死信。 + +### 使用 enum 的状态机 + +枚举类型非常适合用来创建状态机。一个状态机可以具有有限个特定的状态,它通常根据输入,从一个状态转移到下一个状态,不过也可能存在瞬时状态(transient states),而一旦任务执行结束,状态机就会立刻离开瞬时状态。 + +每个状态都具有某些可接受的输入,不同的输入会使状态机从当前状态转移到不同的新状态。由于 enum 对其实例有严格限制,非常适合用来表现不同的状态和输入。一般而言,每个状态都具有一些相关的输出。 + +自动售贷机是一个很好的状态机的例子。首先,我们用一个 enum 定义各种输入: + +```java +// enums/Input.java +import java.util.*; +public enum Input { + NICKEL(5), DIME(10), QUARTER(25), DOLLAR(100), + TOOTHPASTE(200), CHIPS(75), SODA(100), SOAP(50), + ABORT_TRANSACTION { + @Override + public int amount() { // Disallow + throw new RuntimeException("ABORT.amount()"); + } + }, + STOP { // This must be the last instance. + @Override + public int amount() { // Disallow + throw new RuntimeException("SHUT_DOWN.amount()"); + } + }; + int value; // In cents + Input(int value) { this.value = value; } + Input() {} + int amount() { return value; }; // In cents + static Random rand = new Random(47); + public static Input randomSelection() { + // Don't include STOP: + return values()[rand.nextInt(values().length - 1)]; + } +} +``` + +注意,除了两个特殊的 Input 实例之外,其他的 Input 都有相应的价格,因此在接口中定义了 amount(方法。然而,对那两个特殊 Input 实例而言,调用 amount(方法并不合适,所以如果程序员调用它们的 amount)方法就会有异常抛出(在接口内定义了一个方法,然后在你调用该方法的某个实现时就会抛出异常),这似乎有点奇怪,但由于 enum 的限制,我们不得不采用这种方式。 + +VendingMachine 对输入的第一个反应是将其归类为 Category enum 中的某一个 enum 实例,这可以通过 switch 实现。下面的例子演示了 enum 是如何使代码变得更加清晰且易于管理的: + +```java +// enums/VendingMachine.java +// {java VendingMachine VendingMachineInput.txt} +import java.util.*; +import java.io.IOException; +import java.util.function.*; +import java.nio.file.*; +import java.util.stream.*; +enum Category { + MONEY(Input.NICKEL, Input.DIME, + Input.QUARTER, Input.DOLLAR), + ITEM_SELECTION(Input.TOOTHPASTE, Input.CHIPS, + Input.SODA, Input.SOAP), + QUIT_TRANSACTION(Input.ABORT_TRANSACTION), + SHUT_DOWN(Input.STOP); + private Input[] values; + Category(Input... types) { values = types; } + private static EnumMap categories = + new EnumMap<>(Input.class); + static { + for(Category c : Category.class.getEnumConstants()) + for(Input type : c.values) + categories.put(type, c); + } + public static Category categorize(Input input) { + return categories.get(input); + } +} + +public class VendingMachine { + private static State state = State.RESTING; + private static int amount = 0; + private static Input selection = null; + enum StateDuration { TRANSIENT } // Tagging enum + enum State { + RESTING { + @Override + void next(Input input) { + switch(Category.categorize(input)) { + case MONEY: + amount += input.amount(); + state = ADDING_MONEY; + break; + case SHUT_DOWN: + state = TERMINAL; + default: + } + } + }, + ADDING_MONEY { + @Override + void next(Input input) { + switch(Category.categorize(input)) { + case MONEY: + amount += input.amount(); + break; + case ITEM_SELECTION: + selection = input; + if(amount < selection.amount()) + System.out.println( + "Insufficient money for " + selection); + else state = DISPENSING; + break; + case QUIT_TRANSACTION: + state = GIVING_CHANGE; + break; + case SHUT_DOWN: + state = TERMINAL; + default: + } + } + }, + DISPENSING(StateDuration.TRANSIENT) { + @Override + void next() { + System.out.println("here is your " + selection); + amount -= selection.amount(); + state = GIVING_CHANGE; + } + }, + GIVING_CHANGE(StateDuration.TRANSIENT) { + @Override + void next() { + if(amount > 0) { + System.out.println("Your change: " + amount); + amount = 0; + } + state = RESTING; + } + }, + TERMINAL {@Override + void output() { System.out.println("Halted"); } }; + private boolean isTransient = false; + State() {} + State(StateDuration trans) { isTransient = true; } + void next(Input input) { + throw new RuntimeException("Only call " + + "next(Input input) for non-transient states"); + } + void next() { + throw new RuntimeException( + "Only call next() for " + + "StateDuration.TRANSIENT states"); + } + void output() { System.out.println(amount); } + } + static void run(Supplier gen) { + while(state != State.TERMINAL) { + state.next(gen.get()); + while(state.isTransient) + state.next(); + state.output(); + } + } + public static void main(String[] args) { + Supplier gen = new RandomInputSupplier(); + if(args.length == 1) + gen = new FileInputSupplier(args[0]); + run(gen); + } +} + +// For a basic sanity check: +class RandomInputSupplier implements Supplier { + @Override + public Input get() { + return Input.randomSelection(); + } +} + +// Create Inputs from a file of ';'-separated strings: +class FileInputSupplier implements Supplier { + private Iterator input; + FileInputSupplier(String fileName) { + try { + input = Files.lines(Paths.get(fileName)) + .skip(1) // Skip the comment line + .flatMap(s -> Arrays.stream(s.split(";"))) + .map(String::trim) + .collect(Collectors.toList()) + .iterator(); + } catch(IOException e) { + throw new RuntimeException(e); + } + } + @Override + public Input get() { + if(!input.hasNext()) + return null; + return Enum.valueOf(Input.class, input.next().trim()); + } +} +``` + +输出为: + +``` +25 +50 +75 +here is your CHIPS +0 +100 +200 +here is your TOOTHPASTE +0 +25 +35 +Your change: 35 +0 +25 +35 +Insufficient money for SODA +35 +60 +70 +75 +Insufficient money for SODA +75 +Your change: 75 +0 +Halted +``` + +由于用 switch 语句从 enum 实例中进行选择是最常见的一种方式(请注意,为了使 enum 在 switch 语句中的使用变得简单,我们是需要付出其他代价的),所以,我们经常遇到这样的问题:将多个 enum 进行分类时,“我们希望在什么 enum 中使用 switch 语句?”我们通过 VendingMachine 的例子来研究一下这个问题。对于每一个 State,我们都需要在输入动作的基本分类中进行查找:用户塞入钞票,选择了某个货物,操作被取消,以及机器停止。然而,在这些基本分类之下,我们又可以塞人不同类型的钞票,可以选择不同的货物。Category enum 将不同类型的 Input 进行分组,因而,可以使用 categorize0 方法为 switch 语句生成恰当的 Cateroy 实例。并且,该方法使用的 EnumMap 确保了在其中进行查询时的效率与安全。 + +如果读者仔细研究 VendingMachine 类,就会发现每种状态的不同之处,以及对于输入的不同响应,其中还有两个瞬时状态。在 run() 方法中,状态机等待着下一个 Input,并一直在各个状态中移动,直到它不再处于瞬时状态。 + +通过两种不同的 Generator 对象,我们可以使用不同的 Supplier 对象来测试 VendingMachine,首先是 RandomInputSupplier,它会不停地生成除了 SHUT-DOWN 之外的各种输入。通过长时间地运行 RandomInputSupplier,可以起到健全测试(sanity test)的作用,能够确保该状态机不会进入一个错误状态。另一个是 FileInputSupplier,使用文件以文本的方式来描述输入,然后将它们转换成 enum 实例,并创建对应的 Input 对象。上面的程序使用的正是如下的文本文件: + +``` +// enums/VendingMachineInput.txt +QUARTER; QUARTER; QUARTER; CHIPS; +DOLLAR; DOLLAR; TOOTHPASTE; +QUARTER; DIME; ABORT_TRANSACTION; +QUARTER; DIME; SODA; +QUARTER; DIME; NICKEL; SODA; +ABORT_TRANSACTION; +STOP; +``` + +FileInputSupplier 构造函数将此文件转换为流,并跳过注释行。然后它使用 String.split() 以分号进行分割。这会生成一个 String 数组,并可以通过将其转换为 Stream,然后应用 flatMap() 来将其输入到流中。其输出结果将去除所有空格空格,并转换为 List\,且从中获取 Iterator\。 + +这种设计有一个缺陷,它要求 enum State 实例访问的 VendingMachine 属性必须声明为 static,这意味着,你只能有一个 VendingMachine 实例。不过如果我们思考一下实际的(嵌入式 Java)应用,这也许并不是一个大问题,因为在一台机器上,我们可能只有一个应用程序。 -## 多次调度 +## 多路分发 + +当你要处理多种交互类型时,程序可能会变得相当杂乱。举例来说,如果一个系统要分析和执行数学表达式。我们可能会声明 Number.plus(Number),Number.multiple(Number) 等等,其中 Number 是各种数字对象的超类。然而,当你声明 a.plus(b) 时,你并不知道 a 或 b 的确切类型,那你如何能让它们正确地交互呢? + +你可能从未思考过这个问题的答案.Java 只支持单路分发。也就是说,如果要执行的操作包含了不止一个类型未知的对象时,那么 Java 的动态绑定机制只能处理其中一个的类型。这就无法解决我们上面提到的问题。所以,你必须自己来判定其他的类型,从而实现自己的动态线定行为。 + +解决上面问题的办法就是多路分发(在那个例子中,只有两个分发,一般称之为两路分发).多态只能发生在方法调用时,所以,如果你想使用两路分发,那么就必须有两个方法调用:第一个方法调用决定第一个未知类型,第二个方法调用决定第二个未知的类型。要利用多路分发,程序员必须为每一个类型提供一个实际的方法调用,如果你要处理两个不同的类型体系,就需要为每个类型体系执行一个方法调用。一般而言,程序员需要有设定好的某种配置,以便一个方法调用能够引出更多的方法调用,从而能够在这个过程中处理多种类型。为了达到这种效果,我们需要与多个方法一同工作:因为每个分发都需要一个方法调用。在下面的例子中(实现了 “石头、剪刀、布”游戏,也称为 RoShamBo)对应的方法是 compete() 和 eval(),二者都是同一个类型的成员,它们可以产生三种 Outcome 实例中的一个作为结果: + +```java +// enums/Outcome.java +package enums; +public enum Outcome { WIN, LOSE, DRAW } +// enums/RoShamBo1.java +// Demonstration of multiple dispatching +// {java enums.RoShamBo1} +package enums; + import java.util.*; + import static enums.Outcome.*; +interface Item { + Outcome compete(Item it); + Outcome eval(Paper p); + Outcome eval(Scissors s); + Outcome eval(Rock r); +} +class Paper implements Item { + @Override + public Outcome compete(Item it) { + return it.eval(this); + } + @Override + public Outcome eval(Paper p) { return DRAW; } + @Override + public Outcome eval(Scissors s) { return WIN; } + @Override + public Outcome eval(Rock r) { return LOSE; } + @Override + public String toString() { return "Paper"; } +} +class Scissors implements Item { + @Override + public Outcome compete(Item it) { + return it.eval(this); + } + @Override + public Outcome eval(Paper p) { return LOSE; } + @Override + public Outcome eval(Scissors s) { return DRAW; } + @Override + public Outcome eval(Rock r) { return WIN; } + @Override + public String toString() { return "Scissors"; } +} +class Rock implements Item { + @Override + public Outcome compete(Item it) { + return it.eval(this); + } + @Override + public Outcome eval(Paper p) { return WIN; } + @Override + public Outcome eval(Scissors s) { return LOSE; } + @Override + public Outcome eval(Rock r) { return DRAW; } + @Override + public String toString() { return "Rock"; } +} +public class RoShamBo1 { + static final int SIZE = 20; + private static Random rand = new Random(47); + public static Item newItem() { + switch(rand.nextInt(3)) { + default: + case 0: return new Scissors(); + case 1: return new Paper(); + case 2: return new Rock(); + } + } + public static void match(Item a, Item b) { + System.out.println( + a + " vs. " + b + ": " + a.compete(b)); + } + public static void main(String[] args) { + for(int i = 0; i < SIZE; i++) + match(newItem(), newItem()); + } +} +``` + +输出为: + +``` +Rock vs. Rock: DRAW +Paper vs. Rock: WIN +Paper vs. Rock: WIN +Paper vs. Rock: WIN +Scissors vs. Paper: WIN +Scissors vs. Scissors: DRAW +Scissors vs. Paper: WIN +Rock vs. Paper: LOSE +Paper vs. Paper: DRAW +Rock vs. Paper: LOSE +Paper vs. Scissors: LOSE +Paper vs. Scissors: LOSE +Rock vs. Scissors: WIN +Rock vs. Paper: LOSE +Paper vs. Rock: WIN +Scissors vs. Paper: WIN +Paper vs. Scissors: LOSE +Paper vs. Scissors: LOSE +Paper vs. Scissors: LOSE +Paper vs. Scissors: LOSE +``` + +Item 是这几种类型的接口,将会被用作多路分发。RoShamBo1.match() 有两个 Item 参数,通过调用 Item.compete90) 方法开始两路分发。要判定 a 的类型,分发机制会在 a 的实际类型的 compete(内部起到分发的作用。compete() 方法通过调用 eval() 来为另一个类型实现第二次分法。 + +将自身(this)作为参数调用 evalo,能够调用重载过的 eval() 方法,这能够保留第一次分发的类型信息。当第二次分发完成时,你就能够知道两个 Item 对象的具体类型了。 + +要配置好多路分发需要很多的工序,不过要记住,它的好处在于方法调用时的优雅的话法,这避免了在一个方法中判定多个对象的类型的丑陋代码,你只需说,“嘿,你们两个,我不在乎你们是什么类型,请你们自己交流!”不过,在使用多路分发前,请先明确,这种优雅的代码对你确实有重要的意义。 + +### 使用 enum 分发 + +直接将 RoShamBol.java 翻译为基于 enum 的版本是有问题的,因为 enum 实例不是类型,不能将 enum 实例作为参数的类型,所以无法重载 eval() 方法。不过,还有很多方式可以实现多路分发,并从 enum 中获益。 + +一种方式是使用构造器来初始化每个 enum 实例,并以“一组”结果作为参数。这二者放在一块,形成了类似查询表的结构: + +```java +// enums/RoShamBo2.java +// Switching one enum on another +// {java enums.RoShamBo2} +package enums; +import static enums.Outcome.*; +public enum RoShamBo2 implements Competitor { + PAPER(DRAW, LOSE, WIN), + SCISSORS(WIN, DRAW, LOSE), + ROCK(LOSE, WIN, DRAW); + private Outcome vPAPER, vSCISSORS, vROCK; + RoShamBo2(Outcome paper, + Outcome scissors, Outcome rock) { + this.vPAPER = paper; + this.vSCISSORS = scissors; + this.vROCK = rock; + } + @Override + public Outcome compete(RoShamBo2 it) { + switch(it) { + default: + case PAPER: return vPAPER; + case SCISSORS: return vSCISSORS; + case ROCK: return vROCK; + } + } + public static void main(String[] args) { + RoShamBo.play(RoShamBo2.class, 20); + } +} +``` + +输出为: + +``` +ROCK vs. ROCK: DRAW +SCISSORS vs. ROCK: LOSE +SCISSORS vs. ROCK: LOSE +SCISSORS vs. ROCK: LOSE +PAPER vs. SCISSORS: LOSE +PAPER vs. PAPER: DRAW +PAPER vs. SCISSORS: LOSE +ROCK vs. SCISSORS: WIN +SCISSORS vs. SCISSORS: DRAW +ROCK vs. SCISSORS: WIN +SCISSORS vs. PAPER: WIN +SCISSORS vs. PAPER: WIN +ROCK vs. PAPER: LOSE +ROCK vs. SCISSORS: WIN +SCISSORS vs. ROCK: LOSE +PAPER vs. SCISSORS: LOSE +SCISSORS vs. PAPER: WIN +SCISSORS vs. PAPER: WIN +SCISSORS vs. PAPER: WIN +SCISSORS vs. PAPER: WIN +``` + +在 compete() 方法中,一旦两种类型都被确定了,那么唯一的操作就是返回结果 Outcome 然而,你可能还需要调用其他的方法,(例如)甚至是调用在构造器中指定的某个命令对象上的方法。 + +RoShamBo2.javal 之前的例子短小得多,而且更直接,更易于理解。注意,我们仍然是使用两路分发来判定两个对象的类型。在 RoShamBol.java 中,两次分发都是通过实际的方法调用实现,而在这个例子中,只有第一次分发是实际的方法调用。第二个分发使用的是 switch,不过这样做是安全的,因为 enum 限制了 switch 语句的选择分支。 + +在代码中,enum 被单独抽取出来,因此它可以应用在其他例子中。首先,Competitor 接口定义了一种类型,该类型的对象可以与另一个 Competitor 相竞争: + +```java +// enums/Competitor.java +// Switching one enum on another +package enums; +public interface Competitor> { + Outcome compete(T competitor); +} +``` + +然后,我们定义两个 static 方法(static 可以避免显式地指明参数类型),第一个是 match() 方法,它会为一个 Competitor 对象调用 compete() 方法,并与另一个 Competitor 对象作比较。在这个例子中,我们看到,match())方法的参数需要是 Competitor\ 类型。但是在 play() 方法中,类型参数必须同时是 Enum\ 类型(因为它将在 Enums.random() 中使用)和 Competitor\ 类型因为它将被传递给 match() 方法): + +```java +// enums/RoShamBo.java +// Common tools for RoShamBo examples +package enums; +import onjava.*; +public class RoShamBo { + public static > + void match(T a, T b) { + System.out.println( + a + " vs. " + b + ": " + a.compete(b)); + } + public static & Competitor> + void play(Class rsbClass, int size) { + for(int i = 0; i < size; i++) + match(Enums.random(rsbClass),Enums.random(rsbClass)); + } +} +``` + +play() 方法没有将类型参数 T 作为返回值类型,因此,似乎我们应该在 Class\ 中使用通配符来代替上面的参数声明。然而,通配符不能扩展多个基类,所以我们必须采用以上的表达式。 + +### 使用常量相关的方法 + +常量相关的方法允许我们为每个 enum 实例提供方法的不同实现,这使得常量相关的方法似乎是实现多路分发的完美解决方案。不过,通过这种方式,enum 实例虽然可以具有不同的行为,但它们仍然不是类型,不能将其作为方法签名中的参数类型来使用。最好的办法是将 enum 用在 switch 语句中,见下例: + +```java +// enums/RoShamBo3.java +// Using constant-specific methods +// {java enums.RoShamBo3} +package enums; +import static enums.Outcome.*; +public enum RoShamBo3 implements Competitor { + PAPER { + @Override + public Outcome compete(RoShamBo3 it) { + switch(it) { + default: // To placate the compiler + case PAPER: return DRAW; + case SCISSORS: return LOSE; + case ROCK: return WIN; + } + } + }, + SCISSORS { + @Override + public Outcome compete(RoShamBo3 it) { + switch(it) { + default: + case PAPER: return WIN; + case SCISSORS: return DRAW; + case ROCK: return LOSE; + } + } + }, + ROCK { + @Override + public Outcome compete(RoShamBo3 it) { + switch(it) { + default: + case PAPER: return LOSE; + case SCISSORS: return WIN; + case ROCK: return DRAW; + } + } + }; + @Override + public abstract Outcome compete(RoShamBo3 it); + public static void main(String[] args) { + RoShamBo.play(RoShamBo3.class, 20); + } +} +``` + +输出为: + +``` +ROCK vs. ROCK: DRAW +SCISSORS vs. ROCK: LOSE +SCISSORS vs. ROCK: LOSE +SCISSORS vs. ROCK: LOSE +PAPER vs. SCISSORS: LOSE +PAPER vs. PAPER: DRAW +PAPER vs. SCISSORS: LOSE +ROCK vs. SCISSORS: WIN +SCISSORS vs. SCISSORS: DRAW +ROCK vs. SCISSORS: WIN +SCISSORS vs. PAPER: WIN +SCISSORS vs. PAPER: WIN +ROCK vs. PAPER: LOSE +ROCK vs. SCISSORS: WIN +SCISSORS vs. ROCK: LOSE +PAPER vs. SCISSORS: LOSE +SCISSORS vs. PAPER: WIN +SCISSORS vs. PAPER: WIN +SCISSORS vs. PAPER: WIN +SCISSORS vs. PAPER: WIN +``` + +虽然这种方式可以工作,但是却不甚合理,如果采用 RoShamB02.java 的解决方案,那么在添加一个新的类型时,只需更少的代码,而且也更直接。 + +:然而,RoShamBo3.java 还可以压缩简化一下: + +```java +// enums/RoShamBo4.java +// {java enums.RoShamBo4} +package enums; +public enum RoShamBo4 implements Competitor { + ROCK { + @Override + public Outcome compete(RoShamBo4 opponent) { + return compete(SCISSORS, opponent); + } + }, + SCISSORS { + @Override + public Outcome compete(RoShamBo4 opponent) { + return compete(PAPER, opponent); + } + }, + PAPER { + @Override + public Outcome compete(RoShamBo4 opponent) { + return compete(ROCK, opponent); + } + }; + Outcome compete(RoShamBo4 loser, RoShamBo4 opponent) { + return ((opponent == this) ? Outcome.DRAW + : ((opponent == loser) ? Outcome.WIN + : Outcome.LOSE)); + } + public static void main(String[] args) { + RoShamBo.play(RoShamBo4.class, 20); + } +} +``` + +输出为: + +``` +PAPER vs. PAPER: DRAW +SCISSORS vs. PAPER: WIN +SCISSORS vs. PAPER: WIN +SCISSORS vs. PAPER: WIN +ROCK vs. SCISSORS: WIN +ROCK vs. ROCK: DRAW +ROCK vs. SCISSORS: WIN +PAPER vs. SCISSORS: LOSE +SCISSORS vs. SCISSORS: DRAW +PAPER vs. SCISSORS: LOSE +SCISSORS vs. ROCK: LOSE +SCISSORS vs. ROCK: LOSE +PAPER vs. ROCK: WIN +PAPER vs. SCISSORS: LOSE +SCISSORS vs. PAPER: WIN +ROCK vs. SCISSORS: WIN +SCISSORS vs. ROCK: LOSE +SCISSORS vs. ROCK: LOSE +SCISSORS vs. ROCK: LOSE +SCISSORS vs. ROCK: LOSE +``` + +其中,具有两个参数的 compete() 方法执行第二个分发,该方法执行一系列的比较,其行为类似 switch 语句。这个版本的程序更简短,不过却比较难理解,对于一个大型系统而言,难以理解的代码将导致整个系统不够健壮。 + +### 使用 EnumMap 进行分发 + +使用 EnumMap 能够实现“真正的”两路分发。EnumMap 是为 enum 专门设计的一种性能非常好的特殊 Map。由于我们的目的是摸索出两种未知的类型,所以可以用一个 EnumMap 的 EnumMap 来实现两路分发: + +```java +// enums/RoShamBo5.java +// Multiple dispatching using an EnumMap of EnumMaps +// {java enums.RoShamBo5} +package enums; +import java.util.*; +import static enums.Outcome.*; +enum RoShamBo5 implements Competitor { + PAPER, SCISSORS, ROCK; + static EnumMap> + table = new EnumMap<>(RoShamBo5.class); + static { + for(RoShamBo5 it : RoShamBo5.values()) + table.put(it, new EnumMap<>(RoShamBo5.class)); + initRow(PAPER, DRAW, LOSE, WIN); + initRow(SCISSORS, WIN, DRAW, LOSE); + initRow(ROCK, LOSE, WIN, DRAW); + } + static void initRow(RoShamBo5 it, + Outcome vPAPER, Outcome vSCISSORS, Outcome vROCK) { + EnumMap row = + RoShamBo5.table.get(it); + row.put(RoShamBo5.PAPER, vPAPER); + row.put(RoShamBo5.SCISSORS, vSCISSORS); + row.put(RoShamBo5.ROCK, vROCK); + } + @Override + public Outcome compete(RoShamBo5 it) { + return table.get(this).get(it); + } + public static void main(String[] args) { + RoShamBo.play(RoShamBo5.class, 20); + } +} +``` + +输出为: + +``` +ROCK vs. ROCK: DRAW +SCISSORS vs. ROCK: LOSE +SCISSORS vs. ROCK: LOSE +SCISSORS vs. ROCK: LOSE +PAPER vs. SCISSORS: LOSE +PAPER vs. PAPER: DRAW +PAPER vs. SCISSORS: LOSE +ROCK vs. SCISSORS: WIN +SCISSORS vs. SCISSORS: DRAW +ROCK vs. SCISSORS: WIN +SCISSORS vs. PAPER: WIN +SCISSORS vs. PAPER: WIN +ROCK vs. PAPER: LOSE +ROCK vs. SCISSORS: WIN +SCISSORS vs. ROCK: LOSE +PAPER vs. SCISSORS: LOSE +SCISSORS vs. PAPER: WIN +SCISSORS vs. PAPER: WIN +SCISSORS vs. PAPER: WIN +SCISSORS vs. PAPER: WIN +``` + +该程序在一个 static 子句中初始化 EnumMap 对象,具体见表格似的 initRow() 方法调用。请注意 compete() 方法,您可以看到,在一行语句中发生了两次分发。 + +### 使用二维数组 + +我们还可以进一步简化实现两路分发的解决方案。我们注意到,每个 enum 实例都有一个固定的值(基于其声明的次序),并且可以通过 ordinal() 方法取得该值。因此我们可以使用二维数组,将竞争者映射到竞争结果。采用这种方式能够获得最简洁、最直接的解决方案(很可能也是最快速的,虽然我们知道 EnumMap 内部其实也是使用数组实现的)。 + +```java +We can simplify the solution even more by noting that each enum instance has a fixed + value (based on its declaration order) and that ordinal() produces this value. A two- + dimensional array mapping the competitors onto the outcomes produces the smallest + and most straightforward solution (and possibly the fastest, although remember that + EnumMap uses an internal array): +// enums/RoShamBo6.java +// Enums using "tables" instead of multiple dispatch +// {java enums.RoShamBo6} + package enums; + import static enums.Outcome.*; +enum RoShamBo6 implements Competitor { + PAPER, SCISSORS, ROCK; + private static Outcome[][] table = { + { DRAW, LOSE, WIN }, // PAPER + { WIN, DRAW, LOSE }, // SCISSORS + { LOSE, WIN, DRAW }, // ROCK + }; + @Override + public Outcome compete(RoShamBo6 other) { + return table[this.ordinal()][other.ordinal()]; + } + public static void main(String[] args) { + RoShamBo.play(RoShamBo6.class, 20); + } +} +``` + +输出为: + +``` +ROCK vs. ROCK: DRAW +SCISSORS vs. ROCK: LOSE +SCISSORS vs. ROCK: LOSE +SCISSORS vs. ROCK: LOSE +PAPER vs. SCISSORS: LOSE +PAPER vs. PAPER: DRAW +PAPER vs. SCISSORS: LOSE +ROCK vs. SCISSORS: WIN +SCISSORS vs. SCISSORS: DRAW +ROCK vs. SCISSORS: WIN +SCISSORS vs. PAPER: WIN +SCISSORS vs. PAPER: WIN +ROCK vs. PAPER: LOSE +ROCK vs. SCISSORS: WIN +SCISSORS vs. ROCK: LOSE +PAPER vs. SCISSORS: LOSE +SCISSORS vs. PAPER: WIN +SCISSORS vs. PAPER: WIN +SCISSORS vs. PAPER: WIN +SCISSORS vs. PAPER: WIN +``` + +table 与前一个例子中 initRow() 方法的调用次序完全相同。 + +与前面一个例子相比,这个程序代码虽然简短,但表达能力却更强,部分原因是其代码更易于理解与修改,而且也更直接。不过,由于它使用的是数组,所以这种方式不太“安全”。如果使用一个大型数组,可能会不小心使用了错误的尺寸,而且,如果你的测试不能覆盖所有的可能性,有些错误可能会从你眼前溜过。 + +事实上,以上所有的解决方案只是各种不同类型的表罢了。不过,分析各种表的表现形式,找出最适合的那一种,还是很有价值的。注意,虽然上例是最简洁的一种解决方案,但它也是相当僵硬的方案,因为它只能针对给定的常量输入产生常量输出。然而,也没有什么特别的理由阻止你用 table 来生成功能对象。对于某类问题而言,“表驱动式编码”的概念具有非常强大的功能。 + ## 本章小结 +虽然枚举类型本身并不是特别复杂,但我还是将本章安排在全书比较靠后的位置,这是因为,程序员可以将 enum 与 Java 语言的其他功能结合使用,例如多态、泛型和反射。 + +虽然 Java 中的枚举比 C 或 C++中的 enum 更成熟,但它仍然是一个“小”功能,Java 没有它也已经(虽然有点笨拙)存在很多年了。而本章正好说明了一个“小”功能所能带来的价值。有时恰恰因为它,你才能够优雅而干净地解决问题。正如我在本书中一再强调的那样,优雅与清晰很重要,正是它们区别了成功的解决方案与失败的解决方案。而失败的解决方案就是因为其他人无法理解它。 + +关于清晰的话题,Java 1.0 对术语 enumeration 的选择正是一个不幸的反例。对于一个专门用于从序列中选择每一个元素的对象而言,Java 竟然没有使用更通用、更普遍接受的术语 ierator 来表示它(参见[集合 ]() 章节),有些语言甚至将枚举的数据类型称为 “enumerators”!Java 修正了这个错误,但是 Enumeration 接口已经无法轻易地抹去了,因此它将一直存在于旧的(甚至有些新的)代码、类库以及文档中。 + diff --git a/docs/book/25-Patterns.md b/docs/book/25-Patterns.md index 9ae1f0d..345e46d 100644 --- a/docs/book/25-Patterns.md +++ b/docs/book/25-Patterns.md @@ -6,10 +6,102 @@ ## 概念 +最初,你可以将模式视为解决特定类问题的一种特别巧妙且有深刻见解的方法。这就像前辈已经从所有角度去解决问题,并提出了最通用,最灵活的解决方案。问题可能是你之前看到并解决过的问题,但你的解决方案可能没有你在模式中体现的那种完整性。 +虽然它们被称为“设计模式”,但它们实际上并不与设计领域相关联。模式似乎与传统的分析、设计和实现的思维方式不同。相反,模式在程序中体现了一个完整的思想,因此它有时会出现在分析阶段或高级设计阶段。因为模式在代码中有一个直接的实现,所以你可能不会期望模式在低级设计或实现之前出现(而且通常在到达这些阶段之前,你不会意识到需要一个特定的模式)。 + +模式的基本概念也可以看作是程序设计的基本概念:添加抽象层。当你抽象一些东西的时候,就像在剥离特定的细节,而这背后最重要的动机之一是: +> **将易变的事物与不变的事物分开** + +另一种方法是,一旦你发现程序的某些部分可能因某种原因而发生变化,你要保持这些变化不会引起整个代码中其他变化。 如果代码更容易理解,那么维护起来会更容易。 + +通常,开发一个优雅且易维护设计中最困难的部分是发现我称之为变化的载体(也就是最易改变的地方)。这意味着找到系统中最重要的变化,换而言之,找到变化会导致最严重后果的地方。一旦发现变化载体,就可以围绕构建设计的焦点。 + +因此,设计模式的目标是隔离代码中的更改。 如果以这种方式去看,你已经在本书中看到了设计模式。 例如,继承可以被认为是一种设计模式(虽然是由编译器实现的)。它允许你表达所有具有相同接口的对象(即保持相同的行为)中的行为差异(这就是变化的部分)。组合也可以被视为一种模式,因为它允许你动态或静态地更改实现类的对象,从而改变类的工作方式。 + +你还看到了设计模式中出现的另一种模式:迭代器(Java 1.0和1.1随意地将其称为枚举; Java 2 集合才使用Iterator)。当你逐个选择元素时并逐步处理,这会隐藏集合的特定实现。迭代器允许你编写通用代码,该代码对序列中的所有元素执行操作,而不考虑序列的构建方式。因此,你的通用代码可以与任何可以生成迭代器的集合一起使用。 + +即使模式是非常有用的,但有些人断言: +> **设计模式代表语言的失败。** + +这是一个非常重要的见解,因为一个模式在 C++ 有意义,可能在JAVA或者其他语言中就没有意义。出于这个原因,所以一个模式可能出现在设计模式书上,不意味着应用于你的编程语言是有用的。 + +我认为“语言失败”这个观点是有道理的,但是我也认为这个观点过于简单化。如果你试图解决一个特定的问题,而你使用的语言没有直接提供支持你使用的技巧,你可以说这个是语言的失败。但是,你使用特定的技巧的频率的是多少呢?也许平衡是对的:当你使用特定的技巧的时候,你必须付出更多的努力,但是你又没有足够的理由去使得语言支持这个技术。另一方面,没有语言的支持,使用这种技术常常会很混乱,但是在语言支持下,你可能会改变编程方式(例如,Java 8流实现此目的)。 + +### 单例模式 +也许单例模式是最简单的设计模式,它是一种提供一个且只有一个对象实例的方法。这在java库中使用,但是这有个更直接的示例: + +```java +// patterns/SingletonPattern.java +interface Resource { + int getValue(); + void setValue(int x); +} + +/* +* 由于这不是从Cloneable基类继承而且没有添加可克隆性, +* 因此将其设置为final可防止通过继承添加可克隆性。 +* 这也实现了线程安全的延迟初始化: +*/ +final class Singleton { + private static final class ResourceImpl implements Resource { + private int i; + private ResourceImpl(int i) { + this.i = i; + } + public synchronized int getValue() { + return i; + } + public synchronized void setValue(int x) { + i = x; + } + } + + private static class ResourceHolder { + private static Resource resource = new ResourceImpl(47); + } + public static Resource getResource() { + return ResourceHolder.resource; + } +} + +public class SingletonPattern { + public static void main(String[] args) { + Resource r = Singleton.getResource(); + System.out.println(r.getValue()); + Resource s2 = Singleton.getResource(); + s2.setValue(9); + System.out.println(r.getValue()); + try { + // 不能这么做,会发生:compile-time error(编译时错误). + // Singleton s3 = (Singleton)s2.clone(); + } catch(Exception e) { + throw new RuntimeException(e); + } + } +} /* Output: 47 9 */ +``` +创建单例的关键是防止客户端程序员直接创建对象。 在这里,这是通过在Singleton类中将Resource的实现作为私有类来实现的。 + +此时,你将决定如何创建对象。在这里,它是按需创建的,在第一次访问的时候创建。 该对象是私有的,只能通过public getResource()方法访问。 + + +懒惰地创建对象的原因是它嵌套的私有类resourceHolder在首次引用之前不会加载(在getResource()中)。当Resource对象加载的时候,静态初始化块将被调用。由于JVM的工作方式,这种静态初始化是线程安全的。为保证线程安全,Resource中的getter和setter是同步的。 + +### 模式分类 + +“设计模式”一书讨论了23种不同的模式,分为以下三种类别(所有这些模式都围绕着可能变化的特定方面)。 + +1. **创建型**:如何创建对象。 这通常涉及隔离对象创建的细节,这样你的代码就不依赖于具体的对象的类型,因此在添加新类型的对象时不会更改。单例模式(Singleton)被归类为创作模式,本章稍后你将看到Factory Method的示例。 + +2. **构造型**:设计对象以满足特定的项目约束。它们处理对象与其他对象连接的方式,以确保系统中的更改不需要更改这些连接。 + +3. **行为型**:处理程序中特定类型的操作的对象。这些封装要执行的过程,例如解释语言、实现请求、遍历序列(如在迭代器中)或实现算法。本章包含观察者和访问者模式的例子。 + +《设计模式》一书中每个设计模式都有单独的一个章节,每个章节都有一个或者多个例子,通常使用C++,但有时也使用SmallTalk。 本章不重复设计模式中显示的所有模式,因为该书独立存在,应单独研究。 相反,你会看到一些示例,可以为你提供关于模式的理解以及它们如此重要的原因。 -## 构建型 +## 构建应用程序框架 diff --git a/docs/book/Appendix-Collection-Topics.md b/docs/book/Appendix-Collection-Topics.md index f1b3dcc..22ce7a6 100644 --- a/docs/book/Appendix-Collection-Topics.md +++ b/docs/book/Appendix-Collection-Topics.md @@ -246,13 +246,13 @@ public class HTMLColors { } ``` -**MAP** 是使用Streams([第十四章 流式编程]())创建的。 二维数组 **ARRAY** 作为流传输到 **Map** 中,但请注意我们不仅仅是使用简单版本的 **Collectors.toMap()** 。 那个版本生成一个 **HashMap** ,它使用散列函数来控制对键的排序。 为了保留原来的顺序,我们必须将键值对直接放入 **TreeMap** 中,这意味着我们需要使用更复杂的 **Collectors.toMap()** 版本。这需要两个函数从每个流元素中提取键和值,就像简单版本的**Collectors.toMap()** 一样。 然后它需要一个*合并函数*(merge function),它解决了与同一个键相关的两个值之间的冲突。这里的数据已经预先审查过,因此绝不会发生这种情况,如果有的话,这里会抛出异常。最后,传递生成所需类型的空map的函数,然后用流来填充它。 +**MAP** 是使用Streams([第十四章 流式编程]())创建的。 二维数组 **ARRAY** 作为流传输到 **Map** 中,但请注意我们不仅仅是使用简单版本的 `Collectors.toMap()` 。 那个版本生成一个 **HashMap** ,它使用散列函数来控制对键的排序。 为了保留原来的顺序,我们必须将键值对直接放入 **TreeMap** 中,这意味着我们需要使用更复杂的 `Collectors.toMap()` 版本。这需要两个函数从每个流元素中提取键和值,就像简单版本的`Collectors.toMap()` 一样。 然后它需要一个*合并函数*(merge function),它解决了与同一个键相关的两个值之间的冲突。这里的数据已经预先审查过,因此绝不会发生这种情况,如果有的话,这里会抛出异常。最后,传递生成所需类型的空map的函数,然后用流来填充它。 -**rgb()** 方法是一个便捷函数(convenience function),它接受颜色名称 **String** 参数并生成其数字RGB值。为此,我们需要一个反转版本的 **COLORS** ,它接受一个 **String**键并查找RGB的 **Integer** 值。 这是通过 **invert()** 方法实现的,如果任何 **COLORS** 值不唯一,则抛出异常。 +`rgb()` 方法是一个便捷函数(convenience function),它接受颜色名称 **String** 参数并生成其数字RGB值。为此,我们需要一个反转版本的 **COLORS** ,它接受一个 **String**键并查找RGB的 **Integer** 值。 这是通过 `invert()` 方法实现的,如果任何 **COLORS** 值不唯一,则抛出异常。 我们还创建包含所有名称的 **LIST** ,以及包含十六进制表示法的RGB值的 **RGBLIST** 。 -第一个 **show()** 方法接受一个 **Map.Entry** 并显示以十六进制表示的键,以便轻松地对原始 **ARRAY** 进行双重检查。 名称以 **show** 开头的每个方法都会重载两个版本,其中一个版本采用 **count** 参数来指示要显示的元素数量,第二个版本显示序列中的所有元素。 +第一个 `show()` 方法接受一个 **Map.Entry** 并显示以十六进制表示的键,以便轻松地对原始 **ARRAY** 进行双重检查。 名称以 **show** 开头的每个方法都会重载两个版本,其中一个版本采用 **count** 参数来指示要显示的元素数量,第二个版本显示序列中的所有元素。 这里是一个基本的测试: @@ -362,69 +362,2874 @@ Cyan 可以看到,使用 **LinkedHashMap** 确实能够保留 **HTMLColors.ARRAY** 的顺序。 -## List表现 +## List行为 +**Lists** 是存储和检索对象(次于数组)的最基本方法。基本列表操作包括: + +- `add()` 用于插入元素 +- `get()` 用于随机访问元素 +- `iterator()` 获取序列上的一个 **Iterator** +- `stream()` 生成元素的一个 **Stream** + +列表构造方法始终保留元素的添加顺序。 + +以下示例中的方法各自涵盖了一组不同的行为:每个 **List** 可以执行的操作( `basicTest()` ),使用 **Iterator** ( `iterMotion()` )遍历序列,使用 **Iterator** ( `iterManipulation()` )更改内容,查看 **List** 操作( `testVisual()` )的效果,以及仅可用于 **LinkedLists** 的操作: + +```java +// collectiontopics/ListOps.java +// Things you can do with Lists +import java.util.*; +import onjava.HTMLColors; + +public class ListOps { + // Create a short list for testing: + static final List LIST = + HTMLColors.LIST.subList(0, 10); + private static boolean b; + private static String s; + private static int i; + private static Iterator it; + private static ListIterator lit; + public static void basicTest(List a) { + a.add(1, "x"); // Add at location 1 + a.add("x"); // Add at end + // Add a collection: + a.addAll(LIST); + // Add a collection starting at location 3: + a.addAll(3, LIST); + b = a.contains("1"); // Is it in there? + // Is the entire collection in there? + b = a.containsAll(LIST); + // Lists allow random access, which is cheap + // for ArrayList, expensive for LinkedList: + s = a.get(1); // Get (typed) object at location 1 + i = a.indexOf("1"); // Tell index of object + b = a.isEmpty(); // Any elements inside? + it = a.iterator(); // Ordinary Iterator + lit = a.listIterator(); // ListIterator + lit = a.listIterator(3); // Start at location 3 + i = a.lastIndexOf("1"); // Last match + a.remove(1); // Remove location 1 + a.remove("3"); // Remove this object + a.set(1, "y"); // Set location 1 to "y" + // Keep everything that's in the argument + // (the intersection of the two sets): + a.retainAll(LIST); + // Remove everything that's in the argument: + a.removeAll(LIST); + i = a.size(); // How big is it? + a.clear(); // Remove all elements + } + public static void iterMotion(List a) { + ListIterator it = a.listIterator(); + b = it.hasNext(); + b = it.hasPrevious(); + s = it.next(); + i = it.nextIndex(); + s = it.previous(); + i = it.previousIndex(); + } + public static void iterManipulation(List a) { + ListIterator it = a.listIterator(); + it.add("47"); + // Must move to an element after add(): + it.next(); + // Remove the element after the new one: + it.remove(); + // Must move to an element after remove(): + it.next(); + // Change the element after the deleted one: + it.set("47"); + } + public static void testVisual(List a) { + System.out.println(a); + List b = LIST; + System.out.println("b = " + b); + a.addAll(b); + a.addAll(b); + System.out.println(a); + // Insert, remove, and replace elements + // using a ListIterator: + ListIterator x = + a.listIterator(a.size()/2); + x.add("one"); + System.out.println(a); + System.out.println(x.next()); + x.remove(); + System.out.println(x.next()); + x.set("47"); + System.out.println(a); + // Traverse the list backwards: + x = a.listIterator(a.size()); + while(x.hasPrevious()) + System.out.print(x.previous() + " "); + System.out.println(); + System.out.println("testVisual finished"); + } + // There are some things that only LinkedLists can do: + public static void testLinkedList() { + LinkedList ll = new LinkedList<>(); + ll.addAll(LIST); + System.out.println(ll); + // Treat it like a stack, pushing: + ll.addFirst("one"); + ll.addFirst("two"); + System.out.println(ll); + // Like "peeking" at the top of a stack: + System.out.println(ll.getFirst()); + // Like popping a stack: + System.out.println(ll.removeFirst()); + System.out.println(ll.removeFirst()); + // Treat it like a queue, pulling elements + // off the tail end: + System.out.println(ll.removeLast()); + System.out.println(ll); + } + public static void main(String[] args) { + // Make and fill a new list each time: + basicTest(new LinkedList<>(LIST)); + basicTest(new ArrayList<>(LIST)); + iterMotion(new LinkedList<>(LIST)); + iterMotion(new ArrayList<>(LIST)); + iterManipulation(new LinkedList<>(LIST)); + iterManipulation(new ArrayList<>(LIST)); + testVisual(new LinkedList<>(LIST)); + testLinkedList(); + } +} +/* Output: +[AliceBlue, AntiqueWhite, Aquamarine, Azure, Beige, +Bisque, Black, BlanchedAlmond, Blue, BlueViolet] +b = [AliceBlue, AntiqueWhite, Aquamarine, Azure, Beige, +Bisque, Black, BlanchedAlmond, Blue, BlueViolet] +[AliceBlue, AntiqueWhite, Aquamarine, Azure, Beige, +Bisque, Black, BlanchedAlmond, Blue, BlueViolet, +AliceBlue, AntiqueWhite, Aquamarine, Azure, Beige, +Bisque, Black, BlanchedAlmond, Blue, BlueViolet, +AliceBlue, AntiqueWhite, Aquamarine, Azure, Beige, +Bisque, Black, BlanchedAlmond, Blue, BlueViolet] +[AliceBlue, AntiqueWhite, Aquamarine, Azure, Beige, +Bisque, Black, BlanchedAlmond, Blue, BlueViolet, +AliceBlue, AntiqueWhite, Aquamarine, Azure, Beige, one, +Bisque, Black, BlanchedAlmond, Blue, BlueViolet, +AliceBlue, AntiqueWhite, Aquamarine, Azure, Beige, +Bisque, Black, BlanchedAlmond, Blue, BlueViolet] +Bisque +Black +[AliceBlue, AntiqueWhite, Aquamarine, Azure, Beige, +Bisque, Black, BlanchedAlmond, Blue, BlueViolet, +AliceBlue, AntiqueWhite, Aquamarine, Azure, Beige, one, +47, BlanchedAlmond, Blue, BlueViolet, AliceBlue, +AntiqueWhite, Aquamarine, Azure, Beige, Bisque, Black, +BlanchedAlmond, Blue, BlueViolet] +BlueViolet Blue BlanchedAlmond Black Bisque Beige Azure +Aquamarine AntiqueWhite AliceBlue BlueViolet Blue +BlanchedAlmond 47 one Beige Azure Aquamarine +AntiqueWhite AliceBlue BlueViolet Blue BlanchedAlmond +Black Bisque Beige Azure Aquamarine AntiqueWhite +AliceBlue +testVisual finished +[AliceBlue, AntiqueWhite, Aquamarine, Azure, Beige, +Bisque, Black, BlanchedAlmond, Blue, BlueViolet] +[two, one, AliceBlue, AntiqueWhite, Aquamarine, Azure, +Beige, Bisque, Black, BlanchedAlmond, Blue, BlueViolet] +two +two +one +BlueViolet +[AliceBlue, AntiqueWhite, Aquamarine, Azure, Beige, +Bisque, Black, BlanchedAlmond, Blue] +*/ +``` + +在 `basicTest()` 和 `iterMotion()` 中,方法调用是为了展示正确的语法,尽管获取了返回值,但不会使用它。在某些情况下,根本不会去获取返回值。在使用这些方法之前,请查看JDK文档中这些方法的完整用法。 -## Set表现 +## Set行为 +**Set** 的主要用处是测试成员身份,不过也可以将其用作删除重复元素的工具。如果不关心元素顺序或并发性, **HashSet** 总是最好的选择,因为它是专门为了快速查找而设计的(这里使用了在[附录:理解equals和hashCode方法]()章节中探讨的散列函数)。 + +其它的 **Set** 实现产生不同的排序行为: + +```java +// collectiontopics/SetOrder.java +import java.util.*; +import onjava.HTMLColors; + +public class SetOrder { + static String[] sets = { + "java.util.HashSet", + "java.util.TreeSet", + "java.util.concurrent.ConcurrentSkipListSet", + "java.util.LinkedHashSet", + "java.util.concurrent.CopyOnWriteArraySet", + }; + static final List RLIST = + new ArrayList<>(HTMLColors.LIST); + static { + Collections.reverse(RLIST); + } + public static void + main(String[] args) throws Exception { + for(String type: sets) { + System.out.format("[-> %s <-]%n", + type.substring(type.lastIndexOf('.') + 1)); + @SuppressWarnings("unchecked") + Set set = (Set) + Class.forName(type).newInstance(); + set.addAll(RLIST); + set.stream() + .limit(10) + .forEach(System.out::println); + } + } +} +/* Output: +[-> HashSet <-] +MediumOrchid +PaleGoldenRod +Sienna +LightSlateGray +DarkSeaGreen +Black +Gainsboro +Orange +LightCoral +DodgerBlue +[-> TreeSet <-] +AliceBlue +AntiqueWhite +Aquamarine +Azure +Beige +Bisque +Black +BlanchedAlmond +Blue +BlueViolet +[-> ConcurrentSkipListSet <-] +AliceBlue +AntiqueWhite +Aquamarine +Azure +Beige +Bisque +Black +BlanchedAlmond +Blue +BlueViolet +[-> LinkedHashSet <-] +YellowGreen +Yellow +WhiteSmoke +White +Wheat +Violet +Turquoise +Tomato +Thistle +Teal +[-> CopyOnWriteArraySet <-] +YellowGreen +Yellow +WhiteSmoke +White +Wheat +Violet +Turquoise +Tomato +Thistle +Teal +*/ +``` + +这里需要使用 **@SuppressWarnings(“unchecked”)** ,因为这里将一个 **String** (可能是任何东西)传递给了 `Class.forName(type).newInstance()` 。编译器并不能保证这是一次成功的操作。 + +**RLIST** 是 **HTMLColors.LIST** 的反转版本。因为 `Collections.reverse()` 是通过修改参数来执行反向操作,而不是返回包含反向元素的新 **List** ,所以该调用在 **static** 块内执行。 **RLIST** 可以防止我们意外地认为 **Set** 对其结果进行了排序。 + +**HashSet** 的输出结果似乎没有可辨别的顺序,因为它是基于散列函数的。 **TreeSet** 和 **ConcurrentSkipListSet** 都对它们的元素进行了排序,它们都实现了 **SortedSet** 接口来标识这个特点。因为实现该接口的 **Set** 按顺序排列,所以该接口还有一些其他的可用操作。 **LinkedHashSet** 和 **CopyOnWriteArraySet** 尽管没有用于标识的接口,但它们还是保留了元素的插入顺序。 + +**ConcurrentSkipListSet** 和 **CopyOnWriteArraySet** 是线程安全的。 + +在附录的最后,我们将了解在非 **HashSet** 实现的 **Set** 上添加额外排序的性能成本,以及不同实现中的任何其他功能的成本。 ## 在Map中使用函数式操作 +与 **Collection** 接口一样,`forEach()` 也内置在 **Map** 接口中。但是如果想要执行任何其他的基本功能操作,比如 `map()` ,`flatMap()` ,`reduce()` 或 `filter()` 时,该怎么办? 查看 **Map** 接口发现并没有这些。 + +可以通过 `entrySet()` 连接到这些方法,该方法会生成一个由 **Map.Entry** 对象组成的 **Set** 。这个 **Set** 包含 `stream()` 和 `parallelStream()` 方法。只需要记住一件事,这里正在使用的是 **Map.Entry** 对象: + +```java +// collectiontopics/FunctionalMap.java +// Functional operations on a Map +import java.util.*; +import java.util.stream.*; +import java.util.concurrent.*; +import static onjava.HTMLColors.*; + +public class FunctionalMap { + public static void main(String[] args) { + MAP.entrySet().stream() + .map(Map.Entry::getValue) + .filter(v -> v.startsWith("Dark")) + .map(v -> v.replaceFirst("Dark", "Hot")) + .forEach(System.out::println); + } +} +/* Output: +HotBlue +HotCyan +HotGoldenRod +HotGray +HotGreen +HotKhaki +HotMagenta +HotOliveGreen +HotOrange +HotOrchid +HotRed +HotSalmon +HotSeaGreen +HotSlateBlue +HotSlateGray +HotTurquoise +HotViolet +*/ +``` + +生成 **Stream** 后,所有的基本功能方法,甚至更多就都可以使用了。 -## 选择Map的部分 +## 选择Map片段 +由 **TreeMap** 和 **ConcurrentSkipListMap** 实现的 **NavigableMap** 接口解决了需要选择Map片段的问题。下面是一个示例,使用了 **HTMLColors** : + +```java +// collectiontopics/NavMap.java +// NavigableMap produces pieces of a Map +import java.util.*; +import java.util.concurrent.*; +import static onjava.HTMLColors.*; + +public class NavMap { + public static final + NavigableMap COLORS = + new ConcurrentSkipListMap<>(MAP); + public static void main(String[] args) { + show(COLORS.firstEntry()); + border(); + show(COLORS.lastEntry()); + border(); + NavigableMap toLime = + COLORS.headMap(rgb("Lime"), true); + show(toLime); + border(); + show(COLORS.ceilingEntry(rgb("DeepSkyBlue") - 1)); + border(); + show(COLORS.floorEntry(rgb("DeepSkyBlue") - 1)); + border(); + show(toLime.descendingMap()); + border(); + show(COLORS.tailMap(rgb("MistyRose"), true)); + border(); + show(COLORS.subMap( + rgb("Orchid"), true, + rgb("DarkSalmon"), false)); + } +} +/* Output: +0x000000: Black +****************************** +0xFFFFFF: White +****************************** +0x000000: Black +0x000080: Navy +0x00008B: DarkBlue +0x0000CD: MediumBlue +0x0000FF: Blue +0x006400: DarkGreen +0x008000: Green +0x008080: Teal +0x008B8B: DarkCyan +0x00BFFF: DeepSkyBlue +0x00CED1: DarkTurquoise +0x00FA9A: MediumSpringGreen +0x00FF00: Lime +****************************** +0x00BFFF: DeepSkyBlue +****************************** +0x008B8B: DarkCyan +****************************** +0x00FF00: Lime +0x00FA9A: MediumSpringGreen +0x00CED1: DarkTurquoise +0x00BFFF: DeepSkyBlue +0x008B8B: DarkCyan +0x008080: Teal +0x008000: Green +0x006400: DarkGreen +0x0000FF: Blue +0x0000CD: MediumBlue +0x00008B: DarkBlue +0x000080: Navy +0x000000: Black +****************************** +0xFFE4E1: MistyRose +0xFFEBCD: BlanchedAlmond +0xFFEFD5: PapayaWhip +0xFFF0F5: LavenderBlush +0xFFF5EE: SeaShell +0xFFF8DC: Cornsilk +0xFFFACD: LemonChiffon +0xFFFAF0: FloralWhite +0xFFFAFA: Snow +0xFFFF00: Yellow +0xFFFFE0: LightYellow +0xFFFFF0: Ivory +0xFFFFFF: White +****************************** +0xDA70D6: Orchid +0xDAA520: GoldenRod +0xDB7093: PaleVioletRed +0xDC143C: Crimson +0xDCDCDC: Gainsboro +0xDDA0DD: Plum +0xDEB887: BurlyWood +0xE0FFFF: LightCyan +0xE6E6FA: Lavender +*/ +``` + +在主方法中可以看到 **NavigableMap** 的各种功能。 因为 **NavigableMap** 具有键顺序,所以它使用了 `firstEntry()` 和 `lastEntry()` 的概念。调用 `headMap()` 会生成一个 **NavigableMap** ,其中包含了从 **Map** 的开头到 `headMap()` 参数中所指向的一组元素,其中 **boolean** 值指示结果中是否包含该参数。调用 `tailMap()` 执行了类似的操作,只不过是从参数开始到 **Map** 的末尾。 `subMap()` 则允许生成 **Map** 中间的一部分。 + +`ceilingEntry()` 从当前键值对向上搜索下一个键值对,`floorEntry()` 则是向下搜索。 `descendingMap()` 反转了 **NavigableMap** 的顺序。 + +如果需要通过分割 **Map** 来简化所正在解决的问题,则 **NavigableMap** 可以做到。具有类似的功能的其它集合实现也可以用来帮助解决问题。 -## 集合的fill方法 +## 填充集合 +与 **Arrays** 一样,这里有一个名为 **Collections** 的伴随类(companion class),包含了一些 **static** 的实用方法,其中包括一个名为 `fill()` 的方法。 `fill()` 只复制整个集合中的单个对象引用。此外,它仅适用于 **List** 对象,但结果列表可以传递给构造方法或 `addAll()` 方法: + +```java +// collectiontopics/FillingLists.java +// Collections.fill() & Collections.nCopies() +import java.util.*; + +class StringAddress { + private String s; + StringAddress(String s) { this.s = s; } + @Override + public String toString() { + return super.toString() + " " + s; + } +} + +public class FillingLists { + public static void main(String[] args) { + List list = new ArrayList<>( + Collections.nCopies(4, + new StringAddress("Hello"))); + System.out.println(list); + Collections.fill(list, + new StringAddress("World!")); + System.out.println(list); + } +} +/* Output: +[StringAddress@15db9742 Hello, StringAddress@15db9742 +Hello, StringAddress@15db9742 Hello, +StringAddress@15db9742 Hello] +[StringAddress@6d06d69c World!, StringAddress@6d06d69c +World!, StringAddress@6d06d69c World!, +StringAddress@6d06d69c World!] +*/ +``` + +这个示例展示了两种使用对单个对象的引用来填充 **Collection** 的方法。 第一个: `Collections.nCopies()` ,创建一个 **List**,并传递给 **ArrayList** 的构造方法,进而填充了 **ArrayList** 。 + +**StringAddress** 中的 `toString()` 方法调用了 `Object.toString()` ,它先生成类名,后跟着对象的哈希码的无符号十六进制表示(哈希吗由 `hashCode()` 方法生成)。 输出显示所有的引用都指向同一个对象。调用第二个方法 `Collections.fill()` 后也是如此。 `fill()` 方法的用处非常有限,它只能替换 **List** 中已有的元素,而且不会添加新元素, + +### 使用 Suppliers 填充集合 + +[第二十章 泛型]()章节中介绍的 **onjava.Suppliers** 类为填充集合提供了通用解决方案。 这是一个使用 **Suppliers** 初始化几种不同类型的 **Collection** 的示例: + +```java +// collectiontopics/SuppliersCollectionTest.java +import java.util.*; +import java.util.function.*; +import java.util.stream.*; +import onjava.*; + +class Government implements Supplier { + static String[] foundation = ( + "strange women lying in ponds " + + "distributing swords is no basis " + + "for a system of government").split(" "); + private int index; + @Override + public String get() { + return foundation[index++]; + } +} + +public class SuppliersCollectionTest { + public static void main(String[] args) { + // Suppliers class from the Generics chapter: + Set set = Suppliers.create( + LinkedHashSet::new, new Government(), 15); + System.out.println(set); + List list = Suppliers.create( + LinkedList::new, new Government(), 15); + System.out.println(list); + list = new ArrayList<>(); + Suppliers.fill(list, new Government(), 15); + System.out.println(list); + + // Or we can use Streams: + set = Arrays.stream(Government.foundation) + .collect(Collectors.toSet()); + System.out.println(set); + list = Arrays.stream(Government.foundation) + .collect(Collectors.toList()); + System.out.println(list); + list = Arrays.stream(Government.foundation) + .collect(Collectors + .toCollection(LinkedList::new)); + System.out.println(list); + set = Arrays.stream(Government.foundation) + .collect(Collectors + .toCollection(LinkedHashSet::new)); + System.out.println(set); + } +} +/* Output: +[strange, women, lying, in, ponds, distributing, +swords, is, no, basis, for, a, system, of, government] +[strange, women, lying, in, ponds, distributing, +swords, is, no, basis, for, a, system, of, government] +[strange, women, lying, in, ponds, distributing, +swords, is, no, basis, for, a, system, of, government] +[ponds, no, a, in, swords, for, is, basis, strange, +system, government, distributing, of, women, lying] +[strange, women, lying, in, ponds, distributing, +swords, is, no, basis, for, a, system, of, government] +[strange, women, lying, in, ponds, distributing, +swords, is, no, basis, for, a, system, of, government] +[strange, women, lying, in, ponds, distributing, +swords, is, no, basis, for, a, system, of, government] +*/ +``` + +**LinkedHashSet** 中的的元素按插入顺序排列,因为它维护一个链表来保存该顺序。 + +但是请注意示例的第二部分:大多数情况下都可以使用 **Stream** 来创建和填充 **Collection** 。在本例中的 **Stream** 版本不需要声明 **Supplier** 所想要创建的元素数量;,它直接吸收了 **Stream** 中的所有元素。 + +尽可能优先选择 **Stream** 来解决问题。 + +### Map Suppliers + +使用 **Supplier** 来填充 **Map** 时需要一个 **Pair** 类,因为每次调用一个 **Supplier** 的 `get()` 方法时,都必须生成一对对象(一个键和一个值): + +```java +// onjava/Pair.java +package onjava; + +public class Pair { + public final K key; + public final V value; + public Pair(K k, V v) { + key = k; + value = v; + } + public K key() { return key; } + public V value() { return value; } + public static Pair make(K k, V v) { + return new Pair(k, v); + } +} +``` + +**Pair** 是一个只读的 *数据传输对象* (Data Transfer Object)或 *信使* (Messenger)。 这与[第二十章 泛型]()章节中的 **Tuple2** 基本相同,但名字更适合 **Map** 初始化。我还添加了静态的 `make()` 方法,以便为创建 **Pair** 对象提供一个更简洁的名字。 + +Java 8 的 **Stream** 提供了填充 **Map** 的便捷方法: + +```java +// collectiontopics/StreamFillMaps.java +import java.util.*; +import java.util.function.*; +import java.util.stream.*; +import onjava.*; + +class Letters +implements Supplier> { + private int number = 1; + private char letter = 'A'; + @Override + public Pair get() { + return new Pair<>(number++, "" + letter++); + } +} + +public class StreamFillMaps { + public static void main(String[] args) { + Map m = + Stream.generate(new Letters()) + .limit(11) + .collect(Collectors + .toMap(Pair::key, Pair::value)); + System.out.println(m); + + // Two separate Suppliers: + Rand.String rs = new Rand.String(3); + Count.Character cc = new Count.Character(); + Map mcs = Stream.generate( + () -> Pair.make(cc.get(), rs.get())) + .limit(8) + .collect(Collectors + .toMap(Pair::key, Pair::value)); + System.out.println(mcs); + + // A key Supplier and a single value: + Map mcs2 = Stream.generate( + () -> Pair.make(cc.get(), "Val")) + .limit(8) + .collect(Collectors + .toMap(Pair::key, Pair::value)); + System.out.println(mcs2); + } +} +/* Output: +{1=A, 2=B, 3=C, 4=D, 5=E, 6=F, 7=G, 8=H, 9=I, 10=J, +11=K} +{b=btp, c=enp, d=ccu, e=xsz, f=gvg, g=mei, h=nne, +i=elo} +{p=Val, q=Val, j=Val, k=Val, l=Val, m=Val, n=Val, +o=Val} +*/ +``` + +上面的示例中出现了一个模式,可以使用它来创建一个自动创建和填充 **Map** 的工具: + +```java +// onjava/FillMap.java +package onjava; +import java.util.*; +import java.util.function.*; +import java.util.stream.*; + +public class FillMap { + public static Map + basic(Supplier> pairGen, int size) { + return Stream.generate(pairGen) + .limit(size) + .collect(Collectors + .toMap(Pair::key, Pair::value)); + } + public static Map + basic(Supplier keyGen, + Supplier valueGen, int size) { + return Stream.generate( + () -> Pair.make(keyGen.get(), valueGen.get())) + .limit(size) + .collect(Collectors + .toMap(Pair::key, Pair::value)); + } + public static > + M create(Supplier keyGen, + Supplier valueGen, + Supplier mapSupplier, int size) { + return Stream.generate( () -> + Pair.make(keyGen.get(), valueGen.get())) + .limit(size) + .collect(Collectors + .toMap(Pair::key, Pair::value, + (k, v) -> k, mapSupplier)); + } +} +``` + +basic() 方法生成一个默认的 **Map** ,而 `create()` 方法允许指定一个确切的 **Map** 类型,并返回那个确切的类型。 + +下面是一个测试: + +```java +// collectiontopics/FillMapTest.java +import java.util.*; +import java.util.function.*; +import java.util.stream.*; +import onjava.*; + +public class FillMapTest { + public static void main(String[] args) { + Map mcs = FillMap.basic( + new Rand.String(4), new Count.Integer(), 7); + System.out.println(mcs); + HashMap hashm = + FillMap.create(new Rand.String(4), + new Count.Integer(), HashMap::new, 7); + System.out.println(hashm); + LinkedHashMap linkm = + FillMap.create(new Rand.String(4), + new Count.Integer(), LinkedHashMap::new, 7); + System.out.println(linkm); + } +} +/* Output: +{npcc=1, ztdv=6, gvgm=3, btpe=0, einn=4, eelo=5, +uxsz=2} +{npcc=1, ztdv=6, gvgm=3, btpe=0, einn=4, eelo=5, +uxsz=2} +{btpe=0, npcc=1, uxsz=2, gvgm=3, einn=4, eelo=5, +ztdv=6} +*/ +``` -## 使用Flyweight自定义集合和Map +## 使用享元(Flyweight)自定义Collection和Map +本节介绍如何创建自定义 **Collection** 和 **Map** 实现。每个 **java.util** 中的集合都有自己的 **Abstract** 类,它提供了该集合的部分实现,因此只需要实现必要的方法来生成所需的集合。你将看到通过继承 **java.util.Abstract** 类来创建自定义 **Map** 和 **Collection** 是多么简单。例如,要创建一个只读的 **Set** ,则可以从 **AbstractSet** 继承并实现 `iterator()` 和 `size()` 。最后一个示例是生成测试数据的另一种方法。生成的集合通常是只读的,并且所提供的方法最少。 + +该解决方案还演示了 *享元* (Flyweight)设计模式。当普通解决方案需要太多对象时,或者当生成普通对象占用太多空间时,可以使用享元。享元设计模式将对象的一部分外部化(externalizes)。相比于把对象的所有内容都包含在对象中,这样做使得对象的部分或者全部可以在更有效的外部表中查找,或通过一些节省空间的其他计算生成。 + +下面是一个可以是任何大小的 **List** ,并且(有效地)使用 **Integer** 数据进行预初始化。要从 **AbstractList** 创建只读 **List** ,必须实现 `get()` 和 `size()`: + +```java +// onjava/CountingIntegerList.java +// List of any length, containing sample data +// {java onjava.CountingIntegerList} +package onjava; +import java.util.*; + +public class CountingIntegerList +extends AbstractList { + private int size; + public CountingIntegerList() { size = 0; } + public CountingIntegerList(int size) { + this.size = size < 0 ? 0 : size; + } + @Override + public Integer get(int index) { + return index; + } + @Override + public int size() { return size; } + public static void main(String[] args) { + List cil = + new CountingIntegerList(30); + System.out.println(cil); + System.out.println(cil.get(500)); + } +} +/* Output: +[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, +16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29] +500 +*/ +``` + +只有当想要限制 **List** 的长度时, **size** 值才是重要的,就像在主方法中那样。即使在这种情况下, `get()` 也会产生任何值。 + +这个类是享元模式的一个简洁的例子。当需要的时候, `get()` “计算”所需的值,因此没必要存储和初始化实际的底层 **List** 结构。 + +在大多数程序中,这里所保存的存储结构永远都不会改变。但是,它允许用非常大的 **index** 来调用 `List.get()` ,而 **List** 并不需要填充到这么大。此外,还可以在程序中大量使用 **CountingIntegerLists** 而无需担心存储问题。实际上,享元的一个好处是它允许使用更好的抽象而不用担心资源。 + +可以使用享元设计模式来实现具有任何大小数据集的其他“初始化”自定义集合。下面是一个 **Map** ,它为每一个 **Integer** 键产生唯一的值: + +```java +// onjava/CountMap.java +// Unlimited-length Map containing sample data +// {java onjava.CountMap} +package onjava; +import java.util.*; +import java.util.stream.*; + +public class CountMap +extends AbstractMap { + private int size; + private static char[] chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray(); + private static String value(int key) { + return + chars[key % chars.length] + + Integer.toString(key / chars.length); + } + public CountMap(int size) { + this.size = size < 0 ? 0 : size; + } + @Override + public String get(Object key) { + return value((Integer)key); + } + private static class Entry + implements Map.Entry { + int index; + Entry(int index) { this.index = index; } + @Override + public boolean equals(Object o) { + return o instanceof Entry && + Objects.equals(index, ((Entry)o).index); + } + @Override + public Integer getKey() { return index; } + @Override + public String getValue() { + return value(index); + } + @Override + public String setValue(String value) { + throw new UnsupportedOperationException(); + } + @Override + public int hashCode() { + return Objects.hashCode(index); + } + } + @Override + public Set> entrySet() { + // LinkedHashSet retains initialization order: + return IntStream.range(0, size) + .mapToObj(Entry::new) + .collect(Collectors + .toCollection(LinkedHashSet::new)); + } + public static void main(String[] args) { + final int size = 6; + CountMap cm = new CountMap(60); + System.out.println(cm); + System.out.println(cm.get(500)); + cm.values().stream() + .limit(size) + .forEach(System.out::println); + System.out.println(); + new Random(47).ints(size, 0, 1000) + .mapToObj(cm::get) + .forEach(System.out::println); + } +} +/* Output: +{0=A0, 1=B0, 2=C0, 3=D0, 4=E0, 5=F0, 6=G0, 7=H0, 8=I0, +9=J0, 10=K0, 11=L0, 12=M0, 13=N0, 14=O0, 15=P0, 16=Q0, +17=R0, 18=S0, 19=T0, 20=U0, 21=V0, 22=W0, 23=X0, 24=Y0, +25=Z0, 26=A1, 27=B1, 28=C1, 29=D1, 30=E1, 31=F1, 32=G1, +33=H1, 34=I1, 35=J1, 36=K1, 37=L1, 38=M1, 39=N1, 40=O1, +41=P1, 42=Q1, 43=R1, 44=S1, 45=T1, 46=U1, 47=V1, 48=W1, +49=X1, 50=Y1, 51=Z1, 52=A2, 53=B2, 54=C2, 55=D2, 56=E2, +57=F2, 58=G2, 59=H2} +G19 +A0 +B0 +C0 +D0 +E0 +F0 + +Y9 +J21 +R26 +D33 +Z36 +N16 +*/ +``` + +要创建一个只读的 **Map** ,则从 **AbstractMap** 继承并实现 `entrySet()` 。私有的 `value()` 方法计算任何键的值,并在 `get()` 和 `Entry.getValue()` 中使用。可以忽略 **CountMap** 的大小。 + +这里是使用了 **LinkedHashSet** 而不是创建自定义 **Set** 类,因此并未完全实现享元。只有在调用 `entrySet()` 时才会生成此对象。 + +现在创建一个更复杂的享元。这个示例中的数据集是世界各国及其首都的 **Map** 。 `capitals()` 方法生成一个国家和首都的 **Map** 。 `names()` 方法生成一个由国家名字组成的 **List** 。 当给定了表示所需大小的 **int** 参数时,两种方法都生成对应大小的列表片段: + +```java +// onjava/Countries.java +// "Flyweight" Maps and Lists of sample data +// {java onjava.Countries} +package onjava; +import java.util.*; + +public class Countries { + public static final String[][] DATA = { + // Africa + {"ALGERIA","Algiers"}, + {"ANGOLA","Luanda"}, + {"BENIN","Porto-Novo"}, + {"BOTSWANA","Gaberone"}, + {"BURKINA FASO","Ouagadougou"}, + {"BURUNDI","Bujumbura"}, + {"CAMEROON","Yaounde"}, + {"CAPE VERDE","Praia"}, + {"CENTRAL AFRICAN REPUBLIC","Bangui"}, + {"CHAD","N'djamena"}, + {"COMOROS","Moroni"}, + {"CONGO","Brazzaville"}, + {"DJIBOUTI","Dijibouti"}, + {"EGYPT","Cairo"}, + {"EQUATORIAL GUINEA","Malabo"}, + {"ERITREA","Asmara"}, + {"ETHIOPIA","Addis Ababa"}, + {"GABON","Libreville"}, + {"THE GAMBIA","Banjul"}, + {"GHANA","Accra"}, + {"GUINEA","Conakry"}, + {"BISSAU","Bissau"}, + {"COTE D'IVOIR (IVORY COAST)","Yamoussoukro"}, + {"KENYA","Nairobi"}, + {"LESOTHO","Maseru"}, + {"LIBERIA","Monrovia"}, + {"LIBYA","Tripoli"}, + {"MADAGASCAR","Antananarivo"}, + {"MALAWI","Lilongwe"}, + {"MALI","Bamako"}, + {"MAURITANIA","Nouakchott"}, + {"MAURITIUS","Port Louis"}, + {"MOROCCO","Rabat"}, + {"MOZAMBIQUE","Maputo"}, + {"NAMIBIA","Windhoek"}, + {"NIGER","Niamey"}, + {"NIGERIA","Abuja"}, + {"RWANDA","Kigali"}, + {"SAO TOME E PRINCIPE","Sao Tome"}, + {"SENEGAL","Dakar"}, + {"SEYCHELLES","Victoria"}, + {"SIERRA LEONE","Freetown"}, + {"SOMALIA","Mogadishu"}, + {"SOUTH AFRICA","Pretoria/Cape Town"}, + {"SUDAN","Khartoum"}, + {"SWAZILAND","Mbabane"}, + {"TANZANIA","Dodoma"}, + {"TOGO","Lome"}, + {"TUNISIA","Tunis"}, + {"UGANDA","Kampala"}, + {"DEMOCRATIC REPUBLIC OF THE CONGO (ZAIRE)", + "Kinshasa"}, + {"ZAMBIA","Lusaka"}, + {"ZIMBABWE","Harare"}, + // Asia + {"AFGHANISTAN","Kabul"}, + {"BAHRAIN","Manama"}, + {"BANGLADESH","Dhaka"}, + {"BHUTAN","Thimphu"}, + {"BRUNEI","Bandar Seri Begawan"}, + {"CAMBODIA","Phnom Penh"}, + {"CHINA","Beijing"}, + {"CYPRUS","Nicosia"}, + {"INDIA","New Delhi"}, + {"INDONESIA","Jakarta"}, + {"IRAN","Tehran"}, + {"IRAQ","Baghdad"}, + {"ISRAEL","Jerusalem"}, + {"JAPAN","Tokyo"}, + {"JORDAN","Amman"}, + {"KUWAIT","Kuwait City"}, + {"LAOS","Vientiane"}, + {"LEBANON","Beirut"}, + {"MALAYSIA","Kuala Lumpur"}, + {"THE MALDIVES","Male"}, + {"MONGOLIA","Ulan Bator"}, + {"MYANMAR (BURMA)","Rangoon"}, + {"NEPAL","Katmandu"}, + {"NORTH KOREA","P'yongyang"}, + {"OMAN","Muscat"}, + {"PAKISTAN","Islamabad"}, + {"PHILIPPINES","Manila"}, + {"QATAR","Doha"}, + {"SAUDI ARABIA","Riyadh"}, + {"SINGAPORE","Singapore"}, + {"SOUTH KOREA","Seoul"}, + {"SRI LANKA","Colombo"}, + {"SYRIA","Damascus"}, + {"TAIWAN (REPUBLIC OF CHINA)","Taipei"}, + {"THAILAND","Bangkok"}, + {"TURKEY","Ankara"}, + {"UNITED ARAB EMIRATES","Abu Dhabi"}, + {"VIETNAM","Hanoi"}, + {"YEMEN","Sana'a"}, + // Australia and Oceania + {"AUSTRALIA","Canberra"}, + {"FIJI","Suva"}, + {"KIRIBATI","Bairiki"}, + {"MARSHALL ISLANDS","Dalap-Uliga-Darrit"}, + {"MICRONESIA","Palikir"}, + {"NAURU","Yaren"}, + {"NEW ZEALAND","Wellington"}, + {"PALAU","Koror"}, + {"PAPUA NEW GUINEA","Port Moresby"}, + {"SOLOMON ISLANDS","Honaira"}, + {"TONGA","Nuku'alofa"}, + {"TUVALU","Fongafale"}, + {"VANUATU","Port Vila"}, + {"WESTERN SAMOA","Apia"}, + // Eastern Europe and former USSR + {"ARMENIA","Yerevan"}, + {"AZERBAIJAN","Baku"}, + {"BELARUS (BYELORUSSIA)","Minsk"}, + {"BULGARIA","Sofia"}, + {"GEORGIA","Tbilisi"}, + {"KAZAKSTAN","Almaty"}, + {"KYRGYZSTAN","Alma-Ata"}, + {"MOLDOVA","Chisinau"}, + {"RUSSIA","Moscow"}, + {"TAJIKISTAN","Dushanbe"}, + {"TURKMENISTAN","Ashkabad"}, + {"UKRAINE","Kyiv"}, + {"UZBEKISTAN","Tashkent"}, + // Europe + {"ALBANIA","Tirana"}, + {"ANDORRA","Andorra la Vella"}, + {"AUSTRIA","Vienna"}, + {"BELGIUM","Brussels"}, + {"BOSNIA-HERZEGOVINA","Sarajevo"}, + {"CROATIA","Zagreb"}, + {"CZECH REPUBLIC","Prague"}, + {"DENMARK","Copenhagen"}, + {"ESTONIA","Tallinn"}, + {"FINLAND","Helsinki"}, + {"FRANCE","Paris"}, + {"GERMANY","Berlin"}, + {"GREECE","Athens"}, + {"HUNGARY","Budapest"}, + {"ICELAND","Reykjavik"}, + {"IRELAND","Dublin"}, + {"ITALY","Rome"}, + {"LATVIA","Riga"}, + {"LIECHTENSTEIN","Vaduz"}, + {"LITHUANIA","Vilnius"}, + {"LUXEMBOURG","Luxembourg"}, + {"MACEDONIA","Skopje"}, + {"MALTA","Valletta"}, + {"MONACO","Monaco"}, + {"MONTENEGRO","Podgorica"}, + {"THE NETHERLANDS","Amsterdam"}, + {"NORWAY","Oslo"}, + {"POLAND","Warsaw"}, + {"PORTUGAL","Lisbon"}, + {"ROMANIA","Bucharest"}, + {"SAN MARINO","San Marino"}, + {"SERBIA","Belgrade"}, + {"SLOVAKIA","Bratislava"}, + {"SLOVENIA","Ljuijana"}, + {"SPAIN","Madrid"}, + {"SWEDEN","Stockholm"}, + {"SWITZERLAND","Berne"}, + {"UNITED KINGDOM","London"}, + {"VATICAN CITY","Vatican City"}, + // North and Central America + {"ANTIGUA AND BARBUDA","Saint John's"}, + {"BAHAMAS","Nassau"}, + {"BARBADOS","Bridgetown"}, + {"BELIZE","Belmopan"}, + {"CANADA","Ottawa"}, + {"COSTA RICA","San Jose"}, + {"CUBA","Havana"}, + {"DOMINICA","Roseau"}, + {"DOMINICAN REPUBLIC","Santo Domingo"}, + {"EL SALVADOR","San Salvador"}, + {"GRENADA","Saint George's"}, + {"GUATEMALA","Guatemala City"}, + {"HAITI","Port-au-Prince"}, + {"HONDURAS","Tegucigalpa"}, + {"JAMAICA","Kingston"}, + {"MEXICO","Mexico City"}, + {"NICARAGUA","Managua"}, + {"PANAMA","Panama City"}, + {"ST. KITTS AND NEVIS","Basseterre"}, + {"ST. LUCIA","Castries"}, + {"ST. VINCENT AND THE GRENADINES","Kingstown"}, + {"UNITED STATES OF AMERICA","Washington, D.C."}, + // South America + {"ARGENTINA","Buenos Aires"}, + {"BOLIVIA","Sucre (legal)/La Paz(administrative)"}, + {"BRAZIL","Brasilia"}, + {"CHILE","Santiago"}, + {"COLOMBIA","Bogota"}, + {"ECUADOR","Quito"}, + {"GUYANA","Georgetown"}, + {"PARAGUAY","Asuncion"}, + {"PERU","Lima"}, + {"SURINAME","Paramaribo"}, + {"TRINIDAD AND TOBAGO","Port of Spain"}, + {"URUGUAY","Montevideo"}, + {"VENEZUELA","Caracas"}, + }; + // Use AbstractMap by implementing entrySet() + private static class FlyweightMap + extends AbstractMap { + private static class Entry + implements Map.Entry { + int index; + Entry(int index) { this.index = index; } + @Override + public boolean equals(Object o) { + return o instanceof FlyweightMap && + Objects.equals(DATA[index][0], o); + } + @Override + public int hashCode() { + return Objects.hashCode(DATA[index][0]); + } + @Override + public String getKey() { return DATA[index][0]; } + @Override + public String getValue() { + return DATA[index][1]; + } + @Override + public String setValue(String value) { + throw new UnsupportedOperationException(); + } + } + // Implement size() & iterator() for AbstractSet: + static class EntrySet + extends AbstractSet> { + private int size; + EntrySet(int size) { + if(size < 0) + this.size = 0; + // Can't be any bigger than the array: + else if(size > DATA.length) + this.size = DATA.length; + else + this.size = size; + } + @Override + public int size() { return size; } + private class Iter + implements Iterator> { + // Only one Entry object per Iterator: + private Entry entry = new Entry(-1); + @Override + public boolean hasNext() { + return entry.index < size - 1; + } + @Override + public Map.Entry next() { + entry.index++; + return entry; + } + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + @Override + public + Iterator> iterator() { + return new Iter(); + } + } + private static + Set> entries = + new EntrySet(DATA.length); + @Override + public Set> entrySet() { + return entries; + } + } + // Create a partial map of 'size' countries: + static Map select(final int size) { + return new FlyweightMap() { + @Override + public Set> entrySet() { + return new EntrySet(size); + } + }; + } + static Map map = new FlyweightMap(); + public static Map capitals() { + return map; // The entire map + } + public static Map capitals(int size) { + return select(size); // A partial map + } + static List names = + new ArrayList<>(map.keySet()); + // All the names: + public static List names() { return names; } + // A partial list: + public static List names(int size) { + return new ArrayList<>(select(size).keySet()); + } + public static void main(String[] args) { + System.out.println(capitals(10)); + System.out.println(names(10)); + System.out.println(new HashMap<>(capitals(3))); + System.out.println( + new LinkedHashMap<>(capitals(3))); + System.out.println(new TreeMap<>(capitals(3))); + System.out.println(new Hashtable<>(capitals(3))); + System.out.println(new HashSet<>(names(6))); + System.out.println(new LinkedHashSet<>(names(6))); + System.out.println(new TreeSet<>(names(6))); + System.out.println(new ArrayList<>(names(6))); + System.out.println(new LinkedList<>(names(6))); + System.out.println(capitals().get("BRAZIL")); + } +} +/* Output: +{ALGERIA=Algiers, ANGOLA=Luanda, BENIN=Porto-Novo, +BOTSWANA=Gaberone, BURKINA FASO=Ouagadougou, +BURUNDI=Bujumbura, CAMEROON=Yaounde, CAPE VERDE=Praia, +CENTRAL AFRICAN REPUBLIC=Bangui, CHAD=N'djamena} +[ALGERIA, ANGOLA, BENIN, BOTSWANA, BURKINA FASO, +BURUNDI, CAMEROON, CAPE VERDE, CENTRAL AFRICAN +REPUBLIC, CHAD] +{BENIN=Porto-Novo, ANGOLA=Luanda, ALGERIA=Algiers} +{ALGERIA=Algiers, ANGOLA=Luanda, BENIN=Porto-Novo} +{ALGERIA=Algiers, ANGOLA=Luanda, BENIN=Porto-Novo} +{ALGERIA=Algiers, ANGOLA=Luanda, BENIN=Porto-Novo} +[BENIN, BOTSWANA, ANGOLA, BURKINA FASO, ALGERIA, +BURUNDI] +[ALGERIA, ANGOLA, BENIN, BOTSWANA, BURKINA FASO, +BURUNDI] +[ALGERIA, ANGOLA, BENIN, BOTSWANA, BURKINA FASO, +BURUNDI] +[ALGERIA, ANGOLA, BENIN, BOTSWANA, BURKINA FASO, +BURUNDI] +[ALGERIA, ANGOLA, BENIN, BOTSWANA, BURKINA FASO, +BURUNDI] +Brasilia +*/ +``` + +二维数组 **String DATA** 是 **public** 的,因此可以在别处使用。 **FlyweightMap** 必须实现 `entrySet()` 方法,该方法需要一个自定义 **Set** 实现和一个自定义 **Map.Entry** 类。这是实现享元的另一种方法:每个 **Map.Entry** 对象存储它自身的索引,而不是实际的键和值。当调用 `getKey()` 或 `getValue()` 时,它使用索引返回相应的 **DATA** 元素。 **EntrySet** 确保它的 **size** 不大于 **DATA** 。 + +享元的另一部分在 **EntrySet.Iterator** 中实现。相比于为 **DATA** 中的每个数据对创建一个 **Map.Entry** 对象,这里每个迭代器只有一个 **Map.Entry** 对象。 **Entry** 对象作为数据的窗口,它只包含 **String** 静态数组的索引。每次为迭代器调用 `next()` 时,**Entry** 中的索引都会递增,因此它会指向下一个数据对,然后从 `next()` 返回 **Iterators** 的单个 **Entry** 对象。[^1] + +`select()` 方法生成一个包含所需大小的 **EntrySet** 的 **FlyweightMap** ,这用于在主方法中演示的重载的 `capitals()` 和 `names()` 方法。 ## 集合功能 +下面这个表格展示了可以对 **Collection** 执行的所有操作(不包括自动继承自 **Object** 的方法),因此,可以用 **List** , **Set** , **Queue** 或 **Deque** 执行这里的所有操作(这些接口可能也提供了一些其他的功能)。**Map** 不是从 **Collection** 继承的,所以要单独处理它。 + +| 方法名 | 描述 | +| :---: | :--- | +| **boolean add(T)** | 确保集合包含该泛型类型 **T** 的参数。如果不添加参数,则返回 **false** 。 (这是一种“可选”方法,将在下一节中介绍。) | +| **boolean addAll(Collection\)** | 添加参数集合中的所有元素。只要有元素被成功添加则返回 **true**。(“可选的”) | +| **void clear()** | 删除集合中的所有元素。(“可选的”) | +| **boolean contains(T)** | 如果目标集合包含该泛型类型 **T** 的参数,则返回 **true** 。 | +| **boolean containsAll(Collection\)** | 如果目标集合包含参数集合中的所有元素,则返回 **true** | +| **boolean isEmpty()** | 如果集合为空,则返回 **true** | +| **Iterator\ iterator() Spliterator\ spliterator()** | 返回一个迭代器来遍历集合中的元素。 **Spliterators** 更复杂一些,它用在并发场景 | +| **boolean remove(Object)** | 如果目标集合包含该参数,则在集合中删除该参数,如果成功删除则返回 **true** 。(“可选的”) | +| **boolean removeAll(Collection\)** | 删除目标集合中,参数集合所包含的全部元素。如果有元素被成功删除则返回 **true** 。 (“可选的”) | +| **boolean removeIf(Predicate\)** | 删除此集合中,满足给定谓词(predicate)的所有元素 | +| **Stream\ stream() Stream\ parallelStream()** | 返回由该 **Collection** 中元素所组成的一个 **Stream** | +| **int size()** | 返回集合中所包含元素的个数 | +| **Object[] toArrat()** | 返回包含该集合所有元素的一个数组 | +| **\ T[] toArray(T[] a)** | 返回包含该集合所有元素的一个数组。结果的运行时类型是参数数组而不是普通的 **Object** 数组。 | + +这里没有提供用于随机访问元素的 **get()** 方法,因为 **Collection** 还包含 **Set** ,它维护自己的内部排序,所以随机访问查找就没有意义了。因此,要查找 **Collection** 中的元素必须使用迭代器。 + +下面这个示例演示了 **Collection** 的所有方法。这里以 **ArrayList** 为例: + +```java +// collectiontopics/CollectionMethods.java +// Things you can do with all Collections +import java.util.*; +import static onjava.HTMLColors.*; + +public class CollectionMethods { + public static void main(String[] args) { + Collection c = + new ArrayList<>(LIST.subList(0, 4)); + c.add("ten"); + c.add("eleven"); + show(c); + border(); + // Make an array from the List: + Object[] array = c.toArray(); + // Make a String array from the List: + String[] str = c.toArray(new String[0]); + // Find max and min elements; this means + // different things depending on the way + // the Comparable interface is implemented: + System.out.println( + "Collections.max(c) = " + Collections.max(c)); + System.out.println( + "Collections.min(c) = " + Collections.min(c)); + border(); + // Add a Collection to another Collection + Collection c2 = + new ArrayList<>(LIST.subList(10, 14)); + c.addAll(c2); + show(c); + border(); + c.remove(LIST.get(0)); + show(c); + border(); + // Remove all components that are + // in the argument collection: + c.removeAll(c2); + show(c); + border(); + c.addAll(c2); + show(c); + border(); + // Is an element in this Collection? + String val = LIST.get(3); + System.out.println( + "c.contains(" + val + ") = " + c.contains(val)); + // Is a Collection in this Collection? + System.out.println( + "c.containsAll(c2) = " + c.containsAll(c2)); + Collection c3 = + ((List)c).subList(3, 5); + // Keep all the elements that are in both + // c2 and c3 (an intersection of sets): + c2.retainAll(c3); + show(c2); + // Throw away all the elements + // in c2 that also appear in c3: + c2.removeAll(c3); + System.out.println( + "c2.isEmpty() = " + c2.isEmpty()); + border(); + // Functional operation: + c = new ArrayList<>(LIST); + c.removeIf(s -> !s.startsWith("P")); + c.removeIf(s -> s.startsWith("Pale")); + // Stream operation: + c.stream().forEach(System.out::println); + c.clear(); // Remove all elements + System.out.println("after c.clear():" + c); + } +} +/* Output: +AliceBlue +AntiqueWhite +Aquamarine +Azure +ten +eleven +****************************** +Collections.max(c) = ten +Collections.min(c) = AliceBlue +****************************** +AliceBlue +AntiqueWhite +Aquamarine +Azure +ten +eleven +Brown +BurlyWood +CadetBlue +Chartreuse +****************************** +AntiqueWhite +Aquamarine +Azure +ten +eleven +Brown +BurlyWood +CadetBlue +Chartreuse +****************************** +AntiqueWhite +Aquamarine +Azure +ten +eleven +****************************** +AntiqueWhite +Aquamarine +Azure +ten +eleven +Brown +BurlyWood +CadetBlue +Chartreuse +****************************** +c.contains(Azure) = true +c.containsAll(c2) = true +c2.isEmpty() = true +****************************** +PapayaWhip +PeachPuff +Peru +Pink +Plum +PowderBlue +Purple +after c.clear():[] +*/ +``` + +为了只演示 **Collection** 接口的方法,而没有其它额外的内容,所以这里创建包含不同数据集的 **ArrayList** ,并向上转型为 **Collection** 对象。 ## 可选操作 +在 **Collection** 接口中执行各种添加和删除操作的方法是 *可选操作* (optional operations)。这意味着实现类不需要为这些方法提供功能定义。 + +这是一种非常不寻常的定义接口的方式。正如我们所知,接口是一种合约(contract)。它表达的意思是,“无论你如何选择实现这个接口,我保证你可以将这些消息发送到这个对象”(我在这里使用术语“接口”来描述正式的 **interface** 关键字和“任何类或子类都支持的方法”的更一般含义)。但“可选”操作违反了这一基本原则,它表示调用某些方法不会执行有意义的行为。相反,它们会抛出异常!这看起来似乎丢失了编译时的类型安全性。 + +其实没那么糟糕。如果操作是可选的,编译器仍然能够限制你仅调用该接口中的方法。它不像动态语言那样,可以为任何对象调用任何方法,并在运行时查找特定的调用是否可行。[^2]此外,大多数将 **Collection** 作为参数的方法仅从该 **Collection** 中读取,并且 **Collection** 的所有“读取”方法都不是可选的。 + +为什么要将方法定义为“可选”的?因为这样做可以防止设计中的接口爆炸。集合库的其他设计往往会产生令人困惑的过多接口来描述主题的每个变体。这甚至使得不可能捕获到接口中的所有特殊情况,因为总有人能发明一个新的接口。“不支持的操作(unsupported operation)”这种方式实现了Java集合库的一个重要目标:集合要易于学习和使用。不支持的操作是一种特殊情况,可以推迟到必要的时候。但是,要使用此方法: + +1. **UnsupportedOperationException** 必须是一个罕见的事件。也就是说,对于大多数类,所有操作都应该起作用,并且只有在特殊情况下才应该不支持某项操作。这在Java集合库中是正确的,因为99%的时间使用到的类 —— **ArrayList** , **LinkedList** , **HashSet** 和 **HashMap** ,以及其他具体实现,都支持所有操作。该设计确实为创建一个新的 **Collection** 提供了一个“后门”,可以不为 **Collection** 接口中的所有方法都提供有意义的定义,这些定义仍然适合现有的类库。 + +2. 当不支持某个操作时, **UnsupportedOperationException** 应该出现在实现阶段,而不是在将产品发送给客户之后。毕竟,这个异常表示编程错误:错误地使用了一个具体实现。 + +值得注意的是,不支持的操作只能在运行时检测到,因此这代表动态类型检查。如果你来自像 C++ 这样的静态类型语言,Java 可能看起来只是另一种静态类型语言。当然, Java 肯定有静态类型检查,但它也有大量的动态类型,因此很难说它只是静态语言或动态语言。一旦你开始注意到这一点,你就会开始看到 Java 中动态类型检查的其他示例。 + + +### 不支持的操作 + +不支持的操作的常见来源是由固定大小的数据结构所支持的集合。使用 `Arrays.asList()` 方法将数组转换为 **List** 时,就会得到这样的集合。此外,还可以选择使用 **Collections** 类中的“不可修改(unmodifiable)”方法使任何集合(包括 **Map** )抛出 **UnsupportedOperationException** 异常。此示例展示了这两种情况: + +```java +// collectiontopics/Unsupported.java +// Unsupported operations in Java collections +import java.util.*; + +public class Unsupported { + static void + check(String description, Runnable tst) { + try { + tst.run(); + } catch(Exception e) { + System.out.println(description + "(): " + e); + } + } + static void test(String msg, List list) { + System.out.println("--- " + msg + " ---"); + Collection c = list; + Collection subList = list.subList(1,8); + // Copy of the sublist: + Collection c2 = new ArrayList<>(subList); + check("retainAll", () -> c.retainAll(c2)); + check("removeAll", () -> c.removeAll(c2)); + check("clear", () -> c.clear()); + check("add", () -> c.add("X")); + check("addAll", () -> c.addAll(c2)); + check("remove", () -> c.remove("C")); + // The List.set() method modifies the value but + // doesn't change the size of the data structure: + check("List.set", () -> list.set(0, "X")); + } + public static void main(String[] args) { + List list = Arrays.asList( + "A B C D E F G H I J K L".split(" ")); + test("Modifiable Copy", new ArrayList<>(list)); + test("Arrays.asList()", list); + test("unmodifiableList()", + Collections.unmodifiableList( + new ArrayList<>(list))); + } +} +/* Output: +--- Modifiable Copy --- +--- Arrays.asList() --- +retainAll(): java.lang.UnsupportedOperationException +removeAll(): java.lang.UnsupportedOperationException +clear(): java.lang.UnsupportedOperationException +add(): java.lang.UnsupportedOperationException +addAll(): java.lang.UnsupportedOperationException +remove(): java.lang.UnsupportedOperationException +--- unmodifiableList() --- +retainAll(): java.lang.UnsupportedOperationException +removeAll(): java.lang.UnsupportedOperationException +clear(): java.lang.UnsupportedOperationException +add(): java.lang.UnsupportedOperationException +addAll(): java.lang.UnsupportedOperationException +remove(): java.lang.UnsupportedOperationException +List.set(): java.lang.UnsupportedOperationException +*/ +``` + +因为 `Arrays.asList()` 生成的 **List** 由一个固定大小的数组所支持,所以唯一支持的操作是那些不改变数组大小的操作。任何会导致更改基础数据结构大小的方法都会产生 **UnsupportedOperationException** 异常,来说明这是对不支持的方法的调用(编程错误)。 + +请注意,始终可以将 `Arrays.asList()` 的结果作为一个参数传递给任何 **Collection** 的构造方法(或使用 `addAll()` 方法或静态的 `Collections.addAll()` 方法)来创建一个允许使用所有方法的常规集合,在主方法中第一次调用 `test()` 时显示了这种情况。这种调用产生了一个新的可调整大小的底层数据结构。 + +**Collections** 类中的“unmodifiable”方法会将集合包装一个代理中,如果执行任何想要修改集合的操作,则该代理会生成 **UnsupportedOperationException** 异常。使用这些方法的目的是生成一个“常量”集合对象。稍后将描述“unmodifiable“集合方法的完整列表。 + +`test()` 中的最后一个 `check()` 用于测试**List** 的 `set()` 方法。这里,“不支持的操作”技术的粒度(granularity)就派上用场了,得到的“接口”可以通过一种方法在 `Arrays.asList()` 返回的对象和 `Collections.unmodifiableList()` 返回的对象之间变换。 `Arrays.asList()` 返回固定大小的 **List** ,而 `Collections.unmodifiableList()` 生成无法更改的 **List** 。如输出中所示, `Arrays.asList()` 返回的 **List** 中的元素是可以修改的,因为这不会违反该 **List** 的“固定大小”特性。但很明显, `unmodifiableList()` 的结果不应该以任何方式修改。如果使用接口来描述,则需要两个额外的接口,一个具有可用的 `set()` 方法,而另一个没有。 **Collection** 的各种不可修改的子类型都将需要额外的接口。 + +如果一个方法将一个集合作为它的参数,那么它的文档应该说明必须实现哪些可选方法。 ## Set和存储顺序 +[第十二章 集合]()章节中的 **Set** 有关示例对 **Set** 的基本操作做了很好的介绍。 但是,这些示例可以方便地使用预定义的 Java 类型,例如 **Integer** 和 **String** ,它们可以在集合中使用。在创建自己的类型时请注意, **Set** (以及稍后会看到的 **Map** )需要一种维护存储顺序的方法,该顺序因 **Set** 的不同实现而异。因此,不同的 **Set** 实现不仅具有不同的行为,而且它们对可以放入特定 **Set** 中的对象类型也有不同的要求: + +| **Set** 类型 | 约束 | +| :---: | :--- | +| **Set(interface)** | 添加到 **Set** 中的每个元素必须是唯一的,否则,**Set** 不会添加重复元素。添加到 **Set** 的元素必须至少定义 `equals()` 方法以建立对象唯一性。 **Set** 与 **Collection** 具有完全相同的接口。 **Set** 接口不保证它将以任何特定顺序维护其元素。 | +| **HashSet\*** | 注重快速查找元素的集合,其中元素必须定义 `hashCode()` 和 `equals()` 方法。 | +| **TreeSet** | 由树支持的有序 **Set**。这样,就可以从 **Set** 中获取有序序列,其中元素必须实现 **Comparable** 接口。 | +| **LinkedHashSet** | 具有 **HashSet** 的查找速度,但在内部使用链表维护元素的插入顺序。因此,当在遍历 **Set** 时,结果将按元素的插入顺序显示。元素必须定义 `hashCode()` 和 `equals()` 方法。 | + +**HashSet** 上的星号表示,在没有其他约束的情况下,这应该是你的默认选择,因为它针对速度进行了优化。 + +定义 `hashCode()` 方法在[附录:理解equals和hashCode方法]()中进行了描述。必须为散列和树存储结构创建 `equals()` 方法,但只有当把类放在 **HashSet** 中时才需要 `hashCode()` (当然这很有可能,因为 **HashSet** 通常应该是作为 **Set** 实现的首选)或 **LinkedHashSet** 。 但是,作为一种良好的编程风格,在覆盖 `equals()` 时应始终覆盖 `hashCode()` 。 + +下面的示例演示了成功使用具有特定 **Set** 实现的类型所需的方法: + +```java +// collectiontopics/TypesForSets.java +// Methods necessary to put your own type in a Set +import java.util.*; +import java.util.function.*; +import java.util.Objects; + +class SetType { + protected int i; + SetType(int n) { i = n; } + @Override + public boolean equals(Object o) { + return o instanceof SetType && + Objects.equals(i, ((SetType)o).i); + } + @Override + public String toString() { + return Integer.toString(i); + } +} + +class HashType extends SetType { + HashType(int n) { super(n); } + @Override + public int hashCode() { + return Objects.hashCode(i); + } +} + +class TreeType extends SetType +implements Comparable { + TreeType(int n) { super(n); } + @Override + public int compareTo(TreeType arg) { + return Integer.compare(arg.i, i); + // Equivalent to: + // return arg.i < i ? -1 : (arg.i == i ? 0 : 1); + } +} + +public class TypesForSets { + static void + fill(Set set, Function type) { + for(int i = 10; i >= 5; i--) // Descending + set.add(type.apply(i)); + for(int i = 0; i < 5; i++) // Ascending + set.add(type.apply(i)); + } + static void + test(Set set, Function type) { + fill(set, type); + fill(set, type); // Try to add duplicates + fill(set, type); + System.out.println(set); + } + public static void main(String[] args) { + test(new HashSet<>(), HashType::new); + test(new LinkedHashSet<>(), HashType::new); + test(new TreeSet<>(), TreeType::new); + // Things that don't work: + test(new HashSet<>(), SetType::new); + test(new HashSet<>(), TreeType::new); + test(new LinkedHashSet<>(), SetType::new); + test(new LinkedHashSet<>(), TreeType::new); + try { + test(new TreeSet<>(), SetType::new); + } catch(Exception e) { + System.out.println(e.getMessage()); + } + try { + test(new TreeSet<>(), HashType::new); + } catch(Exception e) { + System.out.println(e.getMessage()); + } + } +} +/* Output: +[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +[10, 9, 8, 7, 6, 5, 0, 1, 2, 3, 4] +[10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0] +[1, 6, 8, 6, 2, 7, 8, 9, 4, 10, 7, 5, 1, 3, 4, 9, 9, +10, 5, 3, 2, 0, 4, 1, 2, 0, 8, 3, 0, 10, 6, 5, 7] +[3, 1, 4, 8, 7, 6, 9, 5, 3, 0, 10, 5, 5, 10, 7, 8, 8, +9, 1, 4, 10, 2, 6, 9, 1, 6, 0, 3, 2, 0, 7, 2, 4] +[10, 9, 8, 7, 6, 5, 0, 1, 2, 3, 4, 10, 9, 8, 7, 6, 5, +0, 1, 2, 3, 4, 10, 9, 8, 7, 6, 5, 0, 1, 2, 3, 4] +[10, 9, 8, 7, 6, 5, 0, 1, 2, 3, 4, 10, 9, 8, 7, 6, 5, +0, 1, 2, 3, 4, 10, 9, 8, 7, 6, 5, 0, 1, 2, 3, 4] +SetType cannot be cast to java.lang.Comparable +HashType cannot be cast to java.lang.Comparable +*/ +``` + +为了证明特定 **Set** 需要哪些方法,同时避免代码重复,这里创建了三个类。基类 **SetType** 存储一个 **int** 值,并通过 `toString()` 方法打印它。由于存储在 **Set** 中的所有类都必须具有 `equals()` ,因此该方法也放在基类中。基于 `int i` 来判断元素是否相等。 + +**HashType** 继承自 **SetType** ,并添加了 `hashCode()` 方法,该方法对于 **Set** 的散列实现是必需的。 + +要在任何类型的有序集合中使用对象,由 **TreeType** 实现的 **Comparable** 接口都是必需的,例如 **SortedSet** ( **TreeSet** 是其唯一实现)。在 `compareTo()` 中,请注意我没有使用“简单明了”的形式: `return i-i2` 。虽然这是一个常见的编程错误,但只有当 **i** 和 **i2** 是“无符号(unsigned)”整型时才能正常工作(如果 Java 有一个“unsigned”关键字的话,不过它没有)。它破坏了 Java 的有符号 **int** ,它不足以代表两个有符号整数的差异。如果 **i** 是一个大的正整数而 **j** 是一个大的负整数, `i-j` 将溢出并返回一个负值,这不是我们所需要的。 + +通常希望 `compareTo()` 方法生成与 `equals()` 方法一致的自然顺序。如果 `equals()` 对于特定比较产生 **true**,则 `compareTo()` 应该为该比较返回结果 零,并且如果 `equals()` 为比较产生 **false** ,则 `compareTo()` 应该为该比较产生非零结果。 + +在 **TypesForSets** 中, `fill()` 和 `test()` 都是使用泛型定义的,以防止代码重复。为了验证 **Set** 的行为, `test()` 在测试集上调用 `fill()` 三次,尝试引入重复的对象。 `fill()` 方法的参数可以接收任意一个 **Set** 类型,以及生成该类型的 **Function** 对象。因为此示例中使用的所有对象都有一个带有单个 **int** 参数的构造方法,所以可以将构造方法作为此 **Function** 传递,它将提供用于填充 **Set** 的对象。 + +请注意, `fill()` 方法按降序添加前五个元素,按升序添加后五个元素,以此来指出生成的存储顺序。输出显示 **HashSet** 按升序保留元素,但是,在[附录:理解equals和hashCode方法]()中,你会发现这只是偶然的,因为散列会创建自己的存储顺序。这里只是因为元素是一个简单的 **int** ,在这种情况下它是升序的。 **LinkedHashSet** 按照插入顺序保存元素,**TreeSet** 按排序顺序维护元素(在此示例中因为 `compareTo()` 的实现方式,所以元素按降序排列。) + +特定的 **Set** 类型一般都有所必需的操作,如果尝试使用没能正确支持这些操作的类型,那么事情就会出错。将没有重新定义 `hashCode()` 方法的 **SetType** 或 **TreeType** 对象放入任何散列实现会导致重复值,因此违反了 **Set** 的主要契约。 这是相当令人不安的,因为这甚至不产生运行时错误。但是,默认的 `hashCode()` 是合法的,所以即使它是不正确的,这也是合法的行为。确保此类程序正确性的唯一可靠方法是将单元测试合并到构建系统中。 + +如果尝试在 **TreeSet** 中使用没有实现 **Comparable** 接口的类型,则会得到更明确的结果:当 **TreeSet** 尝试将对象用作一个 **Comparable** 时,将会抛出异常。 + + +### SortedSet + +**SortedSet** 中的元素保证按排序规则顺序, **SortedSet** 接口中的以下方法可以产生其他功能: + +- `Comparator comparator()` :生成用于此 **Set** 的**Comparator** 或 **null** 来用于自然排序。 +- `Object first()` :返回第一个元素。 +- `Object last()` :返回最后一个元素。 +- `SortedSet subSet(fromElement,toElement)` :使用 **fromElement** (包含)和 **toElement** (不包括)中的元素生成此 **Set** 的一个视图。 +- `SortedSet headSet(toElement)` :使用顺序在 **toElement** 之前的元素生成此 **Set** 的一个视图。 +- `SortedSet tailSet(fromElement)` :使用顺序在 **fromElement** 之后(包含 **fromElement** )的元素生成此 **Set** 的一个视图。 + +下面是一个简单的演示: + +```java +// collectiontopics/SortedSetDemo.java +import java.util.*; +import static java.util.stream.Collectors.*; + +public class SortedSetDemo { + public static void main(String[] args) { + SortedSet sortedSet = + Arrays.stream( + "one two three four five six seven eight" + .split(" ")) + .collect(toCollection(TreeSet::new)); + System.out.println(sortedSet); + String low = sortedSet.first(); + String high = sortedSet.last(); + System.out.println(low); + System.out.println(high); + Iterator it = sortedSet.iterator(); + for(int i = 0; i <= 6; i++) { + if(i == 3) low = it.next(); + if(i == 6) high = it.next(); + else it.next(); + } + System.out.println(low); + System.out.println(high); + System.out.println(sortedSet.subSet(low, high)); + System.out.println(sortedSet.headSet(high)); + System.out.println(sortedSet.tailSet(low)); + } +} +/* Output: +[eight, five, four, one, seven, six, three, two] +eight +two +one +two +[one, seven, six, three] +[eight, five, four, one, seven, six, three] +[one, seven, six, three, two] +*/ +``` + +注意, **SortedSet** 表示“根据对象的比较函数进行排序”,而不是“根据插入顺序”。可以使用 **LinkedHashSet** 保留元素的插入顺序。 ## 队列 +有许多 **Queue** 实现,其中大多数是为并发应用程序设计的。许多实现都是通过排序行为而不是性能来区分的。这是一个涉及大多数 **Queue** 实现的基本示例,包括基于并发的队列。队列将元素从一端放入并从另一端取出: + +```java +// collectiontopics/QueueBehavior.java +// Compares basic behavior +import java.util.*; +import java.util.stream.*; +import java.util.concurrent.*; + +public class QueueBehavior { + static Stream strings() { + return Arrays.stream( + ("one two three four five six seven " + + "eight nine ten").split(" ")); + } + static void test(int id, Queue queue) { + System.out.print(id + ": "); + strings().map(queue::offer).count(); + while(queue.peek() != null) + System.out.print(queue.remove() + " "); + System.out.println(); + } + public static void main(String[] args) { + int count = 10; + test(1, new LinkedList<>()); + test(2, new PriorityQueue<>()); + test(3, new ArrayBlockingQueue<>(count)); + test(4, new ConcurrentLinkedQueue<>()); + test(5, new LinkedBlockingQueue<>()); + test(6, new PriorityBlockingQueue<>()); + test(7, new ArrayDeque<>()); + test(8, new ConcurrentLinkedDeque<>()); + test(9, new LinkedBlockingDeque<>()); + test(10, new LinkedTransferQueue<>()); + test(11, new SynchronousQueue<>()); + } +} +/* Output: +1: one two three four five six seven eight nine ten +2: eight five four nine one seven six ten three two +3: one two three four five six seven eight nine ten +4: one two three four five six seven eight nine ten +5: one two three four five six seven eight nine ten +6: eight five four nine one seven six ten three two +7: one two three four five six seven eight nine ten +8: one two three four five six seven eight nine ten +9: one two three four five six seven eight nine ten +10: one two three four five six seven eight nine ten +11: +*/ +``` + +**Deque** 接口也继承自 **Queue** 。 除优先级队列外,**Queue** 按照元素的插入顺序生成元素。 在此示例中,**SynchronousQueue** 不会产生任何结果,因为它是一个阻塞队列,其中每个插入操作必须等待另一个线程执行相应的删除操作,反之亦然。 + + +### 优先级队列 + +考虑一个待办事项列表,其中每个对象包含一个 **String** 以及主要和次要优先级值。通过实现 **Comparable** 接口来控制此待办事项列表的顺序: + +```java +// collectiontopics/ToDoList.java +// A more complex use of PriorityQueue +import java.util.*; + +class ToDoItem implements Comparable { + private char primary; + private int secondary; + private String item; + ToDoItem(String td, char pri, int sec) { + primary = pri; + secondary = sec; + item = td; + } + @Override + public int compareTo(ToDoItem arg) { + if(primary > arg.primary) + return +1; + if(primary == arg.primary) + if(secondary > arg.secondary) + return +1; + else if(secondary == arg.secondary) + return 0; + return -1; + } + @Override + public String toString() { + return Character.toString(primary) + + secondary + ": " + item; + } +} + +class ToDoList { + public static void main(String[] args) { + PriorityQueue toDo = + new PriorityQueue<>(); + toDo.add(new ToDoItem("Empty trash", 'C', 4)); + toDo.add(new ToDoItem("Feed dog", 'A', 2)); + toDo.add(new ToDoItem("Feed bird", 'B', 7)); + toDo.add(new ToDoItem("Mow lawn", 'C', 3)); + toDo.add(new ToDoItem("Water lawn", 'A', 1)); + toDo.add(new ToDoItem("Feed cat", 'B', 1)); + while(!toDo.isEmpty()) + System.out.println(toDo.remove()); + } +} +/* Output: +A1: Water lawn +A2: Feed dog +B1: Feed cat +B7: Feed bird +C3: Mow lawn +C4: Empty trash +*/ +``` + +这展示了通过优先级队列自动排序待办事项。 + + +### 双端队列 + +**Deque** (双端队列)就像一个队列,但是可以从任一端添加和删除元素。 Java 6为 **Deque** 添加了一个显式接口。以下是对实现了 **Deque** 的类的最基本的 **Deque** 方法的测试: + +```java +// collectiontopics/SimpleDeques.java +// Very basic test of Deques +import java.util.*; +import java.util.concurrent.*; +import java.util.function.*; + +class CountString implements Supplier { + private int n = 0; + CountString() {} + CountString(int start) { n = start; } + @Override + public String get() { + return Integer.toString(n++); + } +} + +public class SimpleDeques { + static void test(Deque deque) { + CountString s1 = new CountString(), + s2 = new CountString(20); + for(int n = 0; n < 8; n++) { + deque.offerFirst(s1.get()); + deque.offerLast(s2.get()); // Same as offer() + } + System.out.println(deque); + String result = ""; + while(deque.size() > 0) { + System.out.print(deque.peekFirst() + " "); + result += deque.pollFirst() + " "; + System.out.print(deque.peekLast() + " "); + result += deque.pollLast() + " "; + } + System.out.println("\n" + result); + } + public static void main(String[] args) { + int count = 10; + System.out.println("LinkedList"); + test(new LinkedList<>()); + System.out.println("ArrayDeque"); + test(new ArrayDeque<>()); + System.out.println("LinkedBlockingDeque"); + test(new LinkedBlockingDeque<>(count)); + System.out.println("ConcurrentLinkedDeque"); + test(new ConcurrentLinkedDeque<>()); + } +} +/* Output: +LinkedList +[7, 6, 5, 4, 3, 2, 1, 0, 20, 21, 22, 23, 24, 25, 26, +27] +7 27 6 26 5 25 4 24 3 23 2 22 1 21 0 20 +7 27 6 26 5 25 4 24 3 23 2 22 1 21 0 20 +ArrayDeque +[7, 6, 5, 4, 3, 2, 1, 0, 20, 21, 22, 23, 24, 25, 26, +27] +7 27 6 26 5 25 4 24 3 23 2 22 1 21 0 20 +7 27 6 26 5 25 4 24 3 23 2 22 1 21 0 20 +LinkedBlockingDeque +[4, 3, 2, 1, 0, 20, 21, 22, 23, 24] +4 24 3 23 2 22 1 21 0 20 +4 24 3 23 2 22 1 21 0 20 +ConcurrentLinkedDeque +[7, 6, 5, 4, 3, 2, 1, 0, 20, 21, 22, 23, 24, 25, 26, +27] +7 27 6 26 5 25 4 24 3 23 2 22 1 21 0 20 +7 27 6 26 5 25 4 24 3 23 2 22 1 21 0 20 +*/ +``` + +我只使用了 **Deque** 方法的“offer”和“poll”版本,因为当 **LinkedBlockingDeque** 的大小有限时,这些方法不会抛出异常。请注意, **LinkedBlockingDeque** 仅填充到它的限制大小为止,然后忽略额外的添加。 ## 理解Map +正如在[第十二章 集合]()章节中所了解到的,**Map**(也称为 *关联数组* )维护键值关联(对),因此可以使用键来查找值。标准 Java 库包含不同的 **Map** 基本实现,例如 **HashMap** , **TreeMap** , **LinkedHashMap** , **WeakHashMap** , **ConcurrentHashMap** 和 **IdentityHashMap** 。 它们都具有相同的基本 **Map** 接口,但它们的行为不同,包括效率,键值对的保存顺序和呈现顺序,保存对象的时间,如何在多线程程序中工作,以及如何确定键的相等性。 **Map** 接口的实现数量应该告诉你一些关于此工具重要性的信息。 + +为了更深入地了解 **Map** ,学习如何构造关联数组会很有帮助。下面是一个非常简单的实现: + +```java +// collectiontopics/AssociativeArray.java +// Associates keys with values + +public class AssociativeArray { + private Object[][] pairs; + private int index; + public AssociativeArray(int length) { + pairs = new Object[length][2]; + } + public void put(K key, V value) { + if(index >= pairs.length) + throw new ArrayIndexOutOfBoundsException(); + pairs[index++] = new Object[]{ key, value }; + } + @SuppressWarnings("unchecked") + public V get(K key) { + for(int i = 0; i < index; i++) + if(key.equals(pairs[i][0])) + return (V)pairs[i][1]; + return null; // Did not find key + } + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + for(int i = 0; i < index; i++) { + result.append(pairs[i][0].toString()); + result.append(" : "); + result.append(pairs[i][1].toString()); + if(i < index - 1) + result.append("\n"); + } + return result.toString(); + } + public static void main(String[] args) { + AssociativeArray map = + new AssociativeArray<>(6); + map.put("sky", "blue"); + map.put("grass", "green"); + map.put("ocean", "dancing"); + map.put("tree", "tall"); + map.put("earth", "brown"); + map.put("sun", "warm"); + try { + map.put("extra", "object"); // Past the end + } catch(ArrayIndexOutOfBoundsException e) { + System.out.println("Too many objects!"); + } + System.out.println(map); + System.out.println(map.get("ocean")); + } +} +/* Output: +Too many objects! +sky : blue +grass : green +ocean : dancing +tree : tall +earth : brown +sun : warm +dancing +*/ +``` + +关联数组中的基本方法是 `put()` 和 `get()` ,但为了便于显示,重写了 `toString()` 方法以打印键值对。为了显示它的工作原理,主方法加载一个带有字符串对的 **AssociativeArray** 并打印生成的映射,然后调用其中一个值的 `get()` 方法。 + +要使用 `get()` 方法,可以传入要查找的 **key** ,它将生成相关联的值作为结果,如果找不到则返回 **null** 。 `get()` 方法使用可能是效率最低的方法来定位值:从数组的头部开始并使用 `equals()` 来比较键。但这里是侧重于简单,而不是效率。 + +这个版本很有启发性,但它不是很有效,而且它只有一个固定的大小,这是不灵活的。幸运的是, **java.util** 中的那些 **Map** 没有这些问题。 + + +### 性能 + +性能是 **Map** 的基本问题,在 `get()` 中使用线性方法搜索一个键时会非常慢。这就是 **HashMap** 要加速的地方。它使用一个称为 *哈希码* 的特殊值来替代慢速搜索一个键。哈希码是一种从相关对象中获取一些信息并将其转换为该对象的“相对唯一” **int** 的方法。 `hashCode()` 是根类 **Object** 中的一个方法,因此所有 Java 对象都可以生成哈希码。 **HashMap** 获取对象的 `hashCode()` 并使用它来快速搜索键。这就使得性能有了显著的提升。[^3] + +以下是基本的 **Map** 实现。 **HashMap**上的星号表示,在没有其他约束的情况下,这应该是你的默认选择,因为它针对速度进行了优化。其他实现强调其他特性,因此不如 **HashMap** 快。 + +| **Map** 实现 | 描述 | +| :---: | :--- | +| **HashMap\*** | 基于哈希表的实现。(使用此类来代替 **Hashtable** 。)为插入和定位键值对提供了常数时间性能。可以通过构造方法调整性能,这些构造方法允许你设置哈希表的容量和装填因子。 | +| **LinkedHashMap** | 与 **HashMap** 类似,但是当遍历时,可以按插入顺序或最近最少使用(LRU)顺序获取键值对。只比 **HashMap** 略慢,一个例外是在迭代时,由于其使用链表维护内部顺序,所以会更快些。 | +| **TreeMap** | 基于红黑树的实现。当查看键或键值对时,它们按排序顺序(由 **Comparable** 或 **Comparator** 确定)。 **TreeMap** 的侧重点是按排序顺序获得结果。 **TreeMap** 是唯一使用 `subMap()` 方法的 **Map** ,它返回红黑树的一部分。 | +| **WeakHashMap** | 一种具有 *弱键*(weak keys) 的 **Map** ,为了解决某些类型的问题,它允许释放 **Map** 所引用的对象。如果在 **Map** 外没有对特定键的引用,则可以对该键进行垃圾回收。 | +| **ConcurrentHashMap** | 不使用同步锁定的线程安全 **Mao** 。这在[第二十四章 并发编程]() 一章中讨论。 | +| **IdentityHashMap** | 使用 `==` 而不是 `equals()` 来比较键。仅用于解决特殊问题,不适用于一般用途。 | + +散列是在 **Map** 中存储元素的最常用方法。 + +**Map** 中使用的键的要求与 **Set** 中的元素的要求相同。可以在 **TypesForSets.java** 中看到这些。任何键必须具有 `equals()` 方法。如果键用于散列映射,则它还必须具有正确的 `hashCode()` 方法。如果键在 **TreeMap** 中使用,则必须实现 **Comparable** 接口。 + +以下示例使用先前定义的 **CountMap** 测试数据集显示通过 **Map** 接口可用的操作: + +```java +// collectiontopics/MapOps.java +// Things you can do with Maps +import java.util.concurrent.*; +import java.util.*; +import onjava.*; + +public class MapOps { + public static + void printKeys(Map map) { + System.out.print("Size = " + map.size() + ", "); + System.out.print("Keys: "); + // Produce a Set of the keys: + System.out.println(map.keySet()); + } + public static + void test(Map map) { + System.out.println( + map.getClass().getSimpleName()); + map.putAll(new CountMap(25)); + // Map has 'Set' behavior for keys: + map.putAll(new CountMap(25)); + printKeys(map); + // Producing a Collection of the values: + System.out.print("Values: "); + System.out.println(map.values()); + System.out.println(map); + System.out.println("map.containsKey(11): " + + map.containsKey(11)); + System.out.println( + "map.get(11): " + map.get(11)); + System.out.println("map.containsValue(\"F0\"): " + + map.containsValue("F0")); + Integer key = map.keySet().iterator().next(); + System.out.println("First key in map: " + key); + map.remove(key); + printKeys(map); + map.clear(); + System.out.println( + "map.isEmpty(): " + map.isEmpty()); + map.putAll(new CountMap(25)); + // Operations on the Set change the Map: + map.keySet().removeAll(map.keySet()); + System.out.println( + "map.isEmpty(): " + map.isEmpty()); + } + public static void main(String[] args) { + test(new HashMap<>()); + test(new TreeMap<>()); + test(new LinkedHashMap<>()); + test(new IdentityHashMap<>()); + test(new ConcurrentHashMap<>()); + test(new WeakHashMap<>()); + } +} +/* Output: (First 11 Lines) +HashMap +Size = 25, Keys: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, +12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24] +Values: [A0, B0, C0, D0, E0, F0, G0, H0, I0, J0, K0, +L0, M0, N0, O0, P0, Q0, R0, S0, T0, U0, V0, W0, X0, Y0] +{0=A0, 1=B0, 2=C0, 3=D0, 4=E0, 5=F0, 6=G0, 7=H0, 8=I0, +9=J0, 10=K0, 11=L0, 12=M0, 13=N0, 14=O0, 15=P0, 16=Q0, +17=R0, 18=S0, 19=T0, 20=U0, 21=V0, 22=W0, 23=X0, 24=Y0} +map.containsKey(11): true +map.get(11): L0 +map.containsValue("F0"): true +First key in map: 0 +Size = 24, Keys: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, +12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24] +map.isEmpty(): true +map.isEmpty(): true + ... +*/ +``` + +`printKeys()` 方法演示了如何生成 **Map** 的 **Collection** 视图。 `keySet()` 方法生成一个由 **Map** 中的键组成的 **Set** 。 打印 `values()` 方法的结果会生成一个包含 **Map** 中所有值的 **Collection** 。(请注意,键必须是唯一的,但值可以包含重复项。)由于这些 **Collection** 由 **Map** 支持,因此 **Collection** 中的任何更改都会反映在所关联的 **Map** 中。 + +程序的其余部分提供了每个 **Map** 操作的简单示例,并测试了每种基本类型的 **Map** 。 + + +### SortedMap + +使用 **SortedMap** (由 **TreeMap** 或 **ConcurrentSkipListMap** 实现),键保证按排序顺序,这允许在 **SortedMap** 接口中使用这些方法来提供其他功能: + +- `Comparator comparator()` :生成用于此 **Map** 的比较器, **null** 表示自然排序。 +- `T firstKey()` :返回第一个键。 +- `T lastKey()` :返回最后一个键。 +- `SortedMap subMap(fromKey,toKey)` :生成此 **Map** 的视图,其中键从 **fromKey**(包括),到 **toKey** (不包括)。 +- `SortedMap headMap(toKey)` :使用小于 **toKey** 的键生成此 **Map** 的视图。 +- `SortedMap tailMap(fromKey)` :使用大于或等于 **fromKey** 的键生成此 **Map** 的视图。 + +这是一个类似于 **SortedSetDemo.java** 的示例,显示了 **TreeMap** 的这种额外行为: + +```java +// collectiontopics/SortedMapDemo.java +// What you can do with a TreeMap +import java.util.*; +import onjava.*; + +public class SortedMapDemo { + public static void main(String[] args) { + TreeMap sortedMap = + new TreeMap<>(new CountMap(10)); + System.out.println(sortedMap); + Integer low = sortedMap.firstKey(); + Integer high = sortedMap.lastKey(); + System.out.println(low); + System.out.println(high); + Iterator it = + sortedMap.keySet().iterator(); + for(int i = 0; i <= 6; i++) { + if(i == 3) low = it.next(); + if(i == 6) high = it.next(); + else it.next(); + } + System.out.println(low); + System.out.println(high); + System.out.println(sortedMap.subMap(low, high)); + System.out.println(sortedMap.headMap(high)); + System.out.println(sortedMap.tailMap(low)); + } +} +/* Output: +{0=A0, 1=B0, 2=C0, 3=D0, 4=E0, 5=F0, 6=G0, 7=H0, 8=I0, +9=J0} +0 +9 +3 +7 +{3=D0, 4=E0, 5=F0, 6=G0} +{0=A0, 1=B0, 2=C0, 3=D0, 4=E0, 5=F0, 6=G0} +{3=D0, 4=E0, 5=F0, 6=G0, 7=H0, 8=I0, 9=J0} +*/ +``` + +这里,键值对按照键的排序顺序进行排序。因为 **TreeMap** 中存在顺序感,所以“位置”的概念很有意义,因此可以拥有第一个、最后一个元素或子图。 + + +### LinkedHashMap + +**LinkedHashMap** 针对速度进行哈希处理,但在遍历期间也会按插入顺序生成键值对( `System.out.println()` 可以遍历它,因此可以看到遍历的结果)。 此外,可以在构造方法中配置 **LinkedHashMap** 以使用基于访问的 *最近最少使用*(LRU) 算法,因此未访问的元素(因此是删除的候选者)会出现在列表的前面。 这样可以轻松创建一个能够定期清理以节省空间的程序。下面是一个显示这两个功能的简单示例: + +```java +// collectiontopics/LinkedHashMapDemo.java +// What you can do with a LinkedHashMap +import java.util.*; +import onjava.*; + +public class LinkedHashMapDemo { + public static void main(String[] args) { + LinkedHashMap linkedMap = + new LinkedHashMap<>(new CountMap(9)); + System.out.println(linkedMap); + // Least-recently-used order: + linkedMap = + new LinkedHashMap<>(16, 0.75f, true); + linkedMap.putAll(new CountMap(9)); + System.out.println(linkedMap); + for(int i = 0; i < 6; i++) + linkedMap.get(i); + System.out.println(linkedMap); + linkedMap.get(0); + System.out.println(linkedMap); + } +} +/* Output: +{0=A0, 1=B0, 2=C0, 3=D0, 4=E0, 5=F0, 6=G0, 7=H0, 8=I0} +{0=A0, 1=B0, 2=C0, 3=D0, 4=E0, 5=F0, 6=G0, 7=H0, 8=I0} +{6=G0, 7=H0, 8=I0, 0=A0, 1=B0, 2=C0, 3=D0, 4=E0, 5=F0} +{6=G0, 7=H0, 8=I0, 1=B0, 2=C0, 3=D0, 4=E0, 5=F0, 0=A0} +*/ +``` + +这些键值对确实是按照插入顺序进行遍历,即使对于LRU版本也是如此。 但是,在LRU版本中访问前六项(仅限)后,最后三项将移至列表的前面。然后,当再次访问“ **0** ”后,它移动到了列表的后面。 ## 集合工具类 +集合有许多独立的实用工具程序,在 **java.util.Collections** 中表示为静态方法。之前已经见过其中一些,例如 `addAll()` , `reverseOrder()` 和 `binarySearch()` 。以下是其他内容(同步和不可修改的实用工具程序将在后面的章节中介绍)。在此表中,在需要的时候使用了泛型: + +| 方法 | 描述 | +| :--- | :--- | +| **checkedCollection(Collection\ c, Class\ type)**

**checkedList(List\ list, Class\ type)**

**checkedMap(Map\ m, Class\ keyType, Class\ valueType)**

**checkedSet(Set\ s, Class\ type)**

**checkedSortedMap(SortedMap\ m, Class\ keyType, Class\ valueType)**

**checkedSortedSet(SortedSet\ s, Class\ type)** | 生成 **Collection** 的动态类型安全视图或 **Collection** 的特定子类型。 当无法使用静态检查版本时使用这个版本。

这些方法的使用在[第九章 多态]()章节的“动态类型安全”标题下进行了展示。 | +| **max(Collection)**

**min(Collection)** | 使用 **Collection** 中对象的自然比较方法生成参数集合中的最大或最小元素。 | +| **max(Collection, Comparator)**

**min(Collection, Comparator)** | 使用 **Comparator** 指定的比较方法生成参数集合中的最大或最小元素。 | +| **indexOfSubList(List source, List target)** | 返回 **target** 在 **source** 内第一次出现的起始索引,如果不存在则返回 -1。 | +| **lastIndexOfSubList(List source, List target)** | 返回 **target** 在 **source** 内最后一次出现的起始索引,如果不存在则返回 -1。 | +| **replaceAll(List\ list, T oldVal, T newVal)** | 用 **newVal** 替换列表中所有的 **oldVal** 。 | +| **reverse(List)**| 反转列表 | +| **reverseOrder()**

**reverseOrder(Comparator\)** | 返回一个 **Comparator** ,它与集合中实现了 **comparable\** 接口的对象的自然顺序相反。第二个版本颠倒了所提供的 **Comparator** 的顺序。 | +| **rotate(List, int distance)** | 将所有元素向前移动 **distance** ,将尾部的元素移到开头。(译者注:即循环移动) | +| **shuffle(List)**

**shuffle(List, Random)** | 随机置换指定列表(即打乱顺序)。第一个版本使用了默认的随机化源,或者也可以使用第二个版本,提供自己的随机化源。 | +| **sort(List\)**

**sort(List\, Comparator\ c)** | 第一个版本使用元素的自然顺序排序该 **List\** 。第二个版本根据提供的 **Comparator** 排序。 | +| **copy(List\ dest, List\ src)** | 将 **src** 中的元素复制到 **dest** 。 | +| **swap(List, int i, int j)** | 交换 **List** 中位置 **i** 和 位置 **j** 的元素。可能比你手工编写的速度快。 | +| **fill(List\, T x)** | 用 **x** 替换 **List** 中的所有元素。| +| **nCopies(int n, T x)** | 返回大小为 **n** 的不可变 **List\** ,其引用都指向 **x** 。 | +| **disjoint(Collection, Collection)** | 如果两个集合没有共同元素,则返回 **true** 。 | +| **frequency(Collection, Object x)** | 返回 **Collection** 中,等于 **x** 的元素个数。 | +| **emptyList()**

**emptyMap()**

**emptySet()** | 返回不可变的空 **List** , **Map** 或 **Set** 。这些是泛型的,因此生成的 **Collection** 可以被参数化为所需的类型。 | +| **singleton(T x)**

**singletonList(T x)**

**singletonMap(K key, V value)** | 生成一个不可变的 **List** , **Set** 或 **Map** ,其中只包含基于给定参数的单个元素。 | +| **list(Enumeration\ e)** | 生成一个 **ArrayList\** ,其中元素为(旧式) **Enumeration** ( **Iterator** 的前身)中的元素。用于从遗留代码向新式转换。 | +| **enumeration(Collection\)** | 为参数集合生成一个旧式的 **Enumeration\** 。 | + +请注意, `min()` 和 `max()` 使用 **Collection** 对象,而不使用 **List** ,因此不必担心是否应对 **Collection** 进行排序。(如前所述,在执行 `binarySearch()` 之前,将会对 **List** 或数组进行`sort()` 排序。) + +下面是一个示例,展示了上表中大多数实用工具程序的基本用法: + +```java +// collectiontopics/Utilities.java +// Simple demonstrations of the Collections utilities +import java.util.*; + +public class Utilities { + static List list = Arrays.asList( + "one Two three Four five six one".split(" ")); + public static void main(String[] args) { + System.out.println(list); + System.out.println("'list' disjoint (Four)?: " + + Collections.disjoint(list, + Collections.singletonList("Four"))); + System.out.println( + "max: " + Collections.max(list)); + System.out.println( + "min: " + Collections.min(list)); + System.out.println( + "max w/ comparator: " + Collections.max(list, + String.CASE_INSENSITIVE_ORDER)); + System.out.println( + "min w/ comparator: " + Collections.min(list, + String.CASE_INSENSITIVE_ORDER)); + List sublist = + Arrays.asList("Four five six".split(" ")); + System.out.println("indexOfSubList: " + + Collections.indexOfSubList(list, sublist)); + System.out.println("lastIndexOfSubList: " + + Collections.lastIndexOfSubList(list, sublist)); + Collections.replaceAll(list, "one", "Yo"); + System.out.println("replaceAll: " + list); + Collections.reverse(list); + System.out.println("reverse: " + list); + Collections.rotate(list, 3); + System.out.println("rotate: " + list); + List source = + Arrays.asList("in the matrix".split(" ")); + Collections.copy(list, source); + System.out.println("copy: " + list); + Collections.swap(list, 0, list.size() - 1); + System.out.println("swap: " + list); + Collections.shuffle(list, new Random(47)); + System.out.println("shuffled: " + list); + Collections.fill(list, "pop"); + System.out.println("fill: " + list); + System.out.println("frequency of 'pop': " + + Collections.frequency(list, "pop")); + List dups = + Collections.nCopies(3, "snap"); + System.out.println("dups: " + dups); + System.out.println("'list' disjoint 'dups'?: " + + Collections.disjoint(list, dups)); + // Getting an old-style Enumeration: + Enumeration e = + Collections.enumeration(dups); + Vector v = new Vector<>(); + while(e.hasMoreElements()) + v.addElement(e.nextElement()); + // Converting an old-style Vector + // to a List via an Enumeration: + ArrayList arrayList = + Collections.list(v.elements()); + System.out.println("arrayList: " + arrayList); + } +} +/* Output: +[one, Two, three, Four, five, six, one] +'list' disjoint (Four)?: false +max: three +min: Four +max w/ comparator: Two +min w/ comparator: five +indexOfSubList: 3 +lastIndexOfSubList: 3 +replaceAll: [Yo, Two, three, Four, five, six, Yo] +reverse: [Yo, six, five, Four, three, Two, Yo] +rotate: [three, Two, Yo, Yo, six, five, Four] +copy: [in, the, matrix, Yo, six, five, Four] +swap: [Four, the, matrix, Yo, six, five, in] +shuffled: [six, matrix, the, Four, Yo, five, in] +fill: [pop, pop, pop, pop, pop, pop, pop] +frequency of 'pop': 7 +dups: [snap, snap, snap] +'list' disjoint 'dups'?: true +arrayList: [snap, snap, snap] +*/ +``` + +输出解释了每种实用方法的行为。请注意由于大小写的缘故,普通版本的 `min()` 和 `max()` 与带有 **String.CASE_INSENSITIVE_ORDER** 比较器参数的版本的区别。 + + +### 排序和搜索列表 + +用于执行排序和搜索 **List** 的实用工具程序与用于排序对象数组的程序具有相同的名字和方法签名,只不过是 **Collections** 的静态方法而不是 **Arrays** 。 这是一个使用 **Utilities.java** 中的 **list** 数据的示例: + +```java +// collectiontopics/ListSortSearch.java +// Sorting/searching Lists with Collections utilities +import java.util.*; + +public class ListSortSearch { + public static void main(String[] args) { + List list = + new ArrayList<>(Utilities.list); + list.addAll(Utilities.list); + System.out.println(list); + Collections.shuffle(list, new Random(47)); + System.out.println("Shuffled: " + list); + // Use ListIterator to trim off last elements: + ListIterator it = list.listIterator(10); + while(it.hasNext()) { + it.next(); + it.remove(); + } + System.out.println("Trimmed: " + list); + Collections.sort(list); + System.out.println("Sorted: " + list); + String key = list.get(7); + int index = Collections.binarySearch(list, key); + System.out.println( + "Location of " + key + " is " + index + + ", list.get(" + index + ") = " + + list.get(index)); + Collections.sort(list, + String.CASE_INSENSITIVE_ORDER); + System.out.println( + "Case-insensitive sorted: " + list); + key = list.get(7); + index = Collections.binarySearch(list, key, + String.CASE_INSENSITIVE_ORDER); + System.out.println( + "Location of " + key + " is " + index + + ", list.get(" + index + ") = " + + list.get(index)); + } +} +/* Output: +[one, Two, three, Four, five, six, one, one, Two, +three, Four, five, six, one] +Shuffled: [Four, five, one, one, Two, six, six, three, +three, five, Four, Two, one, one] +Trimmed: [Four, five, one, one, Two, six, six, three, +three, five] +Sorted: [Four, Two, five, five, one, one, six, six, +three, three] +Location of six is 7, list.get(7) = six +Case-insensitive sorted: [five, five, Four, one, one, +six, six, three, three, Two] +Location of three is 7, list.get(7) = three +*/ +``` + +就像使用数组进行搜索和排序一样,如果使用 **Comparator** 进行排序,则必须使用相同的 **Comparator** 执行 `binarySearch()` 。 + +该程序还演示了 **Collections** 中的 `shuffle()` 方法,该方法随机打乱了 **List** 的顺序。 **ListIterator** 是在打乱后的列表中的特定位置创建的,用于从该位置删除元素,直到列表末尾。 + + +### 创建不可修改的 Collection 或 Map + +通常,创建 **Collection** 或 **Map** 的只读版本会很方便。 **Collections** 类通过将原始集合传递给一个方法然后返回一个只读版本的集合。 对于 **Collection** (如果不能将 **Collection** 视为更具体的类型), **List** , **Set** 和 **Map** ,这类方法有许多变体。这个示例展示了针对每种类型,正确构建只读版本集合的方法: + +```java +// collectiontopics/ReadOnly.java +// Using the Collections.unmodifiable methods +import java.util.*; +import onjava.*; + +public class ReadOnly { + static Collection data = + new ArrayList<>(Countries.names(6)); + public static void main(String[] args) { + Collection c = + Collections.unmodifiableCollection( + new ArrayList<>(data)); + System.out.println(c); // Reading is OK + //- c.add("one"); // Can't change it + + List a = Collections.unmodifiableList( + new ArrayList<>(data)); + ListIterator lit = a.listIterator(); + System.out.println(lit.next()); // Reading is OK + //- lit.add("one"); // Can't change it + + Set s = Collections.unmodifiableSet( + new HashSet<>(data)); + System.out.println(s); // Reading is OK + //- s.add("one"); // Can't change it + + // For a SortedSet: + Set ss = + Collections.unmodifiableSortedSet( + new TreeSet<>(data)); + + Map m = + Collections.unmodifiableMap( + new HashMap<>(Countries.capitals(6))); + System.out.println(m); // Reading is OK + //- m.put("Ralph", "Howdy!"); + + // For a SortedMap: + Map sm = + Collections.unmodifiableSortedMap( + new TreeMap<>(Countries.capitals(6))); + } +} +/* Output: +[ALGERIA, ANGOLA, BENIN, BOTSWANA, BURKINA FASO, +BURUNDI] +ALGERIA +[BENIN, BOTSWANA, ANGOLA, BURKINA FASO, ALGERIA, +BURUNDI] +{BENIN=Porto-Novo, BOTSWANA=Gaberone, ANGOLA=Luanda, +BURKINA FASO=Ouagadougou, ALGERIA=Algiers, +BURUNDI=Bujumbura} +*/ +``` + +为特定类型调用 “unmodifiable” 方法不会导致编译时检查,但是一旦发生转换,对修改特定集合内容的任何方法调用都将产生 **UnsupportedOperationException** 异常。 + +在每种情况下,在将集合设置为只读之前,必须使用有意义的数据填充集合。填充完成后,最好的方法是用 “unmodifiable” 方法调用生成的引用替换现有引用。这样,一旦使得内容无法修改,那么就不会冒有意外更改内容的风险。另一方面,此工具还允许将可修改的集合保留为类中的**私有**集合,并从方法调用处返回对该集合的只读引用。所以,你可以在类内修改它,但其他人只能读它。 + + +### 同步 Collection 或 Map + +**synchronized** 关键字是多线程主题的重要组成部分,更复杂的内容在[第二十四章 并发编程]()中介绍。在这里,只需要注意到 **Collections** 类包含一种自动同步整个集合的方法。 语法类似于 “unmodifiable” 方法: + +```java +// collectiontopics/Synchronization.java +// Using the Collections.synchronized methods +import java.util.*; + +public class Synchronization { + public static void main(String[] args) { + Collection c = + Collections.synchronizedCollection( + new ArrayList<>()); + List list = Collections + .synchronizedList(new ArrayList<>()); + Set s = Collections + .synchronizedSet(new HashSet<>()); + Set ss = Collections + .synchronizedSortedSet(new TreeSet<>()); + Map m = Collections + .synchronizedMap(new HashMap<>()); + Map sm = Collections + .synchronizedSortedMap(new TreeMap<>()); + } +} +``` + +最好立即通过适当的 “synchronized” 方法传递新集合,如上所示。这样,就不会意外地暴露出非同步版本。 + + +#### Fail Fast + +Java 集合还具有防止多个进程修改集合内容的机制。如果当前正在迭代集合,然后有其他一些进程介入并插入,删除或更改该集合中的对象,则会出现此问题。也许在集合中已经遍历过了那个元素,也许还没有遍历到,也许在调用 `size()` 之后集合的大小会缩小...有许多灾难情景。 Java 集合库使用一种 *fail-fast* 的机制,该机制可以检测到除了当前进程引起的更改之外,其它任何对集合的更改操作。如果它检测到其他人正在修改集合,则会立即生成 **ConcurrentModificationException** 异常。这就是“fail-fast”的含义——它不会在以后使用更复杂的算法尝试检测问题(快速失败)。 + +通过创建迭代器并向迭代器指向的集合中添加元素,可以很容易地看到操作中的 fail-fast 机制,如下所示: + +```java +// collectiontopics/FailFast.java +// Demonstrates the "fail-fast" behavior +import java.util.*; + +public class FailFast { + public static void main(String[] args) { + Collection c = new ArrayList<>(); + Iterator it = c.iterator(); + c.add("An object"); + try { + String s = it.next(); + } catch(ConcurrentModificationException e) { + System.out.println(e); + } + } +} +/* Output: +java.util.ConcurrentModificationException +*/ +``` + +异常来自于在从集合中获得迭代器之后,又尝试在集合中添加元素。程序的两个部分可能会修改同一个集合,这种可能性的存在会产生不确定状态,因此异常会通知你更改代码。在这种情况下,应先将所有元素添加到集合,然后再获取迭代器。 + +**ConcurrentHashMap** , **CopyOnWriteArrayList** 和 **CopyOnWriteArraySet** 使用了特定的技术来避免产生 **ConcurrentModificationException** 异常。 ## 持有引用 +**java.lang.ref** 中库包含一组类,这些类允许垃圾收集具有更大的灵活性。特别是当拥有可能导致内存耗尽的大对象时,这些类特别有用。这里有三个从抽象类 **Reference** 继承来的类: **SoftReference** (软引用), **WeakReference** (弱引用)和 **PhantomReference** (虚引用)继承了三个类。如果一个对象只能通过这其中的一个 **Reference** 对象访问,那么这三种类型每个都为垃圾收集器提供不同级别的间接引用(indirection)。 + +如果一个对象是 *可达的*(reachable),那么意味着在程序中的某个位置可以找到该对象。这可能意味着在栈上有一个直接引用该对象的普通引用,但也有可能是引用了一个对该对象有引用的对象,这可以有很多中间环节。如果某个对象是可达的,则垃圾收集器无法释放它,因为它仍然被程序所使用。如果某个对象是不可达的,则程序无法使用它,那么垃圾收集器回收该对象就是安全的。 + +使用 **Reference** 对象继续保持对该对象的引用,以到达该对象,但也允许垃圾收集器释放该对象。因此,程序可以使用该对象,但如果内存即将耗尽,则允许释放该对象。 + +可以通过使用 **Reference** 对象作为你和普通引用之间的中介(代理)来实现此目的。此外,必须没有对象的普通引用(未包含在 **Reference** 对象中的对象)。如果垃圾收集器发现对象可通过普通引用访问,则它不会释放该对象。 + +按照 **SoftReference** , **WeakReference** 和 **PhantomReference** 的顺序,每个都比前一个更“弱”,并且对应于不同的可达性级别。软引用用于实现对内存敏感的缓存。弱引用用于实现“规范化映射”( canonicalized mappings)——对象的实例可以在程序的多个位置同时使用,以节省存储,但不会阻止其键(或值)被回收。虚引用用于调度 pre-mortem 清理操作,这是一种比 Java 终结机制(Java finalization mechanism)更灵活的方式。 + +使用 **SoftReference** 和 **WeakReference** ,可以选择是否将它们放在 **ReferenceQueue** (用于 pre-mortem 清理操作的设备)中,但 **PhantomReference** 只能在 **ReferenceQueue** 上构建。下面是一个简单的演示: + +```java +// collectiontopics/References.java +// Demonstrates Reference objects +import java.lang.ref.*; +import java.util.*; + +class VeryBig { + private static final int SIZE = 10000; + private long[] la = new long[SIZE]; + private String ident; + VeryBig(String id) { ident = id; } + @Override + public String toString() { return ident; } + @Override + protected void finalize() { + System.out.println("Finalizing " + ident); + } +} + +public class References { + private static ReferenceQueue rq = + new ReferenceQueue<>(); + public static void checkQueue() { + Reference inq = rq.poll(); + if(inq != null) + System.out.println("In queue: " + inq.get()); + } + public static void main(String[] args) { + int size = 10; + // Or, choose size via the command line: + if(args.length > 0) + size = Integer.valueOf(args[0]); + LinkedList> sa = + new LinkedList<>(); + for(int i = 0; i < size; i++) { + sa.add(new SoftReference<>( + new VeryBig("Soft " + i), rq)); + System.out.println( + "Just created: " + sa.getLast()); + checkQueue(); + } + LinkedList> wa = + new LinkedList<>(); + for(int i = 0; i < size; i++) { + wa.add(new WeakReference<>( + new VeryBig("Weak " + i), rq)); + System.out.println( + "Just created: " + wa.getLast()); + checkQueue(); + } + SoftReference s = + new SoftReference<>(new VeryBig("Soft")); + WeakReference w = + new WeakReference<>(new VeryBig("Weak")); + System.gc(); + LinkedList> pa = + new LinkedList<>(); + for(int i = 0; i < size; i++) { + pa.add(new PhantomReference<>( + new VeryBig("Phantom " + i), rq)); + System.out.println( + "Just created: " + pa.getLast()); + checkQueue(); + } + } +} +/* Output: (First and Last 10 Lines) +Just created: java.lang.ref.SoftReference@15db9742 +Just created: java.lang.ref.SoftReference@6d06d69c +Just created: java.lang.ref.SoftReference@7852e922 +Just created: java.lang.ref.SoftReference@4e25154f +Just created: java.lang.ref.SoftReference@70dea4e +Just created: java.lang.ref.SoftReference@5c647e05 +Just created: java.lang.ref.SoftReference@33909752 +Just created: java.lang.ref.SoftReference@55f96302 +Just created: java.lang.ref.SoftReference@3d4eac69 +Just created: java.lang.ref.SoftReference@42a57993 +...________...________...________...________... +Just created: java.lang.ref.PhantomReference@45ee12a7 +In queue: null +Just created: java.lang.ref.PhantomReference@330bedb4 +In queue: null +Just created: java.lang.ref.PhantomReference@2503dbd3 +In queue: null +Just created: java.lang.ref.PhantomReference@4b67cf4d +In queue: null +Just created: java.lang.ref.PhantomReference@7ea987ac +In queue: null +*/ +``` + +当运行此程序(将输出重定向到文本文件以查看页面中的输出)时,将会看到对象是被垃圾收集了的,虽然仍然可以通过 **Reference** 对象访问它们(使用 `get()` 来获取实际的对象引用)。 还可以看到 **ReferenceQueue** 始终生成包含 **null** 对象的 **Reference** 。 要使用它,请从特定的 **Reference** 类继承,并为新类添加更多有用的方法。 + + + +### WeakHashMap + +集合类库中有一个特殊的 **Map** 来保存弱引用: **WeakHashMap** 。 此类可以更轻松地创建规范化映射。在这种映射中,可以通过仅仅创建一个特定值的实例来节省存储空间。当程序需要该值时,它会查找映射中的现有对象并使用它(而不是从头开始创建一个)。 该映射可以将值作为其初始化的一部分,但更有可能的是在需要时创建该值。 + +由于这是一种节省存储空间的技术,因此 **WeakHashMap** 允许垃圾收集器自动清理键和值,这是非常方便的。不能对放在 **WeakHashMap** 中的键和值做任何特殊操作,它们由 map 自动包装在 **WeakReference** 中。当键不再被使用的时候才允许清理,如下所示: + +```java +// collectiontopics/CanonicalMapping.java +// Demonstrates WeakHashMap +import java.util.*; + +class Element { + private String ident; + Element(String id) { ident = id; } + @Override + public String toString() { return ident; } + @Override + public int hashCode() { + return Objects.hashCode(ident); + } + @Override + public boolean equals(Object r) { + return r instanceof Element && + Objects.equals(ident, ((Element)r).ident); + } + @Override + protected void finalize() { + System.out.println("Finalizing " + + getClass().getSimpleName() + " " + ident); + } +} + +class Key extends Element { + Key(String id) { super(id); } +} + +class Value extends Element { + Value(String id) { super(id); } +} + +public class CanonicalMapping { + public static void main(String[] args) { + int size = 1000; + // Or, choose size via the command line: + if(args.length > 0) + size = Integer.valueOf(args[0]); + Key[] keys = new Key[size]; + WeakHashMap map = + new WeakHashMap<>(); + for(int i = 0; i < size; i++) { + Key k = new Key(Integer.toString(i)); + Value v = new Value(Integer.toString(i)); + if(i % 3 == 0) + keys[i] = k; // Save as "real" references + map.put(k, v); + } + System.gc(); + } +} +``` + +**Key** 类必须具有 `hashCode()` 和 `equals()` ,因为它将被用作散列数据结构中的键。 `hashCode()` 的内容在[附录:理解hashCode和equals方法]()中进行了描述。 + +运行程序,你会看到垃圾收集器每三个键跳过一次。对该键的普通引用也被放置在 **keys** 数组中,因此这些对象不能被垃圾收集。 -## 避免旧式类库 +## Java 1.0 / 1.1 的集合类 +不幸的是,许多代码是使用 Java 1.0 / 1.1 中的集合编写的,甚至新代码有时也是使用这些类编写的。编写新代码时切勿使用旧集合。旧的集合类有限,所以关于它们的讨论不多。由于它们是不合时宜的,所以我会尽量避免过分强调一些可怕的设计决定。 + + +### Vector 和 Enumeration + +Java 1.0 / 1.1 中唯一的自扩展序列是 **Vector** ,因此它被用于很多地方。它的缺陷太多了,无法在这里描述(参见《Java编程思想》第1版,可从[www.OnJava8.com](www.OnJava8.com)免费下载)。基本上,你可以将它看作是具有冗长且笨拙的方法名称的 **ArrayList** 。在修订后的 Java 集合库中,**Vector** 已经被调整适配过,因此可以作为 **Collection** 和 **List** 来使用。事实证明这有点不正常,集合类库仍然包含它只是为了支持旧的 Java 代码,但这会让一些人误以为 **Vector** 已经变得更好了。 + +迭代器的 Java 1.0 / 1.1 版本选择创建一个新名称“enumeration”,而不是使用每个人都熟悉的术语(“iterator”)。 **Enumeration** 接口小于 **Iterator** ,只包含两个方法,并且它使用更长的方法名称:如果还有更多元素,则 `boolean hasMoreElements()` 返回 `true` , `Object nextElement()` 返回此enumeration的下一个元素 (否则会抛出异常)。 + +**Enumeration** 只是一个接口,而不是一个实现,甚至新的类库有时仍然使用旧的 **Enumeration** ,这是不幸的,但通常是无害的。应该总是在自己的代码中使用 **Iterator** ,但要做好准备应对那些提供 **Enumeration** 的类库。 + +此外,可以使用 `Collections.enumeration()` 方法为任何 **Collection** 生成 **Enumeration** ,如下例所示: + +```java +// collectiontopics/Enumerations.java +// Java 1.0/1.1 Vector and Enumeration +import java.util.*; +import onjava.*; + +public class Enumerations { + public static void main(String[] args) { + Vector v = + new Vector<>(Countries.names(10)); + Enumeration e = v.elements(); + while(e.hasMoreElements()) + System.out.print(e.nextElement() + ", "); + // Produce an Enumeration from a Collection: + e = Collections.enumeration(new ArrayList<>()); + } +} +/* Output: +ALGERIA, ANGOLA, BENIN, BOTSWANA, BURKINA FASO, +BURUNDI, CAMEROON, CAPE VERDE, CENTRAL AFRICAN +REPUBLIC, CHAD, +*/ +``` + +要生成 **Enumeration** ,可以调用 `elements()` ,然后可以使用它来执行向前迭代。 + +最后一行创建一个 **ArrayList** ,并使用 `enumeration() ` 来将 **ArrayList** 适配为一个 **Enumeration** 。 因此,如果有旧代码需要使用 **Enumeration** ,你仍然可以使用新集合。 + + +### Hashtable + +正如你在本附录中的性能比较中所看到的,基本的 **Hashtable** 与 **HashMap** 非常相似,甚至方法名称都相似。在新代码中没有理由使用 **Hashtable** 而不是 **HashMap** 。 + + +### Stack + +之前使用 **LinkedList** 引入了栈的概念。 Java 1.0 / 1.1 **Stack** 的奇怪之处在于,不是以组合方式使用 **Vector** ,而是继承自 **Vector** 。 因此它具有 **Vector** 的所有特征和行为以及一些额外的 **Stack** 行为。很难去知道设计师是否有意识地认为这样做是有用的,或者它是否只是太天真了,无论如何,它在进入发行版之前显然没有经过审查,所以这个糟糕的设计仍然存在(但不要使用它)。 + +这是 **Stack** 的简单演示,向栈中放入枚举中每一个类型的 **String** 形式。它还展示了如何轻松地将 **LinkedList** 用作栈,或者使用在[第十二章:集合]()章节中创建的 **Stack** 类: + +```java +// collectiontopics/Stacks.java +// Demonstration of Stack Class +import java.util.*; + +enum Month { JANUARY, FEBRUARY, MARCH, APRIL, + MAY, JUNE, JULY, AUGUST, SEPTEMBER, + OCTOBER, NOVEMBER } + +public class Stacks { + public static void main(String[] args) { + Stack stack = new Stack<>(); + for(Month m : Month.values()) + stack.push(m.toString()); + System.out.println("stack = " + stack); + // Treating a stack as a Vector: + stack.addElement("The last line"); + System.out.println( + "element 5 = " + stack.elementAt(5)); + System.out.println("popping elements:"); + while(!stack.empty()) + System.out.print(stack.pop() + " "); + + // Using a LinkedList as a Stack: + LinkedList lstack = new LinkedList<>(); + for(Month m : Month.values()) + lstack.addFirst(m.toString()); + System.out.println("lstack = " + lstack); + while(!lstack.isEmpty()) + System.out.print(lstack.removeFirst() + " "); + + // Using the Stack class from + // the Collections Chapter: + onjava.Stack stack2 = + new onjava.Stack<>(); + for(Month m : Month.values()) + stack2.push(m.toString()); + System.out.println("stack2 = " + stack2); + while(!stack2.isEmpty()) + System.out.print(stack2.pop() + " "); + + } +} +/* Output: +stack = [JANUARY, FEBRUARY, MARCH, APRIL, MAY, JUNE, +JULY, AUGUST, SEPTEMBER, OCTOBER, NOVEMBER] +element 5 = JUNE +popping elements: +The last line NOVEMBER OCTOBER SEPTEMBER AUGUST JULY +JUNE MAY APRIL MARCH FEBRUARY JANUARY lstack = +[NOVEMBER, OCTOBER, SEPTEMBER, AUGUST, JULY, JUNE, MAY, +APRIL, MARCH, FEBRUARY, JANUARY] +NOVEMBER OCTOBER SEPTEMBER AUGUST JULY JUNE MAY APRIL +MARCH FEBRUARY JANUARY stack2 = [NOVEMBER, OCTOBER, +SEPTEMBER, AUGUST, JULY, JUNE, MAY, APRIL, MARCH, +FEBRUARY, JANUARY] +NOVEMBER OCTOBER SEPTEMBER AUGUST JULY JUNE MAY APRIL +MARCH FEBRUARY JANUARY +*/ +``` + +**String** 形式是由 **Month** 中的枚举常量生成的,使用 `push()` 压入到栈中,然后使用 `pop()` 从栈顶部取出。为了说明一点,将 **Vector** 的操作也在 **Stack** 对象上执行, 这是可能的,因为凭借继承, **Stack** 是 **Vector** 。 因此,可以在 **Vector** 上执行的所有操作也可以在 **Stack** 上执行,例如 `elementAt()` 。 + +如前所述,在需要栈行为时使用 **LinkedList** ,或者从 **LinkedList** 类创建的 **onjava.Stack** 类。 + + +### BitSet + +**BitSet** 用于有效地存储大量的开关信息。仅从尺寸大小的角度来看它是有效的,如果你正在寻找有效的访问,它比使用本机数组(native array)稍慢。 + +此外, **BitSet** 的最小大小是 **long** :64位。这意味着如果你要存储更小的东西,比如8位, **BitSet** 就是浪费,如果尺寸有问题,你最好创建自己的类,或者只是用一个数组来保存你的标志。(只有在你创建许多包含开关信息列表的对象时才会出现这种情况,并且只应根据分析和其他指标来决定。如果你做出此决定只是因为您认为 **BitSet** 太大,那么最终会产生不必要的复杂性并且浪费大量时间。) + +当添加更多元素时,普通集合会扩展, **BitSet**也会这样做。以下示例显示了 **BitSet** 的工作原理: + +```java +// collectiontopics/Bits.java +// Demonstration of BitSet +import java.util.*; + +public class Bits { + public static void printBitSet(BitSet b) { + System.out.println("bits: " + b); + StringBuilder bbits = new StringBuilder(); + for(int j = 0; j < b.size() ; j++) + bbits.append(b.get(j) ? "1" : "0"); + System.out.println("bit pattern: " + bbits); + } + public static void main(String[] args) { + Random rand = new Random(47); + // Take the LSB of nextInt(): + byte bt = (byte)rand.nextInt(); + BitSet bb = new BitSet(); + for(int i = 7; i >= 0; i--) + if(((1 << i) & bt) != 0) + bb.set(i); + else + bb.clear(i); + System.out.println("byte value: " + bt); + printBitSet(bb); + + short st = (short)rand.nextInt(); + BitSet bs = new BitSet(); + for(int i = 15; i >= 0; i--) + if(((1 << i) & st) != 0) + bs.set(i); + else + bs.clear(i); + System.out.println("short value: " + st); + printBitSet(bs); + + int it = rand.nextInt(); + BitSet bi = new BitSet(); + for(int i = 31; i >= 0; i--) + if(((1 << i) & it) != 0) + bi.set(i); + else + bi.clear(i); + System.out.println("int value: " + it); + printBitSet(bi); + + // Test bitsets >= 64 bits: + BitSet b127 = new BitSet(); + b127.set(127); + System.out.println("set bit 127: " + b127); + BitSet b255 = new BitSet(65); + b255.set(255); + System.out.println("set bit 255: " + b255); + BitSet b1023 = new BitSet(512); + b1023.set(1023); + b1023.set(1024); + System.out.println("set bit 1023: " + b1023); + } +} +/* Output: +byte value: -107 +bits: {0, 2, 4, 7} +bit pattern: 101010010000000000000000000000000000000000 +0000000000000000000000 +short value: 1302 +bits: {1, 2, 4, 8, 10} +bit pattern: 011010001010000000000000000000000000000000 +0000000000000000000000 +int value: -2014573909 +bits: {0, 1, 3, 5, 7, 9, 11, 18, 19, 21, 22, 23, 24, +25, 26, 31} +bit pattern: 110101010101000000110111111000010000000000 +0000000000000000000000 +set bit 127: {127} +set bit 255: {255} +set bit 1023: {1023, 1024} +*/ +``` + +随机数生成器用于创建随机 **byte** , **short** 和 **int** ,并且每个都在 **BitSet** 中转换为相应的位模式。这样可以正常工作,因为 **BitSet** 是64位,所以这些都不会导致它的大小增加,然后创建更大的 **BitSet** 。 请注意, **BitSet** 会根据需要进行扩展。 + +对于可以命名的固定标志集, **EnumSet** (参见[第二十二章:枚举]()章节)通常比 **BitSet** 更好,因为 **EnumSet** 允许操作名称而不是数字位位置,从而可以减少错误。 **EnumSet** 还可以防止意外地添加新的标记位置,这可能会导致一些严重的,难以发现的错误。使用 **BitSet** 而不是 **EnumSet** 的唯一原因是,不知道在运行时需要多少标志,或者为标志分配名称是不合理的,或者需要 **BitSet** 中的一个特殊操作(请参阅 **BitSet** 和 **EnumSet** 的 JDK 文档)。 ## 本章小结 +集合可以说是编程语言中最常用的工具。有些语言(例如Python)甚至将基本集合组件(列表,映射和集合)作为内置函数包含在其中。 + +正如在[第十二章:集合]()章节中看到的那样,可以使用集合执行许多非常有用的操作,而不需要太多努力。但是,在某些时候,为了正确地使用它们而不得不更多地了解集合,特别是,必须充分了解散列操作以编写自己的 `hashCode()` 方法(并且必须知道何时需要),并且你必须充分了解各种集合实现,以根据你的需求选择合适的集合。本附录涵盖了这些概念,并讨论了有关集合库的其他有用详细信息。你现在应该已经准备好在日常编程任务中使用 Java 集合了。 + +集合库的设计很困难(大多数库设计问题都是如此)。在 C++ 中,集合类涵盖了许多不同类的基础。这比之前可用的 C++ 集合类更好,但它没有很好地转换为 Java 。在另一个极端,我看到了一个由单个类“collection”组成的集合库,它同时充当线性序列和关联数组。 Java 集合库试图在功能和复杂性之间取得平衡。结果在某些地方看起来有点奇怪。与早期 Java 库中的一些决策不同,这些奇怪的不是事故,而是在基于复杂性的权衡下而仔细考虑的决策。 +[^1]: **java.util** 中的 **Map** 使用 **Map** 的 `getKey()` 和 `getValue()` 执行批量复制,因此这是有效的。如果自定义 **Map** 只是复制整个 **Map.Entry** ,那么这种方法就会出现问题。 +[^2]: 虽然当我用这种方式描述它的时候听起来很奇怪而且好像没什么用处,但在[第十九章 类型信息]()章节中已经看到过,这种动态行为也可以非常强大有用。 - +[^3]: 如果这些加速仍然无法满足性能需求,则可以通过编写自己的 **Map** 并将其自定义为特定类型来进一步加速表查找,以避免因向 **对象** 转换而导致的延迟。为了达到更高的性能水平,速度爱好者可以使用 Donald Knuth 的《计算机程序设计艺术(第3卷):排序与查找》(第二版),将溢出桶列表(overflow bucket lists)替换为具有两个额外优势的阵列:它们可以针对磁盘存储进行优化,并且它们可以节省大部分创建和回收个别记录(individual records)的时间。 diff --git a/docs/book/Appendix-Low-Level-Concurrency.md b/docs/book/Appendix-Low-Level-Concurrency.md index 2c5219e..d85e110 100644 --- a/docs/book/Appendix-Low-Level-Concurrency.md +++ b/docs/book/Appendix-Low-Level-Concurrency.md @@ -3,18 +3,16 @@ # 附录:并发底层原理 -> 尽管不建议您自己编写底层 Java 并发代码,但是这样通常有助于了解它是如何工作的。 - -[并发编程](./24-Concurrent-Programming.md) 章节中介绍了一些用于高级并发的概念,包括为 Java 并发编程而最新提出的,更安全的概念( parallel Streams 和 CompletableFutures )。本附录则介绍在 Java 中底层并发概念,因此在阅读本篇时,您能有所了解掌握这些代码。您还会将进一步了解并发的普遍问题。 - -在 Java 的早期版本中, 底层并发概念是并发编程的重要组成部分。我们会着眼于围绕这些技巧的复杂性以及为何您应该避免它们而谈。 “并发编程” 章节展示最新的 Java 版本(尤其是 Java 8)所提供的改进技巧,这些技巧使得并发的使用,如果本来不容易使用,也会变得更容易些。 +> 尽管不建议你自己编写底层 Java 并发代码,但是这样通常有助于了解它是如何工作的。 +[并发编程](./24-Concurrent-Programming.md) 章节中介绍了一些用于高级并发的概念,包括为 Java 并发编程而最新提出的,更安全的概念( parallel Streams 和 CompletableFutures )。本附录则介绍在 Java 中底层并发概念,因此在阅读本篇时,你能有所了解掌握这些代码。你还会将进一步了解并发的普遍问题。 +在 Java 的早期版本中, 底层并发概念是并发编程的重要组成部分。我们会着眼于围绕这些技巧的复杂性以及为何你应该避免它们而谈。 “并发编程” 章节展示最新的 Java 版本(尤其是 Java 8)所提供的改进技巧,这些技巧使得并发的使用,如果本来不容易使用,也会变得更容易些。 ## 什么是线程? -并发将程序划分成独立分离运行的任务。每个任务都由一个 *执行线程* 来驱动,我们通常将其简称为 *线程* 。而一个 *线程* 就是操作系统进程中单一顺序的控制流。因此,单个进程可以有多个并发执行的任务,但是你的程序使得每个任务都好像有自己的处理器一样。这线程模型为编程带来了便利,它简化了在单一程序中处理变戏法般的多任务过程。操作系统则从处理器上分配时间片到你程序的所有线程中。 +并发将程序划分成独立分离运行的任务。每个任务都由一个 *执行线程* 来驱动,我们通常将其简称为 *线程* 。而一个 *线程* 就是操作系统进程中单一顺序的控制流。因此,单个进程可以有多个并发执行的任务,但是你的程序使得每个任务都好像有自己的处理器一样。此线程模型为编程带来了便利,它简化了在单一程序中处理变戏法般的多任务过程。操作系统则从处理器上分配时间片到你程序的所有线程中。 Java 并发的核心机制是 **Thread** 类,在该语言最初版本中, **Thread (线程)** 是由程序员直接创建和管理的。随着语言的发展以及人们发现了更好的一些方法,中间层机制 - 特别是 **Executor** 框架 - 被添加进来,以消除自己管理线程时候的心理负担(及错误)。 最终,甚至发展出比 **Executor** 更好的机制,如 [并发编程](./24-Concurrent-Programming.md) 一章所示。 @@ -26,14 +24,14 @@ Java 并发的核心机制是 **Thread** 类,在该语言最初版本中, ** * *thread-local variables* (线程本地变量)的存储区域 * 用于控制线程的状态管理变量 -包括 **main()** 在内的所有代码都会在某个线程内运行。 每当调用一个方法时,当前程序计数器被推到该线程的栈上,然后栈指针向下移动以足够来创建一个栈帧,其栈帧里存储该方法的所有局部变量,参数和返回值。所有基本类型变量都直接在栈上,虽然方法中创建(或方法中使用)对象的任何引用都位于栈帧中,但对象本身存于堆中。这仅且只有一个堆,被程序中所有线程所共享。 +包括 `main()` 在内的所有代码都会在某个线程内运行。 每当调用一个方法时,当前程序计数器被推到该线程的栈上,然后栈指针向下移动以足够来创建一个栈帧,其栈帧里存储该方法的所有局部变量,参数和返回值。所有基本类型变量都直接在栈上,虽然方法中创建(或方法中使用)对象的任何引用都位于栈帧中,但对象本身存于堆中。这仅且只有一个堆,被程序中所有线程所共享。 -除此以外,线程必须绑定到操作系统,这样它就可以在某个时候连接到处理器。这是作为线程构建过程的一部分为您管理的。Java 使用底层操作系统中的机制来管理线程的执行。 +除此以外,线程必须绑定到操作系统,这样它就可以在某个时候连接到处理器。这是作为线程构建过程的一部分为你管理的。Java 使用底层操作系统中的机制来管理线程的执行。 ### 最佳线程数 如果你查看第 24 章 [并发编程](./24-Concurrent-Programming.md) 中使用 *CachedThreadPool* 的用例,你会发现 **ExecutorService** 为每个我们提交的任务分配一个线程。然而,并行流(**parallel Stream**)在 [**CountingStream.java** ](https://github.com/BruceEckel/OnJava8-Examples/blob/master/concurrent/CountingStream.java -) 中只分配了 8 个线程(id 中 1-7 为工作线程,8 为 **main()** 方法的主线程,它巧妙地将其用作额外的并行流)。如果你尝试提高 **range()** 方法中的上限值,你会看到没有创建额外的线程。这是为什么? +) 中只分配了 8 个线程(id 中 1-7 为工作线程,8 为 `main()` 方法的主线程,它巧妙地将其用作额外的并行流)。如果你尝试提高 `range()` 方法中的上限值,你会看到没有创建额外的线程。这是为什么? 我们可以查出当前机器上处理器的数量: @@ -53,7 +51,7 @@ public class NumberOfProcessors { 在我的机器上(使用英特尔酷睿i7),我有四个内核,每个内核呈现两个*超线程*(指一种硬件技巧,能在单个处理器上产生非常快速的上下文切换,在某些情况下可以使内核看起来像运行两个硬件线程)。虽然这是 “最近” 计算机上的常见配置(在撰写本文时),但你可能会看到不同的结果,包括 **CountingStream.java ** 中同等数量的默认线程。 -你的操作系统可能有办法来查出关于处理器的更多信息,例如,在Windows 10上,按下 “开始” 键,输入 “任务管理器” 和 Enter 键。点击 “详细信息” 。选择 “性能” 标签,您将会看到各种各样的关于您的硬件信息,包括“内核” 和 “逻辑处理器” 。 +你的操作系统可能有办法来查出关于处理器的更多信息,例如,在Windows 10上,按下 “开始” 键,输入 “任务管理器” 和 Enter 键。点击 “详细信息” 。选择 “性能” 标签,你将会看到各种各样的关于你的硬件信息,包括“内核” 和 “逻辑处理器” 。 事实证明,“通用”线程的最佳数量就算是可用处理器的数量(对于特定的问题可能不是这样)。这原因来自在Java线程之间切换上下文的代价:存储被挂起线程的当前状态,并检索另一个线程的当前状态,以便从它进入挂起的位置继续执行。对于 8 个处理器和 8 个(计算密集型)Java线程,JVM 在运行这8个任务时从不需要切换上下文。对于比处理器数量少的任务,分配更多线程没有帮助。 @@ -94,7 +92,7 @@ public class ThreadSize { } ``` -只要你不断递交任务,**CachedThreadPool** 就会继续创建线程。将 **Dummy** 对象递交到 **execute()** 方法以开始任务,如果线程池无可用线程,则分配一个新线程。执行的暂停方法 **pause()** 运行时间必须足够长,使任务不会开始即完成(从而为新任务释放现有线程)。只要任务不断进入而没有完成,**CachedThreadPool** 最终就会耗尽内存。 +只要你不断递交任务,**CachedThreadPool** 就会继续创建线程。将 **Dummy** 对象递交到 `execute()` 方法以开始任务,如果线程池无可用线程,则分配一个新线程。执行的暂停方法 `pause()` 运行时间必须足够长,使任务不会开始即完成(从而为新任务释放现有线程)。只要任务不断进入而没有完成,**CachedThreadPool** 最终就会耗尽内存。 我并不总是能够在我尝试的每台机器上造成内存不足的错误。在一台机器上,我看到这样的结果: @@ -133,9 +131,9 @@ OutOfMemoryError: 5703 请注意的是操作系统还可能对允许的线程数施加限制。 -因此,“我可以拥有多少线程”这一问题的答案是“几千个”。但是,如果你发现自己分配了数千个线程,那么您可能需要重新考虑您的做法; 恰当的问题是“我需要多少线程?” +因此,“我可以拥有多少线程”这一问题的答案是“几千个”。但是,如果你发现自己分配了数千个线程,那么你可能需要重新考虑你的做法; 恰当的问题是“我需要多少线程?” -### The WorkStealingPool +### The WorkStealingPool (工作窃取线程池) 这是一个 **ExecutorService** ,它使用所有可用的(由JVM报告) 处理器自动创建线程池。 @@ -204,11 +202,11 @@ public class SwallowedException { } ``` -这个程序什么也不输出(然而,如果你用 **execute** 方法替换 **submit()** 方法,你就将会看到异常抛出。这说明在线程中抛出异常是很棘手的,需要特别注意的事情。 +这个程序什么也不输出(然而,如果你用 **execute** 方法替换 `submit()` 方法,你就将会看到异常抛出。这说明在线程中抛出异常是很棘手的,需要特别注意的事情。 -你无法捕获到从线程逃逸的异常。一旦异常越过了任务的 **run()** 方法,它就会传递至控制台,除非您采取特殊步骤来捕获此类错误异常。 +你无法捕获到从线程逃逸的异常。一旦异常越过了任务的 `run()` 方法,它就会传递至控制台,除非你采取特殊步骤来捕获此类错误异常。 -下面是一个抛出异常的代码,该异常会传递到它的 **run()** 方法之外,而 **main()** 方法会显示运行它时会发生什么: +下面是一个抛出异常的代码,该异常会传递到它的 `run()` 方法之外,而 `main()` 方法会显示运行它时会发生什么: ```java // lowlevel/ExceptionThread.java @@ -250,7 +248,7 @@ Exception in thread "pool-1-thread-1" RuntimeException at java.lang.Thread.run(Thread.java:745) ``` -即使在 **main()** 方法体内包裹 **try-catch** 代码块来捕获异常也不成功: +即使在 `main()` 方法体内包裹 **try-catch** 代码块来捕获异常也不成功: ```java // lowlevel/NaiveExceptionHandling.java @@ -288,8 +286,8 @@ er.run(ThreadPoolExecutor.java:617) 为解决这个问题,需要改变 **Executor** (执行器)生成线程的方式。 **Thread.UncaughtExceptionHandler** 是一个添加给每个 **Thread** 对象,用于进行异常处理的接口。 -当该线程即将死于未捕获的异常时,将自动调用 **Thread.UncaughtExceptionHandler.uncaughtException()** - 方法。为了调用该方法,我们创建一个新的 `ThreadFactory` 类型来让 **Thread.UncaughtExceptionHandler** 对象附加到每个它所新创建的 **Thread**(线程)对象上。我们赋值该工厂对象给 **Executors** 对象的 方法,让它的方法来生成新的 **ExecutorService** 对象: +当该线程即将死于未捕获的异常时,将自动调用 `Thread.UncaughtExceptionHandler.uncaughtException()` + 方法。为了调用该方法,我们创建一个新的 **ThreadFactory** 类型来让 **Thread.UncaughtExceptionHandler** 对象附加到每个它所新创建的 **Thread**(线程)对象上。我们赋值该工厂对象给 **Executors** 对象的 方法,让它的方法来生成新的 **ExecutorService** 对象: ```java // lowlevel/CaptureUncaughtException.java @@ -370,16 +368,16 @@ caught java.lang.RuntimeException */ ``` -只有在每个线程没有设置异常处理器时候,默认处理器才会被调用。系统会检查线程专有的版本,如果没有,则检查是否线程组中有专有的 **uncaughtException()** 方法;如果都没有,就会调用 **defaultUncaughtExceptionHandler** 方法。 +只有在每个线程没有设置异常处理器时候,默认处理器才会被调用。系统会检查线程专有的版本,如果没有,则检查是否线程组中有专有的 `uncaughtException()` 方法;如果都没有,就会调用 **defaultUncaughtExceptionHandler** 方法。 -可以将此方法与 **CompletableFuture**s 的改进方法进行比较。 +可以将此方法与 **CompletableFuture** 的改进方法进行比较。 ## 资源共享 -你可以将单线程程序看作一个孤独的实体,在你的问题空间中移动并一次只做一件事。因为只有一个实体,你永远不会想到两个实体试图同时使用相同资源的问题:问题犹如两个人试图同时停放在同一个空间,同时走过一扇门,甚至同时说话。 +你可以将单线程程序看作一个孤独的实体,在你的问题空间中移动并同一时间只做一件事。因为只有一个实体,你永远不会想到两个实体试图同时使用相同资源的问题:问题犹如两个人试图同时停放在同一个空间,同时走过一扇门,甚至同时说话。 -通过并发,事情不再孤单,但现在两个或更多任务可能会相互干扰。如果您不阻止这种冲突,您将有两个任务同时尝试访问同一个银行帐户,打印到同一个打印机,调整同一个阀门,等等。 +通过并发,事情不再孤单,但现在两个或更多任务可能会相互干扰。如果你不阻止这种冲突,你将有两个任务同时尝试访问同一个银行帐户,打印到同一个打印机,调整同一个阀门,等等。 ### 资源竞争 @@ -387,15 +385,15 @@ caught java.lang.RuntimeException 从编程方式上看,副作用似乎更容易:你只需使用结果来操作环境中的某些东西。例如,你的任务可能会执行一些计算,然后直接将其结果写入集合。 -这种方法的问题是集合通常是共享资源。当运行多个任务时,任何任务都可能同时读写 *共享资源* 。这揭示了 *资源竞争* 问题,这是处理任务时的主要陷阱之一。 +伴随这种方式的问题是集合通常是共享资源。当运行多个任务时,任何任务都可能同时读写 *共享资源* 。这揭示了 *资源竞争* 问题,这是处理任务时的主要陷阱之一。 -在单线程系统中,您不会考虑资源竞争,因为你一次只做一件事。当你有多个任务时,必须始终防止资源竞争。 +在单线程系统中,你不需要考虑资源竞争,因为你永远不可能同时做多件事。当你有多个任务时,你就必须始终防止资源竞争。 -解决此问题的的一种方法是使用能够应对资源竞争的集合,如果多个任务同时尝试对此类集合进行写入,那么此类集合可以应付该问题。在 Java 并发库中,你将发现许多尝试解决资源争用问题的类;在本附录中,您将看到其中的一些,但覆盖范围并不全面。 +解决此问题的的一种方法是使用能够应对资源竞争的集合,如果多个任务同时尝试对此类集合进行写入,那么此类集合可以应付该问题。在 Java 并发库中,你将发现许多尝试解决资源竞争问题的类;在本附录中,你将看到其中的一些,但覆盖范围并不全面。 请思考以下的示例,其中一个任务负责生成偶数,其他任务则负责消费这些数字。在这里,消费者任务的唯一工作就是检查偶数的有效性。 -我们将定义消费者任务 **EvenChecker** 类,以便在后续示例中可复用。为了将 **EvenChecker** 与我们的各种实验生成器类解耦,我们首先创建一个名为 **IntGenerator** 的抽象类,它包含 **EvenChecker** 必须知道的最少必要方法:它包含一个 **next()** 方法,以及可以取消生成的方法。 +我们将定义消费者任务 **EvenChecker** 类,以便在后续示例中可复用。为了将 **EvenChecker** 与我们的各种实验生成器类解耦,我们首先创建名为 **IntGenerator** 的抽象类,它包含 **EvenChecker** 必须知道的最低必要方法:它包含 `next()` 方法,以及可以取消它执行生成的方法。 ```java // lowlevel/IntGenerator.java @@ -412,7 +410,7 @@ public abstract class IntGenerator { } ``` -**cancel()** 方法改变 **AtomicBoolean canceled** 标志位的状态, 而 **isCanceled()** 方法则告诉标志位是否设置。因为 **canceled** 标志位是 **AtomicBoolean** 类型,所以它是原子性的,这意味着分配和值返回等简单操作发生时没有中断的可能性,因此你无法在这些简单操作中看到该字段处于中间状态。您将在本附录的后面部分了解有关原子性和 **Atomic** 类的更多信息 +`cancel()` 方法改变 **AtomicBoolean** 类型的 **canceled** 标志位的状态, 而 `isCanceled()` 方法则告诉标志位是否设置。因为 **canceled** 标志位是 **AtomicBoolean** 类型,由于它是原子性的,这意味着分配和值返回等简单操作发生时没有中断的可能性,因此你无法在这些简单操作中看到该字段处于中间状态。你将在本附录的后面部分了解有关原子性和 **Atomic** 类的更多信息 任何 **IntGenerator** 都可以使用下面的 **EvenChecker** 类进行测试: @@ -457,15 +455,15 @@ public class EvenChecker implements Runnable { } ``` -**test()** 方法开启了许多访问同一个 **IntGenerator** 的 **EvenChecker**。**EvenChecker** 任务们会不断读取和测试与其关联的 **IntGenerator** 对象中的生成值。如果 **IntGenerator** 导致失败,**test()** 方法会报告并返回。 +`test()` 方法开启了许多访问同一个 **IntGenerator** 的 **EvenChecker**。**EvenChecker** 任务们会不断读取和测试与其关联的 **IntGenerator** 对象中的生成值。如果 **IntGenerator** 导致失败,`test()` 方法会报告并返回。 -依赖于 **IntGenerator** 对象的所有 **EvenChecker** 任务都会检查它是否已被取消。如果 **generator.isCanceled()** 返回值为 true ,则 **run()** 方法返回。 任何 **EvenChecker** 任务都可以在 **IntGenerator** 上调用**cancel()** ,这会导致使用该 **IntGenerator** 的其他所有 **EvenChecker** 正常关闭。 +依赖于 **IntGenerator** 对象的所有 **EvenChecker** 任务都会检查它是否已被取消。如果 `generator.isCanceled()` 返回值为 true ,则 `run()` 方法返回。 任何 **EvenChecker** 任务都可以在 **IntGenerator** 上调用 `cancel()` ,这会导致使用该 **IntGenerator** 的其他所有 **EvenChecker** 正常关闭。 在本设计中,共享公共资源( **IntGenerator** )的任务会监视该资源的终止信号。这消除所谓的竞争条件,其中两个或更多的任务竞争响应某个条件并因此冲突或不一致结果的情况。 你必须仔细考虑并防止并发系统失败的所有可能途径。例如,一个任务不能依赖于另一个任务,因为任务关闭的顺序无法得到保证。这里,通过使任务依赖于非任务对象,我们可以消除潜在的竞争条件。 -一般来说,我们假设 **test()** 方法最终失败,因为各个 **EvenChecker** 的任务在 **IntGenerator** 处于 “不恰当的” 状态时,仍能够访问其中的信息。但是,直到 **IntGenerator** 完成许多循环之前,它可能无法检测到问题,具体取决于操作系统的详细信息和其他实现细节。为确保本书的自动构建不会卡住,我们使用 **TimedAbort** 类,在此处定义: +一般来说,我们假设 `test()` 方法最终失败,因为各个 **EvenChecker** 的任务在 **IntGenerator** 处于 “不恰当的” 状态时,仍能够访问其中的信息。但是,直到 **IntGenerator** 完成许多循环之前,它可能无法检测到问题,具体取决于操作系统的详细信息和其他实现细节。为确保本书的自动构建不会卡住,我们使用 **TimedAbort** 类,在此处定义: ```java // onjava/TimedAbort.java @@ -497,9 +495,9 @@ public class TimedAbort { } ``` -我们使用 lambda 表达式创建一个 **Runnable** ,该表达式使用 **CompletableFuture** 的 **runAsync()** 静态方法执行。 **runAsync()** 方法的值会立即返回。 因此,**TimedAbort** 不会保持任何打开的任务,否则已完成任务,但如果它需要太长时间,它仍将终止该任务( **TimedAbort** 有时被称为守护进程)。 +我们使用 lambda 表达式创建一个 **Runnable** ,该表达式使用 **CompletableFuture** 的 `runAsync()` 静态方法执行。 `runAsync()` 方法的值会立即返回。 因此,**TimedAbort** 不会保持任何打开的任务,否则已完成任务,但如果它需要太长时间,它仍将终止该任务( **TimedAbort** 有时被称为守护进程)。 -**TimedAbort** 还允许你 **restart()** 方法重启任务,在有某些有用的活动进行时保持程序打开。 +**TimedAbort** 还允许你 `restart()` 方法重启任务,在有某些有用的活动进行时保持程序打开。 我们可以看到正在运行的 **TimedAbort** 示例: @@ -522,7 +520,7 @@ TimedAbort 1.0 如果你注释掉 **Nap** 创建实列那行,程序执行会立即退出,表明 **TimedAbort** 没有维持程序打开。 -我们将看到第一个 **IntGenerator** 示例有一个生成一系列偶数值的 **next()** 方法: +我们将看到第一个 **IntGenerator** 示例有一个生成一系列偶数值的 `next()` 方法: ```java // lowlevel/EvenProducer.java @@ -549,21 +547,21 @@ public class EvenProducer extends IntGenerator { 417 not even! */ ``` -* [1] 一个任务有可能在另外一个任务执行第一个对 **currentEvenValue** 的递增操作之后,但是没有执行第二个操作之前,调用 **next()** 方法。这将使这个值处于 “不恰当” 的状态。 +* [1] 一个任务有可能在另外一个任务执行第一个对 **currentEvenValue** 的自增操作之后,但是没有执行第二个操作之前,调用 `next()` 方法。这将使这个值处于 “不恰当” 的状态。 -为了证明这是可能发生的, **EvenChecker.test()** 创建了一组 **EventChecker** 对象,以连续读取 **EvenProducer** 的输出并测试检查每个数值是否都是偶数。如果不是,就会报告错误,而程序也将关闭。 +为了证明这是可能发生的, `EvenChecker.test()` 创建了一组 **EventChecker** 对象,以连续读取 **EvenProducer** 的输出并测试检查每个数值是否都是偶数。如果不是,就会报告错误,而程序也将关闭。 多线程程序的部分问题是,即使存在 bug ,如果失败的可能性很低,程序仍然可以正确显示。 -重要的是要注意到递增操作自身需要多个步骤,并且在递增过程中任务可能会被线程机制挂起 - 也就是说,在 Java 中,递增不是原子性的操作。因此,如果不保护任务,即使单一的递增也不是线程安全的。 +重要的是要注意到自增操作自身需要多个步骤,并且在自增过程中任务可能会被线程机制挂起 - 也就是说,在 Java 中,自增不是原子性的操作。因此,如果不保护任务,即使单纯的自增也不是线程安全的。 该示例程序并不总是在第一次非偶数产生时终止。所有任务都不会立即关闭,这是并发程序的典型特征。 ### 解决资源竞争 -前面的示例揭示了当你使用线程时的基本问题:你永远不知道线程何时运行。想象一下坐在一张桌子上,用叉子,将最后一块食物放在盘子上,当叉子到达时,食物突然消失...因为你的线程被挂起而另一个用餐者进来吃了食物了。这就是在编写并发程序时要处理的问题。为了使并发工作,您需要某种方式来阻止两个任务访问同一个资源,至少在关键时期是这样。 +前面的示例揭示了当你使用线程时的基本问题:你永远不知道线程哪个时刻运行。想象一下坐在一张桌子上,用叉子,将最后一块食物放在盘子上,当叉子到达时,食物突然消失...仅因为你的线程被挂起而另一个用餐者进来吃了食物了。这就是在编写并发程序时要处理的问题。为了使并发工作有效,你需要某种方式来阻止两个任务访问同一个资源,至少在关键时期是这样。 -防止这种冲突的方法就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须锁定这项资源,使其他任务在其被解锁之前,就无法访问它了,而在其被解锁之时,另一个任务就可以锁定并使用它,以此类推。如果汽车前排座位是受限资源,那么大喊着 “冲呀” 的孩子就会(在这次旅途过程中)获得该资源的锁。 +防止这种冲突的方法就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须锁定这项资源,使其他任务在其被解锁之前,就无法访问它,而在其被解锁时候,另一个任务就可以锁定并使用它,以此类推。如果汽车前排座位是受限资源,那么大喊着 “冲呀” 的孩子就会(在这次旅途过程中)获得该资源的锁。 为了解决线程冲突的问题,基本的并发方案将序列化访问共享资源。这意味着一次只允许一个任务访问共享资源。这通常是通过在访问资源的代码片段周围加上一个子句来实现的,该子句一次只允许一个任务访问这段代码。因为这个子句产生 *互斥* 效果,所以这种机制的通常称为是 *mutex* (互斥量)。 @@ -582,11 +580,11 @@ synchronized void f() { /* ... */ } synchronized void g() { /* ... */ } ``` -所有对象都自动包含单一的锁(也称为 *monitor*,即监视器)。当你调用对象上任何 **synchronized** 方法,此对象将被加锁,并且该对象上的的其他 **synchronized** 方法调用只有等到前一个方法执行完成并释放了锁之后才能被调用。如果一个任务对对象调用了 **f()** ,对于同一个对象而言,就只能等到 **f()** 调用结束并释放了锁之后,其他任务才能调用 **f()** 和 **g()**。所以,某个特定对象的所有 **synchronized** 方法共享同一个锁,这个锁可以防止多个任务同时写入对象内存。 +所有对象都自动包含独立的锁(也称为 *monitor*,即监视器)。当你调用对象上任何 **synchronized** 方法,此对象将被加锁,并且该对象上的的其他 **synchronized** 方法调用只有等到前一个方法执行完成并释放了锁之后才能被调用。如果一个任务对对象调用了 `f()` ,对于同一个对象而言,就只能等到 `f()` 调用结束并释放了锁之后,其他任务才能调用 `f()` 和 `g()`。所以,某个特定对象的所有 **synchronized** 方法共享同一个锁,这个锁可以防止多个任务同时写入对象内存。 在使用并发时,将字段设为 **private** 特别重要;否则,**synchronized** 关键字不能阻止其他任务直接访问字段,从而产生资源冲突。 -一个线程可以多次获取对象的锁。如果一个方法在同一个对象上调用第二个方法,而后者又在同一个对象上调用另一个方法,就会发生这种情况。 JVM 会跟踪对象被锁定的次数。如果对象已解锁,则其计数为 0 。当一个线程第一次锁时,计数变为 1 。每次同一线程在同一对象上获取另一个锁时,计数就会递增。显然,只有首先获得锁的线程才允许多次获取多个锁。每当线程离开 **synchronized** 方法时,计数递减,当、直到计数变为 0 ,完全释放锁以给其他线程使用。每个类也有一个锁(作为该类的 **Class** 对象的一部分),因此 **synchronized** 静态方法可以在类范围的基础上彼此锁定,不让同时访问静态数据。 +一个线程可以获取对象的锁多次。如果一个方法调用在同一个对象上的第二个方法,而后者又在同一个对象上调用另一个方法,就会发生这种情况。 JVM 会跟踪对象被锁定的次数。如果对象已解锁,则其计数为 0 。当一个线程首次获得锁时,计数变为 1 。每次同一线程在同一对象上获取另一个锁时,计数就会自增。显然,只有首先获得锁的线程才允许多次获取多个锁。每当线程离开 **synchronized** 方法时,计数递减,直到计数变为 0 ,完全释放锁以给其他线程使用。每个类也有一个锁(作为该类的 **Class** 对象的一部分),因此 **synchronized** 静态方法可以在类范围的基础上彼此锁定,不让同时访问静态数据。 你应该什么时候使用同步呢?可以永远 *Brian* 的同步法则[^2]。 @@ -622,66 +620,1063 @@ No odd numbers discovered */ ``` -在两个递增操作之间插入 **Nap()** 构造器方法,以提高在 **currentEvenValue** 是奇数的状态时上下文切换的可能性。因为互斥锁可以阻止多个任务同时进入临界区,所有这不会产生失败。第一个进入 **next()** 方法的任务将获得锁,任何试图获取锁的后续任务都将被阻塞,直到第一个任务释放锁。此时,调度机制选择另一个等待锁的任务。通过这种方式,任何时刻只能有一个任务通过互斥锁保护的代码。 +在两个自增操作之间插入 `Nap()` 构造器方法,以提高在 **currentEvenValue** 是奇数的状态时上下文切换的可能性。因为互斥锁可以阻止多个任务同时进入临界区,所有这不会产生失败。第一个进入 `next()` 方法的任务将获得锁,任何试图获取锁的后续任务都将被阻塞,直到第一个任务释放锁。此时,调度机制选择另一个等待锁的任务。通过这种方式,任何时刻只能有一个任务通过互斥锁保护的代码。 ## volatile 关键字 -**volatile** 可能是Java中最微妙和最难用的关键字。幸运的是,在现代 Java 中,你几乎总能避免使用它,如果你确实看到它在代码中使用,你应该保持怀疑态度和怀疑 - 这很有可能代码是过时的,或者编写代码的人不清楚在大体上(或两者都有)易变性(**volatile**) 或并发性的后果。 +**volatile** 可能是 Java 中最微妙和最难用的关键字。幸运的是,在现代 Java 中,你几乎总能避免使用它,如果你确实看到它在代码中使用,你应该保持怀疑态度和怀疑 - 这很有可能代码是过时的,或者编写代码的人不清楚使用它在大体上(或两者都有)易变性(**volatile**) 或并发性的后果。 使用 **volatile** 有三个理由。 ### 字分裂 -当你的 Java 数据类型足够大(在 Java 中 **long** 和 **double** 类型都是 64 位),写入变量的过程分两步进行,就会发生 *Word tearing* (字分裂)情况。 JVM 被允许将64位数量的读写作为两个单独的32位操作执行[^3] ,这增加了在读写过程中发生上下文切换的可能性,因此其他任务会看到不正确的结果。这被称为 *Word tearing* (字分裂),因为你可能只看到其中一部分修改后的值。基本上,任务有时可以在第一步之后但在第二步之前读取变量,从而产生垃圾值(对于例如 **boolean** 或 **int** 类型的小变量是没有问题的;任何 **long** 或 **double** 类型则除外)。 +当你的 Java 数据类型足够大(在 Java 中 **long** 和 **double** 类型都是 64 位),写入变量的过程分两步进行,就会发生 *Word tearing* (字分裂)情况。 JVM 被允许将64位数量的读写作为两个单独的32位操作执行[^3],这增加了在读写过程中发生上下文切换的可能性,因此其他任务会看到不正确的结果。这被称为 *Word tearing* (字分裂),因为你可能只看到其中一部分修改后的值。基本上,任务有时可以在第一步之后但在第二步之前读取变量,从而产生垃圾值(对于例如 **boolean** 或 **int** 类型的小变量是没有问题的;任何 **long** 或 **double** 类型则除外)。 -在缺乏任何其他保护的情况下,用 **volatile** 修饰符定义一个 **long** 或 **double** 变量,可阻止字分裂情况。然而,如果使用 **synchronized** 或 **java.util.concurrent.atomic** 类之一保护这些变量,则volatile将被取代。此外,**volatile** 不会影响到增量操作并不是原子操作的事实。 +在缺乏任何其他保护的情况下,用 **volatile** 修饰符定义一个 **long** 或 **double** 变量,可阻止字分裂情况。然而,如果使用 **synchronized** 或 **java.util.concurrent.atomic** 类之一保护这些变量,则 **volatile** 将被取代。此外,**volatile** 不会影响到增量操作并不是原子操作的事实。 ### 可见性 +第二个问题属于 [Java 并发的四句格言](./24-Concurrent-Programming.md#四句格言)里第二句格言 “一切都重要” 的部分。你必须假设每个任务拥有自己的处理器,并且每个处理器都有自己的本地内存缓存。该缓存准许处理器允许的更快,因为处理器并不总是需要从比起使用缓存显著花费更多时间的主内存中获取数据。 + +出现这个问题是因为 Java 尝试尽可能地提高执行效率。缓存的主要目的是避免从主内存中读取数据。当并发时,有时不清楚 Java 什么时候应该将值从主内存刷新到本地缓存 — 而这个问题称为 *缓存一致性* ( *cache coherence* )。 + +每个线程都可以在处理器缓存中存储变量的本地副本。将字段定义为 **volatile** 可以防止这些编译器优化,这样读写就可以直接进入内存,而不会被缓存。一旦该字段发生写操作,所有任务的读操作都将看到更改。如果一个 **volatile** 字段刚好存储在本地缓存,则会立即将其写入主内存,并且该字段的任何读取都始终发生在主内存中。 + +**volatile** 应该在何时适用于变量: + +1. 该变量同时被多个任务访问。 +2. 这些访问中至少有一个是写操作。 +3. 你尝试避免同步 (在现代 Java 中,你可以使用高级工具来避免进行同步)。 + +举个例字,如果你使用变量作为停止任务的标志值。那么该变量至少必须声明为 **volatile** (尽管这并不一定能保证这种标志的线程安全)。否则,当一个任务更改标志值时,这些更改可以存储在本地处理器缓存中,而不会刷新到主内存。当另一个任务查看标记值时,它不会看到更改。我更喜欢在 [并发编程](./24-Concurrent-Programming.md) 中 [终止耗时任务](./24-Concurrent-Programming.md#终止耗时任务) 章节中使用 **AtomicBoolean** 类型作为标志值的办法 + +任务对其自身变量所做的任何写操作都始终对该任务可见,因此,如果只在任务中使用变量,你不需要使其变量声明为 **volatile** 。 + +如果单个线程对变量写入而其他线程只读取它,你可以放弃该变量声明为 **volatile**。通常,如果你有多个线程对变量写入,**volatile** 无法解决你的问题,并且你必须使用 **synchronized** 来防止竞争条件。 这有一个特殊的例外:可以让多个线程对该变量写入,*只要它们不需要先读取它并使用该值创建新值来写入变量* 。如果这些多个线程在结果中使用旧值,则会出现竞争条件,因为其余一个线程之一可能会在你的线程进行计算时修改该变量。即使你开始做对了,想象一下在代码修改或维护过程中忘记和引入一个重大变化是多么容易,或者对于不理解问题的不同程序员来说是多么容易(这在 Java 中尤其成问题因为程序员倾向于严重依赖编译时检查来告诉他们,他们的代码是否正确)。 + +重要的是要理解原子性和可见性是两个不同的概念。在非 **volatile** 变量上的原子操作是不能保证是否将其刷新到主内存。 + +同步也会让主内存刷新,所以如果一个变量完全由 **synchronized** 的方法或代码段(或者 **java.util.concurrent.atomic** 库里类型之一)所保护,则不需要让变量用 **volatile**。 + ### 重排与 *Happen-Before* 原则 +只要结果不会改变程序表现,Java 可以通过重排指令来优化性能。然而,重排可能会影响本地处理器缓存与主内存交互的方式,从而产生细微的程序 bug 。直到 Java 5 才理解并解决了这个无法阻止重排的问题。现在,**volatile** 关键字可以阻止重排 **volatile** 变量周围的读写指令。这种重排规则称为 *happens before* 担保原则 。 + +这项原则保证在 **volatile** 变量读写之前发生的指令先于它们的读写之前发生。同样,任何跟随 **volatile** 变量之后读写的操作都保证发生在它们的读写之后。例如: + +```java +// lowlevel/ReOrdering.java + +public class ReOrdering implements Runnable { + int one, two, three, four, five, six; + volatile int volaTile; + @Override + public void run() { + one = 1; + two = 2; + three = 3; + volaTile = 92; + int x = four; + int y = five; + int z = six; + } +} +``` + +例子中 **one**,**two**,**three** 变量赋值操作就可以被重排,只要它们都发生在 **volatile** 变量写操作之前。同样,只要 **volatile** 变量写操作发生在所有语句之前, **x**,**y**,**z** 语句可以被重排。这种 **volatile** (易变性)操作通常称为 *memory barrier* (内存屏障)。 *happens before* 担保原则确保 **volatile** 变量的读写指令不能跨过内存屏障进行重排。 + +*happens before* 担保原则还有另一个作用:当线程向一个 **volatile** 变量写入时,在线程写入之前的其他所有变量(包括非 **volatile** 变量)也会刷新到主内存。当线程读取一个 **volatile** 变量时,它也会读取其他所有变量(包括非 **volatile** 变量)与 **volatile** 变量一起刷新到主内存。尽管这是一个重要的特性,它解决了 Java 5 版本之前出现的一些非常狡猾的 bug ,但是你不应该依赖这项特性来“自动”使周围的变量变得易变性 ( **volatile** )的 。如果你希望变量是易变性 ( **volatile** )的,那么维护代码的任何人都应该清楚这一点。 + ### 什么时候使用 volatile +对于 Java 早期版本,编写一个证明需要 **volatile** 的示例并不难。如果你进行搜索,你可以找到这样的例子,但是如果你在 Java 8 中尝试这些例子,它们就不起作用了(我没有找到任何一个)。我努力写这样一个例子,但没什么用。这可能原因是 JVM 或者硬件,或两者都得到了改进。这种效果对现有的应该 **volatile** (易变性) 但不 **volatile** 的存储的程序是有益的;对于此类程序,失误发生的频率要低得多,而且问题更难追踪。 + +如果你尝试使用 **volatile** ,你可能更应该尝试让一个变量线程安全而不是引起同步的成本。因为 **volatile** 使用起来非常微妙和棘手,所以我建议根本不要使用它;相反,请使用本附录后面介绍的 **java.util.concurrent.atomic** 里面类之一。它们以比同步低得多的成本提供了完全的线程安全性。 + +如果你正在尝试调试其他人的并发代码,请首先查找使用 **volatile** 的代码并将其替换为**Atomic** 变量。除非你确定程序员对并发性有很高的理解,否则它们很可能会误用 **volatile** 。 + ## 原子性 -### Josh 的序列数字 +在 Java 线程的讨论中,经常反复提交但不正确的知识是:“原子操作不需要同步”。 一个 *原子操作* 是不能被线程调度机制中断的操作;一旦操作开始,那么它一定可以在可能发生的“上下文切换”之前(切换到其他线程执行)执行完毕。依赖于原子性是很棘手且很危险的,如果你是一个并发编程专家,或者你得到了来自这样的专家的帮助,你才应该使用原子性来代替同步,如果你认为自己足够聪明可以应付这种玩火似的情况,那么请接受下面的测试: -### 使用显式锁定对象 +> Goetz 测试:如果你可以编写用于现代微处理器的高性能 JVM ,那么就有资格考虑是否可以避免同步[^4] 。 + +了解原子性是很有用的,并且知道它与其他高级技术一起用于实现一些更加巧妙的 **java.util.concurrent** 库组件。 但是要坚决抵制自己依赖它的冲动。 + +原子性可以应用于除 **long** 和 **double** 之外的所有基本类型之上的 “简单操作”。对于读写和写入除 **long** 和 **double** 之外的基本类型变量这样的操作,可以保证它们作为不可分 (原子) 的操作执行。 + + +因为原子操作不能被线程机制中断。专家程序员可以利用这个来编写无锁代码(*lock-free code*),这些代码不需要被同步。但即使这样也过于简单化了。有时候,甚至看起来应该是安全的原子操作,实际上也可能不安全。本书的读者通常不会通过前面提到的 Goetz 测试,因此也就不具备用原子操作来替换同步的能力。尝试着移除同步通常是一种表示不成熟优化的信号,并且会给你带来大量的麻烦,可能不会获得太多或任何的好处。 + +在多核处理器系统,相对于单核处理器而言,可见性问题远比原子性问题多得多。一个任务所做的修改,即使它们是原子性的,也可能对其他任务不可见(例如,修改只是暂时性存储在本地处理器缓存中),因此不同的任务对应用的状态有不同的视图。另一方面,同步机制强制多核处理器系统上的一个任务做出的修改必须在应用程序中是可见的。如果没有同步机制,那么修改时可见性将无法确认。 + +什么才属于原子操作时?对于属性中的值做赋值和返回操作通常都是原子性的,但是在 C++ 中,甚至下面的操作都可能是原子性的: + +```c++ +i++; // Might be atomic in C++ +i += 2; // Might be atomic in C++ +``` + +但是在 C++ 中,这取决于编译器和处理器。你无法编写出依赖于原子性的 C++ 跨平台代码,因为 C++ [^5]没有像 Java 那样的一致 *内存模型* (memory model)。 + +在 Java 中,上面的操作肯定不是原子性的,正如下面的方法产生的 JVM 指令中可以看到的那样: + +```java +// lowlevel/NotAtomic.java +// {javap -c NotAtomic} +// {VisuallyInspectOutput} + +public class NotAtomic { + int i; + void f1() { i++; } + void f2() { i += 3; } +} +/* Output: +Compiled from "NotAtomic.java" +public class NotAtomic { + int i; + + public NotAtomic(); + Code: + 0: aload_0 + 1: invokespecial #1 // Method +java/lang/Object."":()V + 4: return + + void f1(); + Code: + 0: aload_0 + 1: dup + 2: getfield #2 // Field +i:I + 5: iconst_1 + 6: iadd + 7: putfield #2 // Field +i:I + 10: return + + void f2(); + Code: + 0: aload_0 + 1: dup + 2: getfield #2 // Field +i:I + 5: iconst_3 + 6: iadd + 7: putfield #2 // Field +i:I + 10: return +} +*/ +``` + +每条指令都会产生一个 “get” 和 “put”,它们之间还有一些其他指令。因此在获取指令和放置指令之间,另有一个任务可能会修改这个属性,所有,这些操作不是原子性的。 + +让我们通过定义一个抽象类来测试原子性的概念,这个抽象类的方法是将一个整数类型进行偶数自增,并且 `run()` 不断地调用这个方法: + +```java +// lowlevel/IntTestable.java +import java.util.function.*; + +public abstract class +IntTestable implements Runnable, IntSupplier { + abstract void evenIncrement(); + @Override + public void run() { + while(true) + evenIncrement(); + } +} +``` + +**IntSupplier** 是一个带 `getAsInt()` 方法的函数式接口。 + +现在我们可以创建一个测试,它作为一个独立的任务启动 `run()` 方法 ,然后获取值来检查它们是否为偶数: + +```java +// lowlevel/Atomicity.java +import java.util.concurrent.*; +import onjava.TimedAbort; + +public class Atomicity { + public static void test(IntTestable it) { + new TimedAbort(4, "No failures found"); + CompletableFuture.runAsync(it); + while(true) { + int val = it.getAsInt(); + if(val % 2 != 0) { + System.out.println("failed with: " + val); + System.exit(0); + } + } + } +} +``` + +很容易盲目地应用原子性的概念。在这里,`getAsInt()` 似乎是安全的原子性方法: + +```java +// lowlevel/UnsafeReturn.java +import java.util.function.*; +import java.util.concurrent.*; + +public class UnsafeReturn extends IntTestable { + private int i = 0; + public int getAsInt() { return i; } + public synchronized void evenIncrement() { + i++; i++; + } + public static void main(String[] args) { + Atomicity.test(new UnsafeReturn()); + } +} +/* Output: +failed with: 79 +*/ +``` + +但是, `Atomicity.test()` 方法还是出现有非偶数的失败。尽管,返回 **i** 变量确实是原子操作,但是同步缺失允许了在对象处于不稳定的中间状态时读取值。最重要的是,由于 **i** 也不是 **volatile** 变量,所以存在可见性问题。包括 `getValue()` 和 `evenIncrement()` 都必须同步(这也顾及到没有使用 **volatile** 修饰的 **i** 变量): + +```java +// lowlevel/SafeReturn.java +import java.util.function.*; +import java.util.concurrent.*; + +public class SafeReturn extends IntTestable { + private int i = 0; + public synchronized int getAsInt() { return i; } + public synchronized void evenIncrement() { + i++; i++; + } + public static void main(String[] args) { + Atomicity.test(new SafeReturn()); + } +} +/* Output: +No failures found +*/ +``` + +只有并发编程专家有能力去尝试做像前面例子情况的优化;再次强调,请遵循 Brain 的同步法则。 + +### Josh 的序列号 + +作为第二个示例,考虑某些更简单的东西:创建一个产生序列号的类,灵感启发于 Joshua Bloch 的 *Effective Java Programming Language Guide* (Addison-Wesley 出版社, 2001) 第 190 页。每次调用 `nextSerialNumber()` 都必须返回唯一值。 + +```java +// lowlevel/SerialNumbers.java + +public class SerialNumbers { + private volatile int serialNumber = 0; + public int nextSerialNumber() { + return serialNumber++; // Not thread-safe + } +} +``` + +**SerialNumbers** 是你可以想象到最简单的类,如果你具备 C++ 或者其他底层的知识背景,你可能会认为自增是一个原子操作,因为 C++ 的自增操作通常被单个微处理器指令所实现(尽管不是以任何一致,可靠,跨平台的方式)。但是,正如前面所提到的,Java 自增操作不是原子性的,并且操作同时涉及读取和写入,因此即使在这样一个简单的操作中,也存在有线程问题的空间。 + +我们在这里加入 volatile ,看看它是否有帮助。然而,真正的问题是 `nextSerialNumber()` 方法在不进行线程同步的情况下访问共享的可变变量值。 + +为了测试 **SerialNumbers**,我们将创建一个不会耗尽内存的集合,假如需要很长时间来检测问题。这里展示的 **CircularSet** 重用了存储 **int** 变量的内存,最终新值会覆盖旧值(复制的速度通常发生足够快,你也可以使用 **java.util.Set** 来代替): + +```java +// lowlevel/CircularSet.java +// Reuses storage so we don't run out of memory +import java.util.*; + +public class CircularSet { + private int[] array; + private int size; + private int index = 0; + public CircularSet(int size) { + this.size = size; + array = new int[size]; + // Initialize to a value not produced + // by SerialNumbers: + Arrays.fill(array, -1); + } + public synchronized void add(int i) { + array[index] = i; + // Wrap index and write over old elements: + index = ++index % size; + } + public synchronized boolean contains(int val) { + for(int i = 0; i < size; i++) + if(array[i] == val) return true; + return false; + } +} +``` + +`add()` 和 `contains()` 方法是线程同步的,以防止线程冲突。 +The add() and contains() methods are synchronized to prevent thread collisions. + +**SerialNumberChecker** 类包含一个存储最近序列号的 **CircularSet** 变量,以及一个填充数值给 **CircularSet** 和确保它里面的序列号是唯一的 `run()` 方法。 + +```java +// lowlevel/SerialNumberChecker.java +// Test SerialNumbers implementations for thread-safety +import java.util.concurrent.*; +import onjava.Nap; + +public class SerialNumberChecker implements Runnable { + private CircularSet serials = new CircularSet(1000); + private SerialNumbers producer; + public SerialNumberChecker(SerialNumbers producer) { + this.producer = producer; + } + @Override + public void run() { + while(true) { + int serial = producer.nextSerialNumber(); + if(serials.contains(serial)) { + System.out.println("Duplicate: " + serial); + System.exit(0); + } + serials.add(serial); + } + } + static void test(SerialNumbers producer) { + for(int i = 0; i < 10; i++) + CompletableFuture.runAsync( + new SerialNumberChecker(producer)); + new Nap(4, "No duplicates detected"); + } +} +``` + +`test()` 方法创建多个任务来竞争单独的 **SerialNumbers** 对象。这时参于竞争的的 SerialNumberChecker 任务们就会试图生成重复的序列号(这情况在具有更多内核处理器的机器上发生得更快)。 + +当我们测试基本的 **SerialNumbers** 类,它会失败(产生重复序列号): + +```java +// lowlevel/SerialNumberTest.java + +public class SerialNumberTest { + public static void main(String[] args) { + SerialNumberChecker.test(new SerialNumbers()); + } +} +/* Output: +Duplicate: 148044 +*/ +``` + +**volatile** 在这里没有帮助。要解决这个问题,将 **synchronized** 关键字添加到 `nextSerialNumber()` 方法 : + +```java +// lowlevel/SynchronizedSerialNumbers.java + +public class +SynchronizedSerialNumbers extends SerialNumbers { + private int serialNumber = 0; + public synchronized int nextSerialNumber() { + return serialNumber++; + } + public static void main(String[] args) { + SerialNumberChecker.test( + new SynchronizedSerialNumbers()); + } +} +/* Output: +No duplicates detected +*/ +``` + +**volatile** 不再是必需的,因为 **synchronized** 关键字保证了 volatile (易变性) 的特性。 + +读取和赋值原语应该是安全的原子操作。然后,正如在 **UnsafeReturn.java** 中所看到,使用原子操作访问处于不稳定中间状态的对象仍然很容易。对这个问题做出假设既棘手又危险。最明智的做法就是遵循 Brian 的同步规则(如果可以,首先不要共享变量)。 ### 原子类 +Java 5 引入了专用的原子变量类,例如 **AtomicInteger**、**AtomicLong**、**AtomicReference** 等。这些提供了原子性升级。这些快速、无锁的操作,它们是利用了现代处理器上可用的机器级原子性。 + +下面,我们可以使用 **atomicinteger** 重写 **unsafereturn.java** 示例: + +```java +// lowlevel/AtomicIntegerTest.java +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; +import java.util.*; +import onjava.*; + +public class AtomicIntegerTest extends IntTestable { + private AtomicInteger i = new AtomicInteger(0); + public int getAsInt() { return i.get(); } + public void evenIncrement() { i.addAndGet(2); } + public static void main(String[] args) { + Atomicity.test(new AtomicIntegerTest()); + } +} +/* Output: +No failures found +*/ +``` + +现在,我们通过使用 **AtomicInteger** 来消除了 **synchronized** 关键字。 + +下面使用 **AtomicInteger** 来重写 **SynchronizedEvenProducer.java** 示例: + +```java +// lowlevel/AtomicEvenProducer.java +// Atomic classes: occasionally useful in regular code +import java.util.concurrent.atomic.*; + +public class AtomicEvenProducer extends IntGenerator { + private AtomicInteger currentEvenValue = + new AtomicInteger(0); + @Override + public int next() { + return currentEvenValue.addAndGet(2); + } + public static void main(String[] args) { + EvenChecker.test(new AtomicEvenProducer()); + } +} +/* Output: +No odd numbers discovered +*/ +``` + +再次,使用 **AtomicInteger** 消除了对所有其他同步方式的需要。 + +下面是一个使用 **AtomicInteger** 实现 **SerialNumbers** 的例子: + +```java +// lowlevel/AtomicSerialNumbers.java +import java.util.concurrent.atomic.*; + +public class +AtomicSerialNumbers extends SerialNumbers { + private AtomicInteger serialNumber = + new AtomicInteger(); + public synchronized int nextSerialNumber() { + return serialNumber.getAndIncrement(); + } + public static void main(String[] args) { + SerialNumberChecker.test( + new AtomicSerialNumbers()); + } +} +/* Output: +No duplicates detected +*/ +``` + +这些都是对单一字段的简单示例; 当你创建更复杂的类时,你必须确定哪些字段需要保护,在某些情况下,你可能仍然最后在方法上使用 **synchronized** 关键字。 + ## 临界区 +有时,你只是想防止多线程访问方法中的部分代码,而不是整个方法。要隔离的代码部分称为临界区,它使用我们用于保护整个方法相同的 **synchronized** 关键字创建,但使用不同的语法。语法如下, **synchronized** 指定某个对象作为锁用于同步控制花括号内的代码: + +```java +synchronized(syncObject) { + // This code can be accessed + // by only one task at a time +} +``` + +这也被称为 *同步控制块* (synchronized block);在进入此段代码前,必须得到 **syncObject** 对象的锁。如果一些其他任务已经得到这个锁,那么就得等到锁被释放以后,才能进入临界区。当发生这种情况时,尝试获取该锁的任务就会挂起。线程调度会定期回来并检查锁是否已经释放;如果释放了锁则唤醒任务。 + +使用同步控制块而不是同步控制整个方法的主要动机是性能(有时,算法确实聪明,但还是要特别警惕来自并发性问题上的聪明)。下面的示例演示了同步控制代码块而不是整个方法可以使方法更容易被其他任务访问。该示例会统计成功访问 `method()` 的计数并且发起一些任务来尝试竞争调用 `method()` 方法。 + +```java +// lowlevel/SynchronizedComparison.java +// speeds up access. +import java.util.*; +import java.util.stream.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; +import onjava.Nap; + +abstract class Guarded { + AtomicLong callCount = new AtomicLong(); + public abstract void method(); + @Override + public String toString() { + return getClass().getSimpleName() + + ": " + callCount.get(); + } +} + +class SynchronizedMethod extends Guarded { + public synchronized void method() { + new Nap(0.01); + callCount.incrementAndGet(); + } +} + +class CriticalSection extends Guarded { + public void method() { + new Nap(0.01); + synchronized(this) { + callCount.incrementAndGet(); + } + } +} + +class Caller implements Runnable { + private Guarded g; + Caller(Guarded g) { this.g = g; } + private AtomicLong successfulCalls = + new AtomicLong(); + private AtomicBoolean stop = + new AtomicBoolean(false); + @Override + public void run() { + new Timer().schedule(new TimerTask() { + public void run() { stop.set(true); } + }, 2500); + while(!stop.get()) { + g.method(); + successfulCalls.getAndIncrement(); + } + System.out.println( + "-> " + successfulCalls.get()); + } +} + +public class SynchronizedComparison { + static void test(Guarded g) { + List> callers = + Stream.of( + new Caller(g), + new Caller(g), + new Caller(g), + new Caller(g)) + .map(CompletableFuture::runAsync) + .collect(Collectors.toList()); + callers.forEach(CompletableFuture::join); + System.out.println(g); + } + public static void main(String[] args) { + test(new CriticalSection()); + test(new SynchronizedMethod()); + } +} +/* Output: +-> 243 +-> 243 +-> 243 +-> 243 +CriticalSection: 972 +-> 69 +-> 61 +-> 83 +-> 36 +SynchronizedMethod: 249 +*/ +``` + +**Guarded** 类负责跟踪 **callCount** 中成功调用 `method()` 的次数。**SynchronizedMethod** 的方式是同步控制整个 `method` 方法,而 **CriticalSection** 的方式是使用同步控制块来仅同步 `method` 方法的一部分代码。这样,耗时的 **Nap** 对象可以被排除到同步控制块外。输出会显示 **CriticalSection** 中可用的 `method()` 有多少。 + +请记住,使用同步控制块是有风险;它要求你确切知道同步控制块外的非同步代码是实际上要线程安全的。 + +**Caller** 是尝试在给定的时间周期内尽可能多地调用 `method()` 方法(并报告调用次数)的任务。为了构建这个时间周期,我们会使用虽然有点过时但仍然可以很好地工作的 **java.util.Timer** 类。此类接收一个 **TimerTask** 参数, 但该参数并不是函数式接口,所以我们不能使用 **lambda** 表达式,必须显式创建该类对象(在这种情况下,使用匿名内部类)。当超时的时候,定时对象将设置 **AtomicBoolean** 类型的 **stop** 字段为 true ,这样循环就会退出。 + +`test()` 方法接收一个 **Guarded** 类对象并创建四个 **Caller** 任务。所有这些任务都添加到同一个 **Guarded** 对象上,因此它们竞争来获取使用 `method()` 方法的锁。 + +你通常会看到从一次运行到下一次运行的输出变化。结果表明, **CriticalSection** 方式比起 **SynchronizedMethod** 方式允许更多地访问 `method()` 方法。这通常是使用 **synchronized** 块取代同步控制整个方法的原因:允许其他任务更多访问(只要这样做是线程安全的)。 + ### 在其他对象上同步 +**synchronized** 块必须给定一个在其上进行同步的对象。并且最合理的方式是,使用其方法正在被调用的当前对象: **synchronized(this)**,这正是前面示例中 **CriticalSection** 采取的方式。在这种方式中,当 **synchronized** 块获得锁的时候,那么该对象其他的 **synchronized** 方法和临界区就不能被调用了。因此,在进行同步时,临界区的作用是减小同步的范围。 + +有时必须在另一个对象上同步,但是如果你要这样做,就必须确保所有相关的任务都是在同一个任务上同步的。下面的示例演示了当对象中的方法在不同的锁上同步时,两个任务可以同时进入同一对象: + +```java +// lowlevel/SyncOnObject.java +// Synchronizing on another object +import java.util.*; +import java.util.stream.*; +import java.util.concurrent.*; +import onjava.Nap; + +class DualSynch { + ConcurrentLinkedQueue trace = + new ConcurrentLinkedQueue<>(); + public synchronized void f(boolean nap) { + for(int i = 0; i < 5; i++) { + trace.add(String.format("f() " + i)); + if(nap) new Nap(0.01); + } + } + private Object syncObject = new Object(); + public void g(boolean nap) { + synchronized(syncObject) { + for(int i = 0; i < 5; i++) { + trace.add(String.format("g() " + i)); + if(nap) new Nap(0.01); + } + } + } +} + +public class SyncOnObject { + static void test(boolean fNap, boolean gNap) { + DualSynch ds = new DualSynch(); + List> cfs = + Arrays.stream(new Runnable[] { + () -> ds.f(fNap), () -> ds.g(gNap) }) + .map(CompletableFuture::runAsync) + .collect(Collectors.toList()); + cfs.forEach(CompletableFuture::join); + ds.trace.forEach(System.out::println); + } + public static void main(String[] args) { + test(true, false); + System.out.println("****"); + test(false, true); + } +} +/* Output: +f() 0 +g() 0 +g() 1 +g() 2 +g() 3 +g() 4 +f() 1 +f() 2 +f() 3 +f() 4 +**** +f() 0 +g() 0 +f() 1 +f() 2 +f() 3 +f() 4 +g() 1 +g() 2 +g() 3 +g() 4 +*/ +``` + +`DualSync.f()` 方法(通过同步整个方法)在 **this** 上同步,而 `g()` 方法有一个在 **syncObject** 上同步的 **synchronized** 块。因此,这两个同步是互相独立的。在 `test()` 方法中运行的两个调用 `f()` 和 `g()` 方法的独立任务演示了这一点。**fNap** 和 **gNap** 标志变量分别指示 `f()` 和 `g()` 是否应该在其 **for** 循环中调用 `Nap()` 方法。例如,当 f() 线程休眠时 ,该线程继续持有它的锁,但是你可以看到这并不阻止调用 `g()` ,反之亦然。 + +### 使用显式锁对象 + +**java.util.concurrent** 库包含在 **java.util.concurrent.locks** 中定义的显示互斥锁机制。 必须显式地创建,锁定和解锁 **Lock** 对象,因此它产出的代码没有内置 **synchronized** 关键字那么优雅。然而,它在解决某些类型的问题时更加灵活。下面是使用显式 **Lock** 对象重写 **SynchronizedEvenProducer.java** 代码: + +```java +// lowlevel/MutexEvenProducer.java +// Preventing thread collisions with mutexes +import java.util.concurrent.locks.*; +import onjava.Nap; + +public class MutexEvenProducer extends IntGenerator { + private int currentEvenValue = 0; + private Lock lock = new ReentrantLock(); + @Override + public int next() { + lock.lock(); + try { + ++currentEvenValue; + new Nap(0.01); // Cause failure faster + ++currentEvenValue; + return currentEvenValue; + } finally { + lock.unlock(); + } + } + public static void main(String[] args) { + EvenChecker.test(new MutexEvenProducer()); + } +} +/* +No odd numbers discovered +*/ +``` +**MutexEvenProducer** 添加一个名为 **lock** 的互斥锁并在 `next()` 中使用 `lock()` 和 `unlock()` 方法创建一个临界区。当你使用 **Lock** 对象时,使用下面显示的习惯用法很重要:在调用 `Lock()` 之后,你必须放置 **try-finally** 语句,该语句在 **finally** 子句中带有 `unlock()` 方法 - 这是确保锁总是被释放的惟一方法。注意,**return** 语句必须出现在 **try** 子句中,以确保 **unlock()** 不会过早发生并将数据暴露给第二个任务。 + +尽管 **try-finally** 比起使用 **synchronized** 关键字需要用得更多代码,但它也代表了显式锁对象的优势之一。如果使用 **synchronized** 关键字失败,就会抛出异常,但是你没有机会进行任何清理以保持系统处于良好状态。而使用显式锁对象,可以使用 **finally** 子句在系统中维护适当的状态。 + +一般来说,当你使用 **synchronized** 的时候,需要编写的代码更少,并且用户出错的机会也大大减少,因此通常只在解决特殊问题时使用显式锁对象。例如,使用 **synchronized** 关键字,你不能尝试获得锁并让其失败,或者你在一段时间内尝试获得锁,然后放弃 - 为此,你必须使用这个并发库。 + +```java +// lowlevel/AttemptLocking.java +// Locks in the concurrent library allow you +// to give up on trying to acquire a lock +import java.util.concurrent.*; +import java.util.concurrent.locks.*; +import onjava.Nap; + +public class AttemptLocking { + private ReentrantLock lock = new ReentrantLock(); + public void untimed() { + boolean captured = lock.tryLock(); + try { + System.out.println("tryLock(): " + captured); + } finally { + if(captured) + lock.unlock(); + } + } + public void timed() { + boolean captured = false; + try { + captured = lock.tryLock(2, TimeUnit.SECONDS); + } catch(InterruptedException e) { + throw new RuntimeException(e); + } + try { + System.out.println( + "tryLock(2, TimeUnit.SECONDS): " + captured); + } finally { + if(captured) + lock.unlock(); + } + } + public static void main(String[] args) { + final AttemptLocking al = new AttemptLocking(); + al.untimed(); // True -- lock is available + al.timed(); // True -- lock is available + // Now create a second task to grab the lock: + CompletableFuture.runAsync( () -> { + al.lock.lock(); + System.out.println("acquired"); + }); + new Nap(0.1); // Give the second task a chance + al.untimed(); // False -- lock grabbed by task + al.timed(); // False -- lock grabbed by task + } +} +/* Output: +tryLock(): true +tryLock(2, TimeUnit.SECONDS): true +acquired +tryLock(): false +tryLock(2, TimeUnit.SECONDS): false +*/ +``` + +**ReentrantLock** 可以尝试或者放弃获取锁,因此如果某些任务已经拥有锁,你可以决定放弃并执行其他操作,而不是一直等到锁释放,就像 `untimed()` 方法那样。而在 `timed()` 方法中,则尝试获取可能在 2 秒后没成功而放弃的锁。在 `main()` 方法中,一个单独的线程被匿名类所创建,并且它会获得锁,因此让 `untimed()` 和 `timed() ` 方法有东西可以去竞争。 + +显式锁比起内置同步锁提供更细粒度的加锁和解锁控制。这对于实现专门的同步并发结构,比如用于遍历链表节点的 *交替锁* ( *hand-over-hand locking* ) ,也称为 *锁耦合* ( *lock coupling* )- 该遍历代码要求必须在当前节点的解锁之前捕获下一个节点的锁。 + ## 库组件 +**java.util.concurrent** 库提供大量旨在解决并发问题的类,可以帮助你生成更简单,更鲁棒的并发程序。但请注意,这些工具是比起并行流和 **CompletableFuture** 更底层的机制。 + +在本节中,我们将看一些使用不同组件的示例,然后讨论一下 *lock-free*(无锁) 库组件是如何工作的。 + ### DelayQueue +这是一个无界阻塞队列 ( **BlockingQueue** ),用于放置实现了 **Delayed** 接口的对象,其中的对象只能在其到期时才能从队列中取走。这种队列是有序的,因此队首对象的延迟到期的时间最长。如果没有任何延迟到期,那么就不会有队首元素,并且 `poll()` 将返回 **null**(正因为这样,你不能将 **null** 放置到这种队列中)。 + +下面是一个示例,其中的 **Delayed** 对象自身就是任务,而 **DelayedTaskConsumer** 将最“紧急”的任务(到期时间最长的任务)从队列中取出,然后运行它。注意的是这样 **DelayQueue** 就成为了优先级队列的一种变体。 + +```java +// lowlevel/DelayQueueDemo.java +import java.util.*; +import java.util.stream.*; +import java.util.concurrent.*; +import static java.util.concurrent.TimeUnit.*; + +class DelayedTask implements Runnable, Delayed { + private static int counter = 0; + private final int id = counter++; + private final int delta; + private final long trigger; + protected static List sequence = + new ArrayList<>(); + DelayedTask(int delayInMilliseconds) { + delta = delayInMilliseconds; + trigger = System.nanoTime() + + NANOSECONDS.convert(delta, MILLISECONDS); + sequence.add(this); + } + @Override + public long getDelay(TimeUnit unit) { + return unit.convert( + trigger - System.nanoTime(), NANOSECONDS); + } + @Override + public int compareTo(Delayed arg) { + DelayedTask that = (DelayedTask)arg; + if(trigger < that.trigger) return -1; + if(trigger > that.trigger) return 1; + return 0; + } + @Override + public void run() { + System.out.print(this + " "); + } + @Override + public String toString() { + return + String.format("[%d] Task %d", delta, id); + } + public String summary() { + return String.format("(%d:%d)", id, delta); + } + public static class EndTask extends DelayedTask { + EndTask(int delay) { super(delay); } + @Override + public void run() { + sequence.forEach(dt -> + System.out.println(dt.summary())); + } + } +} + +public class DelayQueueDemo { + public static void + main(String[] args) throws Exception { + DelayQueue tasks = + Stream.concat( // Random delays: + new Random(47).ints(20, 0, 4000) + .mapToObj(DelayedTask::new), + // Add the summarizing task: + Stream.of(new DelayedTask.EndTask(4000))) + .collect(Collectors + .toCollection(DelayQueue::new)); + while(tasks.size() > 0) + tasks.take().run(); + } +} +/* Output: +[128] Task 12 [429] Task 6 [551] Task 13 [555] Task 2 +[693] Task 3 [809] Task 15 [961] Task 5 [1258] Task 1 +[1258] Task 20 [1520] Task 19 [1861] Task 4 [1998] Task +17 [2200] Task 8 [2207] Task 10 [2288] Task 11 [2522] +Task 9 [2589] Task 14 [2861] Task 18 [2868] Task 7 +[3278] Task 16 (0:4000) +(1:1258) +(2:555) +(3:693) +(4:1861) +(5:961) +(6:429) +(7:2868) +(8:2200) +(9:2522) +(10:2207) +(11:2288) +(12:128) +(13:551) +(14:2589) +(15:809) +(16:3278) +(17:1998) +(18:2861) +(19:1520) +(20:1258) +*/ +``` + +**DelayedTask** 包含一个称为 **sequence** 的 **List<DelayedTask>** ,它保存了任务被创建的顺序,因此我们可以看到排序是按照实际发生的顺序执行的。 + +**Delay** 接口有一个方法, `getDelay()` , 该方法用来告知延迟到期有多长时间,或者延迟在多长时间之前已经到期了。这个方法强制我们去使用 **TimeUnit** 类,因为这就是参数类型。这会产生一个非常方便的类,因为你可以很容易地转换单位而无需作任何声明。例如,**delta** 的值是以毫秒为单位存储的,但是 `System.nanoTime()` 产生的时间则是以纳秒为单位的。你可以转换 **delta** 的值,方法是声明它的单位以及你希望以什么单位来表示,就像下面这样: + +```java +NANOSECONDS.convert(delta, MILLISECONDS); +``` + +在 `getDelay()` 中, 所希望的单位是作为 **unit** 参数传递进来的,你使用它将当前时间与触发时间之间的差转换为调用者要求的单位,而无需知道这些单位是什么(这是*策略*设计模式的一个简单示例,在这种模式中,算法的一部分是作为参数传递进来的)。 + +为了排序, **Delayed** 接口还继承了 **Comparable** 接口,因此必须实现 `compareTo()` , 使其可以产生合理的比较。 + +从输出中可以看到,任务创建的顺序对执行顺序没有任何影响 - 相反,任务是按照所期望的延迟顺序所执行的。 + ### PriorityBlockingQueue -### Lock-Free Collections +这是一个很基础的优先级队列,它具有可阻塞的读取操作。在下面的示例中, **Prioritized** 对象会被赋予优先级编号。几个 **Producer** 任务的实例会插入 **Prioritized** 对象到 **PriorityBlockingQueue** 中,但插入之间会有随机延时。然后,单个 **Consumer** 任务在执行 `take()` 时会显示多个选项,**PriorityBlockingQueue** 会将当前具有最高优先级的 **Prioritized** 对象提供给它。 + +在 **Prioritized** 中的静态变量 **counter** 是 **AtomicInteger** 类型。这是必要的,因为有多个 **Producer** 并行运行;如果不是 **AtomicInteger** 类型,你将会看到重复的 **id** 号。 这个问题在 [并发编程](./24-Concurrent-Programming.md) 的 [构造函数非线程安全](./24-Concurrent-Programming.md) 一节中讨论过。 + +```java +// lowlevel/PriorityBlockingQueueDemo.java +import java.util.*; +import java.util.stream.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; +import onjava.Nap; + +class Prioritized implements Comparable { + private static AtomicInteger counter = + new AtomicInteger(); + private final int id = counter.getAndIncrement(); + private final int priority; + private static List sequence = + new CopyOnWriteArrayList<>(); + Prioritized(int priority) { + this.priority = priority; + sequence.add(this); + } + @Override + public int compareTo(Prioritized arg) { + return priority < arg.priority ? 1 : + (priority > arg.priority ? -1 : 0); + } + @Override + public String toString() { + return String.format( + "[%d] Prioritized %d", priority, id); + } + public void displaySequence() { + int count = 0; + for(Prioritized pt : sequence) { + System.out.printf("(%d:%d)", pt.id, pt.priority); + if(++count % 5 == 0) + System.out.println(); + } + } + public static class EndSentinel extends Prioritized { + EndSentinel() { super(-1); } + } +} + +class Producer implements Runnable { + private static AtomicInteger seed = + new AtomicInteger(47); + private SplittableRandom rand = + new SplittableRandom(seed.getAndAdd(10)); + private Queue queue; + Producer(Queue q) { + queue = q; + } + @Override + public void run() { + rand.ints(10, 0, 20) + .mapToObj(Prioritized::new) + .peek(p -> new Nap(rand.nextDouble() / 10)) + .forEach(p -> queue.add(p)); + queue.add(new Prioritized.EndSentinel()); + } +} + +class Consumer implements Runnable { + private PriorityBlockingQueue q; + private SplittableRandom rand = + new SplittableRandom(47); + Consumer(PriorityBlockingQueue q) { + this.q = q; + } + @Override + public void run() { + while(true) { + try { + Prioritized pt = q.take(); + System.out.println(pt); + if(pt instanceof Prioritized.EndSentinel) { + pt.displaySequence(); + break; + } + new Nap(rand.nextDouble() / 10); + } catch(InterruptedException e) { + throw new RuntimeException(e); + } + } + } +} + +public class PriorityBlockingQueueDemo { + public static void main(String[] args) { + PriorityBlockingQueue queue = + new PriorityBlockingQueue<>(); + CompletableFuture.runAsync(new Producer(queue)); + CompletableFuture.runAsync(new Producer(queue)); + CompletableFuture.runAsync(new Producer(queue)); + CompletableFuture.runAsync(new Consumer(queue)) + .join(); + } +} +/* Output: +[15] Prioritized 2 +[17] Prioritized 1 +[17] Prioritized 5 +[16] Prioritized 6 +[14] Prioritized 9 +[12] Prioritized 0 +[11] Prioritized 4 +[11] Prioritized 12 +[13] Prioritized 13 +[12] Prioritized 16 +[14] Prioritized 18 +[15] Prioritized 23 +[18] Prioritized 26 +[16] Prioritized 29 +[12] Prioritized 17 +[11] Prioritized 30 +[11] Prioritized 24 +[10] Prioritized 15 +[10] Prioritized 22 +[8] Prioritized 25 +[8] Prioritized 11 +[8] Prioritized 10 +[6] Prioritized 31 +[3] Prioritized 7 +[2] Prioritized 20 +[1] Prioritized 3 +[0] Prioritized 19 +[0] Prioritized 8 +[0] Prioritized 14 +[0] Prioritized 21 +[-1] Prioritized 28 +(0:12)(2:15)(1:17)(3:1)(4:11) +(5:17)(6:16)(7:3)(8:0)(9:14) +(10:8)(11:8)(12:11)(13:13)(14:0) +(15:10)(16:12)(17:12)(18:14)(19:0) +(20:2)(21:0)(22:10)(23:15)(24:11) +(25:8)(26:18)(27:-1)(28:-1)(29:16) +(30:11)(31:6)(32:-1) +*/ +``` + +与前面的示例一样,**Prioritized** 对象的创建顺序在 **sequence** 的 **list** 对象上所记入,以便与实际执行顺序进行比较。 **EndSentinel** 是用于告知 **Consumer** 对象关闭的特殊类型。 + +**Producer** 使用 **AtomicInteger** 变量为 **SplittableRandom** 设置随机生成种子,以便不同的 **Producer** 生成不同的队列。 这是必需的,因为多个生产者并行创建,如果不是这样,创建过程并不会是线程安全的。 + +**Producer** 和 **Consumer** 通过 **PriorityBlockingQueue** 相互连接。因为阻塞队列的性质提供了所有必要的同步,因为阻塞队列的性质提供了所有必要的同步,请注意,显式同步是并不需要的 — 从队列中读取数据时,你不用考虑队列中是否有任何元素,因为队列在没有元素时将阻塞读取。 + +### 无锁集合 + +[集合](./12-Collections.md) 章节强调集合是基本的编程工具,这也要求包含并发性。因此,早期的集合比如 **Vector** 和 **Hashtable** 有许多使用 **synchronized** 机制的方法。当这些集合不是在多线程应用中使用时,这就导致了不可接收的开销。在 Java 1.2 版本中,新的集合库是非同步的,而给 **Collection** 类赋予了各种 **static** **synchronized** 修饰的方法来同步不同的集合类型。虽然这是一个改进,因为它让你可以选择是否对集合使用同步,但是开销仍然基于同步锁定。 Java 5 版本添加新的集合类型,专门用于增加线程安全性能,使用巧妙的技术来消除锁定。 + +无锁集合有一个有趣的特性:只要读取者仅能看到已完成修改的结果,对集合的修改就可以同时发生在读取发生时。这是通过一些策略实现的。为了让你了解它们是如何工作的,我们来看看其中的一些。 + +#### 复制策略 + +使用“复制”策略,修改是在数据结构一部分的单独副本(或有时是整个数据的副本)上进行的,并且在整个修改过程期间这个副本是不可见的。仅当修改完成时,修改后的结构才与“主”数据结构安全地交换,然后读取者才会看到修改。 + +在 **CopyOnWriteArrayList** ,写入操作会复制整个底层数组。保留原来的数组,以便在修改复制的数组时可以线程安全地进行读取。当修改完成后,原子操作会将其交换到新数组中,以便新的读取操作能够看到新数组内容。 **CopyOnWriteArrayList** 的其中一个好处是,当多个迭代器遍历和修改列表时,它不会抛出 **ConcurrentModificationException** 异常,因此你不用就像过去必须做的那样,编写特殊的代码来防止此类异常。 + +**CopyOnWriteArraySet** 使用 **CopyOnWriteArrayList** 来实现其无锁行为。 + +**ConcurrentHashMap** 和 **ConcurrentLinkedQueue** 使用类似的技术来允许并发读写,但是只复制和修改集合的一部分,而不是整个集合。然而,读取者仍然不会看到任何不完整的修改。**ConcurrentHashMap** **不会抛出concurrentmodificationexception** 异常。 + +#### 比较并交换 (CAS) + +在 比较并交换 (CAS) 中,你从内存中获取一个值,并在计算新值时保留原始值。然后使用 CAS 指令,它将原始值与当前内存中的值进行比较,如果这两个值是相等的,则将内存中的旧值替换为计算新值的结果,所有操作都在一个原子操作中完成。如果原始值比较失败,则不会进行交换,因为这意味着另一个线程同时修改了内存。在这种情况下,你的代码必须再次尝试,获取一个新的原始值并重复该操作。 + +如果内存仅轻量竞争,CAS操作几乎总是在没有重复尝试的情况下完成,因此它非常快。相反,**synchronized** 操作需要考虑每次获取和释放锁的成本,这要昂贵得多,而且没有额外的好处。随着内存竞争的增加,使用 CAS 的操作会变慢,因为它必须更频繁地重复自己的操作,但这是对更多资源竞争的动态响应。这确实是一种优雅的方法。 + +最重要的是,许多现代处理器的汇编语言中都有一条 CAS 指令,并且也被 JVM 中的 CAS 操作(例如 **Atomic** 类中的操作)所使用。CAS 指令在硬件层面中是原子性的,并且与你所期望的操作一样快。 ## 本章小结 -本附录主要是为了让您在遇到底层并发代码时能对此有一定的了解,尽管本文还远没对这个主题进行全面的讨论。为此,你需要先从阅读由 Brian Goetz, Tim Peierls, Joshua Bloch, Joseph Bowbeer, David Holmes, and Doug Lea (Addison-Wesley 出版社, 2006)所著作的 *Java Concurrency in Practice* (国内译名:Java并发编程实战)开始了解。理想情况下,这本书会完全吓跑你在 Java 中尝试去编写底层并发代码。如果没有,那么你几乎肯定患上了达克效应(DunningKruger Effect),这是一种认知偏差,“你知道的越少,对自己的能力就越有信心”。请记住,当前的语言设计人员仍然在清理早期语言设计人员过于自信造成的混乱(例如,查看 Thread 类中有多少方法被弃用,而 volatile 直到 Java 5 才正确工作)。 +本附录主要是为了让你在遇到底层并发代码时能对此有一定的了解,尽管本文还远没对这个主题进行全面的讨论。为此,你需要先从阅读由 Brian Goetz, Tim Peierls, Joshua Bloch, Joseph Bowbeer, David Holmes, and Doug Lea (Addison-Wesley 出版社, 2006)所著作的 *Java Concurrency in Practice* (国内译名:Java并发编程实战)开始了解。理想情况下,这本书会完全吓跑你在 Java 中尝试去编写底层并发代码。如果没有,那么你几乎肯定患上了达克效应(DunningKruger Effect),这是一种认知偏差,“你知道的越少,对自己的能力就越有信心”。请记住,当前的语言设计人员仍然在清理早期语言设计人员过于自信造成的混乱(例如,查看 Thread 类中有多少方法被弃用,而 volatile 直到 Java 5 才正确工作)。 以下是并发编程的步骤: 1. 不要使用它。想一些其他方法来使你写的程序变的更快。 2. 如果你必须使用它,请使用在 [并发编程](./24-Concurrent-Programming.md) - parallel Streams and CompletableFutures 中展示的现代高级工具。 -3. 不要在任务间共享变量,必须在任务之间传递的任何信息都应该使用 Java.util.concurrent 库中的并发数据结构。 +3. 不要在任务间共享变量,在任务之间必须传递的任何信息都应该使用 Java.util.concurrent 库中的并发数据结构。 4. 如果必须在任务之间共享变量,请使用 java.util.concurrent.atomic 里面其中一种类型,或在任何直接或间接访问这些变量的方法上应用 synchronized。 当你不这样做时,很容易被愚弄,以为你已经把所有东西都包括在内。 说真的,尝试使用步骤 3。 5. 如果步骤 4 产生的结果太慢,你可以尝试使用volatile 或其他技术来调整代码,但是如果你正在阅读本书并认为你已经准备好尝试这些方法,那么你就超出了你的深度。 返回步骤#1。 通常可以只使用 java.util.concurrent 库组件来编写并发程序,完全避免来自应用 volatile 和 synchronized 的挑战。注意,我可以通过 [并发编程](./24-Concurrent-Programming.md) 中的示例来做到这一点。 -[^1]: 在某些平台上,特别是 Windows ,默认值可能非常难以查明。您可以使用 -Xss 标志调整堆栈大小。 +[^1]: 在某些平台上,特别是 Windows ,默认值可能非常难以查明。你可以使用 -Xss 标志调整堆栈大小。 [^2]: 引自 Brian Goetz, Java Concurrency in Practice 一书的作者 , 该书由 Brian Goetz, Tim Peierls, Joshua Bloch, Joseph Bowbeer, David Holmes, and Doug Lea 联合著作 (Addison-Wesley 出版社, 2006)。↩ @@ -689,8 +1684,7 @@ No odd numbers discovered [^4]: 这个测试的推论是,“如果某人表示线程是容易并且简单的,请确保这个人没有对你的项目做出重要的决策。如果那个人已经做出,那么你就已经陷入麻烦之中了。” -[^5]: 这版本是我参与的;这可能在以后的标准中得到了修正 - +[^5]: 这在即将产生的 C++ 的标准中得到了补救。
diff --git a/docs/images/collection.png b/docs/images/collection.png new file mode 100644 index 0000000..ab24df4 Binary files /dev/null and b/docs/images/collection.png differ diff --git a/docs/images/map.png b/docs/images/map.png new file mode 100644 index 0000000..ca7a97c Binary files /dev/null and b/docs/images/map.png differ