Merge branch 'master' of https://github.com/LingCoder/OnJava8 into lowlevel

This commit is contained in:
AlfredAlan
2019-07-19 01:51:05 +08:00
3 changed files with 87 additions and 14 deletions

View File

@@ -46,7 +46,7 @@
- [ ] [第二十四章 并发编程](docs/book/24-Concurrent-Programming.md)
- [ ] [第二十五章 设计模式](docs/book/25-Patterns.md)
- [x] [附录:补充](docs/book/Appendix-Supplements.md)
- [ ] [附录:编程指南](docs/book/Appendix-Programming-Guidelines.md)
- [x] [附录:编程指南](docs/book/Appendix-Programming-Guidelines.md)
- [ ] [附录:文档注释](docs/book/Appendix-Javadoc.md)
- [ ] [附录:对象传递和返回](docs/book/Appendix-Passing-and-Returning-Objects.md)
- [ ] [附录:流式IO](docs/book/Appendix-IO-Streams.md)

View File

@@ -7,22 +7,22 @@
通常,程序总是根据运行时才知道的某些条件去创建新的对象。在此之前,无法知道所需对象的数量甚至确切类型。为了解决这个普遍的编程问题,需要在任意时刻和任意位置创建任意数量的对象。因此,不能依靠创建命名的引用来持有每一个对象:
```java
MyType aReference
MyType aReference;
```
因为从来不会知道实际需要多少个这样的引用。
大多数编程语言都提供了某种方法来解决这个基本问题。Java有多种方式保存对象确切地说是对象的引用。例如前边曾经学习过的数组它是编译器支持的类型。数组是保存一组对象的最有效的方式如果想要保存一组基本类型数据也推荐使用数组。但是数组具有固定的大小尺寸而且在更一般的情况下在写程序的时候并不知道将需要多少个对象或者是否需要更复杂的方式来存储对象因此数组尺寸固定这一限制就显得太过受限了。
**java.util** 库提供了一套相当完整的*集合类*collection classes来解决这个问题其中基本的类型有 **List** 、**Set** 、 **Queue****Map**。这些类型也被称作*容器类*container classes但我将使用Java类库使用的术语。集合提供了完善的方法来保存对象可以使用这些工具来解决大量的问题。
**java.util** 库提供了一套相当完整的*集合类*collection classes来解决这个问题其中基本的类型有 **List** **Set****Queue****Map**。这些类型也被称作*容器类*container classes但我将使用Java类库使用的术语。集合提供了完善的方法来保存对象可以使用这些工具来解决大量的问题。
集合还有一些其它特性。例如, **Set** 对于每个值都只保存一个对象, **Map** 是一个关联数组允许将某些对象与其他对象关联起来。Java集合类都可以自动地调整自己的大小。因此与数组不同在编程时可以将任意数量的对象放置在集合中而不用关心集合应该有多大。
尽管在Java中没有直接的关键字支持[^1]但集合类仍然是可以显著增强编程能力的基本工具。在本章中将介绍Java集合类库的基本知识并重点介绍一些典型用法。这里将专注于在日常编程中使用的集合。稍后在[附录:集合主题]()中,还将学习到其余的那些集合和相关功能,以及如何使用它们的更多详细信息。
尽管在 Java 中没有直接的关键字支持,[^1]但集合类仍然是可以显著增强编程能力的基本工具。在本章中,将介绍 Java 集合类库的基本知识,并重点介绍一些典型用法。这里将专注于在日常编程中使用的集合。稍后,在[附录:集合主题]()中,还将学习到其余的那些集合和相关功能,以及如何使用它们的更多详细信息。
<!-- Generics and Type-Safe Collections -->
## 泛型和类型安全的集合
使用Java SE5之前的集合的一个主要问题是编译器允许你向集合中插入不正确的类型。例如考虑一个 **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”类型的警告[注解]()章节将介绍更多有关注解的信息):
@@ -66,9 +66,9 @@ ndOrangesWithoutGenerics.java:23)
**Apple****Orange** 是截然不同的,它们除了都是 **Object** 之外没有任何共同点(如果一个类没有显式地声明继承自哪个类,那么它就自动继承自 **Object**)。因为 **ArrayList** 保存的是 **Object** ,所以不仅可以通过 **ArrayList****add()** 方法将 **Apple** 对象放入这个集合,而且可以放入 **Orange** 对象,这无论在编译期还是运行时都不会有问题。当使用 **ArrayList****get()** 方法来取出你认为是 **Apple** 的对象时,得到的只是 **Object** 引用,必须将其转型为 **Apple**。然后需要将整个表达式用括号括起来,以便在调用 **Apple****id()** 方法之前,强制执行转型。否则,将会产生语法错误。
在运行时,当尝试将 **Orange** 对象转为 **Apple**时,会出现输出中显示的错误。
在运行时,当尝试将 **Orange** 对象转为 **Apple** 时,会出现输出中显示的错误。
在[泛型]()章节中你将了解到使用Java泛型来创建类可能很复杂。但是使用预先定义的泛型类却相当简单。例如要定义一个用于保存 **Apple** 对象的 **ArrayList** ,只需要使用 **ArrayList<Apple>** 来代替 **ArrayList** 。尖括号括起来的是*类型参数*(可能会有多个),它指定了这个集合实例可以保存的类型。
在[泛型]()章节中,你将了解到使用 Java 泛型来创建类可能很复杂。但是,使用预先定义的泛型类却相当简单。例如,要定义一个用于保存 **Apple** 对象的 **ArrayList** ,只需要使用 **ArrayList<Apple>** 来代替 **ArrayList** 。尖括号括起来的是*类型参数*(可能会有多个),它指定了这个集合实例可以保存的类型。
通过使用泛型,就可以在编译期防止将错误类型的对象放置到集合中。[^3]下面还是这个示例,但是使用了泛型:
```java
@@ -94,13 +94,13 @@ public class ApplesAndOrangesWithGenerics {
*/
```
**apples** 定义的右侧,可以看到 **new ArrayList<>()** 。这有时被称为“菱形语法”(diamond syntax)。在Java 7之前必须要在两端都进行类型声明如下所示
**apples** 定义的右侧,可以看到 **new ArrayList<>()** 。这有时被称为“菱形语法”(diamond syntax)。在 Java 7 之前,必须要在两端都进行类型声明,如下所示:
```java
ArrayList<Apple> apples = new ArrayList<Apple>();
```
随着类型变得越来越复杂,这种重复产生的代码非常混乱且难以阅读。程序员发现所有类型信息都可以从左侧获得,因此,编译器没有理由强迫右侧重复这些。Java语言团队采纳了这种*类型推断*type inference的请求即使只是这么小的一点。
随着类型变得越来越复杂,这种重复产生的代码非常混乱且难以阅读。程序员发现所有类型信息都可以从左侧获得,因此,编译器没有理由强迫右侧重复这些。Java语言团队采纳了这种*类型推断*type inference的请求即使只是这么小的一点。
有了 **ArrayList** 声明中的类型指定,编译器会阻止将 **Orange** 放入 **apples** ,因此,这会成为一个编译期错误而不是运行时错误。
@@ -142,9 +142,9 @@ Braeburn@4e25154f
<!-- Basic Concepts -->
## 基本概念
Java集合类库采用“持对象”holding objects的思想并将其分为两个不同的概念表示为类库的基本接口
Java集合类库采用“持对象”holding objects的思想并将其分为两个不同的概念表示为类库的基本接口
1. **集合Collection** :一个独立元素的序列,这些元素都服从一条或多条规则。 **List** 必须以插入的方式保存元素, **Set** 不能包含重复元素, **Queue** 按照*排队规则*来确定对象产生的顺序(通常与它们被插入的顺序相同)。
1. **集合Collection** :一个独立元素的序列,这些元素都服从一条或多条规则。**List** 必须以插入的顺序保存元素, **Set** 不能包含重复元素, **Queue** 按照*排队规则*来确定对象产生的顺序(通常与它们被插入的顺序相同)。
2. **映射Map** 一组成对的“键值对”对象,允许使用键来查找值。 **ArrayList** 使用数字来查找对象,因此在某种意义上讲,它是将数字和对象关联在一起。 **map**允许我们使用一个对象来查找另一个对象,它也被称作*关联数组*associative array因为它将对象和其它对象关联在一起或者称作*字典*dictionary因为可以使用一个键对象来查找值对象就像在字典中使用单词查找定义一样。 **Map**s是强大的编程工具。
尽管并非总是可行,但在理想情况下,你编写的大部分代码都在与这些接口打交道,并且唯一需要指定所使用的精确类型的地方就是在创建的时候。因此,可以像下面这样创建一个 **List**
@@ -189,9 +189,12 @@ public class SimpleCollection {
可以使用 *for-in* 语法来遍历所有的 **Collection** ,就像这里所展示的那样。在本章的后续部分,还将学习到一个更灵活的概念,*迭代器*。
<!-- Adding Groups of Elements -->
## 添加元素组
在**java.util**包中的 **Arrays****Collections** 类中都有很多实用的方法,可以在一个 **Collection** 中添加一组元素。 **Arrays.asList()** 方法接受一个数组或是逗号分隔的元素列表(使用可变参数),并将其转换为 **List** 对象。 **Collections.addAll()** 方法接受一个 **Collection** 对象,以及一个数组或是一个逗号分隔的列表,将其中元素添加到 **Collection** 中。下边的示例展示了这两个方法,以及更通用的 **addAll()** 方法,所有 **Collection** 类型都包含该方法:
**java.util** 包中的 **Arrays****Collections** 类中都有很多实用的方法,可以在一个 **Collection** 中添加一组元素。
**Arrays.asList()** 方法接受一个数组或是逗号分隔的元素列表(使用可变参数),并将其转换为 **List** 对象。 **Collections.addAll()** 方法接受一个 **Collection** 对象,以及一个数组或是一个逗号分隔的列表,将其中元素添加到 **Collection** 中。下边的示例展示了这两个方法,以及更通用的 **addAll()** 方法,所有 **Collection** 类型都包含该方法:
```java
// collections/AddingGroups.java
@@ -716,7 +719,7 @@ pets.removeLast(): Hamster
```
**Pets.list()** 的结果被传递给 **LinkedList** 的构造器,以便使用它来填充 **LinkedList** 。如果查看 **Queue** 接口就会发现,它在 **LinkedList** 的基础上添加了 **element()** **offer()** **peek()** **poll()****remove()** 方法,以使其可以成为一个 **Queue** 的实现。 **Queue** 的完整示例将在本章稍后给出。
<!-- Stack -->
## 堆栈Stack

View File

@@ -26,7 +26,7 @@
8. **在编写类之前,先编写测试代码,以验证类的设计是完善的**。如果不编写测试代码,那么就不知道类是什么样的。此外,通过编写测试代码,往往能够激发出类中所需的其他功能或约束。而这些功能或约束并不总是出现在分析和设计过程中。测试还会提供示例代码,显示了如何使用这个类。
9. **所有的软件设计问题都可以通过引入一个额外的间接概念层次extra level of conceptual indirection来解决**。这个软件工程的基本规则是抽象的基础,是面向对象编程的主要特征。在面向对象编程中,我们也可以这样说:“如果你的代码太复杂,就要生成更多的对象。”
9. **所有的软件设计问题都可以通过引入一个额外的间接概念层次extra level of conceptual indirection来解决**。这个软件工程的基本规则[^1]是抽象的基础,是面向对象编程的主要特征。在面向对象编程中,我们也可以这样说:“如果你的代码太复杂,就要生成更多的对象。”
10. **间接indirection应具有意义与准则9一致**。这个含义可以简单到“将常用代码放在单个方法中。”如果添加没有意义的间接(抽象,封装等)级别,那么它就像没有足够的间接性那样糟糕。
@@ -85,7 +85,77 @@
<!-- Implementation -->
## 实现
36. **遵循编码惯例**。有很多不同的约定,例如,[谷歌使用的约定](https://google.github.io/styleguide/javaguide.html)本书中的代码尽可能地遵循这些约定。如果坚持使用其他语言的编码风格那么读者就会很难去阅读。无论决定采用何种编码约定都要确保它们在整个项目中保持一致。集成开发环境通常包含内置的重新格式化reformatter和检查器checker
37. **无论使用何种编码风格,如果你的团队(甚至更好是公司)对其进行标准化,它就确实会产生重大影响**。这意味着,如果不符合这个标准,那么每个人都认为修复别人的编码风格是公平的游戏。标准化的价值在于解析代码可以花费较少的脑力,因此可以更专注于代码的含义。
38. **遵循标准的大写规则**。类名的第一个字母大写。字段,方法和对象(引用)的第一个字母应为小写。所有标识符应该将各个单词组合在一起,并将所有中间单词的首字母大写。例如:
- **ThisIsAClassName**
- **thisIsAMethodOrFieldName**
**static final** 类型的标识符的所有字母全部大写,并用下划线分隔各个单词,这些标识符在其定义中具有常量初始值。这表明它们是编译时常量。
- **包是一个特例**它们都是小写的字母即使是中间词。域扩展comorgnetedu等也应该是小写的。这是Java 1.1和Java 2之间的变化。
39. **不要创建自己的“装饰”私有字段名称**。这通常以前置下划线和字符的形式出现。匈牙利命名法(译者注:一种命名规范,基本原则是:变量名=属性+类型+对象描述。Win32程序风格采用这种命名法`WORD wParam1;LONG lParam2;HANDLE hInstance`)是最糟糕的例子,你可以在其中附加额外字符用于指示数据类型,用途,位置等,就好像你正在编写汇编语言一样,编译器根本没有提供额外的帮助。这些符号令人困惑,难以阅读,并且难以执行和维护。让类和包来指定名称范围。如果认为必须装饰名称以防止混淆,那么代码就可能过于混乱,这应该被简化。
40. 在创建一般用途的类时,**遵循“规范形式”**。包括**equals()****hashCode()****toString()****clone()**的定义(实现**Cloneable**,或选择其他一些对象复制方法,如序列化),并实现**Comparable**和**Serializable**。
41. **对读取和更改私有字段的方法使用“get”“set”和“is”命名约定**。这种做法不仅使类易于使用,而且也是命名这些方法的标准方法,因此读者更容易理解。
42. **对于所创建的每个类请包含该类的JUnit测试**(请参阅*junit.org*以及[第十六章:代码校验]()中的示例)。无需删除测试代码即可在项目中使用该类,如果进行更改,则可以轻松地重新运行测试。测试代码也能成为如何使用这个类的示例。
43. **有时需要继承才能访问基类的protected成员**。这可能导致对多种基类型的感知需求perceived need。如果不需要向上转型则可以首先派生一个新类来执行受保护的访问。然后把该新类作为使用它的任何类中的成员对象以此来代替直接继承。
44. **为了提高效率,避免使用*final*方法**。只有在分析后发现方法调用是瓶颈时,才将**final**用于此目的。
45. **如果两个类以某种功能方式相互关联(例如集合和迭代器),则尝试使一个类成为另一个类的内部类**。这不仅强调了类之间的关联而且通过将类嵌套在另一个类中可以允许在单个包中重用类名。Java集合库通过在每个集合类中定义内部**Iterator**类来实现此目的,从而为集合提供通用接口。使用内部类的另一个原因是作为**私有**实现的一部分。这里,内部类将有利于实现隐藏,而不是上面提到的类关联和防止命名空间污染。
46. **只要你注意到类似乎彼此之间具有高耦合,请考虑如果使用内部类可能获得的编码和维护改进**。内部类的使用不会解耦类,而是明确耦合关系,并且更方便。
47. **不要成为过早优化的牺牲品**。过早优化是很疯狂的行为。特别是不要担心编写或避免本机方法native methods将某些方法设置为**final**,或者在首次构建系统时调整代码以使其高效。你的主要目标应该是验证设计。即使设计需要一定的效率,也*先让它工作,然后再让它变快*。
48. **保持作用域尽可能小,以便能见度和对象的寿命尽可能小**。这减少了在错误的上下文中使用对象并隐藏了难以发现的bug的机会。例如假设有一个集合和一段迭代它的代码。如果复制该代码以用于一个新集合那么可能会意外地将旧集合的大小用作新集合的迭代上限。但是如果旧集合比较大则会在编译时捕获错误。
49. **使用标准Java库中的集合**。熟练使用它们,将会大大提高工作效率。首选**ArrayList**用于序列,**HashSet**用于集合,**HashMap**用于关联数组,**LinkedList**用于堆栈(而不是**Stack**,尽管也可以创建一个适配器来提供堆栈接口)和队列(也可以使用适配器,如本书所示)。当使用前三个时,将其分别向上转型为**List****Set**和**Map**,那么就可以根据需要轻松更改为其他实现。
50. **为使整个程序健壮,每个组件必须健壮**。在所创建的每个类中使用Java所提供的所有工具如访问控制异常类型检查同步等。这样就可以在构建系统时安全地进入下一级抽象。
51. **编译时错误优于运行时错误**。尝试尽可能在错误发生点处理错误。在最近的处理程序中尽其所能地捕获它能处理的所有异常。在当前层面处理所能处理的所有异常,如果解决不了,就重新抛出异常。
52. **注意长方法定义**。方法应该是简短的功能单元,用于描述和实现类接口的离散部分。维护一个冗长而复杂的方法是很困难的,而且代价很大,并且这个方法可能是试图做了太多事情。如果看到这样的方法,这表明,至少应该将它分解为多种方法。也可能建议去创建一个新类。小的方法也可以促进类重用。(有时方法必须很大,但它们应该只做一件事。)
53. **保持“尽可能私有”**。一旦公开了你的类库中的一个方面(一个方法,一个类,一个字段),你就永远无法把它拿回来。如果这样做,就将破坏某些人的现有代码,迫使他们重写和重新设计。如果你只公开了必须公开的内容,就可以轻易地改变其他一切,而不会对其他人造成影响,而且由于设计趋于发展,这是一个重要的自由。通过这种方式,更改具体实现将对派生类造成的影响最小。在处理多线程时,私有尤其重要,只有**私有**字段可以防止不同步使用。具有包访问权限的类应该仍然具有**私有**字段,但通常有必要提供包访问权限的方法而不是将它们**公开**。
54. **大量使用注释,并使用*Javadoc commentdocumentation*语法生成程序文档**。但是注释应该为代码增加真正的意义如果注释只是重申了代码已经清楚表达的内容这是令人讨厌的。请注意Java类和方法名称的典型详细信息减少了对某些注释的需求。
55. **避免使用“魔法数字”**。这些是指硬编码到代码中的数字。如果后续必须要更改它们那将是一场噩梦因为你永远不知道“100”是指“数组大小”还是“完全不同的东西”。相反创建一个带有描述性名称的常量并在整个程序中使用常量标识符。这使程序更易于理解更易于维护。
56. **在创建构造方法时,请考虑异常**。最好的情况是,构造方法不会做任何抛出异常的事情。次一级的最佳方案是,该类仅由健壮的类组成或继承自健壮的类,因此如果抛出异常则不需要处理。否则,必须清除**finally**子句中的组合类。如果构造方法必然失败,则适当的操作是抛出异常,因此调用者不会认为该对象是正确创建的而盲目地继续下去。
57. **在构造方法内部,只需要将对象设置为正确的状态**。主动避免调用其他方法(**final**方法除外),因为这些方法可以被其他人覆盖,从而在构造期间产生意外结果。(有关详细信息,请参阅[第六章:初始化和清理]()章节。)较小,较简单的构造方法不太可能抛出异常或导致问题。
58. **如果类在客户端程序员用完对象时需要进行任何清理,请将清理代码放在一个明确定义的方法中**,并使用像 **dispose()** 这样的名称来清楚地表明其目的。另外,在类中放置一个 **boolean** 标志来指示是否调用了 **dispose()** ,因此 **finalize()** 可以检查“终止条件”(参见[第六章:初始化和清理]()章节)。
59. ***finalize()* 的职责只能是验证对象的“终止条件”以进行调试**。(参见[第六章:初始化和清理]()一章)在特殊情况下,可能需要释放垃圾收集器无法释放的内存。因为可能无法为对象调用垃圾收集器,所以无法使用 **finalize()** 执行必要的清理。为此,必须创建自己的 **dispose()** 方法。在类的 **finalize()** 方法中,检查以确保对象已被清理,如果没有被清理,则抛出一个派生自**RuntimeException**的异常,以指示编程错误。在依赖这样的计划之前,请确保 **finalize()** 适用于你的系统。(可能需要调用 **System.gc()** 来确保此行为。)
60. **如果必须在特定范围内清理对象(除了通过垃圾收集器),请使用以下准则:** 初始化对象,如果成功,立即进入一个带有 **finally** 子句的 **try** 块,并在 **finally**中执行清理操作。
61. **在继承期间覆盖 *finalize()* 时,记得调用 *super.finalize()***。(如果是直接继承自 **Object** 则不需要这样做。)调用 **super.finalize()** 作为重写的 **finalize()** 的最终行为而不是在第一行调用它,这样可以确保基类组件在需要时仍然有效。
62. **创建固定大小的对象集合时,将它们转换为数组,** 尤其是在从方法中返回此集合时。这样就可以获得数组编译时类型检查的好处,并且数组的接收者可能不需要在数组中强制转换对象来使用它们。请注意,集合库的基类 **java.util.Collection** 有两个 **toArray()** 方法来完成此任务。
63. **优先选择 *接口* 而不是 *抽象类***。如果知道某些东西应该是基类,那么第一选择应该是使其成为一个接口,并且只有在需要方法定义或成员变量时才将其更改为抽象类。一个接口关心客户端想要做什么,而一个类倾向于关注(或允许)实现细节。
64. **为了避免非常令人沮丧的经历,请确保类路径中的每个名称只对应一个不在包中的类**。否则,编译器可以首先找到具有相同名称的其他类,并报告没有意义的错误消息。如果你怀疑自己有类路径问题,请尝试在类路径的每个起始点查找具有相同名称的 **.class** 文件。理想情况下,应该将所有类放在包中。
65. **注意意外重载**。如果尝试覆盖基类方法但是拼写错误,则最终会添加新方法而不是覆盖现有方法。但是,这是完全合法的,因此在编译时或运行时不会获得任何错误消息,但代码将无法正常工作。始终使用 **@Override** 注释来防止这种情况。
66. **注意过早优化**。先让它工作,然后再让它变快。除非发现代码的特定部分存在性能瓶颈。除非是使用分析器发现瓶颈,否则过早优化会浪费时间。性能调整所隐藏的额外成本是代码将变得难以理解和维护。
67. **请注意,相比于编写代码,代码被阅读的机会更多**。清晰的设计可能产生易于理解的程序但注释详细解释测试和示例是非常宝贵的它们可以帮助你和你的所有后继者。如果不出意外试图从JDK文档中找出有用信息的挫败感应该可以说服你。
[^1]: Andrew Koenig向我解释了它。
<!-- 分页 -->