mirror of
https://github.com/sjsdfg/effective-java-3rd-chinese.git
synced 2026-08-21 11:13:28 +08:00
Compare commits
124 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a08bace35b | ||
|
|
06362dd1d5 | ||
|
|
0afc7e0a11 | ||
|
|
45285d5d35 | ||
|
|
8a83c54a85 | ||
|
|
37eed9ae6a | ||
|
|
112bc1656e | ||
|
|
c15990d3c2 | ||
|
|
1aebd99908 | ||
|
|
532ff616b1 | ||
|
|
56ab08becc | ||
|
|
75b8ffa95a | ||
|
|
6382ef67d6 | ||
|
|
57aa1989e7 | ||
|
|
a9d2d277e0 | ||
|
|
174ed8f7dc | ||
|
|
0386750f10 | ||
|
|
af0af8089e | ||
|
|
6c957ff70f | ||
|
|
580c686cbf | ||
|
|
814f2bcca0 | ||
|
|
15ea9cd233 | ||
|
|
5116927665 | ||
|
|
3a5a953b7d | ||
|
|
e553edf61a | ||
|
|
85836c052b | ||
|
|
c17e86d486 | ||
|
|
088ac22296 | ||
|
|
792a89d6a2 | ||
|
|
fa29acb97e | ||
|
|
4a418054e6 | ||
|
|
e45fa2e0b6 | ||
|
|
90b0acde9d | ||
|
|
7f483112df | ||
|
|
ce8142866e | ||
|
|
aa72c44b93 | ||
|
|
1f8b18c16d | ||
|
|
e2c1a23be2 | ||
|
|
1911207698 | ||
|
|
27940eb7b7 | ||
|
|
a55d08683b | ||
|
|
5e6ba00f11 | ||
|
|
a3369c057c | ||
|
|
91ee5f09ba | ||
|
|
d0f607b51c | ||
|
|
6ca5e2aa03 | ||
|
|
49e0089cf1 | ||
|
|
c80bb35d21 | ||
|
|
79648b16ae | ||
|
|
0c111d428d | ||
|
|
a175f3dac5 | ||
|
|
0bdcb4c65c | ||
|
|
f93ea70d70 | ||
|
|
78b92b4b18 | ||
|
|
111cdac7e7 | ||
|
|
2c052cec8f | ||
|
|
03078fe5a8 | ||
|
|
44b177fe4f | ||
|
|
32273113c0 | ||
|
|
18291cc8fc | ||
|
|
991ce8bc48 | ||
|
|
326c97043f | ||
|
|
69ee1d0ac2 | ||
|
|
eabddb9751 | ||
|
|
00581d7a8f | ||
|
|
34683114dd | ||
|
|
f911331fba | ||
|
|
a9e86c4a29 | ||
|
|
fd6b34977c | ||
|
|
b09a31df1e | ||
|
|
9a3c957a7c | ||
|
|
0057aaa203 | ||
|
|
0fbaba10f5 | ||
|
|
c46f61b357 | ||
|
|
c362daea33 | ||
|
|
1dc980f84e | ||
|
|
fc0f14364d | ||
|
|
83c1a31be1 | ||
|
|
650c961dc2 | ||
|
|
1e4dd8797d | ||
|
|
1d45e9e6b0 | ||
|
|
325766758b | ||
|
|
80f720fa9f | ||
|
|
aac63ecafe | ||
|
|
853f75d431 | ||
|
|
7b2bd2b15a | ||
|
|
5ef7049303 | ||
|
|
b882b056e4 | ||
|
|
543dacc4bf | ||
|
|
8b220000ea | ||
|
|
a718228e14 | ||
|
|
f932527b58 | ||
|
|
dea7c2c1e1 | ||
|
|
c1cbb7f072 | ||
|
|
0cfa1275ef | ||
|
|
c5fbb35f5c | ||
|
|
a71dbfd0da | ||
|
|
d9a539e643 | ||
|
|
1a7f221902 | ||
|
|
345f31851d | ||
|
|
175dde7754 | ||
|
|
b5df370624 | ||
|
|
1478ffd0ac | ||
|
|
dc9839e914 | ||
|
|
83b8ec9a6e | ||
|
|
9e9d80eb73 | ||
|
|
28e51ff3ca | ||
|
|
d6a294f38f | ||
|
|
7c69ce0de7 | ||
|
|
1ef48d01ff | ||
|
|
61ea173f86 | ||
|
|
46a3ce0480 | ||
|
|
eea845f1c0 | ||
|
|
ddf1ec699d | ||
|
|
5a2fcb9983 | ||
|
|
68acbcf2e1 | ||
|
|
7ec45f0974 | ||
|
|
157dde3c48 | ||
|
|
402b216416 | ||
|
|
30260c05b7 | ||
|
|
8364f91953 | ||
|
|
b170a5ac4d | ||
|
|
d2a537aa79 | ||
|
|
5b49011517 |
23
.github/ISSUE_TEMPLATE/contribute-md.md
vendored
Normal file
23
.github/ISSUE_TEMPLATE/contribute-md.md
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: 贡献力量
|
||||
about: Describe this issue template's purpose here.
|
||||
title: "【翻译/错别字校对】条目xxx"
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
### 类型
|
||||
|
||||
1. 错别字
|
||||
2. 翻译校对
|
||||
|
||||
### 校正条目
|
||||
|
||||
- 第 xxx 条
|
||||
|
||||
### 校正内容
|
||||
|
||||
- 英语原文(翻译校对时使用):
|
||||
- 翻译原文:
|
||||
- 校对之后的内容:
|
||||
20
README.md
20
README.md
@@ -5,18 +5,34 @@
|
||||
现在全部章节已经更新完成
|
||||
- [github pages](https://sjsdfg.github.io/effective-java-3rd-chinese/#/):提供更好的在线阅读版本
|
||||
- [gitee pages](http://sjsdfg.gitee.io/effective-java-3rd-chinese/#/): 提供更快的访问速度
|
||||
- [pdf 下载](https://github.com/sjsdfg/effective-java-3rd-chinese/releases/download/v1.1/Effective.Java.pdf)
|
||||
- [pdf 下载](https://github.com/sjsdfg/effective-java-3rd-chinese/releases/download/v1.3/Effective-Java-3rd-LaTex-Pattern.pdf)
|
||||
|
||||
## about this repository
|
||||
|
||||
本来以为只是个直接搬运的活,实际上不是。主要工作如下:
|
||||
|
||||
* 改进排版,原有博文排版不太优秀,根据**markdwon 排版指北**重新排版。
|
||||
* 改进排版,原有博文排版不太优秀,根据**markdown 排版指北**重新排版。
|
||||
* 内容修改,原作者在翻译过程中有笔误(可能打字太快了),这里进行修改。
|
||||
* 等待内容搬运完成,使用**cmd markdown**生成 pdf 离线文件
|
||||
|
||||
markdown 文件以及英文版原版链接也都放在了 github 上面,希望小伙伴也可以中英文对照,给出一些意见。
|
||||
|
||||
## 一起来校对翻译
|
||||
|
||||
Effective Java 第三版翻译校对群:[909059971](https://jq.qq.com/?_wv=1027&k=5tscKwN)
|
||||
|
||||
<center>
|
||||
<img width="300" src="http://sjsdfg.gitee.io/effective-java-3rd-chinese/images/groupcode.png" />
|
||||
</center>
|
||||
|
||||
|
||||
|
||||
## 品牌衣服一折购
|
||||
|
||||
<center>
|
||||
<img width="400" src="http://sjsdfg.gitee.io/effective-java-3rd-chinese/images/shop.jpg" />
|
||||
</center>
|
||||
|
||||
## 额外资源
|
||||
|
||||
* [effective-java-2nd 中文版 ](https://pan.baidu.com/s/1R6H9UHbFYubWWY9HrclZ2A)
|
||||
|
||||
@@ -10,12 +10,32 @@
|
||||
* [effective-java-3rd 英文版 ](https://pan.baidu.com/s/1mJx5ZrOD_RPjf3ghQnBV5g)
|
||||
* [effective-java-3rd 源代码](https://github.com/jbloch/effective-java-3e-source-code)
|
||||
|
||||
## 一起来校对翻译
|
||||
|
||||
Effective Java 第三版翻译校对群:[909059971](https://jq.qq.com/?_wv=1027&k=5tscKwN)
|
||||
|
||||
<center>
|
||||
<img width="300" src="http://sjsdfg.gitee.io/effective-java-3rd-chinese/images/groupcode.png" />
|
||||
</center>
|
||||
|
||||
|
||||
|
||||
## 品牌衣服一折购
|
||||
|
||||
<center>
|
||||
<img width="400" src="https://upload.cc/i1/2019/10/26/q0FTRg.jpg" />
|
||||
</center>
|
||||
|
||||
|
||||
|
||||
## 友情链接
|
||||
|
||||
- [On Java 8中文版 - 即 thinking in java 第五版](https://github.com/LingCoder/OnJava8)
|
||||
|
||||
## 📚 高效 Java 第三版
|
||||
|
||||
### Chapter 2. Creating and Destroying Objects
|
||||
|
||||
- [01. 考虑使用静态工厂方法替代构造方法](notes/01.%20考虑使用静态工厂方法替代构造方法.md)
|
||||
- [02. 当构造方法参数过多时使用builder模式](notes/02.%20当构造方法参数过多时使用builder模式.md)
|
||||
- [03. 使用私有构造方法或枚类实现Singleton属性](notes/03.%20使用私有构造方法或枚类实现Singleton属性.md)
|
||||
@@ -25,11 +45,17 @@
|
||||
- [07. 消除过期的对象引用](notes/07.%20消除过期的对象引用.md)
|
||||
- [08. 避免使用Finalizer和Cleaner机制](notes/08.%20避免使用Finalizer和Cleaner机制.md)
|
||||
- [09. 使用try-with-resources语句替代try-finally语句](notes/09.%20使用try-with-resources语句替代try-finally语句.md)
|
||||
|
||||
### Chapter 3. Methods Common to All Objects
|
||||
|
||||
- [10. 重写equals方法时遵守通用约定](notes/10.%20重写equals方法时遵守通用约定.md)
|
||||
- [11. 重写equals方法时同时也要重写hashcode方法](notes/11.%20重写equals方法时同时也要重写hashcode方法.md)
|
||||
- [12. 始终重写 toString 方法](notes/12.%20始终重写%20toString%20方法.md)
|
||||
- [13. 谨慎地重写 clone 方法](notes/13.%20谨慎地重写%20clone%20方法.md)
|
||||
- [14. 考虑实现Comparable接口](notes/14.%20考虑实现Comparable接口.md)
|
||||
|
||||
### Chapter 4. Classes and Interfaces
|
||||
|
||||
- [15. 使类和成员的可访问性最小化](notes/15.%20使类和成员的可访问性最小化.md)
|
||||
- [16. 在公共类中使用访问方法而不是公共属性](notes/16.%20在公共类中使用访问方法而不是公共属性.md)
|
||||
- [17. 最小化可变性](notes/17.%20最小化可变性.md)
|
||||
@@ -41,6 +67,9 @@
|
||||
- [23. 类层次结构优于标签类](notes/23.%20类层次结构优于标签类.md)
|
||||
- [24. 支持使用静态成员类而不是非静态类](notes/24.%20支持使用静态成员类而不是非静态类.md)
|
||||
- [25. 将源文件限制为单个顶级类](notes/25.%20将源文件限制为单个顶级类.md)
|
||||
|
||||
### Chapter 5. Generics
|
||||
|
||||
- [26. 不要使用原始类型](notes/26.%20不要使用原始类型.md)
|
||||
- [27. 消除非检查警告](notes/27.%20消除非检查警告.md)
|
||||
- [28. 列表优于数组](notes/28.%20列表优于数组.md)
|
||||
@@ -49,6 +78,9 @@
|
||||
- [31. 使用限定通配符来增加API的灵活性](notes/31.%20使用限定通配符来增加API的灵活性.md)
|
||||
- [32. 合理地结合泛型和可变参数](notes/32.%20合理地结合泛型和可变参数.md)
|
||||
- [33. 优先考虑类型安全的异构容器](notes/33.%20优先考虑类型安全的异构容器.md)
|
||||
|
||||
### Chapter 6. Enums and Annotations
|
||||
|
||||
- [34. 使用枚举类型替代整型常量](notes/34.%20使用枚举类型替代整型常量.md)
|
||||
- [35. 使用实例属性替代序数](notes/35.%20使用实例属性替代序数.md)
|
||||
- [36. 使用EnumSet替代位属性](notes/36.%20使用EnumSet替代位属性.md)
|
||||
@@ -57,6 +89,9 @@
|
||||
- [39. 注解优于命名模式](notes/39.%20注解优于命名模式.md)
|
||||
- [40. 始终使用Override注解](notes/40.%20始终使用Override注解.md)
|
||||
- [41. 使用标记接口定义类型](notes/41.%20使用标记接口定义类型.md)
|
||||
|
||||
### Chapter 7. Lambdas and Streams
|
||||
|
||||
- [42. lambda表达式优于匿名类](notes/42.%20lambda表达式优于匿名类.md)
|
||||
- [43. 方法引用优于lambda表达式](notes/43.%20方法引用优于lambda表达式.md)
|
||||
- [44. 优先使用标准的函数式接口](notes/44.%20优先使用标准的函数式接口.md)
|
||||
@@ -64,6 +99,9 @@
|
||||
- [46. 优先考虑流中无副作用的函数](notes/46.%20优先考虑流中无副作用的函数.md)
|
||||
- [47. 优先使用Collection而不是Stream来作为方法的返回类型](notes/47.%20优先使用Collection而不是Stream来作为方法的返回类型.md)
|
||||
- [48. 谨慎使用流并行](notes/48.%20谨慎使用流并行.md)
|
||||
|
||||
### Chapter 8. Methods
|
||||
|
||||
- [49. 检查参数有效性](notes/49.%20检查参数有效性.md)
|
||||
- [50. 必要时进行防御性拷贝](notes/50.%20必要时进行防御性拷贝.md)
|
||||
- [51. 仔细设计方法签名](notes/51.%20仔细设计方法签名.md)
|
||||
@@ -72,6 +110,9 @@
|
||||
- [54. 返回空的数组或集合,不要返回 null](notes/54.%20返回空的数组或集合,不要返回%20null.md)
|
||||
- [55. 明智审慎地返回 Optional](notes/55.%20明智审慎地返回%20Optional.md)
|
||||
- [56. 为所有已公开的 API 元素编写文档注释](notes/56.%20为所有已公开的%20API%20元素编写文档注释.md)
|
||||
|
||||
### Chapter 9. General Programming
|
||||
|
||||
- [57. 最小化局部变量的作用域](notes/57.%20最小化局部变量的作用域.md)
|
||||
- [58. for-each 循环优于传统 for 循环](notes/58.%20for-each%20循环优于传统%20for%20循环.md)
|
||||
- [59. 了解并使用库](notes/59.%20了解并使用库.md)
|
||||
@@ -84,6 +125,9 @@
|
||||
- [66. 明智审慎地本地方法](notes/66.%20明智审慎地本地方法.md)
|
||||
- [67. 明智审慎地进行优化](notes/67.%20明智审慎地进行优化.md)
|
||||
- [68. 遵守被广泛认可的命名约定](notes/68.%20遵守被广泛认可的命名约定.md)
|
||||
|
||||
### Chapter 10. Exceptions
|
||||
|
||||
- [69. 只针对异常的情况下才使用异常](notes/69.%20只针对异常的情况下才使用异常.md)
|
||||
- [70. 对可恢复的情况使用受检异常,对编程错误使用运行时异常](notes/70.%20对可恢复的情况使用受检异常,对编程错误使用运行时异常.md)
|
||||
- [71. 避免不必要的使用受检异常](notes/71.%20避免不必要的使用受检异常.md)
|
||||
@@ -93,6 +137,9 @@
|
||||
- [75. 在细节消息中包含失败一捕获信息](notes/75.%20在细节消息中包含失败一捕获信息.md)
|
||||
- [76. 保持失败原子性](notes/76.%20保持失败原子性.md)
|
||||
- [77. 不要忽略异常](notes/77.%20不要忽略异常.md)
|
||||
|
||||
### Chapter 11. Concurrency
|
||||
|
||||
- [78. 同步访问共享的可变数据](notes/78.%20同步访问共享的可变数据.md)
|
||||
- [79. 避免过度同步](notes/79.%20避免过度同步.md)
|
||||
- [80. executor 、task 和 stream 优先于线程](notes/80.%20executor%20、task%20和%20stream%20优先于线程.md)
|
||||
@@ -100,6 +147,9 @@
|
||||
- [82. 文档应包含线程安全属性](notes/82.%20文档应包含线程安全属性.md)
|
||||
- [83. 明智审慎的使用延迟初始化](notes/83.%20明智审慎的使用延迟初始化.md)
|
||||
- [84. 不要依赖线程调度器](notes/84.%20不要依赖线程调度器.md)
|
||||
|
||||
### Chapter 12. Serialization
|
||||
|
||||
- [85. 优先选择 Java 序列化的替代方案](notes/85.%20优先选择%20Java%20序列化的替代方案.md)
|
||||
- [86. 非常谨慎地实现 Serializable](notes/86.%20非常谨慎地实现%20Serializable.md)
|
||||
- [87. 考虑使用自定义的序列化形式](notes/87.%20考虑使用自定义的序列化形式.md)
|
||||
@@ -107,11 +157,6 @@
|
||||
- [89. 对于实例控制,枚举类型优于 readResolve](notes/89.%20对于实例控制,枚举类型优于%20readResolve.md)
|
||||
- [90. 考虑用序列化代理代替序列化实例](notes/90.%20考虑用序列化代理代替序列化实例.md)
|
||||
|
||||
## 📖 高效 Java 第三版完整版阅读
|
||||
|
||||
- [高效 Java 第三版完整版](doc/effective-java-3rd-chinese.md)</br>
|
||||
|
||||
|
||||
## 😋 Give me a Favor
|
||||
<center>
|
||||
<img width="600" src="http://static.zybuluo.com/ZzzJoe/yflamvkjh2i7zn5qcp9wpj61/%E5%AF%92%E6%B2%A7.jpg" />
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
|
||||
## 📚 高效 Java 第三版
|
||||
|
||||
|
||||
### Chapter 2. Creating and Destroying Objects
|
||||
|
||||
- [01. 考虑使用静态工厂方法替代构造方法](notes/01.%20考虑使用静态工厂方法替代构造方法.md)
|
||||
- [02. 当构造方法参数过多时使用builder模式](notes/02.%20当构造方法参数过多时使用builder模式.md)
|
||||
- [03. 使用私有构造方法或枚类实现Singleton属性](notes/03.%20使用私有构造方法或枚类实现Singleton属性.md)
|
||||
@@ -12,12 +15,18 @@
|
||||
- [06. 避免创建不必要的对象](notes/06.%20避免创建不必要的对象.md)
|
||||
- [07. 消除过期的对象引用](notes/07.%20消除过期的对象引用.md)
|
||||
- [08. 避免使用Finalizer和Cleaner机制](notes/08.%20避免使用Finalizer和Cleaner机制.md)
|
||||
|
||||
### Chapter 3. Methods Common to All Objects
|
||||
|
||||
- [09. 使用try-with-resources语句替代try-finally语句](notes/09.%20使用try-with-resources语句替代try-finally语句.md)
|
||||
- [10. 重写equals方法时遵守通用约定](notes/10.%20重写equals方法时遵守通用约定.md)
|
||||
- [11. 重写equals方法时同时也要重写hashcode方法](notes/11.%20重写equals方法时同时也要重写hashcode方法.md)
|
||||
- [12. 始终重写 toString 方法](notes/12.%20始终重写%20toString%20方法.md)
|
||||
- [13. 谨慎地重写 clone 方法](notes/13.%20谨慎地重写%20clone%20方法.md)
|
||||
- [14. 考虑实现Comparable接口](notes/14.%20考虑实现Comparable接口.md)
|
||||
|
||||
### Chapter 4. Classes and Interfaces
|
||||
|
||||
- [15. 使类和成员的可访问性最小化](notes/15.%20使类和成员的可访问性最小化.md)
|
||||
- [16. 在公共类中使用访问方法而不是公共属性](notes/16.%20在公共类中使用访问方法而不是公共属性.md)
|
||||
- [17. 最小化可变性](notes/17.%20最小化可变性.md)
|
||||
@@ -29,6 +38,9 @@
|
||||
- [23. 类层次结构优于标签类](notes/23.%20类层次结构优于标签类.md)
|
||||
- [24. 支持使用静态成员类而不是非静态类](notes/24.%20支持使用静态成员类而不是非静态类.md)
|
||||
- [25. 将源文件限制为单个顶级类](notes/25.%20将源文件限制为单个顶级类.md)
|
||||
|
||||
### Chapter 5. Generics
|
||||
|
||||
- [26. 不要使用原始类型](notes/26.%20不要使用原始类型.md)
|
||||
- [27. 消除非检查警告](notes/27.%20消除非检查警告.md)
|
||||
- [28. 列表优于数组](notes/28.%20列表优于数组.md)
|
||||
@@ -37,6 +49,9 @@
|
||||
- [31. 使用限定通配符来增加API的灵活性](notes/31.%20使用限定通配符来增加API的灵活性.md)
|
||||
- [32. 合理地结合泛型和可变参数](notes/32.%20合理地结合泛型和可变参数.md)
|
||||
- [33. 优先考虑类型安全的异构容器](notes/33.%20优先考虑类型安全的异构容器.md)
|
||||
|
||||
### Chapter 6. Enums and Annotations
|
||||
|
||||
- [34. 使用枚举类型替代整型常量](notes/34.%20使用枚举类型替代整型常量.md)
|
||||
- [35. 使用实例属性替代序数](notes/35.%20使用实例属性替代序数.md)
|
||||
- [36. 使用EnumSet替代位属性](notes/36.%20使用EnumSet替代位属性.md)
|
||||
@@ -45,6 +60,9 @@
|
||||
- [39. 注解优于命名模式](notes/39.%20注解优于命名模式.md)
|
||||
- [40. 始终使用Override注解](notes/40.%20始终使用Override注解.md)
|
||||
- [41. 使用标记接口定义类型](notes/41.%20使用标记接口定义类型.md)
|
||||
|
||||
### Chapter 7. Lambdas and Streams
|
||||
|
||||
- [42. lambda表达式优于匿名类](notes/42.%20lambda表达式优于匿名类.md)
|
||||
- [43. 方法引用优于lambda表达式](notes/43.%20方法引用优于lambda表达式.md)
|
||||
- [44. 优先使用标准的函数式接口](notes/44.%20优先使用标准的函数式接口.md)
|
||||
@@ -52,6 +70,9 @@
|
||||
- [46. 优先考虑流中无副作用的函数](notes/46.%20优先考虑流中无副作用的函数.md)
|
||||
- [47. 优先使用Collection而不是Stream来作为方法的返回类型](notes/47.%20优先使用Collection而不是Stream来作为方法的返回类型.md)
|
||||
- [48. 谨慎使用流并行](notes/48.%20谨慎使用流并行.md)
|
||||
|
||||
### Chapter 8. Methods
|
||||
|
||||
- [49. 检查参数有效性](notes/49.%20检查参数有效性.md)
|
||||
- [50. 必要时进行防御性拷贝](notes/50.%20必要时进行防御性拷贝.md)
|
||||
- [51. 仔细设计方法签名](notes/51.%20仔细设计方法签名.md)
|
||||
@@ -60,6 +81,9 @@
|
||||
- [54. 返回空的数组或集合,不要返回 null](notes/54.%20返回空的数组或集合,不要返回%20null.md)
|
||||
- [55. 明智审慎地返回 Optional](notes/55.%20明智审慎地返回%20Optional.md)
|
||||
- [56. 为所有已公开的 API 元素编写文档注释](notes/56.%20为所有已公开的%20API%20元素编写文档注释.md)
|
||||
|
||||
### Chapter 9. General Programming
|
||||
|
||||
- [57. 最小化局部变量的作用域](notes/57.%20最小化局部变量的作用域.md)
|
||||
- [58. for-each 循环优于传统 for 循环](notes/58.%20for-each%20循环优于传统%20for%20循环.md)
|
||||
- [59. 了解并使用库](notes/59.%20了解并使用库.md)
|
||||
@@ -72,6 +96,9 @@
|
||||
- [66. 明智审慎地本地方法](notes/66.%20明智审慎地本地方法.md)
|
||||
- [67. 明智审慎地进行优化](notes/67.%20明智审慎地进行优化.md)
|
||||
- [68. 遵守被广泛认可的命名约定](notes/68.%20遵守被广泛认可的命名约定.md)
|
||||
|
||||
### Chapter 10. Exceptions
|
||||
|
||||
- [69. 只针对异常的情况下才使用异常](notes/69.%20只针对异常的情况下才使用异常.md)
|
||||
- [70. 对可恢复的情况使用受检异常,对编程错误使用运行时异常](notes/70.%20对可恢复的情况使用受检异常,对编程错误使用运行时异常.md)
|
||||
- [71. 避免不必要的使用受检异常](notes/71.%20避免不必要的使用受检异常.md)
|
||||
@@ -81,6 +108,9 @@
|
||||
- [75. 在细节消息中包含失败一捕获信息](notes/75.%20在细节消息中包含失败一捕获信息.md)
|
||||
- [76. 保持失败原子性](notes/76.%20保持失败原子性.md)
|
||||
- [77. 不要忽略异常](notes/77.%20不要忽略异常.md)
|
||||
|
||||
### Chapter 11. Concurrency
|
||||
|
||||
- [78. 同步访问共享的可变数据](notes/78.%20同步访问共享的可变数据.md)
|
||||
- [79. 避免过度同步](notes/79.%20避免过度同步.md)
|
||||
- [80. executor 、task 和 stream 优先于线程](notes/80.%20executor%20、task%20和%20stream%20优先于线程.md)
|
||||
@@ -88,6 +118,9 @@
|
||||
- [82. 文档应包含线程安全属性](notes/82.%20文档应包含线程安全属性.md)
|
||||
- [83. 明智审慎的使用延迟初始化](notes/83.%20明智审慎的使用延迟初始化.md)
|
||||
- [84. 不要依赖线程调度器](notes/84.%20不要依赖线程调度器.md)
|
||||
|
||||
### Chapter 12. Serialization
|
||||
|
||||
- [85. 优先选择 Java 序列化的替代方案](notes/85.%20优先选择%20Java%20序列化的替代方案.md)
|
||||
- [86. 非常谨慎地实现 Serializable](notes/86.%20非常谨慎地实现%20Serializable.md)
|
||||
- [87. 考虑使用自定义的序列化形式](notes/87.%20考虑使用自定义的序列化形式.md)
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
// Copied from https://github.com/jeluard/prism-clojure
|
||||
Prism.languages.clojure = {
|
||||
comment: /;+.*/,
|
||||
string: /"(?:\\.|[^\\"\r\n])*"/,
|
||||
operator: /(?:::|[:|'])\b[a-z][\w*+!?-]*\b/i, //used for symbols and keywords
|
||||
keyword: {
|
||||
pattern: /([^\w+*'?-])(?:def|if|do|let|\.\.|quote|var|->>|->|fn|loop|recur|throw|try|monitor-enter|\.|new|set!|def\-|defn|defn\-|defmacro|defmulti|defmethod|defstruct|defonce|declare|definline|definterface|defprotocol|==|defrecord|>=|deftype|<=|defproject|ns|\*|\+|\-|\/|<|=|>|accessor|agent|agent-errors|aget|alength|all-ns|alter|and|append-child|apply|array-map|aset|aset-boolean|aset-byte|aset-char|aset-double|aset-float|aset-int|aset-long|aset-short|assert|assoc|await|await-for|bean|binding|bit-and|bit-not|bit-or|bit-shift-left|bit-shift-right|bit-xor|boolean|branch\?|butlast|byte|cast|char|children|class|clear-agent-errors|comment|commute|comp|comparator|complement|concat|conj|cons|constantly|cond|if-not|construct-proxy|contains\?|count|create-ns|create-struct|cycle|dec|deref|difference|disj|dissoc|distinct|doall|doc|dorun|doseq|dosync|dotimes|doto|double|down|drop|drop-while|edit|end\?|ensure|eval|every\?|false\?|ffirst|file-seq|filter|find|find-doc|find-ns|find-var|first|float|flush|for|fnseq|frest|gensym|get-proxy-class|get|hash-map|hash-set|identical\?|identity|if-let|import|in-ns|inc|index|insert-child|insert-left|insert-right|inspect-table|inspect-tree|instance\?|int|interleave|intersection|into|into-array|iterate|join|key|keys|keyword|keyword\?|last|lazy-cat|lazy-cons|left|lefts|line-seq|list\*|list|load|load-file|locking|long|loop|macroexpand|macroexpand-1|make-array|make-node|map|map-invert|map\?|mapcat|max|max-key|memfn|merge|merge-with|meta|min|min-key|name|namespace|neg\?|new|newline|next|nil\?|node|not|not-any\?|not-every\?|not=|ns-imports|ns-interns|ns-map|ns-name|ns-publics|ns-refers|ns-resolve|ns-unmap|nth|nthrest|or|parse|partial|path|peek|pop|pos\?|pr|pr-str|print|print-str|println|println-str|prn|prn-str|project|proxy|proxy-mappings|quot|rand|rand-int|range|re-find|re-groups|re-matcher|re-matches|re-pattern|re-seq|read|read-line|reduce|ref|ref-set|refer|rem|remove|remove-method|remove-ns|rename|rename-keys|repeat|replace|replicate|resolve|rest|resultset-seq|reverse|rfirst|right|rights|root|rrest|rseq|second|select|select-keys|send|send-off|seq|seq-zip|seq\?|set|short|slurp|some|sort|sort-by|sorted-map|sorted-map-by|sorted-set|special-symbol\?|split-at|split-with|str|string\?|struct|struct-map|subs|subvec|symbol|symbol\?|sync|take|take-nth|take-while|test|time|to-array|to-array-2d|tree-seq|true\?|union|up|update-proxy|val|vals|var-get|var-set|var\?|vector|vector-zip|vector\?|when|when-first|when-let|when-not|with-local-vars|with-meta|with-open|with-out-str|xml-seq|xml-zip|zero\?|zipmap|zipper)(?=[^\w+*'?-])/,
|
||||
lookbehind: true
|
||||
},
|
||||
boolean: /\b(?:true|false|nil)\b/,
|
||||
number: /\b[0-9A-Fa-f]+\b/,
|
||||
punctuation: /[{}\[\](),]/
|
||||
};
|
||||
BIN
docs/assets/groupcode.png
Normal file
BIN
docs/assets/groupcode.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
File diff suppressed because it is too large
Load Diff
BIN
docs/images/groupcode.png
Normal file
BIN
docs/images/groupcode.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
BIN
docs/images/shop.jpg
Normal file
BIN
docs/images/shop.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
@@ -13,55 +13,55 @@ public static Boolean valueOf(boolean b) {
|
||||
|
||||
类可以为其客户端提供静态工厂方法,而不是公共构造方法。提供静态工厂方法而不是公共构造方法有优点也有缺点。
|
||||
|
||||
**静态工厂方法的一个优点是,不像构造方法,它们是有名字的。** 如果构造方法的参数本身并不描述被返回的对象,则具有精心选择名称的静态工厂更易于使用,并且生成的客户端代码更易于阅读。 例如,返回一个可能为素数的 `BigInteger` 的构造方法 `BigInteger(int,int,Random)` 可以更好地表示为名为 `BigInteger.probablePrime` 的静态工厂方法。 (这个方法是在 Java 1.4 中添加的。)
|
||||
**静态工厂方法的一个优点是,与构造方法不同,它们是有名字的。** 如果构造方法的参数本身并不描述被返回的对象,则具有精心选择名称的静态工厂更易于使用,并且生成的客户端代码更易于阅读。 例如,返回一个可能为素数的 `BigInteger` 的构造方法 `BigInteger(int,int,Random)` 可以更好地表示为名为 `BigInteger.probablePrime` 的静态工厂方法。 (这个方法是在 Java 1.4 中添加的。)
|
||||
|
||||
|
||||
一个类只能有一个给定签名的构造方法。 程序员知道通过提供两个构造方法来解决这个限制,这两个构造方法的参数列表只有它们的参数类型的顺序不同。 这是一个非常糟糕的主意。 这样的 API 用户将永远不会记得哪个构造方法是哪个,最终会错误地调用。 阅读使用这些构造方法的代码的人只有在参考类文档的情况下才知道代码的作用。
|
||||
|
||||
因为他们有名字,所以静态工厂方法不会受到上面讨论中的限制。在类中似乎需要具有相同签名的多个构造方法的情况下,用静态工厂方法替换构造方法,并仔细选择名称来突出它们的差异。
|
||||
|
||||
**静态工厂方法的第二个优点是,与构造方法不同,它们不需要每次调用时都创建一个新对象。** 这允许不可变的类 (详见第 17 条)使用预先构建的实例,或者在构造时缓存实例,并反复分配它们以避免创建不必要的重复对象。`Boolean.valueof(boolean)` 方法说明了这种方法:它从不创建对象。这种技术类似于 `Flyweight` 模式[Gamma95]。如果经常请求等价对象,那么它可以极大地提高性能,特别是如果在创建它们非常昂贵的情况下。
|
||||
**静态工厂方法的第二个优点是,与构造方法不同,它们不需要每次调用时都创建一个新对象。** 这允许不可变类 (详见第 17 条)使用预先构建的实例,或者在构造时缓存实例,并反复分配它们以避免创建不必要的重复对象。`Boolean.valueof(boolean)` 方法说明了这种方法:它从不创建对象。这种技术类似于 `Flyweight` 模式[Gamma95]。如果经常请求等价对象,那么它可以极大地提高性能,特别是在创建它们的代价非常昂贵的情况下。
|
||||
|
||||
静态工厂方法从重复调用返回相同对象的能力允许类保持在任何时候存在的实例的严格控制。这样做的类被称为实例控制(instance-controlled)。编写实例控制类的原因有很多。实例控制允许一个类来保证它是一个单例(详见第 3 条)项或不可实例化的(详见第 4 条)。同时,它允许一个不可变的值类(详见第 17 条)保证不存在两个相同的实例:当且仅当 `a == b` 时 `a.equals(b)`。这是享元模式的基础[Gamma95]。`Enum` 类型(详见第 34 条)提供了这个保证。
|
||||
静态工厂方法重复调用返回相同实例这个特点可以让类在任何时候都能对实例保持严格的控制。这样做的类被称为实例控制类( instance-controlled)。有很多理由足以让我们去我们编写实例控制类。实例控制可以保证一个类是单例 的(详见第 3 条) 或不可实例化的 (详见第 4 条)。同时,它允许一个不可变的值类 (详见第 17 条) 保证不存在两个相等但不相同的实例,也就是说当且仅当 `a == b` 时才有 `a.equals(b)`。这是`Flyweight`模式的基础[Gamma95]。`Enum` 类型 (详见第 34 条)可以做到这点。
|
||||
|
||||
**静态工厂方法的第三个优点是,与构造方法不同,它们可以返回其返回类型的任何子类型的对象。** 这为你在选择返回对象的类时提供了很大的灵活性。
|
||||
|
||||
这种灵活性的一个应用是 API 可以返回对象而不需要公开它的类。 以这种方式隐藏实现类会使 API 非常紧凑。 这种技术适用于基于接口的框架(详见第 20 条),其中接口为静态工厂方法提供自然返回类型。
|
||||
|
||||
在 Java 8 之前,接口不能有静态方法。根据约定,一个名为 `Type` 的接口的静态工厂方法被放入一个非实例化的伙伴类(companion class)(详见第 4 条)`Types` 类中。例如,Java 集合框架有 45 个接口的实用工具实现,提供不可修改的集合、同步集合等等。几乎所有这些实现都是通过静态工厂方法在一个非实例类 (`java .util. collections`) 中导出的。返回对象的类都是非公开的。
|
||||
在 Java 8 之前,接口不能有静态方法。根据约定,一个名为 `Type` 的接口的静态工厂方法被放入一个不可实例化的伙伴类(companion class)(详见第 4 条)`Types` 类中。例如,Java 集合框架有 45 个接口的实用工具实现,提供不可修改的集合、同步集合等等。几乎所有这些实现都是通过静态工厂方法在一个不可实例化的类 (`java .util. collections`) 中返回的。返回对象的类都隐藏的。
|
||||
|
||||
`Collections` 框架 API 的规模要比它之前输出的 45 个单独的公共类要小得多,每个类有个便利类的实现。不仅是 API 的大部分减少了,还包括概念上的权重:程序员必须掌握的概念的数量和难度,才能使用 API。程序员知道返回的对象恰好有其接口指定的 API,因此不需要为实现类读阅读额外的类文档。此外,使用这种静态工厂方法需要客户端通过接口而不是实现类来引用返回的对象,这通常是良好的实践(详见第 64 条)。
|
||||
`Collections` 框架 API 的规模要比它之前返回的 45 个单独的公共类要小得多,每个类在集合框架中都有一个便利的实现。不仅是 API 的大部分减少了,还包括概念上的权重:程序员要想使用 API必须掌握的概念的数量和难度。程序员知道返回的对象恰好有其接口指定的 API,因此不需要为实现类读阅读额外的类文档。此外,使用这种静态工厂方法需要客户端通过接口而不是实现类来引用返回的对象,这通常是良好的实践(详见第 64 条)。
|
||||
|
||||
从 Java 8 开始,接口不能包含静态方法的限制被取消了,所以通常没有理由为接口提供一个不可实例化的伴随类。 很多公开的静态成员应该放在这个接口本身。 但是,请注意,将这些静态方法的大部分实现代码放在单独的包私有类中仍然是必要的。 这是因为 Java 8 要求所有接口的静态成员都是公共的。 Java 9 允许私有静态方法,但静态字段和静态成员类仍然需要公开。
|
||||
|
||||
**静态工厂的第四个优点是返回对象的类可以根据输入参数的不同而不同。** 声明的返回类型的任何子类都是允许的。 返回对象的类也可以随每次发布而不同。
|
||||
|
||||
`EnumSet` 类(详见第 36 条)没有公共构造方法,只有静态工厂。 在 OpenJDK 实现中,它们根据底层枚举类型的大小返回两个子类中的一个的实例:如果大多数枚举类型具有 64 个或更少的元素,静态工厂将返回一个 `RegularEnumSet` 实例, 返回一个 `long` 类型;如果枚举类型具有六十五个或更多元素,则工厂将返回一个 `JumboEnumSet` 实例,返回一个 `long` 类型的数组。
|
||||
`EnumSet` 类(详见第 36 条)没有公共构造方法,只有静态工厂。 在 OpenJDK 实现中,它们根据底层枚举类型的大小返回两个子类中的一个的实例:大多数枚举类型具有 64 个或更少的元素,静态工厂将返回一个 `RegularEnumSet` 实例, 底层是`long` 类型;如果枚举类型具有六十五个或更多元素,则工厂将返回一个 `JumboEnumSet` 实例,底层是`long` 类型的数组。
|
||||
|
||||
这两个实现类的存在对于客户是不可见的。 如果 `RegularEnumSet` 不再为小枚举类型提供性能优势,则可以在未来版本中将其淘汰,而不会产生任何不良影响。 同样,未来的版本可能会添加 `EnumSet` 的第三个或第四个实现,如果它证明有利于性能。 客户既不知道也不关心他们从工厂返回的对象的类别; 他们只关心它是 `EnumSet` 的一些子类。
|
||||
这两个实现类的存在对于客户端而言是不可见的。 如果 `RegularEnumSet` 对于小的枚举类型不再具有性能优势,则可以在未来版本中将其淘汰,且不会产生任何不良影响。 同样,如果可以证明添加 `EnumSet` 的更多的实现可以提高性能,那么在未来的版本可能就会这样做。 客户既不知道也不关心他们从工厂返回的对象的类别; 他们只需要知道它是 `EnumSet` 的子类。
|
||||
|
||||
**静态工厂的第 5 个优点是,在编写包含该方法的类时,返回的对象的类不需要存在。** 这种灵活的静态工厂方法构成了服务提供者框架的基础,比如 Java 数据库连接 AP(JDBC)。服务提供者框架是提供者实现服务的系统,并且系统使得实现对客户端可用,从而将客户端从实现中分离出来。
|
||||
**静态工厂的第五个优点是,在编写包含该方法的类时,返回的对象的类不需要存在。** 这种灵活的静态工厂方法构成了服务提供者框架的基础,比如 Java 数据库连接 API(JDBC)。服务提供者框架是提供者实现服务的系统,并且系统使得实现对客户端可用,从而将客户端从实现中分离出来。
|
||||
|
||||
服务提供者框架中有三个基本组:服务接口,它表示实现;提供者注册 API,提供者用来注册实现;以及服务访问 API,客户端使用该 API 获取服务的实例。服务访问 API 允许客户指定选择实现的标准。在缺少这样的标准的情况下,API 返回一个默认实现的实例,或者允许客户通过所有可用的实现进行遍历。服务访问 API 是灵活的静态工厂,它构成了服务提供者框架的基础。
|
||||
|
||||
服务提供者框架的一个可选的第四个组件是一个服务提供者接口,它描述了一个生成服务接口实例的工厂对象。在没有服务提供者接口的情况下,必须对实现进行反射实例化(详见第 65 条)。在 JDBC 的情况下,`Connection` 扮演服务接口的一部分,`DriverManager.registerDriver` 提供程序注册 API、`DriverManager.getConnection` 是服务访问 API,`Driver` 是服务提供者接口。
|
||||
|
||||
服务提供者框架模式有许多变种。 例如,服务访问 API 可以向客户端返回比提供者提供的更丰富的服务接口。 这是桥接模式[Gamma95]。 依赖注入框架(详见第 5 条)可以被看作是强大的服务提供者。 从 Java 6 开始,平台包含一个通用的服务提供者框架 `java.util.ServiceLoader`,所以你不需要,一般也不应该自己编写(条目 59)。 JDBC 不使用 `ServiceLoader`,因为前者早于后者。
|
||||
服务提供者框架模式有许多变种。 例如,服务访问 API 可以向客户端返回比提供者提供的更丰富的服务接口。 这是桥接模式[Gamma95]。 依赖注入框架(详见第 5 条)可以被看作是强大的服务提供者。 从 Java 6 开始,平台包含一个通用的服务提供者框架 `java.util.ServiceLoader`,所以你不需要,一般也不应该自己编写(详见第 59 条)。 JDBC 不使用 `ServiceLoader`,因为前者早于后者。
|
||||
|
||||
**只提供静态工厂方法的主要限制是,没有公共或受保护构造方法的类不能被子类化。** 例如,在 `Collections` 框架中不可能将任何方便实现类子类化。可以说,这可能是因祸得福,因为它鼓励程序员使用组合而不是继承(详见第 18 条),并且是不可变类型(详见第 17 条)。
|
||||
**只提供静态工厂方法的主要限制是,没有公共或受保护构造方法的类不能被子类化。** 例如,要想将 `Collections` 框架中任何遍历的实现类进行子类化,是不可能的。但是这样也会因祸得福,因为它鼓励程序员使用组合(composition)而不是继承(详见第 18 条),并且是不可变类型锁需要的(详见第 17 条)。
|
||||
|
||||
**静态工厂方法的第二个缺点是,程序员很难找到它们。** 它们不像构造方法那样在 API 文档中突出,因此很难找出如何实例化一个提供静态工厂方法而不是构造方法的类。Javadoc 工具可能有一天会引起对静态工厂方法的注意。与此同时,可以通过将注意力吸引到类或接口文档中的静态工厂以及遵守通用的命名约定来减少这个问题。下面是一些静态工厂方法的常用名称。以下清单并非完整:
|
||||
**静态工厂方法的第二个缺点是,程序员很难找到它们。** 它们不像构造方法那样在 API 文档中明确的标注出来。因此,对于提供了静态方法而不是构造器的类来说,想要查明如何实例化一个类是十分困难的。Javadoc 工具可能有一天会注意到静态工厂方法。与此同时,通过关注类或者接口的文档中静态方法,并且遵守标准的命名习惯,也可以弥补这一劣势。下面是一些静态工厂方法的常用名称。以下清单这是列出了其中的一小部分:
|
||||
|
||||
- from——A 类型转换方法,它接受单个参数并返回此类型的相应实例,例如:**Date d = Date.from(instant)**;
|
||||
- of——一个聚合方法,接受多个参数并返回该类型的实例,并把他们合并在一起,例如:**Set\<Rank\> faceCards = EnumSet.of(JACK, QUEEN, KING)**;
|
||||
- valueOf——from 和 to 更为详细的替代 方式,例如:**BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE)**;
|
||||
- instance 或 getinstance——返回一个由其参数 (如果有的话) 描述的实例,但不能说它具有相同的值,例如:**StackWalker luke = StackWalker.getInstance(options)**;
|
||||
- create 或 newInstance——与 instance 或 getInstance 类似,除了该方法保证每个调用返回一个新的实例,例如:**Object newArray = Array.newInstance(classObject, arrayLen)**;
|
||||
- getType——与 getInstance 类似,但是如果在工厂方法中不同的类中使用。**Type** 是工厂方法返回的对象类型,例如:**FileStore fs = Files.getFileStore(path)**;
|
||||
- newType——与 newInstance 类似,但是如果在工厂方法中不同的类中使用。Type 是工厂方法返回的对象类型,例如:**BufferedReader br = Files.newBufferedReader(path)**;
|
||||
- type—— getType 和 newType 简洁的替代方式,例如:**List\<Complaint\> litany = Collections.list(legacyLitany)**;
|
||||
- from —— 类型转换方法,它接受单个参数并返回此类型的相应实例,例如:**Date d = Date.from(instant)**;
|
||||
- of —— 聚合方法,接受多个参数并返回该类型的实例,并把他们合并在一起,例如:**Set\<Rank\> faceCards = EnumSet.of(JACK, QUEEN, KING)**;
|
||||
- valueOf —— from 和 to 更为详细的替代 方式,例如:**BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE)**;
|
||||
- instance 或 getinstance —— 返回一个由其参数 (如果有的话) 描述的实例,但不能说它具有相同的值,例如:**StackWalker luke = StackWalker.getInstance(options)**;
|
||||
- create 或 newInstance —— 与 instance 或 getInstance 类似,除此之外该方法保证每次调用返回一个新的实例,例如:**Object newArray = Array.newInstance(classObject, arrayLen)**;
|
||||
- getType —— 与 getInstance 类似,但是在工厂方法处于不同的类中的时候使用。**getType** 中的 **Type** 是工厂方法返回的对象类型,例如:**FileStore fs = Files.getFileStore(path)**;
|
||||
- newType —— 与 newInstance 类似,但是在工厂方法处于不同的类中的时候使用。**newType**中的 **Type** 是工厂方法返回的对象类型,例如:**BufferedReader br = Files.newBufferedReader(path)**;
|
||||
- type —— getType 和 newType 简洁的替代方式,例如:**List\<Complaint\> litany = Collections.list(legacyLitany)**;
|
||||
|
||||
总之,静态工厂方法和公共构造方法都有它们的用途,并且了解它们的相对优点是值得的。通常,静态工厂更可取,因此避免在没有考虑静态工厂的情况下提供公共构造方法。
|
||||
总之,静态工厂方法和公共构造方法都有它们的用途,并且了解它们的相对优点是值得的。通常,静态工厂更可取,因此避免在没有考虑静态工厂的情况下直接选择使用公共构造方法。
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
# 2. 当构造方法参数过多时使用 builder 模式
|
||||
|
||||
|
||||
静态工厂和构造方法都有一个限制:它们不能很好地扩展到很多可选参数的情景。请考虑一个代表包装食品上的营养成分标签的例子。这些标签有几个必需的属性——每次建议的摄入量,每罐的份量和每份卡路里 ,以及超过 20 个可选的属性——总脂肪、饱和脂肪、反式脂肪、胆固醇、钠等等。大多数产品都有非零值,只有少数几个可选属性。
|
||||
静态工厂和构造方法都有一个限制:它们不能很好地扩展到很多可选参数的情景。请考虑一个代表包装食品上的营养成分标签的例子。这些标签有几个必需的属性——每次建议的摄入量,每罐的份量和每份卡路里 ,以及超过 20 个可选的属性——总脂肪、饱和脂肪、反式脂肪、胆固醇、钠等等。大多数产品只有这些可选字段中的少数,且具有非零值。
|
||||
|
||||
应该为这样的类编写什么样的构造方法或静态工厂?传统上,程序员使用了可伸缩(telescoping constructor)构造方法模式,在这种模式中,只提供了一个只所需参数的构造函数,另一个只有一个可选参数,第三个有两个可选参数,等等,最终在构造函数中包含所有可选参数。这就是它在实践中的样子。为了简便起见,只显示了四个可选属性:
|
||||
应该为这样的类编写什么样的构造方法或静态工厂?传统上,程序员使用了可伸缩(telescoping constructor)构造方法模式,在这种模式中,首先提供一个只有必需参数的构造方法,接着提供增加了一个可选参数的构造函数,然后提供增加了两个可选参数的构造函数,等等,最终在构造函数中包含所有必需和可选参数。以下就是它在实践中的样子。为了简便起见,只显示了四个可选属性:
|
||||
|
||||
```java
|
||||
// Telescoping constructor pattern - does not scale well!
|
||||
|
||||
public class NutritionFacts {
|
||||
private final int servingSize; // (mL) required
|
||||
private final int servings; // (per container) required
|
||||
@@ -53,15 +52,14 @@ public class NutritionFacts {
|
||||
NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);
|
||||
```
|
||||
|
||||
通常情况下,这个构造方法的调用需要许多你不想设置的参数,但是你不得不为它们传递一个值。 在这种情况下,我们为 `fat` 属性传递了 0 值。「只有」六个参数可能看起来并不那么糟糕,但随着参数数量的增加,它会很快失控。
|
||||
通常情况下,这个构造方法的调用需要许多你不想设置的参数,但是你不得不为它们传递一个值。 在这种情况下,我们为 `fat` 属性传递了 0 值。「只有」六个参数可能看起来并不那么糟糕,但随着参数数量的增加,它很快就会失控。
|
||||
|
||||
简而言之,可伸缩构造方法模式是有效的,但是当有很多参数时,很难编写客户端代码,而且很难读懂它。读者不知道这些值是什么意思,并且必须仔细地计算参数才能找到答案。一长串相同类型的参数可能会导致一些细微的 bug。如果客户端意外地反转了两个这样的参数,编译器并不会抱怨,但是程序在运行时会出现错误行为 (条目 51)。
|
||||
简而言之,**可伸缩构造方法模式是有效的,但是当有很多参数时,很难编写客户端代码,而且很难读懂它。** 读者不知道这些值是什么意思,并且必须仔细地去数参数才能找到答案。一长串相同类型的参数可能会导致一些细微的 bug。如果客户端不小心写反了两个这样的参数,编译器并不会报错,但是程序在运行时会出现错误行为 (详见第 51 条)。
|
||||
|
||||
当在构造方法中遇到许多可选参数时,另一种选择是 JavaBeans 模式,在这种模式中,调用一个无参数的构造函数来创建对象,然后调用 `setter` 方法来设置每个必需的参数和可选参数:
|
||||
当在构造方法中遇到许多可选参数时,另一种选择是 JavaBeans 模式,在这种模式中,调用一个无参的构造方法来创建对象,然后调用 `setter` 方法来设置每个必需的参数和可选参数:
|
||||
|
||||
```java
|
||||
// JavaBeans Pattern - allows inconsistency, mandates mutability
|
||||
|
||||
public class NutritionFacts {
|
||||
// Parameters initialized to default values (if any)
|
||||
private int servingSize = -1; // Required; no default value
|
||||
@@ -94,15 +92,14 @@ cocaCola.setSodium(35);
|
||||
cocaCola.setCarbohydrate(27);
|
||||
```
|
||||
|
||||
不幸的是,JavaBeans 模式本身有严重的缺陷。由于构造方法在多次调用中被分割,所以在构造过程中 JavaBean 可能处于不一致的状态。该类没有通过检查构造参数参数的有效性来执行一致性的选项。在不一致的状态下尝试使用对象可能会导致与包含 bug 的代码大相径庭的错误,因此很难调试。一个相关的缺点是,JavaBeans 模式排除了让类不可变的可能性(详见第 17 条),并且需要在程序员的部分增加工作以确保线程安全。
|
||||
不幸的是,JavaBeans 模式本身有严重的缺陷。由于构造方法被分割成了多次调用,所以在构造过程中 JavaBean 可能处于不一致的状态。该类没有通过检查构造参数参数的有效性来强制一致性的选项。在不一致的状态下尝试使用对象可能会导致一些错误,这些错误与平常代码的BUG很是不同,因此很难调试。一个相关的缺点是,JavaBeans 模式排除了让类不可变的可能性(详见第 17 条),并且需要程序员增加工作以确保线程安全。
|
||||
|
||||
通过在对象构建完成时手动「冻结」对象,并且不允许它在解冻之前使用,可以减少这些缺点,但是这种变体在实践中很难使用并且很少使用。 而且,在运行时会导致错误,因为编译器无法确保程序员在使用对象之前调用 `freeze` 方法。
|
||||
通过在对象构建完成时手动「冻结」对象,并且不允许它在解冻之前使用,可以减少这些缺点,但是这种变体在实践中很难使用并且很少使用。 而且,在运行时会导致错误,因为编译器无法确保程序员会在使用对象之前调用 `freeze` 方法。
|
||||
|
||||
幸运的是,还有第三种选择,它结合了可伸缩构造方法模式的安全性和 JavaBean 模式的可读性。 它是 Builder 模式[Gamma95] 的一种形式。客户端不直接调用所需的对象,而是调用构造方法 (或静态工厂),并使用所有必需的参数,并获得一个 builder 对象。然后,客户端调用 builder 对象的 `setter` 相似方法来设置每个可选参数。最后,客户端调用一个无参的 `build` 方法来生成对象,该对象通常是不可变的。Builder 通常是它所构建的类的一个静态成员类(详见第 24 条)。以下是它在实践中的示例:
|
||||
幸运的是,还有第三种选择,它结合了可伸缩构造方法模式的安全性和 JavaBean 模式的可读性。 它是 Builder 模式[Gamma95] 的一种形式。客户端不直接构造所需的对象,而是调用一个包含所有必需参数的构造方法 (或静态工厂)得到获得一个 builder 对象。然后,客户端调用 builder 对象的与 `setter` 相似方法来设置你想设置的可选参数。最后,客户端调用builder对象的一个无参的 `build` 方法来生成对象,该对象通常是不可变的。Builder 通常是它所构建的类的一个静态成员类(详见第 24 条)。以下是它在实践中的示例:
|
||||
|
||||
```java
|
||||
// Builder Pattern
|
||||
|
||||
public class NutritionFacts {
|
||||
private final int servingSize;
|
||||
private final int servings;
|
||||
@@ -163,22 +160,21 @@ public class NutritionFacts {
|
||||
}
|
||||
```
|
||||
|
||||
`NutritionFacts` 类是不可变的,所有的参数默认值都在一个地方。builder 的 setter 方法返回 builder 本身,这样调用就可以被链接起来,从而生成一个流畅的 API。下面是客户端代码的示例:
|
||||
`NutritionFacts` 类是不可变的,所有的参数默认值都在一个地方。builder 的 setter 方法返回 builder 本身,这样就可以进行链式调用,从而生成一个流畅的 API。下面是客户端代码的示例:
|
||||
|
||||
```java
|
||||
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
|
||||
.calories(100).sodium(35).carbohydrate(27).build();
|
||||
```
|
||||
|
||||
这个客户端代码很容易编写,更重要的是易于阅读。 Builder 模式模拟 Python 和 Scala 中的命名可选参数。
|
||||
这个客户端代码很容易编写,更重要的是易于阅读。 采用Builder 模式模拟实现的的可选参数可以在Python和Scala都可以找到。
|
||||
|
||||
为了简洁起见,省略了有效性检查。 要尽快检测无效参数,检查 builder 的构造方法和方法中的参数有效性。 在 `build` 方法调用的构造方法中检查包含多个参数的不变性。为了确保这些不变性不受攻击,在从 builder 复制参数后对对象属性进行检查(详见第 50 条)。 如果检查失败,则抛出 `IllegalArgumentException` 异常(详见第 72 条),其详细消息指示哪些参数无效(详见第 75 条)。
|
||||
|
||||
Builder 模式非常适合类层次结构。 使用平行层次的 builder,每个嵌套在相应的类中。 抽象类有抽象的 builder;具体的类有具体的 builder。 例如,考虑代表各种比萨饼的根层次结构的抽象类:
|
||||
Builder 模式非常适合类层次结构。 使用平行层次的 builder,每个builder嵌套在相应的类中。 抽象类有抽象的 builder;具体的类有具体的 builder。 例如,考虑代表各种比萨饼的根层次结构的抽象类:
|
||||
|
||||
```java
|
||||
// Builder pattern for class hierarchies
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
@@ -207,7 +203,7 @@ public abstract class Pizza {
|
||||
}
|
||||
```
|
||||
|
||||
请注意,`Pizza.Builder` 是一个带有递归类型参数( recursive type parameter)(详见第 30 条)的泛型类型。 这与抽象的 `self` 方法一起,允许方法链在子类中正常工作,而不需要强制转换。 Java 缺乏自我类型的这种变通解决方法被称为模拟自我类型(simulated self-type)的习惯用法。
|
||||
请注意,`Pizza.Builder` 是一个带有递归类型参数( recursive type parameter)(详见第 30 条)的泛型类型。 这与抽象的 `self` 方法一起,允许方法链在子类中正常工作,而不需要强制转换。 Java 缺乏自我类型的这种变通解决方法被称为模拟自我类型(simulated self-type)。
|
||||
|
||||
这里有两个具体的 `Pizza` 的子类,其中一个代表标准的纽约风格的披萨,另一个是半圆形烤乳酪馅饼。前者有一个所需的尺寸参数,而后者则允许指定酱汁是否应该在里面或在外面:
|
||||
|
||||
@@ -282,6 +278,6 @@ Calzone calzone = new Calzone.Builder()
|
||||
|
||||
Builder 模式非常灵活。 单个 builder 可以重复使用来构建多个对象。 builder 的参数可以在构建方法的调用之间进行调整,以改变创建的对象。 builder 可以在创建对象时自动填充一些属性,例如每次创建对象时增加的序列号。
|
||||
|
||||
Builder 模式也有缺点。为了创建对象,首先必须创建它的 builder。虽然创建这个 builder 的成本在实践中不太可能被注意到,但在性能关键的情况下可能会出现问题。而且,builder 模式比伸缩构造方法模式更冗长,因此只有在有足够的参数时才值得使用它,比如四个或更多。但是请记住,如果希望在将来添加更多的参数。但是,如果从构造方法或静态工厂开始,并切换到 builder,当类演化到参数数量失控的时候,过时的构造方法或静态工厂就会面临尴尬的处境。因此,所以,最好从一开始就创建一个 builder。
|
||||
Builder 模式也有缺点。为了创建对象,首先必须创建它的 builder。虽然创建这个 builder 的成本在实践中不太可能被注意到,但在看中性能的场合下这可能就是一个问题。而且,builder 模式比伸缩构造方法模式更冗长,因此只有在有足够的参数时才值得使用它,比如四个或更多。但是请记住,你可能在以后会想要添加更多的参数。但是,如果你一开始是使用的构造方法或静态工厂,当类演化到参数数量失控的时候再转到Builder模式,过时的构造方法或静态工厂就会面临尴尬的处境。因此,通常最好从一开始就创建一个 builder。
|
||||
|
||||
总而言之,当设计类的构造方法或静态工厂的参数超过几个时,Builder 模式是一个不错的选择,特别是如果许多参数是可选的或相同类型的。客户端代码比使用伸缩构造方法(telescoping constructors)更容易读写,并且 builder 比 JavaBeans 更安全。
|
||||
总而言之,当设计类的构造方法或静态工厂的参数超过几个时,Builder 模式是一个不错的选择,特别是如果许多参数是可选的或相同类型的。builder模式客户端代码比使用伸缩构造方法(telescoping constructors)更容易读写,并且builder模式比 JavaBeans 更安全。
|
||||
|
||||
@@ -33,9 +33,9 @@ public class Elvis {
|
||||
|
||||
公共属性方法的主要优点是 API 明确表示该类是一个单例:公共静态属性是 final 的,所以它总是包含相同的对象引用。 第二个好处是它更简单。
|
||||
|
||||
静态工厂方法的一个优点是,它可以灵活地改变你的想法,无论该类是否为单例而不必更改其 API。 工厂方法返回唯一的实例,但是可以修改,比如,返回调用它的每个线程的单独实例。 第二个好处是,如果你的应用程序需要它,可以编写一个泛型单例工厂(generic singleton factory )(详见第30 条)。 使用静态工厂的最后一个优点是方法引用可以用 `supplier`,例如 `Elvis::instance` 等同于 `Supplier<Elvis>`。 除非与这些优点相关的,否则公共属性方法是可取的。
|
||||
静态工厂方法的优势之一在于,它提供了灵活性:在不改变其 API 的前提下,我们可以改变该类是否应该为单例的想法。工厂方法返回该类的唯一实例,但是,它很容易被修改,比如,改为每个调用该方法的线程返回一个唯一的实例。 第二个好处是,如果你的应用程序需要它,可以编写一个泛型单例工厂(generic singleton factory )(详见第30 条)。 使用静态工厂的最后一个优点是,可以通过方法引用(method reference)作为提供者,例如 `Elvis::instance` 等同于 `Supplier<Elvis>`。 除非满足以上任意一种优势,否则还是优先考虑公有域(public-field)的方法。
|
||||
|
||||
创建一个使用这两种方法的单例类(第 12 章),仅仅将 `implements Serializable` 添加到声明中是不够的。为了维护单例的保证,声明所有的实例属性为 `transient`,并提供一个 `readResolve` 方法(详见第 89条)。否则,每当序列化实例被反序列化时,就会创建一个新的实例,在我们的例子中,导致出现新的 Elvis 实例。为了防止这种情况发生,将这个 `readResolve` 方法添加到 Elvis 类:
|
||||
为了将上述方法中实现的单例类变成是可序列化的 (第 12 章),仅仅将 `implements Serializable` 添加到声明中是不够的。为了保证单例模式不被破坏,必须声明所有的实例字段为 `transient`,并提供一个 `readResolve` 方法(详见第 89 条)。否则,每当序列化的实例被反序列化时,就会创建一个新的实例,在我们的例子中,导致出现新的 Elvis 实例。为了防止这种情况发生,将如下的 `readResolve` 方法添加到 Elvis 类:
|
||||
|
||||
```java
|
||||
// readResolve method to preserve singleton property
|
||||
@@ -56,4 +56,4 @@ public enum Elvis {
|
||||
}
|
||||
```
|
||||
|
||||
这种方式类似于公共属性方法,但更简洁,提供了免费的序列化机制,并提供了针对多个实例化的坚固保证,即使是在复杂的序列化或反射攻击的情况下。这种方法可能感觉有点不自然,但是单一元素枚举类通常是实现单例的最佳方式。注意,如果单例必须继承 `Enum` 以外的父类 (尽管可以声明一个 `Enum` 来实现接口),那么就不能使用这种方法。
|
||||
这种方式类似于公共属性方法,但更简洁,无偿地提供了序列化机制,并提供了防止多个实例化的坚固保证,即使是在复杂的序列化或反射攻击的情况下。这种方法可能感觉有点不自然,但是 **单一元素枚举类通常是实现单例的最佳方式**。注意,如果单例必须继承 `Enum` 以外的父类 (尽管可以声明一个 `Enum` 来实现接口),那么就不能使用这种方法。
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# 4. 使用私有构造方法执行非实例化
|
||||
# 4. 使用私有构造器执行非实例化
|
||||
|
||||
|
||||
偶尔你会想写一个只包含静态方法和静态属性的类。 这样的类获得了不好的名声,因为有些人滥用这些类从而避免以面向对象方式思考,但是它们确实有着特殊的用途。 它们可以用来按照 `java.lang.Math` 或 `java.util.Arrays` 的方式,把基本类型的值或数组类型上的相关方法组织起来。我们也可以通过 `java.util.Collections` 的方式,把实现特定接口上面的静态方法进行分组,也包括工厂方法(详见第 1 条)。 (从 Java 8 开始,你也可以将这些方法放在接口中,假定是你编写的接口并可以进行修改。)最后,这样的类可以用于在 final 类上对方法进行分组,因为不能将它们放在子类中。
|
||||
偶尔你会想写一个只包含静态方法和静态字段的类。 这些类的名声非常不好,因为有些人滥用这些类从而避免以面向对象方式思考从而编写过程化的程序,但是它们确实有着特殊的用途。 它们可以用来按照 `java.lang.Math` 或 `java.util.Arrays` 的方式,把基本类型的值或数组类型上的相关方法组织起来。我们也可以通过 `java.util.Collections` 的方式,把实现特定接口上面的静态方法进行分组,也包括工厂方法(详见第 1 条)。 (从 Java 8 开始,你也可以将这些方法放在接口中,假定该接口是你编写的并可以进行修改。)最后,这样的类可以用于在 final 类上对方法进行分组,因为不能将它们放在子类中。
|
||||
|
||||
这样的实用类(utility classes)不是设计用来被实例化的:一个实例是没有意义的。然而,在没有显式构造方法的情况下,编译器提供了一个公共的、无参的默认构造方法。对于用户来说,该构造方法与其他构造方法没有什么区别。在已发布的 API 中经常看到无意识的被实例的类。
|
||||
这样的工具类(utility classes)不是设计用来被实例化的,因为实例化对它没有任何意义。然而,在没有显式构造器的情况下,编译器提供了一个公共的、无参的默认构造器。对于用户来说,该构造器与其他构造器没有什么区别。在已发布的 API 中经常看到无意识的被实例的类。
|
||||
|
||||
**试图通过创建抽象类来强制执行非实例化是行不通的。** 该类可以被子类化,子类可以被实例化。此外,它误导用户认为该类是为继承而设计的(详见第 19 条)。不过,有一个简单的方法来确保非实例化。只有当类不包含显式构造方法时,才会生成一个默认构造方法,**因此可以通过包含一个私有构造方法来实现类的非实例化:**
|
||||
**试图通过创建抽象类来强制执行非实例化是行不通的。** 该类可以被子类化,并且子类可以被实例化。此外,它误导用户认为该类是为继承而设计的(详见第 19 条)。不过,有一个简单的方法来确保非实例化。只有当类不包含显式构造器时,才会生成一个默认构造器,**因此可以通过包含一个私有构造器来实现类的非实例化:**
|
||||
|
||||
```java
|
||||
// Noninstantiable utility class
|
||||
@@ -18,6 +18,6 @@ public class UtilityClass {
|
||||
}
|
||||
```
|
||||
|
||||
因为显式构造方法是私有的,所以在类之外是不可访问的。`AssertionError` 异常不是严格要求的,但是它提供了一种保证,以防在类中意外地调用构造方法。它保证类在任何情况下都不会被实例化。这个习惯用法有点违反直觉,好像构造方法就是设计成不能调用的一样。因此,如前面所示,添加注释是种明智的做法。
|
||||
因为显式构造器是私有的,所以不可以在类的外部访问它。`AssertionError` 异常不是严格要求的,但是它可以避免不小心在类的内部调用构造器。它保证类在任何情况下都不会被实例化。这个习惯用法有点违反直觉,好像构造器就是设计成不能调用的一样。因此,如前面所示,添加注释是种明智的做法。
|
||||
|
||||
这种习惯有一个副作用,阻止了类的子类化。所有的构造方法都必须显式或隐式地调用父类构造方法,而子类则没有可访问的父类构造方法来调用。
|
||||
这种习惯有一个副作用,就是使得一个类不能子类化。所有的构造器都必须显式或隐式地调用父类构造器,而在这群情况下子类则没有可访问的父类构造器来调用。
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 05. 依赖注入优于硬连接资源(hardwiring resources)
|
||||
|
||||
|
||||
许多类依赖于一个或多个底层资源。例如,拼写检查器依赖于字典。将此类类实现为静态实用工具类并不少见 (详见第 4 条):
|
||||
许多类依赖于一个或多个底层资源。例如,拼写检查器依赖于字典。将此类类实现为静态工具类并不少见 (详见第 4 条):
|
||||
|
||||
```java
|
||||
// Inappropriate use of static utility - inflexible & untestable!
|
||||
@@ -15,7 +15,7 @@ public class SpellChecker {
|
||||
}
|
||||
```
|
||||
|
||||
同样地,将它们实现为单例也并不少见(详见第 3 条):
|
||||
同样地,将它们实现为单例的做法也并不少见(详见第 3 条):
|
||||
|
||||
|
||||
```java
|
||||
@@ -31,11 +31,11 @@ public class SpellChecker {
|
||||
}
|
||||
```
|
||||
|
||||
这两种方法都不令人满意,因为他们假设只有一本字典值得使用。在实际中,每种语言都有自己的字典,特殊的字典被用于特殊的词汇表。另外,使用专门的字典来进行测试也是可取的。想当然地认为一本字典就足够了,这是一厢情愿的想法。
|
||||
这两种方法都不令人满意,因为他们都是假设只有一本字典可用。实际上,每种语言都有自己的字典,特殊的字典被用于特殊的词汇表。另外,可能还需要用特殊的词典进行测试。想当然地认为一本字典就足够了,这是一厢情愿的想法。
|
||||
|
||||
可以通过使 `dictionary` 属性设置为非 `final`,并添加一个方法来更改现有拼写检查器中的字典,从而让拼写检查器支持多个字典,但是在并发环境中,这是笨拙的、容易出错的和不可行的。静态实用类和单例对于那些行为被底层资源参数化的类来说是不合适的。
|
||||
可以通过使 `dictionary` 属性设置为非 `final`,并添加一个方法来更改现有拼写检查器中的字典,从而让`SpellChecker` 支持多个字典,但是这样的设置显得非常笨拙、容易出错、并且无法并行工作。**静态工具类和单例类不适合与需要引用底层资源的类。**
|
||||
|
||||
所需要的是能够支持类的多个实例 (在我们的示例中,即 `SpellChecker`),每个实例都使用客户端所期望的资源(在我们的例子中是 `dictionary`)。满足这一需求的简单模式是在创建新实例时将资源传递到构造方法中。这是依赖项注入(dependency injection)的一种形式:字典是拼写检查器的一个依赖项,当它创建时被注入到拼写检查器中。
|
||||
这里所需要的是能够支持类的多个实例 (在我们的示例中是指 `SpellChecker`),每个实例都使用客户端所期望的资源(在我们的例子中是 `dictionary`)。满足这一需求的简单模式是,**在创建新实例时将资源传递到构造器中。** 这是依赖项注入(dependency injection)的一种形式:字典是拼写检查器的一个依赖项,当它创建时被注入到拼写检查器中。
|
||||
|
||||
|
||||
```java
|
||||
@@ -51,9 +51,9 @@ public class SpellChecker {
|
||||
public List<String> suggestions(String typo) { ... }
|
||||
}
|
||||
```
|
||||
依赖注入模式非常简单,许多程序员使用它多年而不知道它有一个名字。 虽然我们的拼写检查器的例子只有一个资源(字典),但是依赖项注入可以使用任意数量的资源和任意依赖图。 它保持了不变性(详见第 17 条),因此多个客户端可以共享依赖对象(假设客户需要相同的底层资源)。 依赖注入同样适用于构造方法,静态工厂(详见第 1 条)和 builder 模式(详见第 2 条)。
|
||||
依赖注入模式非常简单,许多程序员使用它多年而不知道它有一个名字。 虽然我们的拼写检查器的例子只有一个资源(字典),但是依赖项注入可以使用任意数量的资源和任意依赖图。 它保持了不变性(详见第 17 条),因此多个客户端可以共享依赖对象(假设客户需要相同的底层资源)。 依赖注入同样适用于构造器、静态工厂(详见第 1 条)和 builder 模式(详见第 2 条)。
|
||||
|
||||
该模式的一个有用的变体是将资源工厂传递给构造方法。 工厂是可以重复调用以创建类型实例的对象。 这种工厂体现了工厂方法模式(Factory Method pattern)[Gamma95]。 Java 8 中引入的 `Supplier<T>` 接口非常适合代表工厂。 在输入上采用 `Supplier<T>` 的方法通常应该使用有界的通配符类型(bounded wildcard type)(详见第 31 条)约束工厂的类型参数,以允许客户端传入工厂,创建指定类型的任何子类型。 例如,下面是一个使用客户端提供的工厂生成 tile 的方法:
|
||||
该模式的一个有用的变体是将资源工厂传递给构造器。 工厂是可以重复调用以创建类型实例的对象。 这种工厂体现了工厂方法模式(Factory Method pattern)[Gamma95]。 Java 8 中引入的 `Supplier<T>` 接口非常适合代表工厂。 在输入上采用 `Supplier<T>` 的方法通常应该使用有界的通配符类型(bounded wildcard type)(详见第 31 条)约束工厂的类型参数,以便客户端能够传入一个工厂,来创建指定类型的任意子类型。例如,下面是一个生产马赛克的方法,它利用客户端提供的工厂来生产每一片马赛克:
|
||||
|
||||
```java
|
||||
Mosaic create(Supplier<? extends Tile> tileFactory) { ... }
|
||||
@@ -61,4 +61,4 @@ Mosaic create(Supplier<? extends Tile> tileFactory) { ... }
|
||||
|
||||
尽管依赖注入极大地提高了灵活性和可测试性,但它可能使大型项目变得混乱,这些项目通常包含数千个依赖项。使用依赖注入框架(如 Dagger [Dagger]、Guice [Guice] 或 Spring [Spring])可以消除这些混乱。这些框架的使用超出了本书的范围,但是请注意,为手动依赖注入而设计的 API 非常适合这些框架的使用。
|
||||
|
||||
总之,不要使用单例或静态的实用类来实现一个类,该类依赖于一个或多个底层资源,这些资源的行为会影响类的行为,并且不让类直接创建这些资源。相反,将资源或工厂传递给构造方法(或静态工厂或 builder 模式)。这种称为依赖注入的实践将极大地增强类的灵活性、可重用性和可测试性。
|
||||
总而言之,不要用单例和静态工具类来实现依赖一个或多个底层资源的类,且该资源的行为会影响到该类的行为;也不要直接用这个类来创建这些资源。而应该将这些资源或者工厂传给构造器(或者静态工厂,或者构建器),通过它们来创建类。这个实践就被称作依赖注人,它极大地提升了类的灵活性、可重用性和可测试性。
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
String s = new String("bikini"); // DON'T DO THIS!
|
||||
```
|
||||
|
||||
语句每次执行时都会创建一个新的 String 实例,而这些对象的创建都不是必需的。String 构造方法 `("bikini")` 的参数本身就是一个 `bikini` 实例,它与构造方法创建的所有对象的功能相同。如果这种用法发生在循环中,或者在频繁调用的方法中,就可以毫无必要地创建数百万个 String 实例。
|
||||
语句每次执行时都会创建一个新的 String 实例,而这些对象的创建都不是必需的。String 构造方法 `("bikini")` 的参数本身就是一个 `bikini` 实例,它与构造方法创建的所有对象的功能相同。如果这种用法发生在循环中,或者在频繁调用的方法中,就会毫无必要地创建数百万个 String 实例。
|
||||
|
||||
改进后的版本如下:
|
||||
```java
|
||||
@@ -18,7 +18,7 @@ String s = "bikini";
|
||||
|
||||
该版本使用单个 String 实例,而不是每次执行时创建一个新实例。此外,它可以保证对象运行在同一虚拟机上的任何其他代码重用,而这些代码恰好包含相同的字符串字面量[[JLS, 3.10.5]](https://docs.oracle.com/javase/specs/jls/se12/html/jls-3.html#jls-3.10.5)。
|
||||
|
||||
通过使用静态工厂方法(static factory methods, 条目 1),可以避免创建不需要的对象。例如,工厂方法 `Boolean.valueOf(String)` 比构造方法 `Boolean(String)` 更可取,后者在 Java 9 中被弃用。构造方法每次调用时都必须创建一个新对象,而工厂方法永远不需要这样做,在实践中也不需要。除了重用不可变对象,如果知道它们不会被修改,还可以重用可变对象。
|
||||
通过使用静态工厂方法(详见第 1 条)和构造器,可以避免创建不需要的对象。例如,工厂方法 `Boolean.valueOf(String)` 比构造方法 `Boolean(String)` 更可取,后者在 Java 9 中被弃用。构造方法每次调用时都必须创建一个新对象,而工厂方法永远不需要这样做,在实践中也不需要。除了重用不可变对象,如果知道它们不会被修改,还可以重用可变对象。
|
||||
|
||||
一些对象的创建比其他对象的创建要昂贵得多。 如果要重复使用这样一个「昂贵的对象」,建议将其缓存起来以便重复使用。 不幸的是,当创建这样一个对象时并不总是很直观明显的。 假设你想写一个方法来确定一个字符串是否是一个有效的罗马数字。 以下是使用正则表达式完成此操作时最简单方法:
|
||||
|
||||
@@ -56,7 +56,7 @@ public class RomanNumerals {
|
||||
|
||||
例如,Map 接口的 `keySet` 方法返回 Map 对象的 Set 视图,包含 Map 中的所有 key。 天真地说,似乎每次调用 `keySet` 都必须创建一个新的 Set 实例,但是对给定 Map 对象的 `keySet` 的每次调用都返回相同的 Set 实例。 尽管返回的 Set 实例通常是可变的,但是所有返回的对象在功能上都是相同的:当其中一个返回的对象发生变化时,所有其他对象也都变化,因为它们全部由相同的 Map 实例支持。 虽然创建 `keySet` 视图对象的多个实例基本上是无害的,但这是没有必要的,也没有任何好处。
|
||||
|
||||
另一种创建不必要的对象的方法是自动装箱(autoboxing),它允许程序员混用基本类型和包装的基本类型,根据需要自动装箱和拆箱。 自动装箱模糊不清,但不会消除基本类型和装箱基本类型之间的区别。 有微妙的语义区别和不那么细微的性能差异(详见第 61 条)。 考虑下面的方法,它计算所有正整数的总和。 要做到这一点,程序必须使用 `long` 类型,因为 `int` 类型不足以保存所有正整数的总和:
|
||||
另一种创建不必要的对象的方法是自动装箱(auto boxing),它允许程序员混用基本类型和包装的基本类型,根据需要自动装箱和拆箱。 自动装箱模糊不清,但不会消除基本类型和装箱基本类型之间的区别。 有微妙的语义区别和不那么细微的性能差异(详见第 61 条)。 考虑下面的方法,它计算所有正整数的总和。 要做到这一点,程序必须使用 `long` 类型,因为 `int` 类型不足以保存所有正整数的总和:
|
||||
|
||||
```java
|
||||
// Hideously slow! Can you spot the object creation?
|
||||
@@ -68,7 +68,7 @@ private static long sum() {
|
||||
}
|
||||
```
|
||||
|
||||
这个程序的结果是正确的,但由于写错了一个字符,运行的结果要比实际慢很多。变量 `sum` 被声明成了 `Long` 而不是 `long`,这意味着程序构造了大约 231 不必要的 `Long` 实例(大约每次往 `Long` 类型的 `sum` 变量中增加一个 `long` 类型构造的实例),把 `sum` 变量的类型由 `Long` 改为 `long`,在我的机器上运行时间从 6.3 秒降低到 0.59 秒。这个教训很明显:**优先使用基本类型而不是装箱的基本类型,也要注意无意识的自动装箱。**
|
||||
这个程序的结果是正确的,但由于写错了一个字符,运行的结果要比实际慢很多。变量 `sum` 被声明成了 `Long` 而不是 `long`,这意味着程序构造了大约 2<sup>31</sup> 不必要的 `Long` 实例(大约每次往 `Long` 类型的 `sum` 变量中增加一个 `long` 类型构造的实例),把 `sum` 变量的类型由 `Long` 改为 `long`,在我的机器上运行时间从 6.3 秒降低到 0.59 秒。这个教训很明显:**优先使用基本类型而不是装箱的基本类型,也要注意无意识的自动装箱。**
|
||||
|
||||
这个条目不应该被误解为暗示对象创建是昂贵的,应该避免创建对象。 相反,使用构造方法创建和回收小的对象是非常廉价,构造方法只会做很少的显示工作,尤其是在现代 JVM 实现上。 创建额外的对象以增强程序的清晰度,简单性或功能性通常是件好事。
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
如果你从使用手动内存管理的语言(如 C 或 C++)切换到像 Java 这样的带有垃圾收集机制的语言,那么作为程序员的工作就会变得容易多了,因为你的对象在使用完毕以后就自动回收了。当你第一次体验它的时候,它就像魔法一样。这很容易让人觉得你不需要考虑内存管理,但这并不完全正确。
|
||||
|
||||
考虑以下简单的堆栈实现:
|
||||
考虑以下简单的栈实现:
|
||||
|
||||
```java
|
||||
// Can you spot the "memory leak"?
|
||||
@@ -37,9 +37,9 @@ public class Stack {
|
||||
}
|
||||
}
|
||||
```
|
||||
这个程序没有什么明显的错误(但是对于泛型版本,请参阅条目 29)。 你可以对它进行详尽的测试,它都会成功地通过每一项测试,但有一个潜在的问题。 笼统地说,程序有一个“内存泄漏”,由于垃圾回收器的活动的增加,或内存占用的增加,静默地表现为性能下降。 在极端的情况下,这样的内存泄漏可能会导致磁盘分页(disk paging),甚至导致内存溢出(OutOfMemoryError)的失败,但是这样的故障相对较少。
|
||||
这个程序没有什么明显的错误(但是对于泛型版本,请参阅条目 29)。 你可以对它进行详尽的测试,它都会成功地通过每一项测试,但有一个潜在的问题。 笼统地说,程序有一个「内存泄漏」,由于垃圾回收器的活动的增加,或内存占用的增加,静默地表现为性能下降。 在极端的情况下,这样的内存泄漏可能会导致磁盘分页(disk paging),甚至导致内存溢出(OutOfMemoryError)的失败,但是这样的故障相对较少。
|
||||
|
||||
那么哪里发生了内存泄漏? 如果一个栈增长后收缩,那么从栈弹出的对象不会被垃圾收集,即使使用栈的程序不再引用这些对象。 这是因为栈维护对这些对象的过期引用(obsolete references)。 过期引用简单来说就是永远不会解除的引用。 在这种情况下,元素数组“活动部分(active portion)”之外的任何引用都是过期的。 活动部分是由索引下标小于 size 的元素组成。
|
||||
那么哪里发生了内存泄漏? 如果一个栈增长后收缩,那么从栈弹出的对象不会被垃圾收集,即使使用栈的程序不再引用这些对象。 这是因为栈维护对这些对象的过期引用(obsolete references)。 过期引用简单来说就是永远不会解除的引用。 在这种情况下,元素数组「活动部分(active portion)」之外的任何引用都是过期的。 活动部分是由索引下标小于 size 的元素组成。
|
||||
|
||||
垃圾收集语言中的内存泄漏(更适当地称为无意的对象保留 unintentional object retentions)是隐蔽的。 如果无意中保留了对象引用,那么不仅这个对象排除在垃圾回收之外,而且该对象引用的任何对象也是如此。 即使只有少数对象引用被无意地保留下来,也可以阻止垃圾回收机制对许多对象的回收,这对性能产生很大的影响。
|
||||
|
||||
@@ -68,7 +68,7 @@ public Object pop() {
|
||||
|
||||
第三个常见的内存泄漏来源是监听器和其他回调。如果你实现了一个 API,其客户端注册回调,但是没有显式地撤销注册回调,除非采取一些操作,否则它们将会累积。确保回调是垃圾收集的一种方法是只存储弱引用(weak references),例如,仅将它们保存在 `WeakHashMap` 的键(key)中。
|
||||
|
||||
因为内存泄漏通常不会表现为明显的故障,所以它们可能会在系统中保持多年。 通常仅在仔细的代码检查或借助堆分析器( heap profiler)的调试工具才会被发现。 因此,学习如何预见这些问题,并防止这些问题发生,是非常值得的。
|
||||
因为内存泄漏通常不会表现为明显的故障,所以它们可能会在系统中保持多年。 通常仅在仔细的代码检查或借助堆分析器(heap profiler)的调试工具才会被发现。 因此,学习如何预见这些问题,并防止这些问题发生,是非常值得的。
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 10. 重写 equals 方法时遵守通用约定
|
||||
|
||||
|
||||
虽然 `Object` 是一个具体的类,但它主要是为继承而设计的。它的所有非 final 方法(equals、hashCode、toString、clone 和 finalize)都有清晰的通用约定( general contracts),因为它们被设计为被子类重写。任何类都有义务重写这些方法,以遵从他们的通用约定;如果不这样做,将会阻止其他依赖于约定的类 (例如 HashMap 和 HashSet) 与此类一起正常工作。
|
||||
虽然 `Object` 是一个具体的类,但它主要是为继承而设计的。它的所有非 final 方法(equals、hashCode、toString、clone 和 finalize)都有清晰的通用约定( general contracts),因为它们被设计为被子类重写。任何类要重写这些方法时,都有义务去遵从它们的通用约定;如果不这样做,将会阻止其他依赖于约定的类 (例如 HashMap 和 HashSet) 与此类一起正常工作。
|
||||
|
||||
本章论述何时以及如何重写 `Object` 类的非 final 的方法。这一章省略了 finalize 方法,因为它在条目 8 中进行了讨论。`Comparable.compareTo` 方法虽然不是 `Object` 中的方法,因为具有很多的相似性,所以也在这里讨论。
|
||||
|
||||
@@ -79,7 +79,6 @@ System.out.println(s.equals(cis)); // false
|
||||
|
||||
```java
|
||||
List<CaseInsensitiveString> list = new ArrayList<>();
|
||||
list.add(cis);List<CaseInsensitiveString> list = new ArrayList<>();
|
||||
list.add(cis);
|
||||
```
|
||||
|
||||
@@ -228,7 +227,7 @@ public class CounterPoint extends Point {
|
||||
|
||||
里氏替代原则(Liskov substitution principle)指出,任何类型的重要属性都应该适用于所有的子类型,因此任何为这种类型编写的方法都应该在其子类上同样适用[Liskov87]。 这是我们之前声明的一个正式陈述,即 Point 的子类(如 CounterPoint)仍然是一个 Point,必须作为一个 Point 类来看待。 但是,假设我们将一个 CounterPoint 对象传递给 onUnitCircle 方法。 如果 Point 类使用基于 getClass 的 equals 方法,则无论 CounterPoint 实例的 x 和 y 坐标如何,onUnitCircle 方法都将返回 false。 这是因为大多数集合(包括 onUnitCircle 方法使用的 HashSet)都使用 equals 方法来测试是否包含元素,并且 CounterPoint 实例并不等于任何 Point 实例。 但是,如果在 Point 上使用了适当的基于 `instanceof` 的 equals 方法,则在使用 CounterPoint 实例呈现时,同样的 onUnitCircle 方法可以正常工作。
|
||||
|
||||
虽然没有令人满意的方法来继承一个可实例化的类并添加一个值组件,但是有一个很好的变通方法:按照条目 18 的建议,“优先使用组合而不是继承”。取代继承 Point 类的 ColorPoint 类,可以在 ColorPoint 类中定义一个私有 Point 属性,和一个公共的试图(view)(详见第 6 条)方法,用来返回具有相同位置的 ColorPoint 对象。
|
||||
虽然没有令人满意的方法来继承一个可实例化的类并添加一个值组件,但是有一个很好的变通方法:按照条目 18 的建议,“优先使用组合而不是继承”。取代继承 Point 类的 ColorPoint 类,可以在 ColorPoint 类中定义一个私有 Point 属性,和一个公共的视图(view)(详见第 6 条)方法,用来返回具有相同位置的 ColorPoint 对象。
|
||||
|
||||
```java
|
||||
// Adds a value component without violating the equals contract
|
||||
@@ -350,8 +349,8 @@ public final class PhoneNumber {
|
||||
以下是一些最后提醒:
|
||||
|
||||
1. **当重写 equals 方法时,同时也要重写 hashCode 方法(详见第 11 条)**。
|
||||
2. **不要让 equals 方法试图太聪明。**如果只是简单地测试用于相等的属性,那么要遵守 equals 约定并不困难。如果你在寻找相等方面过于激进,那么很容易陷入麻烦。一般来说,考虑到任何形式的别名通常是一个坏主意。例如,File 类不应该试图将引用的符号链接等同于同一文件对象。幸好 File 类并没这么做。
|
||||
3. **在 equal 时方法声明中,不要将参数 Object 替换成其他类型。**对于程序员来说,编写一个看起来像这样的 equals 方法并不少见,然后花上几个小时苦苦思索为什么它不能正常工作:在 equal 时方法声明中,不要将参数 Object 替换成其他类型。对于程序员来说,编写一个看起来像这样的 equals 方法并不少见,然后花上几个小时苦苦思索为什么它不能正常工作。
|
||||
2. **不要让 equals 方法试图太聪明。** 如果只是简单地测试用于相等的属性,那么要遵守 equals 约定并不困难。如果你在寻找相等方面过于激进,那么很容易陷入麻烦。一般来说,考虑到任何形式的别名通常是一个坏主意。例如,File 类不应该试图将引用的符号链接等同于同一文件对象。幸好 File 类并没这么做。
|
||||
3. **在 equal 时方法声明中,不要将参数 Object 替换成其他类型。** 对于程序员来说,编写一个如下所示的 equals 方法,然后花上几个小时苦苦思索为什么不能正常工作的情况并不少见:
|
||||
|
||||
```java
|
||||
// Broken - parameter type must be Object!
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
**在每个类中,在重写 equals 方法的时侯,一定要重写 hashcode 方法。** 如果不这样做,你的类违反了 hashCode 的通用约定,这会阻止它在 HashMap 和 HashSet 这样的集合中正常工作。根据 Object 规范,以下时具体约定。
|
||||
|
||||
1. 当在一个应用程序执行过程中,如果在 equals 方法比较中没有修改任何信息,在一个对象上重复调用 hashCode 方法时,它必须始终返回相同的值。从一个应用程序到另一个应用程序的每一次执行返回的值可以是不一致的。
|
||||
1. 如果没有修改 equals 方法中用以比较的信息,在应用程序的一次执行过程中对一个对象重复调用 hashCode 方法时,它必须始终返回相同的值。在应用程序的多次执行过程中,每个执行过程在该对象上获取的结果值可以不相同。
|
||||
2. 如果两个对象根据 equals(Object) 方法比较是相等的,那么在两个对象上调用 hashCode 就必须产生的结果是相同的整数。
|
||||
3. 如果两个对象根据 equals(Object) 方法比较并不相等,则不要求在每个对象上调用 hashCode 都必须产生不同的结果。 但是,程序员应该意识到,为不相等的对象生成不同的结果可能会提高散列表(hash tables)的性能。
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ public interface Comparable<T> {
|
||||
public final class CaseInsensitiveString
|
||||
implements Comparable<CaseInsensitiveString> {
|
||||
public int compareTo(CaseInsensitiveString cis) {
|
||||
return String.CASE_INSENSITIVE_[ORDER.compare(s](http://ORDER.compare(s), cis.s);
|
||||
return String.CASE_INSENSITIVE_ORDER.compare(s, cis.s);
|
||||
}
|
||||
... // Remainder omitted
|
||||
}
|
||||
@@ -71,11 +71,11 @@ public final class CaseInsensitiveString
|
||||
```java
|
||||
// Multiple-field `Comparable` with primitive fields
|
||||
public int compareTo(PhoneNumber pn) {
|
||||
int result = [Short.compare(areaCode](http://Short.compare(areaCode), pn.areaCode);
|
||||
if (result == 0) {
|
||||
result = [Short.compare(prefix](http://Short.compare(prefix), pn.prefix);
|
||||
int result = Short.compare(areaCode, pn.areaCode);
|
||||
if (result == 0) {
|
||||
result = Short.compare(prefix, pn.prefix);
|
||||
if (result == 0)
|
||||
result = [Short.compare(lineNum](http://Short.compare(lineNum), pn.lineNum);
|
||||
result = Short.compare(lineNum, pn.lineNum);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 15. 使类和成员的可访问性最小化
|
||||
|
||||
将设计良好的组件与设计不佳的组件区分开来的最重要的因素是,组件将其内部数据和其他组件的其他实现细节隐藏起来。一个设计良好的组件隐藏了它的所有实现细节,干净地将它的 API 与它的实现分离开来。然后,组件只通过它们的 API 进行通信,并且对彼此的内部工作一无所知。这一概念,被称为信息隐藏或封装,是软件设计的基本原则[Parnas72]。
|
||||
将设计良好的组件与设计不佳的组件区分开来的最重要的因素是,隐藏内部数据和其他实现细节的程度。一个设计良好的组件隐藏了它的所有实现细节,干净地将它的 API 与它的实现分离开来。然后,组件只通过它们的 API 进行通信,并且对彼此的内部工作一无所知。这一概念,被称为信息隐藏或封装,是软件设计的基本原则[Parnas72]。
|
||||
|
||||
信息隐藏很重要有很多原因,其中大部分来源于它将组成系统的组件分离开来,允许它们被独立地开发,测试,优化,使用,理解和修改。这加速了系统开发,因为组件可以并行开发。它减轻了维护的负担,因为可以更快速地理解组件,调试或更换组件,而不用担心损害其他组件。虽然信息隐藏本身并不会导致良好的性能,但它可以有效地进行性能调整:一旦系统完成并且分析确定了哪些组件导致了性能问题(条目 67),则可以优化这些组件,而不会影响别人的正确的组件。信息隐藏增加了软件重用,因为松耦合的组件通常在除开发它们之外的其他环境中证明是有用的。最后,隐藏信息降低了构建大型系统的风险,因为即使系统不能运行,各个独立的组件也可能是可用的。
|
||||
|
||||
@@ -8,18 +8,18 @@
|
||||
|
||||
经验法则很简单:**让每个类或成员尽可能地不可访问。** 换句话说,使用尽可能低的访问级别,与你正在编写的软件的对应功能保持一致。
|
||||
|
||||
对于顶层(非嵌套的)类和接口,只有两个可能的访问级别:包级私有(package-private)和公共的(public)。如果你使用 public 修饰符声明顶级类或接口,那么它是公开的;否则,它是包级私有的。如果一个顶层类或接口可以被做为包级私有,那么它应该是。通过将其设置为包级私有,可以将其作为实现的一部分,而不是导出的 API,你可以修改它、替换它,或者在后续版本中消除它,而不必担心损害现有的客户端。如果你把它公开,你就有义务永远地支持它,以保持兼容性。
|
||||
对于顶层(非嵌套的)类和接口,只有两个可能的访问级别:包级私有(package-private)和公共的(public)。如果你使用 public 修饰符声明顶级类或接口,那么它是公开的;否则,它是包级私有的。如果一个顶层类或接口可以被做为包级私有,那么它应该是。通过将其设置为包级私有,可以将其作为实现的一部分,而不是导出的 API,你可以修改它、替换它,或者在后续版本中消除它,而不必担心损害现有的客户端。如果你把它公开,你就有义务永远地支持它,以保持兼容性。
|
||||
|
||||
如果一个包级私有顶级类或接口只被一个类使用,那么可以考虑这个类作为使用它的唯一类的私有静态嵌套类 (条目 24)。这将它的可访问性从包级的所有类减少到使用它的一个类。但是,减少不必要的公共类的可访问性要比包级私有的顶级类更重要:公共类是包的 API 的一部分,而包级私有的顶级类已经是这个包实现的一部分了。
|
||||
如果一个包级私有顶级类或接口只被一个类使用,那么可以考虑这个类作为使用它的唯一类的私有静态嵌套类 (详见第 24 条)。这将它的可访问性从包级的所有类减少到使用它的一个类。但是,减少不必要的公共类的可访问性要比包级私有的顶级类更重要:公共类是包的 API 的一部分,而包级私有的顶级类已经是这个包实现的一部分了。
|
||||
|
||||
对于成员 (属性、方法、嵌套类和嵌套接口),有四种可能的访问级别,在这里,按照可访问性从小到大列出:
|
||||
对于成员(字段、方法、嵌套类和嵌套接口),有四种可能的访问级别,在这里,按照可访问性从小到大列出:
|
||||
|
||||
- private —— 该成员只能在声明它的顶级类内访问。
|
||||
- package-private —— 成员可以从被声明的包中的任何类中访问。从技术上讲,如果没有指定访问修饰符 (接口成员除外,它默认是公共的),这是默认访问级别。
|
||||
- protected —— 成员可以从被声明的类的子类中访问(受一些限制,JLS,6.6.2),以及它声明的包中的任何类。
|
||||
- package-private —— 成员可以从被声明的包中的任何类中访问。从技术上讲,如果没有指定访问修饰符(接口成员除外,它默认是公共的),这是默认访问级别。
|
||||
- protected —— 成员可以从被声明的类的子类中访问(会受一些限制 [JLS, 6.6.2]),以及它声明的包中的任何类。
|
||||
- public —— 该成员可以从任何地方被访问。
|
||||
|
||||
在仔细设计你的类的公共 API 之后,你的反应应该是让所有其他成员设计为私有的。 只有当同一个包中的其他类真的需要访问成员时,需要删除私有修饰符,从而使成员包成为包级私有的。 如果你发现自己经常这样做,你应该重新检查你的系统的设计,看看另一个分解可能产生更好的解耦的类。 也就是说,私有成员和包级私有成员都是类实现的一部分,通常不会影响其导出的 API。 但是,如果类实现 Serializable 接口(详见第 86 和 87 条),则这些属性可以「泄漏(leak)」到导出的 API 中。
|
||||
在仔细设计你的类的公共 API 之后,你的反应应该是让所有其他成员设计为私有的。 只有当同一个包中的其他类真的需要访问成员时,需要删除私有修饰符,从而使成员包成为包级私有的。 如果你发现自己经常这样做,你应该重新检查你的系统的设计,看看另一个分解可能产生更好的解耦的类。 也就是说,私有成员和包级私有成员都是类实现的一部分,通常不会影响其导出的 API。 但是,如果类实现 Serializable 接口(详见第 86 和 87 条),则这些字段可以「泄漏(leak)」到导出的 API 中。
|
||||
|
||||
对于公共类的成员,当访问级别从包私有到受保护级时,可访问性会大大增加。 受保护(protected)的成员是类导出的 API 的一部分,并且必须永远支持。 此外,导出类的受保护成员表示对实现细节的公开承诺(详见第 19 条)。 对受保护成员的需求应该相对较少。
|
||||
|
||||
@@ -27,25 +27,22 @@
|
||||
|
||||
为了便于测试你的代码,你可能会想要让一个类,接口或者成员更容易被访问。 这没问题。 为了测试将公共类的私有成员指定为包级私有是可以接受的,但是提高到更高的访问级别却是不可接受的。 换句话说,将类,接口或成员作为包级导出的 API 的一部分来促进测试是不可接受的。 幸运的是,这不是必须的,因为测试可以作为被测试包的一部分运行,从而获得对包私有元素的访问。
|
||||
|
||||
**公共类的实例属性很少公开(详见第 16 条)。** 如果一个实例属性是非 final 的,或者是对可变对象的引用,那么通过将其公开,你就放弃了限制可以存储在属性中的值的能力。这意味着你放弃了执行涉及该属性的不变量的能力。另外,当属性被修改时,就放弃了采取任何操作的能力,**因此公共可变属性的类通常不是线程安全的** 。即使属性是 final 的,并且引用了一个不可变的对象,通过使它公开,你就放弃切换到不存在属性的新的内部数据表示的灵活性。
|
||||
**公共类的实例字段很少情况下采用 public 修饰(详见第 16 条)。** 如果一个实例字段是非 final 的,或者是对可变对象的引用,那么通过将其公开,你就放弃了限制可以存储在字段中的值的能力。这意味着你放弃了执行涉及该字段的不变量的能力。另外,当字段被修改时,就放弃了采取任何操作的能力,**因此带有公共可变字段的类通常不是线程安全的** 。即使一个字段是 final 的,并且引用了一个不可变的对象,通过将其公开,你放弃了切换到一个新的内部数据表示的灵活性,而该字段并不存在。
|
||||
|
||||
同样的建议适用于静态属性,但有一个例外。 假设常量是类的抽象的一个组成部分,你可以通过 public static final 属性暴露常量。 按照惯例,这些属性的名字由大写字母组成,字母用下划线分隔(详见第 68 条)。 很重要的一点是,这些属性包含基本类型的值或对不可变对象的引用(详见第 17 条)。 包含对可变对象的引用的属性具有非 final 属性的所有缺点。 虽然引用不能被修改,但引用的对象可以被修改,并会带来灾难性的结果。
|
||||
同样的建议适用于静态字段,但有一个例外。 假设常量是类的抽象的一个组成部分,你可以通过 public static final 字段暴露常量。 按照惯例,这些字段的名字由大写字母组成,字母用下划线分隔(详见第 68 条)。 很重要的一点是,这些字段包含基本类型的值或对不可变对象的引用(详见第 17 条)。 包含对可变对象的引用的字段具有非 final 字段的所有缺点。 虽然引用不能被修改,但引用的对象可以被修改,并会带来灾难性的结果。
|
||||
|
||||
请注意,非零长度的数组总是可变的,**所以类具有公共静态 final 数组属性,或返回这样一个属性的访问器是错误的。** 如果一个类有这样的属性或访问方法,客户端将能够修改数组的内容。 这是安全漏洞的常见来源:
|
||||
请注意,非零长度的数组总是可变的,**所以类具有公共静态 final 数组字段,或返回这样一个字段的访问器是错误的。** 如果一个类有这样的字段或访问方法,客户端将能够修改数组的内容。 这是安全漏洞的常见来源:
|
||||
|
||||
```java
|
||||
// Potential security hole!
|
||||
public static final Thing[] VALUES = { ... };
|
||||
```
|
||||
|
||||
要小心这样的事实,一些 IDE 生成的访问方法返回对私有数组属性的引用,导致了这个问题。 有两种方法可以解决这个问题。 你可以使公共数组私有并添加一个公共的不可变列表:
|
||||
要小心这样的事实,一些 IDE 生成的访问方法返回对私有数组字段的引用,导致了这个问题。 有两种方法可以解决这个问题。 你可以使公共数组私有并添加一个公共的不可变列表:
|
||||
|
||||
```java
|
||||
private static final Thing[] PRIVATE_VALUES = { ... };
|
||||
|
||||
public static final List<Thing> VALUES =
|
||||
|
||||
Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
|
||||
public static final List<Thing> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
|
||||
```
|
||||
|
||||
或者,可以将数组设置为 private,并添加一个返回私有数组拷贝的公共方法:
|
||||
@@ -66,4 +63,4 @@ public static final Thing[] values() {
|
||||
|
||||
对于典型的 Java 程序员来说,不仅程序模块所提供的访问保护存在局限性,而且在本质上是很大程度上建议性的;为了利用它,你必须把你的包组合成模块,在模块声明中明确所有的依赖关系,重新安排你的源码树层级,并采取特殊的行动来适应你的模块内任何对非模块化包的访问[Reinhold, 3]。 现在说模块是否会在 JDK 之外得到广泛的使用还为时尚早。 与此同时,除非你有迫切的需要,否则似乎最好避免它们。
|
||||
|
||||
总而言之,应该尽可能地减少程序元素的可访问性(在合理范围内)。 在仔细设计一个最小化的公共 API 之后,你应该防止任何散乱的类,接口或成员成为 API 的一部分。 除了作为常量的公共静态 `final` 属性之外,公共类不应该有公共属性。 确保 `public static final` 属性引用的对象是不可变的。
|
||||
总而言之,应该尽可能地减少程序元素的可访问性(在合理范围内)。 在仔细设计一个最小化的公共 API 之后,你应该防止任何散乱的类,接口或成员成为 API 的一部分。 除了作为常量的公共静态 `final` 字段之外,公共类不应该有公共字段。 确保 `public static final` 字段引用的对象是不可变的。
|
||||
|
||||
@@ -39,7 +39,7 @@ class Point {
|
||||
|
||||
但是,**如果一个类是包级私有的,或者是一个私有的内部类,那么暴露它的数据属性就没有什么本质上的错误——假设它们提供足够描述该类提供的抽象。** 在类定义和使用它的客户端代码中,这种方法比访问方法产生更少的视觉混乱。 虽然客户端代码绑定到类的内部表示,但是这些代码仅限于包含该类的包。 如果类的内部表示是可取的,可以在不触碰包外的任何代码的情况下进行更改。 在私有内部类的情况下,更改作用范围进一步限制在封闭类中。
|
||||
|
||||
Java 平台类库中的几个类违反了公共类不应直接暴露属性的建议。 着名的例子包括 `java.awt` 包中的 `Point` 和 `Dimension` 类。 这些类别应该被视为警示性的示例,而不是模仿的例子。 如条目 67 所述,暴露 `Dimension` 的内部结构的决定是一个严重的性能问题,这个问题在今天仍然存在。
|
||||
Java 平台类库中的几个类违反了公共类不应直接暴露属性的建议。 著名的例子包括 `java.awt` 包中的 `Point` 和 `Dimension` 类。 这些类别应该被视为警示性的示例,而不是模仿的例子。 如条目 67 所述,时至今日,暴露 `Dimension` 的内部结构的决定仍然导致着严重的性能问题。
|
||||
|
||||
虽然公共类直接暴露属性并不是一个好主意,但是如果属性是不可变的,那么危害就不那么大了。当一个属性是只读的时候,除了更改类的 API 外,你不能改变类的内部表示形式,也不能采取一些辅助的行为,但是可以加强不变性。例如,下面的例子中保证每个实例表示一个有效的时间:
|
||||
|
||||
@@ -66,4 +66,4 @@ public final class Time {
|
||||
}
|
||||
```
|
||||
|
||||
总之,公共类不应该暴露可变属性。 公共累暴露不可变属性的危害虽然仍然存在问题,但其危害较小。 然而,有时需要包级私有或私有内部类来暴露属性,无论此类是否是可变的。
|
||||
总之,公共类不应该暴露可变属性。 公共类暴露不可变属性的危害虽然仍然存在问题,但其危害较小。 然而,有时需要包级私有或私有内部类来暴露属性,无论此类是否是可变的。
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
# 17. 最小化可变性
|
||||
|
||||
不可变类简单来说是它的实例不能被修改的类。 包含在每个实例中的所有信息在对象的生命周期中是固定的,因此不会观察到任何变化。 Java 平台类库包含许多不可变的类,包括 `String` 类,基本类型包装类以及 `BigInteger` 类和 `BigDecimal` 类。 有很多很好的理由:不可变类比可变类更容易设计,实现和使用。 他们不太容易出错,更安全。
|
||||
不可变类简单来说是其实例不能被修改的类。 包含在每个实例中的所有信息在对象的生命周期中是固定的,因此不会观察到任何变化。 Java 平台类库包含许多不可变的类,包括 `String` 类、基本类型包装类以及 `BigInteger` 类和 `BigDecimal` 类。 有很多很好的理由:不可变类比可变类更易于设计,实现和使用。 他们不容易出错,并且更安全。
|
||||
|
||||
要使一个类不可变,请遵循以下五条规则:
|
||||
要使一个类成为不可变类,请遵循以下五条规则:
|
||||
|
||||
1. **不要提供修改对象状态的方法(也称为 mutators)。**
|
||||
2. **确保这个类不能被继承。** 这可以防止粗心的或恶意的子类,假设对象的状态已经改变,从而破坏类的不可变行为。 防止子类化通常是通过 `final` 修饰类,但是我们稍后将讨论另一种方法。
|
||||
3. **把所有属性设置为 final。** 通过系统强制执行,清楚地表达了你的意图。 另外,如果一个新创建的实例的引用从一个线程传递到另一个线程而没有同步,就必须保证正确的行为,正如内存模型[JLS,17.5; Goetz06,16] 所述。
|
||||
4. **把所有的属性设置为 private。** 这可以防止客户端获得对属性引用的可变对象的访问权限并直接修改这些对象。 虽然技术上允许不可变类具有包含基本类型数值的公共 `final` 属性或对不可变对象的引用,但不建议这样做,因为它不允许在以后的版本中更改内部表示(详见第 15 和 16 条)。
|
||||
5. **确保对任何可变组件的互斥访问。** 如果你的类有任何引用可变对象的属性,请确保该类的客户端无法获得对这些对象的引用。 切勿将这样的属性初始化为客户端提供的对象引用,或从访问方法返回属性。 在构造方法,访问方法和 `readObject` 方法(详见第 88 条)中进行防御性拷贝(详见第 50 条)。
|
||||
1. **不要提供修改对象状态的方法(也称为 mutators,设值方法)。**
|
||||
2. **确保这个类不能被继承。** 这可以防止粗心或者恶意的子类假装对象的状态已经改变,从而破坏类的不可变行为。 防止子类化,通常是通过 `final` 修饰类,但是我们稍后将讨论另一种方法。
|
||||
3. **把所有字段设置为 final。** 通过系统强制执行的方式,清楚地表达了你的意图。 另外,如果一个新创建的实例的引用在缺乏同步机制的情况下从一个线程传递到另一个线程,就必须保证正确的行为,正如内存模型[JLS,17.5; Goetz06 16] 所述。
|
||||
4. **把所有的字段设置为 private。** 这可以防止客户端获得对字段引用的可变对象的访问权限,并直接修改这些对象。 虽然技术上允许不可变类具有包含基本类型数值的公有的 `final` 字段或对不可变对象的引用,但不建议这样做,因为这样使得在以后的版本中无法再改变内部的表示状态(详见第 15 和 16 条)。
|
||||
5. **确保对任何可变组件的互斥访问。** 如果你的类有任何引用可变对象的字段,请确保该类的客户端无法获得对这些对象的引用。 切勿将这样的属性初始化为客户端提供的对象引用,或从访问方法返回属性。 在构造方法,访问方法和 `readObject` 方法(详见第 88 条)中进行防御性拷贝(详见第 50 条)。
|
||||
|
||||
|
||||
|
||||
以前条目中的许多示例类都是不可变的。 其中这样的类是条目 11 中的 `PhoneNumber` 类,它具有每个属性的访问方法(accessors),但没有相应的设值方法(mutators)。 这是一个稍微复杂一点的例子:
|
||||
以前条目中的许多示例类都是不可变的。 其中一个例子是条目 11 中的 `PhoneNumber` 类,它具有每个字段的访问方法(accessors),但没有相应的设值方法(mutators)。下面一个稍微复杂一点的例子:
|
||||
|
||||
```java
|
||||
// Immutable complex number class
|
||||
|
||||
public final class Complex {
|
||||
|
||||
private final double re;
|
||||
@@ -69,7 +66,6 @@ public final class Complex {
|
||||
// See page 47 to find out why we use compare instead of ==
|
||||
return Double.compare(c.re, re) == 0
|
||||
&& Double.compare(c.im, im) == 0;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -84,11 +80,11 @@ public final class Complex {
|
||||
}
|
||||
```
|
||||
|
||||
这个类代表了一个复数(包含实部和虚部的数字)。 除了标准的 `Object` 方法之外,它还为实部和虚部提供访问方法,并提供四个基本的算术运算:加法,减法,乘法和除法。 注意算术运算如何创建并返回一个新的 `Complex` 实例,而不是修改这个实例。 这种模式被称为函数式方法,因为方法返回将操作数应用于函数的结果,而不修改它们。 与其对应的过程(procedural)或命令(imperative)的方法相对比,在这种方法中,将一个过程作用在操作数上,导致其状态改变。 请注意,方法名称是介词(如 plus)而不是动词(如 add)。 这强调了方法不会改变对象的值的事实。 `BigInteger` 和 `BigDecimal` 类没有遵守这个命名约定,并导致许多使用错误。
|
||||
这个类代表了一个复数(包含实部和虚部的数字)。 除了标准的 `Object` 方法之外,它还为实部和虚部提供访问方法,并提供四个基本的算术运算:加法,减法,乘法和除法。 注意算术运算如何创建并返回一个新的 `Complex` 实例,而不是修改这个实例。 这种模式被称为函数式方法,因为方法返回将操作数应用于函数的结果,而不修改它们。 与其对应的过程式的(procedural)或命令式的(imperative)的方法相对比,在这种方法中,将一个过程作用在操作数上,导致其状态改变。 请注意,方法名称是介词(如 plus)而不是动词(如 add)。 这强调了方法不会改变对象的值的事实。 `BigInteger` 和 `BigDecimal` 类没有遵守这个命名约定,并导致许多使用错误。
|
||||
|
||||
如果你不熟悉函数式方法,可能会显得不自然,但它具有不变性,具有许多优点。 **不可变对象很简单。** 一个不可变的对象可以完全处于一种状态,也就是被创建时的状态。 如果确保所有的构造方法都建立了类不变量,那么就保证这些不变量在任何时候都保持不变,使用此类的程序员无需再做额外的工作。 另一方面,可变对象可以具有任意复杂的状态空间。 如果文档没有提供由设置(mutator)方法执行的状态转换的精确描述,那么可靠地使用可变类可能是困难的或不可能的。
|
||||
如果你不熟悉函数式方法,可能会觉得它显得不自然,但它具有不变性,具有许多优点。 **不可变对象很简单。** 一个不可变的对象可以完全处于一种状态,也就是被创建时的状态。 如果确保所有的构造方法都建立了类不变量,那么就保证这些不变量在任何时候都保持不变,使用此类的程序员无需再做额外的工作。 另一方面,可变对象可以具有任意复杂的状态空间。 如果文档没有提供由设置(mutator)方法执行的状态转换的精确描述,那么可靠地使用可变类可能是困难的或不可能的。
|
||||
|
||||
**不可变对象本质上是线程安全的; 它们不需要同步。** 被多个线程同时访问它们时并不会被破坏。 这是实现线程安全的最简单方法。 由于没有线程可以观察到另一个线程对不可变对象的影响,所以**不可变对象可以被自由地共享。** 因此,不可变类应鼓励客户端尽可能重用现有的实例。 一个简单的方法是为常用的值提供公共的静态 final 常量。 例如,`Complex` 类可能提供这些常量:
|
||||
**不可变对象本质上是线程安全的;它们不需要同步。** 被多个线程同时访问它们时,不会遭到破坏。 这是实现线程安全的最简单方法。 由于没有线程可以观察到另一个线程对不可变对象的影响,所以**不可变对象可以被自由地共享。** 因此,不可变类应鼓励客户端尽可能重用现有的实例。 一个简单的方法是为常用的值提供公共的静态 final 常量。 例如,`Complex` 类可能提供这些常量:
|
||||
|
||||
```java
|
||||
public static final Complex ZERO = new Complex(0, 0);
|
||||
@@ -101,11 +97,11 @@ public static final Complex I = new Complex(0, 1);
|
||||
|
||||
**不仅可以共享不可变的对象,而且可以共享内部信息。** 例如,`BigInteger` 类在内部使用符号数值表示法。 符号用 `int` 值表示,数值用 `int` 数组表示。 `negate` 方法生成了一个数值相同但符号相反的新 `BigInteger` 实例。 即使它是可变的,也不需要复制数组;新创建的 `BigInteger` 指向与原始相同的内部数组。
|
||||
|
||||
**不可变对象为其他对象提供了很好的构件(building blocks)**,无论是可变的还是不可变的。 如果知道一个复杂组件的内部对象不会发生改变,那么维护复杂对象的不变量就容易多了。这一原则的特例是,不可变对象可以构成 `Map` 对象的键和 `Set` 的元素,一旦不可变对象作为 `Map` 的键或 `Set` 里的元素,即使破坏了 `Map` 和 `Set` 的不可变性,但不用担心它们的值会发生变化。
|
||||
**不可变对象为其他对象提供了很好的构件(building blocks)**,无论是可变的还是不可变的。 如果知道一个复杂组件的内部对象不会发生改变,那么维护复杂对象的不变性就容易多了。不可变对象是优秀的映射键和集合元素是这一原则的重要例子: 一旦不可变对象作为 `Map` 的键或 `Set` 里的元素,你就不需要担心`Map` 或 `Set` 的不变性被这些对象的值的变化所破坏。
|
||||
|
||||
**不可变对象提供了免费的原子失败机制(详见第 76 条)。** 它们的状态永远不会改变,所以不可能出现临时的不一致。
|
||||
**不可变对象无偿地提供了的原子失败机制(详见第 76 条)。** 它们的状态永远不会改变,所以不可能出现临时的不一致。
|
||||
|
||||
**不可变类的主要缺点是对于每个不同的值都需要一个单独的对象。** 创建这些对象可能代价很高,特别是如果是大型的对象下。 例如,假设你有一个百万位的 `BigInteger` ,你想改变它的低位:
|
||||
**不可变类的主要缺点是对于每个不同的值都需要一个单独的对象。** 创建这些对象可能代价很高,特别是是大型的对象下。 例如,假设你有一个百万位的 `BigInteger` ,你想改变它的低位:
|
||||
|
||||
```java
|
||||
BigInteger moby = ...;
|
||||
@@ -145,7 +141,7 @@ public class Complex {
|
||||
```
|
||||
这种方法往往是最好的选择。 这是最灵活的,因为它允许使用多个包级私有实现类。 对于驻留在包之外的客户端,不可变类实际上是 `final` 的,因为不可能继承来自另一个包的类,并且缺少公共或受保护的构造方法。 除了允许多个实现类的灵活性以外,这种方法还可以通过改进静态工厂的对象缓存功能来调整后续版本中类的性能。
|
||||
|
||||
当 `BigInteger` 和 `BigDecimal` 被写入时,不可变类必须是有效的 `final`,因此它们的所有方法都可能被重写。不幸的是,在保持向后兼容性的同时,这一事实无法纠正。如果你编写一个安全性取决于来自不受信任的客户端的 `BigInteger` 或 `BigDecimal` 参数的不变类时,则必须检查该参数是“真实的”`BigInteger` 还是 `BigDecimal`,而不应该是不受信任的子类的实例。如果是后者,则必须在假设可能是可变的情况下保护性拷贝(defensively copy)(详见第 50 条):
|
||||
当 `BigInteger` 和 `BigDecimal` 刚被编写出来的时候,“不可变类必须是 `final`”的说法还没有得到广泛地理解,因此它们的所有方法都可能被重写。不幸的是,为了保持向后兼容性,这一问题无法得以纠正。如果你编写一个安全性取决于来自不受信任的客户端的 `BigInteger` 或 `BigDecimal` 参数的不变类时,则必须检查该参数是否为“真实的”`BigInteger` 或者 `BigDecimal`,而不应该是不受信任的子类的实例。如果是后者,则必须在假设可能是可变的情况下保护性拷贝(defensively copy)(详见第 50 条):
|
||||
|
||||
```java
|
||||
public static BigInteger safeInstance(BigInteger val) {
|
||||
@@ -156,11 +152,11 @@ public static BigInteger safeInstance(BigInteger val) {
|
||||
|
||||
在本条目开头关于不可变类的规则说明,没有方法可以修改对象,并且它的所有属性必须是 `final` 的。事实上,这些规则比实际需要的要强硬一些,其实可以有所放松来提高性能。 事实上,任何方法都不能在对象的状态中产生外部可见的变化。 然而,一些不可变类具有一个或多个非 `final` 属性,在第一次需要时将开销昂贵的计算结果缓存在这些属性中。 如果再次请求相同的值,则返回缓存的值,从而节省了重新计算的成本。 这个技巧的作用恰恰是因为对象是不可变的,这保证了如果重复的话,计算会得到相同的结果。
|
||||
|
||||
例如,`PhoneNumber` 类的 `hashCode` 方法(第 53 页的条目 11)在第一次调用改方法时计算哈希码,并在再次调用时对其进行缓存。 这种延迟初始化(详见第 83 条)的一个例子,String 类也使用到了。
|
||||
例如,`PhoneNumber` 类的 `hashCode` 方法(详见第 11 条)在第一次调用改方法时计算哈希码,并在再次调用时 对其进行缓存。 这种延迟初始化(详见第 83 条)的一个例子,String 类也使用到了。
|
||||
|
||||
关于序列化应该加上一个警告。 如果你选择使您的不可变类实现 `Serializable` 接口,并且它包含一个或多个引用可变对象的属性,则必须提供显式的 `readObject` 或 `readResolve` 方法,或者使用 `ObjectOutputStream.writeUnshared` 和 `ObjectInputStream.readUnshared` 方法,即默认的序列化形式也是可以接受的。 否则攻击者可能会创建一个可变的类的实例。 这个主题会在条目 88 中会详细介绍。
|
||||
|
||||
总而言之,坚决不要为每个属性编写一个 get 方法后再编写一个对应的 set 方法。 **除非有充分的理由使类成为可变类,否则类应该是不可变的。** 不可变类提供了许多优点,唯一的缺点是在某些情况下可能会出现性能问题。 你应该始终使用较小的值对象(如 `PhoneNumber` 和 `Complex`),使其不可变。 (Java 平台类库中有几个类,如 `java.util.Date` 和 `java.awt.Point`,本应该是不可变的,但实际上并不是)。你应该认真考虑创建更大的值对象,例如 `String` 和 `BigInteger` ,设成不可改变的。 只有当你确认有必要实现令人满意的性能(详见第 67 条)时,才应该为不可改变类提供一个公开的可变伙伴类。
|
||||
总而言之,坚决不要为每个属性编写一个 get 方法后再编写一个对应的 set 方法。 **除非有充分的理由使类成为可变类,否则类应该是不可变的。** 不可变类提供了许多优点,唯一的缺点是在某些情况下可能会出现性能问题。 你应该始终使用较小的值对象(如 `PhoneNumber` 和 `Complex`),使其不可变。(Java 平台类库中有几个类,如 `java.util.Date` 和 `java.awt.Point`,本应该是不可变的,但实际上并不是)。你应该认真考虑创建更大的值对象,例如 `String` 和 `BigInteger` ,设成不可改变的。 只有当你确认有必要实现令人满意的性能(详见第 67 条)时,才应该为不可改变类提供一个公开的可变伙伴类。
|
||||
|
||||
对于一些类来说,不变性是不切实际的。**如果一个类不能设计为不可变类,那么也要尽可能地限制它的可变性** 。减少对象可以存在的状态数量,可以更容易地分析对象,以及降低出错的可能性。因此,除非有足够的理由把属性设置为非 `final` 的情况下,否则应该每个属性都设置为 `final` 的。把本条目的建议与条目 15 的建议结合起来,你自然的倾向就是:**除非有充分的理由不这样做,否则应该把每个属性声明为私有 final 的。**
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 18. 组合优于继承
|
||||
|
||||
继承是实现代码重用的有效方式,但并不总是最好的工具。使用不当,会导致脆弱的软件。 在包中使用继承是安全的,其中子类和父类的实现都在同一个程序员的控制之下。对应专门为了继承而设计的,并且有文档说明的类来说(详见第 19 条),使用继承也是安全的。 然而,从普通的具体类跨越包级边界继承,是危险的。 提醒一下,本书使用“继承”一词来表示实现继承(当一个类继承另一个类时)。 在这个项目中讨论的问题不适用于接口继承(当类实现接口或当接口继承另一个接口时)。
|
||||
继承是实现代码重用的有效方式,但并不总是最好的工具。使用不当,会导致脆弱的软件。 在包中使用继承是安全的,其中子类和父类的实现都在同一个程序员的控制之下。对应专门为了继承而设计的,并且有文档说明的类来说(详见第 19 条),使用继承也是安全的。 然而,从普通的具体类跨越包级边界继承,是危险的。 提醒一下,本书使用「继承」一词,其含义是*实现继承*(当一个类扩展另一个类时)。 在本条目中讨论的问题不适用于*接口继承*(当类实现接口,或者当接口继承另一个接口时)。
|
||||
|
||||
**与方法调用不同,继承打破了封装[Snyder86]。** 换句话说,一个子类依赖于其父类的实现细节来保证其正确的功能。 父类的实现可能会从发布版本不断变化,如果是这样,子类可能会被破坏,即使它的代码没有任何改变。 因此,一个子类必须与其超类一起更新而变化,除非父类的作者为了继承的目的而专门设计它,并对应有文档的说明。
|
||||
|
||||
@@ -42,9 +42,9 @@ s.addAll(List.of("Snap", "Crackle", "Pop"));
|
||||
|
||||
我们可以通过消除 `addAll` 方法的重写来“修复”子类。 尽管生成的类可以正常工作,但是它依赖于它的正确方法,因为 `HashSet` 的 `addAll` 方法是在其 `add` 方法之上实现的。 这个“自我使用(self-use)”是一个实现细节,并不保证在 `Java` 平台的所有实现中都可以适用,并且可以随发布版本而变化。 因此,产生的 `InstrumentedHashSet` 类是脆弱的。
|
||||
|
||||
稍微好一点的做法是,重写 `addAll` 方法遍历指定集合,为每个元素调用 `add` 方法一次。 不管 `HashSet` 的 `addAll` 方法是否在其 `add` 方法上实现,都会保证正确的结果,因为 `HashSet` 的 `addAll` 实现将不再被调用。然而,这种技术并不能解决所有的问题。 这相当于重新实现了父类方法,这样的方法可能不能确定到底是否时自用(self-use)的,实现起来也是困难的,耗时的,容易出错的,并且可能会降低性能。 此外,这种方式并不能总是奏效,因为子类无法访问一些私有属性,所以有些方法就无法实现。
|
||||
稍微好一点的做法是,重写 `addAll` 方法遍历指定集合,为每个元素调用 `add` 方法一次。 不管 `HashSet` 的 `addAll` 方法是否在其 `add` 方法上实现,都会保证正确的结果,因为 `HashSet` 的 `addAll` 实现将不再被调用。然而,这种技术并不能解决所有的问题。 这相当于重新实现了父类方法,这样的方法可能不能确定是否是自用(self-use)的,实现起来也是困难的,耗时的,容易出错的,并且可能会降低性能。 此外,这种方式并不能总是奏效,因为子类无法访问一些私有属性,所以有些方法就无法实现。
|
||||
|
||||
导致子类脆弱的一个相关原因是,它们的父类在后续的发布版本中可以添加新的方法。假设一个程序的安全性依赖于这样一个事实:所有被插入到集中的元素都满足一个先决条件。可以通过对集合进行子类化,然后并重写所有添加元素的方法,以确保在添加每个元素之前满足这个先决条件,来确保这一问题。如果在后续的版本中,父类没有新增添加元素的方法,那么这样做没有问题。但是,一旦父类增加了这样的新方法,则很有肯能由于调用了未被重写的新方法,将非法的元素添加到子类的实例中。这不是个纯粹的理论问题。在把 `Hashtable` 和 `Vector` 类加入到 `Collections` 框架中的时候,就修复了几个类似性质的安全漏洞。
|
||||
导致子类脆弱的一个相关原因是,它们的父类在后续的发布版本中可以添加新的方法。假设一个程序的安全性依赖于这样一个事实:所有被插入到集中的元素都满足一个先决条件。可以通过对集合进行子类化,然后并重写所有添加元素的方法,以确保在添加每个元素之前满足这个先决条件,来确保这一问题。如果在后续的版本中,父类没有新增添加元素的方法,那么这样做没有问题。但是,一旦父类增加了这样的新方法,则很有可能由于调用了未被重写的新方法,将非法的元素添加到子类的实例中。这不是个纯粹的理论问题。在把 `Hashtable` 和 `Vector` 类加入到 `Collections` 框架中的时候,就修复了几个类似性质的安全漏洞。
|
||||
|
||||
这两个问题都源于重写方法。 如果仅仅添加新的方法并且不要重写现有的方法,可能会认为继承一个类是安全的。 虽然这种扩展更为安全,但这并非没有风险。 如果父类在后续版本中添加了一个新的方法,并且你不幸给了子类一个具有相同签名和不同返回类型的方法,那么你的子类编译失败[JLS,8.4.8.3]。 如果已经为子类提供了一个与新的父类方法具有相同签名和返回类型的方法,那么你现在正在重写它,因此将遇到前面所述的问题。 此外,你的方法是否会履行新的父类方法的约定,这是值得怀疑的,因为在你编写子类方法时,这个约定还没有写出来。
|
||||
|
||||
@@ -177,11 +177,11 @@ static void walk(Set<Dog> dogs) {
|
||||
}
|
||||
```
|
||||
|
||||
`InstrumentedSet` 类被称为包装类,因为每个 `InstrumentedSet` 实例都包含(“包装”)另一个 Set 实例。 这也被称为装饰器模式[Gamma95],因为 `InstrumentedSet` 类通过添加计数功能来“装饰”一个集合。 有时组合和转发的结合被不精确地地称为委托(delegation)。 从技术上讲,除非包装对象把自身传递给被包装对象,否则不是委托[Lieberman86;Gamma95]。
|
||||
`InstrumentedSet` 类被称为包装类,因为每个 `InstrumentedSet` 实例都包含(“包装”)另一个 Set 实例。 这也被称为装饰器模式[Gamma95],因为 `InstrumentedSet` 类通过添加计数功能来“装饰”一个集合。 有时组合和转发的结合被不精确地地称为委托(delegation)。 从技术上讲,除非包装对象把自身传递给被包装对象,否则不是委托[Lieberman86; Gamma95]。
|
||||
|
||||
包装类的缺点很少。 一个警告是包装类不适合在回调框架(callback frameworks)中使用,其中对象将自我引用传递给其他对象以用于后续调用(「回调」)。 因为一个被包装的对象不知道它外面的包装对象,所以它传递一个指向自身的引用(this),回调时并不记得外面的包装对象。 这被称为 SELF 问题[Lieberman86]。 有些人担心转发方法调用的性能影响,以及包装对象对内存占用。 两者在实践中都没有太大的影响。 编写转发方法有些繁琐,但是只需为每个接口编写一次可重用的转发类,并且提供转发类。 例如,`Guava` 为所有的 `Collection` 接口提供转发类[Guava]。
|
||||
|
||||
只有在子类真的是父类的子类型的情况下,继承才是合适的。 换句话说,只有在两个类之间存在「is-a」关系的情况下,B 类才能继承 A 类。 如果你试图让 B 类继承 A 类时,问自己这个问题:每个 B 都是 A 吗? 如果你不能如实回答这个问题,那么 B 就不应该继承 A。如果答案是否定的,那么 B 通常包含一个 A 的私有实例,并且暴露一个不同的 API:A 不是 B 的重要部分 ,只是其实现细节。
|
||||
只有在子类真的是父类的子类型的情况下,继承才是合适的。 换句话说,只有在两个类之间存在「is-a」关系的情况下,B 类才能继承 A 类。 如果你试图让 B 类继承 A 类时,问自己这个问题:每个 B 都是 A 吗? 如果你不能明确的以“是的”来回答这个问题,那么 B 就不应该继承 A。如果答案是否定的,那么 B 通常包含一个 A 的私有实例,并且暴露一个不同的 API :A 不是 B 的重要部分 ,只是其实现细节。
|
||||
|
||||
在 Java 平台类库中有一些明显的违反这个原则的情况。 例如,`stacks` 实例并不是 `vector` 实例,所以 `Stack` 类不应该继承 `Vector` 类。 同样,一个属性列表不是一个哈希表,所以 `Properties` 不应该继承 `Hashtable` 类。 在这两种情况下,组合方式更可取。
|
||||
|
||||
@@ -189,7 +189,7 @@ static void walk(Set<Dog> dogs) {
|
||||
|
||||
在决定使用继承来代替组合之前,你应该问自己最后一组问题。对于试图继承的类,它的 API 有没有缺陷呢? 如果有,你是否愿意将这些缺陷传播到你的类的 API 中?继承传播父类的 API 中的任何缺陷,而组合可以让你设计一个隐藏这些缺陷的新 API。
|
||||
|
||||
总之,继承是强大的,但它是有问题的,因为它违反封装。 只有在子类和父类之间存在真正的子类型关系时才适用。 即使如此,如果子类与父类不在同一个包中,并且父类不是为继承而设计的,继承可能会导致脆弱性。 为了避免这种脆弱性,使用合成和转发代替继承,特别是如果存在一个合适的接口来实现包装类。 包装类不仅比子类更健壮,而且更强大。
|
||||
总之,继承是强大的,但它是有问题的,因为它违反封装。 只有在子类和父类之间存在真正的子类型关系时才适用。 即使如此,如果子类与父类不在同一个包中,并且父类不是为继承而设计的,继承可能会导致脆弱性。 为了避免这种脆弱性,使用组合和转发代替继承,特别是如果存在一个合适的接口来实现包装类。 包装类不仅比子类更健壮,而且更强大。
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# 19. 要么设计继承并提供文档说明,要么禁用继承
|
||||
|
||||
条目 18 中提醒你注意继承没有设计和文档说明的「外来」类的子类化的危险。 那么为了继承而设计和文档说明一个类是什么意思呢?
|
||||
条目 18 中提醒你注意继承没有设计和文档说明的「外来」类的子类化的危险。 那么对于专门为了继承而设计并且具有良好文档说明的类而言,这又意味着什么呢?
|
||||
|
||||
首先,这个类必须准确地描述重写这个方法带来的影响。 换句话说,该类必须文档说明可重写方法的自用性(self-use)。 对于每个公共或受保护的方法,文档必须指明方法调用哪些重写方法,以何种顺序以及每次调用的结果如何影响后续处理。 (重写方法,这里是指非 `final` 修饰的方法,无论是公开还是保护的。)更一般地说,一个类必须文档说明任何可能调用可重写方法的情况。 例如,后台线程或者静态初始化代码块可能会调用这样的方法。
|
||||
首先,这个类必须准确地描述重写每个方法带来的影响。 换句话说,该类必须文档说明可重写方法的自用性(self-use)。 对于每个 public 或者 protected 的方法,文档必须指明方法调用哪些可重写方法,以何种顺序调用的,以及每次调用的结果又是如何影响后续处理。 (重写方法,这里是指非 `final` 修饰的方法,无论是公开还是保护的。)更一般地说,一个类必须文档说明任何可能调用可重写方法的情况。 例如,后台线程或者静态初始化代码块可能会调用这样的方法。
|
||||
|
||||
调用可重写方法的方法在文档注释结束时包含对这些调用的描述。 这些描述在规范中特定部分,标记为“Implementation Requirements”,由 Javadoc 标签 `@implSpec` 生成。 本节介绍该方法的内部工作原理。 下面是从 `java.util.AbstractCollection` 类的规范中拷贝的例子:
|
||||
调用可重写方法的方法在文档注释结束时包含对这些调用的描述。 这些描述在规范中特定部分,标记为「Implementation Requirements」,由 Javadoc 标签 `@implSpec` 生成。 这段话介绍该方法的内部工作原理。 下面是从 `java.util.AbstractCollection` 类的规范中拷贝的例子:
|
||||
|
||||
```java
|
||||
public boolean remove(Object o)
|
||||
@@ -14,17 +14,17 @@ Removes a single instance of the specified element from this collection, if it i
|
||||
Implementation Requirements: This implementation iterates over the collection looking for the specified element. If it finds the element, it removes the element from the collection using the iterator’s remove method. Note that this implementation throws an UnsupportedOperationException if the iterator returned by this collection’s iterator method does not implement the remove method and this collection contains the specified object.
|
||||
```
|
||||
|
||||
从该集合中删除指定元素的单个实例(如果存在,optional 实例操作)。 更正式地说,如果这个集合包含一个或多个这样的元素,删除使得 `Objects.equals(o, e)` 的一个元素 e。 如果此集合包含指定的元素(或者等同于此集合因调用而发生了更改),则返回 true。
|
||||
从该集合中删除指定元素的单个实例(如果存在,optional 实例操作)。 更广义地说,如果这个集合包含一个或多个这样的元素 e,就删除其中的一个满足 `Objects.equals(o, e)` 的元素 e。 如果此集合包含指定的元素(或者等同于此集合因调用而发生了更改),则返回 true。
|
||||
|
||||
**实现要求:** 这个实现迭代遍历集合查找指定元素。 如果找到元素,则使用迭代器的 `remove` 方法从集合中删除元素。 请注意,如果此集合的 `iterator` 方法返回的迭代器未实现 `remove` 方法,并且此集合包含指定的对象,则此实现将引发 `UnsupportedOperationException` 异常。
|
||||
**实现要求:** 这个实现迭代遍历集合查找指定元素。 如果找到元素,则使用迭代器的 `remove` 方法从集合中删除元素。 请注意,如果此集合的 `iterator` 方法返回的迭代器未实现 `remove` 方法,并且此集合包含指定的对象,则该实现将引发 `UnsupportedOperationException` 异常。
|
||||
|
||||
这个文档毫无疑问地说明,重写 `iterator` 方法会影响 `remove` 方法的行为。 它还描述了 `iterator` 方法返回的 `Iterator` 行为将如何影响 `remove` 方法的行为。 与条目 18 中的情况相反,在这种情况下,程序员继承 HashSet 并不能说明重写 `add` 方法是否会影响 `addAll` 方法的行为。
|
||||
这个文档清楚地说明,重写 `iterator` 方法将会影响 `remove` 方法的行为。 它还描述了 `iterator` 方法返回的 `Iterator` 行为将如何影响 `remove` 方法的行为。 与条目 18 中的情况相反,在这种情况下,程序员继承 `HashSet` 并不能说明重写 `add` 方法是否会影响 `addAll` 方法的行为。
|
||||
|
||||
但是,这是否违背了一个良好的 API 文档应该描述给定的方法是什么,而不是它是如何做的呢? 是的,它确实!这是继承违反封装这一事实的不幸后果。要文档说明一个类以便可以安全地进行子类化,必须描述清楚那些没有详细说明的实现细节。
|
||||
关于程序文档有句格言:好的 API 文档应该描述一个给定的方法做了什么工作,而不是描述它是如何做到的。那么,上面这种做法是否违背了这句格言呢?是的,它确实违背了!这正是继承破坏了封装性所带来的不幸后果。所以,为了设计一个类的文档,以便它能够被安全地子类化,你必须描述清楚那些有可能未定义的实现细节。
|
||||
|
||||
`@implSpec` 标签是在 Java 8 中添加的,并且在 Java 9 中被大量使用。这个标签应该默认启用,但是从 Java 9 开始,除非通过命令行开关`-tag "apiNote:a:API Note:"`,否则 Javadoc 实用工具仍然会忽略它。
|
||||
`@implSpec` 标签是在 Java 8 中添加的,并且在 Java 9 中被大量使用。这个标签应该默认启用,但是从 Java 9 开始,除非通过命令行开关`-tag "apiNote:a:API Note:"`,否则 Javadoc 工具仍然会忽略它。
|
||||
|
||||
设计继承涉及的不仅仅是文档说明自用的模式。 为了让程序员能够写出有效的子类而不会带来不适当的痛苦,一个类可能以明智选择的受保护方法的形式提供内部工作,或者在罕见的情况下,提供受保护的属性。 例如,考虑 `java.util.AbstractList` 中的 `removeRange` 方法:
|
||||
为了继承而进行的设计不仅仅涉及自用模式的文档设计。为了使程序员能够编写出更加有效的子类,而无须承受不必要的痛苦,**类必须以精心挑选的 protected 方法的形式,提供适当的钩子(hook),以便进入其内部工作中**。或者在罕见的情况下,提供受保护的属性。 例如,考虑 `java.util.AbstractList` 中的 `removeRange` 方法:
|
||||
|
||||
```java
|
||||
protected void removeRange(int fromIndex, int toIndex)
|
||||
@@ -44,11 +44,11 @@ Parameters:
|
||||
|
||||
这个方法是通过列表及其子类的 `clear` 操作来调用的。重写这个方法利用列表内部实现的优势,可以大大提高列表和子类的 `clear` 操作性能。
|
||||
|
||||
实现要求:这个实现获取一个列表迭代器,它位于 `fromIndex` 之前,并重复调用 `ListIterator.remove` 和 `ListIterator.next` 方法,直到整个范围被删除。 注意:如果 `ListIterator.remove` 需要线性时间,则此实现需要平方级时间。
|
||||
**实现要求:** 这个实现获取一个列表迭代器,它位于 `fromIndex` 之前,并重复调用 `ListIterator.remove` 和 `ListIterator.next` 方法,直到整个范围被删除。 **注意:如果 `ListIterator.remove` 需要线性时间,则此实现需要平方级时间。**
|
||||
|
||||
>参数:
|
||||
> fromIndex 要移除的第一个元素的索引
|
||||
> toIndex 要移除的最后一个元素之后的索引
|
||||
参数:<br>
|
||||
fromIndex 要移除的第一个元素的索引<br>
|
||||
toIndex 要移除的最后一个元素之后的索引
|
||||
|
||||
这个方法对 `List` 实现的最终用户来说是没有意义的。 它仅仅是为了使子类很容易提供一个快速 `clear` 方法。 在没有 `removeRange` 方法的情况下,当在子列表上调用 `clear` 方法,子类将不得不使用平方级的时间,否则,或从头重写整个 `subList` 机制——这不是一件容易的事情!
|
||||
|
||||
@@ -111,16 +111,16 @@ public final class Sub extends Super {
|
||||
|
||||
但是普通的具体类呢? 传统上,它们既不是 `final` 的,也不是为了子类化而设计和文档说明的,但是这种情况是危险的。每次修改这样的类,则继承此类的子类将被破坏。 这不仅仅是一个理论问题。 在修改非 `final` 的具体类的内部之后,接收与子类相关的错误报告并不少见,这些类没有为继承而设计和文档说明。
|
||||
|
||||
**解决这个问题的最好办法是,在没有想要安全地子类化的设计和文档说明的类中禁止子类化。 有两种方法禁止子类化。** 两者中较容易的是声明类为 `final`。 另一种方法是使所有的构造方法都是私有的或包级私有的,并且添加公共静态工厂来代替构造方法。 这个方案在内部提供了使用子类的灵活性,在条目 17 中讨论过。两种方法都是可以接受的。
|
||||
**解决这个问题的最佳方法是禁止对在设计上和文档说明中都不支持安全子类化的类进行子类化。** 这有两种方法禁止子类化。 两者中较容易的是声明类为 `final`。 另一种方法是使所有的构造方法都是私有的或包级私有的,并且添加公共静态工厂来代替构造方法。 这个方案在内部提供了使用子类的灵活性,在条目 17 中讨论过。两种方法都是可以接受的。
|
||||
|
||||
这个建议可能有些争议,因为许多程序员已经习惯于继承普通的具体类来增加功能,例如通知和同步等功能,或限制原有类的功能。 如果一个类实现了捕获其本质的一些接口,比如 `Set`,`List` 或 `Map`,那么不应该为了禁止子类化而感到愧疚。 在条目 18 中描述的包装类模式为增强功能提供了继承的优越选择。
|
||||
|
||||
如果一个具体的类没有实现一个标准的接口,那么你可能会通过禁止继承来给一些程序员带来不便。 如果你觉得你必须允许从这样的类继承,一个合理的方法是确保类从不调用任何可重写的方法,并文档说明这个事实。 换句话说,完全消除类的自用(self-use)的可重写的方法。 这样做,你将创建一个合理安全的子类。 重写一个方法不会影响任何其他方法的行为。
|
||||
如果一个具体的类没有实现一个标准的接口,那么你禁止继承可能给一些程序员带来不便。 如果你觉得你必须允许从这样的类继承,一个合理的方法是确保类从不调用任何可重写的方法,并文档说明这个事实。 换句话说,完全消除类的自用(self-use)的可重写的方法。 这样做,你将创建一个合理安全的子类。 重写一个方法不会影响任何其他方法的行为。
|
||||
|
||||
你可以机械地消除类的自我使用的重写方法,而不会改变其行为。 将每个可重写的方法的主体移动到一个私有的“帮助器方法”,并让每个可重写的方法调用其私有的帮助器方法。 然后用直接调用可重写方法的专用帮助器方法来替换每个自用的可重写方法。
|
||||
|
||||
|
||||
总之,设计一个继承类是一件很辛苦的事情。 你必须文档说明所有的自用模式,一旦你文档说明了它们,必须承诺为他们的整个生命周期。 如果你不这样做,子类可能会依赖于父类的实现细节,并且如果父类的实现发生改变,子类可能会损坏。 为了允许其他人编写高效的子类,可能还需要导出一个或多个受保护的方法。 除非你知道有一个真正的子类需要,否则你可能最好是通过声明你的类为 `final` 禁止继承,或者确保没有可访问的构造方法。
|
||||
简而言之,专门为了继承而设计类是一件很辛苦的工作。你必须建立文档说明其所有的自用模式,并且一旦建立了文档,在这个类的整个生命周期中都必须遵守。如果没有做到,子类就会依赖父类的实现细节,如果父类的实现发生了变化,它就有可能遭到破坏。为了允许其他人能编写出高效的子类,你还必须暴露一个或者多个受保护的方法。除非意识到真的需要子类,否则最好通过将类声明为 `final`,或者确保没有可访问的构造器来禁止类被继承。
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ static List<Integer> intArrayAsList(int[] a) {
|
||||
|
||||
@Override
|
||||
public Integer set(int i, Integer val) {
|
||||
int oldVal = a[I];
|
||||
int oldVal = a[i];
|
||||
a[i] = val; // Auto-unboxing
|
||||
return oldVal; // Autoboxing
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 23. 类层次结构优于标签类
|
||||
|
||||
有时你可能会碰到一个类,它的实例有两个或更多的风格,并且包含一个标签属性(tag field),表示实例的风格。 例如,考虑这个类,它可以表示一个圆形或矩形:
|
||||
有时你可能会碰到一个类,它的实例有两个或更多的风格,并且包含一个标签字段(tag field),表示实例的风格。 例如,考虑这个类,它可以表示一个圆形或矩形:
|
||||
|
||||
```java
|
||||
// Tagged class - vastly inferior to a class hierarchy!
|
||||
@@ -43,13 +43,13 @@ class Figure {
|
||||
}
|
||||
```
|
||||
|
||||
这样的标签类具有许多缺点。 他们杂乱无章的样板代码,包括枚举声明,标签属性和 `switch` 语句。 可读性更差,因为多个实现在一个类中混杂在一起。 内存使用增加,因为实例负担属于其他风格不相关的领域。 属性不能成为 final,除非构造方法初始化不相关的属性,导致更多的样板代码。 构造方法在编译器的帮助下,必须设置标签属性并初始化正确的数据属性:如果初始化错误的属性,程序将在运行时失败。 除非可以修改其源文件,否则不能将其添加到标记的类中。 如果你添加一个风格,你必须记得给每个 `switch` 语句添加一个 `case`,否则这个类将在运行时失败。 最后,一个实例的数据类型没有提供任何关于风格的线索。 总之,**标签类是冗长的,容易出错的,而且效率低下。**
|
||||
这样的标签类具有许多缺点。 它们充斥着杂乱无章的样板代码,包括枚举声明,标签字段和 `switch` 语句。 可读性更差,因为多个实现在一个类中混杂在一起。 内存使用增加,因为实例负担属于其他风格不相关的领域。 字段不能成为 final,除非构造方法初始化不相关的字段,导致更多的样板代码。 构造方法在编译器的帮助下,必须设置标签字段并初始化正确的数据字段:如果初始化错误的字段,程序将在运行时失败。 除非可以修改其源文件,否则不能将其添加到标记的类中。 如果你添加一个风格,你必须记得给每个 `switch` 语句添加一个 `case`,否则这个类将在运行时失败。 最后,一个实例的数据类型没有提供任何关于风格的线索。 总之,**标签类是冗长的,容易出错的,而且效率低下。**
|
||||
|
||||
幸运的是,像 Java 这样的面向对象的语言为定义一个能够表示多种风格对象的单一数据类型提供了更好的选择:子类型化(subtyping)。标签类仅仅是一个类层次的简单的模仿。
|
||||
|
||||
要将标签类转换为类层次,首先定义一个包含抽象方法的抽象类,该标签类的行为取决于标签值。 在 `Figure` 类中,只有一个这样的方法,就是 `area` 方法。 这个抽象类是类层次的根。 如果有任何方法的行为不依赖于标签的值,把它们放在这个类中。 同样,如果有所有的方法使用的数据属性,把它们放在这个类。`Figure` 类中不存在这种与类型无关的方法或属性。
|
||||
要将标签类转换为类层次,首先定义一个包含抽象方法的抽象类,该标签类的行为取决于标签值。 在 `Figure` 类中,只有一个这样的方法,就是 `area` 方法。 这个抽象类是类层次的根。 如果有任何方法的行为不依赖于标签的值,把它们放在这个类中。 同样,如果有所有的方法使用的数据字段,把它们放在这个类。`Figure` 类中不存在这种与类型无关的方法或字段。
|
||||
|
||||
接下来,为原始标签类的每种类型定义一个根类的具体子类。 在我们的例子中,有两个类型:圆形和矩形。 在每个子类中包含特定于改类型的数据字段。 在我们的例子中,半径属性是属于圆的,长度和宽度属性都是矩形的。 还要在每个子类中包含根类中每个抽象方法的适当实现。 这里是对应于 `Figure` 类的类层次:
|
||||
接下来,为原始标签类的每种类型定义一个根类的具体子类。 在我们的例子中,有两个类型:圆形和矩形。 在每个子类中包含特定于改类型的数据字段。 在我们的例子中,半径字段是属于圆的,长度和宽度字段都是矩形的。 还要在每个子类中包含根类中每个抽象方法的适当实现。 这里是对应于 `Figure` 类的类层次:
|
||||
|
||||
```java
|
||||
// Class hierarchy replacement for a tagged class
|
||||
@@ -76,7 +76,7 @@ class Rectangle extends Figure {
|
||||
}
|
||||
```
|
||||
|
||||
这个类层次纠正了之前提到的标签类的每个缺点。 代码简单明了,不包含原文中的样板文件。 每种类型的实现都是由自己的类来分配的,而这些类都没有被无关的数据属性所占用。 所有的属性是 `final` 的。 编译器确保每个类的构造方法初始化其数据属性,并且每个类都有一个针对在根类中声明的每个抽象方法的实现。 这消除了由于缺少 `switch-case` 语句而导致的运行时失败的可能性。 多个程序员可以独立地继承类层次,并且可以相互操作,而无需访问根类的源代码。 每种类型都有一个独立的数据类型与之相关联,允许程序员指出变量的类型,并将变量和输入参数限制为特定的类型。
|
||||
这个类层次纠正了之前提到的标签类的每个缺点。 代码简单明了,不包含原文中的样板文件。 每种类型的实现都是由自己的类来分配的,而这些类都没有被无关的数据字段所占用。 所有的字段是 `final` 的。 编译器确保每个类的构造方法初始化其数据字段,并且每个类都有一个针对在根类中声明的每个抽象方法的实现。 这消除了由于缺少 `switch-case` 语句而导致的运行时失败的可能性。 多个程序员可以独立地继承类层次,并且可以相互操作,而无需访问根类的源代码。 每种类型都有一个独立的数据类型与之相关联,允许程序员指出变量的类型,并将变量和输入参数限制为特定的类型。
|
||||
|
||||
类层次的另一个优点是可以使它们反映类型之间的自然层次关系,从而提高了灵活性,并提高了编译时类型检查的效率。 假设原始示例中的标签类也允许使用正方形。 类层次可以用来反映一个正方形是一种特殊的矩形(假设它们是不可变的):
|
||||
|
||||
@@ -87,9 +87,9 @@ class Square extends Rectangle {
|
||||
}
|
||||
}
|
||||
```
|
||||
请注意,上述层中的属性是直接访问的,而不是访问方法。 这里是为了简洁起见,如果类层次是公开的(详见第 16 条),这将是一个糟糕的设计。
|
||||
请注意,上述层次结构中的字段是直接访问的,而不是通过访问器方法访问的。 这里是为了简洁起见,如果类层次是公开的(详见第 16 条),这将是一个糟糕的设计。
|
||||
|
||||
总之,标签类很少有适用的情况。 如果你想写一个带有明显标签属性的类,请考虑标签属性是否可以被删除,而类是否被类层次替换。 当遇到一个带有标签属性的现有类时,可以考虑将其重构为一个类层次中。
|
||||
总之,标签类很少有适用的情况。 如果你想写一个带有显式标签字段的类,请考虑标签字段是否可以被删除,并是否能被类层次结构替换。 当遇到一个带有标签字段的现有类时,可以考虑将其重构为一个类层次结构。
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -123,11 +123,11 @@ converted to CAP#1
|
||||
CAP#1 extends Object from capture of ?
|
||||
```
|
||||
|
||||
不可否认的是,这个错误信息留下了一些需要的东西,但是编译器已经完成了它的工作,不管它的元素类型是什么,都不会破坏集合的类型不变性。 你不仅可以将任何元素(除 `null` 以外)放入一个 `Collection<?>` 中,但是不能保证你所得到的对象的类型。 如果这些限制是不可接受的,可以使用泛型方法(详见第 30 条)或有限制配符类型(详见第 31 条)。
|
||||
不可否认的是,这个错误信息留下了一些需要的东西,但是编译器已经完成了它的工作,不管它的元素类型是什么,都不会破坏集合的类型不变性。 你不仅不能将任何元素(除 `null` 以外)放入一个 `Collection<?>` 中,并且根本无法猜测你会得到那种类型的对象。 如果这些限制是不可接受的,可以使用泛型方法(详见第 30 条)或有限制的通配符类型(详见第 31 条)。
|
||||
|
||||
对于不应该使用原始类型的规则,有一些小例外。 **你必须在类字面值(class literals)中使用原始类型。** 规范中不允许使用参数化类型(尽管它允许数组类型和基本类型)[JLS,15.8.2]。 换句话说,`List.class`,`String[].class` 和 `int.class` 都是合法的,但 `List<String>.class` 和 `List<?>.class` 不是合法的。
|
||||
对于不应该使用原始类型的规则,有一些小例外。 **你必须在类字面值(class literals)中使用原始类型。** 规范中不允许使用参数化类型(尽管它允许数组类型和基本类型)[JLS,15.8.2]。 换句话说,`List.class`,`String[].class` 和 `int.class` 都是合法的,但 `List<String>.class` 和 `List<?>.class` 都是不合法的。
|
||||
|
||||
规则的第二个例外涉及 `instanceof` 操作符。 因为泛型类型信息在运行时被删除,所以在无限制通配符类型以外的参数化类型上使用 `instanceof` 运算符是非法的。 使用无限制通配符类型代替原始类型不会以任何方式影响 `instanceof` 运算符的行为。 在这种情况下,尖括号和问号就显得多余。 **以下是使用泛型类型的 `instanceof` 运算符的首选方法:**
|
||||
规则的第二个例外与 `instanceof` 操作符有关。 因为泛型类型信息在运行时被擦除,所以在无限制通配符类型以外的参数化类型上使用 `instanceof` 运算符是非法的。 使用无限制通配符类型代替原始类型,不会对 `instanceof` 运算符的行为产生任何影响。 在这种情况下,尖括号(<>)和问号(?)就显得多余。 **以下是使用泛型类型的 `instanceof` 运算符的首选方法:**
|
||||
|
||||
```java
|
||||
// Legitimate use of raw type - instanceof operator
|
||||
@@ -137,9 +137,9 @@ if (o instanceof Set) { // Raw type
|
||||
}
|
||||
```
|
||||
|
||||
请注意,一旦确定 `o` 对象是一个 `Set`,则必须将其转换为通配符 `Set<?>`,而不是原始类型 `Set`。 这是一个强制转换,所以不会导致编译器警告。
|
||||
请注意,一旦确定 `o` 对象是一个 `Set`,则必须将其转换为通配符 `Set<?>`,而不是原始类型 `Set`。 这是一个受检查的(checked)转换,所以不会导致编译器警告。
|
||||
|
||||
总之,使用原始类型可能导致运行时异常,所以不要使用它们。 它们仅用于与泛型引入之前的传统代码的兼容性和互操作性。 作为一个快速回顾,`Set<Object>` 是一个参数化类型,表示一个可以包含任何类型对象的集合,`Set<?>` 是一个通配符类型,表示一个只能包含某些未知类型对象的集合,`Set` 是一个原始类型,它不在泛型类型系统之列。 前两个类型是安全的,最后一个不是。
|
||||
总之,使用原始类型可能导致运行时异常,所以不要使用它们。 原始类型只是为了与引入泛型机制之前的遗留代码进行兼容和互用而提供的。 作为一个快速回顾,`Set<Object>` 是一个参数化类型,表示一个可以包含任何类型对象的集合,`Set<?>` 是一个通配符类型,表示一个只能包含某些未知类型对象的集合,`Set` 是一个原始类型,它不在泛型类型系统之列。 前两个类型是安全的,最后一个不是。
|
||||
|
||||
为了快速参考,下表中总结了本条目(以及本章稍后介绍的一些)中介绍的术语:
|
||||
|
||||
@@ -147,7 +147,7 @@ if (o instanceof Set) { // Raw type
|
||||
|:--:|:--:|:--:|:--:|
|
||||
|Parameterized type|参数化类型|`List<String>`|条目 26|
|
||||
|Actual type parameter |实际类型参数|`String`|条目 26|
|
||||
|Generic type|泛型类型 |`List<E>`|条目 26|
|
||||
|Generic type|泛型类型 |`List<E>`|条目 26 和 条目 29|
|
||||
|Formal type parameter|形式类型参数 |`E`|条目 26|
|
||||
|Unbounded wildcard type|无限制通配符类型|`List<?>`|条目 26|
|
||||
|Raw type|原始类型|`List`|条目 26|
|
||||
|
||||
@@ -26,7 +26,7 @@ Set<Lark> exaltation = new HashSet<>();
|
||||
|
||||
但一些警告更难以消除。 本章充满了这种警告的例子。 当你收到需要进一步思考的警告时,坚持不懈! **尽可能地消除每一个未经检查的警告。** 如果你消除所有的警告,你可以放心,你的代码是类型安全的,这是一件非常好的事情。 这意味着在运行时你将不会得到一个 `ClassCastException` 异常,并且增加了你的程序将按照你的意图行事的信心。
|
||||
|
||||
**如果你不能消除警告,但你可以证明引发警告的代码是类型安全的,那么(并且只能这样)用 `@SuppressWarnings(“unchecked”)` 注解来抑制警告。** 如果你在没有首先证明代码是类型安全的情况下压制警告,那么你给自己一个错误的安全感。 代码可能会在不发出任何警告的情况下进行编译,但是它仍然可以在运行时抛出 `ClassCastException` 异常。 但是,如果你忽略了你认为是安全的未经检查的警告(而不是抑制它们),那么当一个新的警告出现时,你将不会注意到这是一个真正的问题。 新出现的警告就会淹没在所有的错误警告当中。
|
||||
**如果你不能消除警告,但你可以证明引发警告的代码是类型安全的,那么(并且只能这样)用 `@SuppressWarnings("unchecked")` 注解来抑制警告。** 如果你在没有首先证明代码是类型安全的情况下压制警告,那么你给自己一个错误的安全感。 代码可能会在不发出任何警告的情况下进行编译,但是它仍然可以在运行时抛出 `ClassCastException` 异常。 但是,如果你忽略了你认为是安全的未经检查的警告(而不是抑制它们),那么当一个新的警告出现时,你将不会注意到这是一个真正的问题。 新出现的警告就会淹没在所有的错误警告当中。
|
||||
|
||||
`SuppressWarnings` 注解可用于任何声明,从单个局部变量声明到整个类。 始终在尽可能最小的范围内使用 `SuppressWarnings` 注解。 通常这是一个变量声明或一个非常短的方法或构造方法。 切勿在整个类上使用 `SuppressWarnings` 注解。 这样做可能会掩盖重要的警告。
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ String s = stringLists[0].get(0); // (5)
|
||||
|
||||
当你在强制转换为数组类型时,得到泛型数组创建错误,或是未经检查的强制转换警告时,最佳解决方案通常是使用集合类型 `List<E>` 而不是数组类型 `E[]`。 这样可能会牺牲一些简洁性或性能,但作为交换,你会获得更好的类型安全性和互操作性。
|
||||
|
||||
例如,假设你想用带有集合的构造方法来编写一个 `Chooser` 类,并且有个方法返回随机选择的集合的一个元素。 根据传递给构造方法的集合,可以使用选择器作为游戏模具,魔术 8 球或数据源进行蒙特卡罗模拟。 这是一个没有泛型的简单实现:
|
||||
例如,假设你想用带有集合的构造方法来编写一个 `Chooser` 类,并且有个方法返回随机选择的集合的一个元素。 根据传递给构造方法的集合,可以使用该类的实例对象作为游戏骰子,魔力 8 号球或蒙特卡罗模拟的数据源。 这是一个没有泛型的简单实现:
|
||||
|
||||
```java
|
||||
// Chooser - a class badly in need of generics!
|
||||
@@ -63,7 +63,7 @@ public class Chooser {
|
||||
}
|
||||
```
|
||||
|
||||
要使用这个类,每次调用方法时,都必须将 `Object` 的 `choose` 方法的返回值转换为所需的类型,如果类型错误,则转换在运行时失败。 我们先根据条目 29 的建议,试图修改 `Chooser` 类,使其成为泛型的。
|
||||
要使用这个类,每次调用方法时,都必须将 `choose` 方法的返回值从 `Object` 转换为所需的类型,如果类型错误,则转换在运行时失败。 我们先根据条目 29 的建议,试图修改 `Chooser` 类,使其成为泛型的。
|
||||
|
||||
```java
|
||||
// A first cut at making Chooser generic - won't compile
|
||||
@@ -130,4 +130,5 @@ public class Chooser<T> {
|
||||
|
||||
这个版本有些冗长,也许运行比较慢,但是值得一提的是,在运行时不会得到 `ClassCastException` 异常。
|
||||
|
||||
总之,数组和泛型具有非常不同的类型规则。 数组是协变和具体化的; 泛型是不变的,类型擦除的。 因此,数组提供运行时类型的安全性,但不提供编译时类型的安全性,反之亦然。 一般来说,数组和泛型不能很好地混合工作。 如果你发现把它们混合在一起,得到编译时错误或者警告,你的第一个冲动应该是用列表来替换数组。
|
||||
总之,数组和泛型具有非常不同的类型规则。 数组是协变和具体化的; 泛型是不变的,类型擦除的。 因此,数组提供运行时类型的安全性,但不提供编译时类型的安全性,对于泛型则是相反。 一般来说,数组和泛型不能很好地混合工作。 如果你发现把它们混合在一起,得到编译时错误或者警告,你的第一个冲动应该是用列表来替换数组。
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ public E pop() {
|
||||
}
|
||||
```
|
||||
|
||||
两种消除泛型数组创建的技术都有其追随者。 第一个更可读:数组被声明为 `E[]` 类型,清楚地表明它只包含 `E` 实例。 它也更简洁:在一个典型的泛型类中,你从代码中的许多点读取数组; 第一种技术只需要一次转换(创建数组的地方),而第二种技术每次读取数组元素都需要单独转换。 因此,第一种技术是优选的并且在实践中更常用。 但是,它确实会造成堆污染(heap pollution)(详见第 32 条):数组的运行时类型与编译时类型不匹配(除非 `E` 碰巧是 `Object`)。 这使得一些程序员非常不安,他们选择了第二种技术,尽管在这种情况下堆的污染是无害的。
|
||||
两种消除泛型数组创建的技术都有其追随者。 第一个更可读:数组被声明为 `E[]` 类型,清楚地表明它只包含 `E` 实例。 它也更简洁:在一个典型的泛型类中,你从代码中的许多点读取数组;第一种技术只需要一次转换(创建数组的地方),而第二种技术每次读取数组元素都需要单独转换。 因此,第一种技术是优选的并且在实践中更常用。 但是,它确实会造成堆污染(heap pollution)(详见第 32 条):数组的运行时类型与编译时类型不匹配(除非 `E` 碰巧是 `Object`)。 这使得一些程序员非常不安,他们选择了第二种技术,尽管在这种情况下堆的污染是无害的。
|
||||
|
||||
下面的程序演示了泛型 `Stack` 类的使用。 该程序以相反的顺序打印其命令行参数,并将其转换为大写。 对从堆栈弹出的元素调用 `String` 的 `toUpperCase` 方法不需要显式强制转换,而自动生成的强制转换将保证成功:
|
||||
|
||||
@@ -155,7 +155,7 @@ public static void main(String[] args) {
|
||||
|
||||
上面的例子似乎与条目 28 相矛盾,条目 28 中鼓励使用列表优先于数组。 在泛型类型中使用列表并不总是可行或可取的。 Java 本身生来并不支持列表,所以一些泛型类型(如 `ArrayList`)必须在数组上实现。 其他的泛型类型,比如 `HashMap`,是为了提高性能而实现的。
|
||||
|
||||
绝大多数泛型类型就像我们的 `Stack` 示例一样,它们的类型参数没有限制:可以创建一个 `Stack<Object>,Stack<int[]>`,`Stack<List<String>>` 或者其他任何对象的 `Stack` 引用类型。 请注意,不能创建基本类型的堆栈:尝试创建 `Stack<int>` 或 `Stack<double>` 将导致编译时错误。 这是 Java 泛型类型系统的一个基本限制。 可以使用基本类型的包装类(详见第 61 条)来解决这个限制。
|
||||
绝大多数泛型类型就像我们的 `Stack` 示例一样,它们的类型参数没有限制:可以创建一个 `Stack<Object>, Stack<int[]>`,`Stack<List<String>>` 或者其他任何对象的 `Stack` 引用类型。 请注意,不能创建基本类型的堆栈:尝试创建 `Stack<int>` 或 `Stack<double>` 将导致编译时错误。 这是 Java 泛型类型系统的一个基本限制。 可以使用基本类型的包装类(详见第 61 条)来解决这个限制。
|
||||
|
||||
有一些泛型类型限制了它们类型参数的允许值。 例如,考虑 `java.util.concurrent.DelayQueue`,它的声明如下所示:
|
||||
|
||||
@@ -163,7 +163,7 @@ public static void main(String[] args) {
|
||||
class DelayQueue<E extends Delayed> implements BlockingQueue<E>
|
||||
```
|
||||
|
||||
类型参数列表(`<E extends Delayed>`)要求实际的类型参数 `E` 是 `java.util.concurrent.Delayed` 的子类型。 这使得 DelayQueue 实现及其客户端可以利用 `DelayQueue` 元素上的 `Delayed` 方法,而不需要显式的转换或 `ClassCastException` 异常的风险。 类型参数 `E` 被称为限定类型参数。 请注意,子类型关系被定义为每个类型都是自己的子类型[JLS,4.10],因此创建 `DelayQueue<Delayed>` 是合法的。
|
||||
类型参数列表(`<E extends Delayed>`)要求实际的类型参数 `E` 是 `java.util.concurrent.Delayed` 的子类型。 这使得 `DelayQueue` 实现及其客户端可以利用 `DelayQueue` 元素上的 `Delayed` 方法,而不需要显式的转换或 `ClassCastException` 异常的风险。 类型参数 `E` 被称为限定类型参数。 请注意,子类型关系被定义为每个类型都是自己的子类型[JLS,4.10],因此创建 `DelayQueue<Delayed>` 是合法的。
|
||||
|
||||
总之,泛型类型比需要在客户端代码中强制转换的类型更安全,更易于使用。 当你设计新的类型时,确保它们可以在没有这种强制转换的情况下使用。 这通常意味着使类型泛型化。 如果你有任何现有的类型,应该是泛型的但实际上却不是,那么把它们泛型化。 这使这些类型的新用户的使用更容易,而不会破坏现有的客户端(条目 26)。
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
|
||||
}
|
||||
```
|
||||
|
||||
至少对于简单的泛型方法来说,就是这样。 此方法编译时不会生成任何警告,并提供类型安全性和易用性。 这是一个简单的程序来运行该方法。 这个程序不包含强制转换和编译时没有错误或警告:至少对于简单的泛型方法来说,就是这样。 此方法编译时不会生成任何警告,并提供类型安全性和易用性。 这是一个简单的程序来运行该方法。 这个程序不包含强制转换和编译时没有错误或警告:
|
||||
至少对于简单的泛型方法来说,就是这样。 此方法编译时不会生成任何警告,并提供类型安全性和易用性。 这是一个简单的程序来运行该方法。 这个程序不包含强制转换,并且编译时没有错误或警告:
|
||||
|
||||
```java
|
||||
// Simple program to exercise generic method
|
||||
@@ -54,7 +54,7 @@ public static void main(String[] args) {
|
||||
|
||||
`union` 方法的一个限制是所有三个集合(输入参数和返回值)的类型必须完全相同。 通过使用限定通配符类型( bounded wildcard types)(详见第 31 条),可以使该方法更加灵活。
|
||||
|
||||
有时,需要创建一个不可改变但适用于许多不同类型的对象。 因为泛型是通过擦除来实现的(详见第 28 条),所以可以使用单个对象进行所有必需的类型参数化,但是需要编写一个静态工厂方法来重复地为每个请求的类型参数化分配对象。 这种称为泛型单例工厂(generic singleton factory)的模式用于方法对象(function objects)(详见第 42 条),比如 `Collections.reverseOrder` 方法,偶尔也用于 `Collections.emptySet` 之类的集合。
|
||||
有时,需要创建一个不可改变但适用于许多不同类型的对象。 因为泛型是通过擦除来实现的(详见第 28 条),所以可以使用单个对象进行所有必需的类型参数化,但是需要编写一个静态工厂方法来重复地为每个请求的类型参数化分配对象。 这种被称为泛型单例工厂(generic singleton factory)的模式,用于函数对象(function objects)(详见第 42 条),比如 `Collections.reverseOrder` 方法,偶尔也用于 `Collections.emptySet` 之类的集合。
|
||||
|
||||
假设你想写一个恒等方法分配器( identity function dispenser)。 类库提供了 `Function.identity` 方法,所以没有理由编写你自己的实现(详见第 59 条),但它是有启发性的。 如果每次要求的时候都去创建一个新的恒等方法对象是浪费的,因为它是无状态的。 如果 Java 的泛型被具体化,那么每个类型都需要一个恒等方法,但是由于它们被擦除以后,所以泛型的单例就足够了。 以下是它的实例:
|
||||
|
||||
@@ -114,18 +114,13 @@ public static <E extends Comparable<E>> E max(Collection<E> c);
|
||||
```java
|
||||
// Returns max value in a collection - uses recursive type bound
|
||||
public static <E extends Comparable<E>> E max(Collection<E> c) {
|
||||
|
||||
if (c.isEmpty())
|
||||
throw new IllegalArgumentException("Empty collection");
|
||||
|
||||
E result = null;
|
||||
|
||||
for (E e : c)
|
||||
if (result == null || [e.compareTo(result](http://e.compareTo(result)) > 0)
|
||||
result = Objects.requireNonNull(e);
|
||||
|
||||
return result;
|
||||
|
||||
if (c.isEmpty())
|
||||
throw new IllegalArgumentException("Empty collection");
|
||||
E result = null;
|
||||
for (E e : c)
|
||||
if (result == null || e.compareTo(result) > 0)
|
||||
result = Objects.requireNonNull(e);
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 31. 使用限定通配符来增加 API 的灵活性
|
||||
|
||||
如条目 28 所述,参数化类型是不变的。换句话说,对于任何两个不同类型的 `Type1` 和 `Type`,`List<Type1>` 既不是 `List<Type2>` 子类型也不是其父类型。尽管 `List<String>` 不是 `List<Object>` 的子类型是违反直觉的,但它确实是有道理的。 可以将任何对象放入 `List<Object>` 中,但是只能将字符串放入 `List<String>` 中。 由于 `List<String>` 不能做 `List<Object>` 所能做的所有事情,所以它不是一个子类型(条目 10 中的里氏替代原则)。
|
||||
如条目 28 所述,参数化类型是不变的。换句话说,对于任何两个不同类型的 `Type1` 和 `Type2`,`List<Type1>` 既不是 `List<Type2>` 的子类型也不是其父类型。尽管 `List<String>` 不是 `List<Object>` 的子类型是违反直觉的,但它确实是有道理的。 可以将任何对象放入 `List<Object>` 中,但是只能将字符串放入 `List<String>` 中。 由于 `List<String>` 不能做 `List<Object>` 所能做的所有事情,所以它不是一个子类型(条目 10 中的里氏替代原则)。
|
||||
|
||||
相对于提供的不可变的类型,有时你需要比此更多的灵活性。 考虑条目 29 中的 `Stack` 类。下面是它的公共 API:
|
||||
|
||||
@@ -68,7 +68,7 @@ public void popAll(Collection<E> dst) {
|
||||
}
|
||||
```
|
||||
|
||||
同样,如果目标集合的元素类型与栈的元素类型完全匹配,则干净编译并且工作正常。 但是,这又不完全令人满意。 假设你有一个 `Stac<Number>` 和 `Object` 类型的变量。 如果从栈中弹出一个元素并将其存储在该变量中,它将编译并运行而不会出错。 所以你也不能这样做吗?
|
||||
同样,如果目标集合的元素类型与栈的元素类型完全匹配,则干净编译并且工作正常。 但是,这又不完全令人满意。 假设你有一个 `Stac<Number>` 和 `Object` 类型的变量。 如果从栈中弹出一个元素并将其存储在该变量中,它将编译并运行而不会出错。 你不应该也这样做吗?
|
||||
|
||||
```java
|
||||
Stack<Number> numberStack = new Stack<Number>();
|
||||
@@ -87,14 +87,14 @@ public void popAll(Collection<? super E> dst) {
|
||||
dst.add(pop());
|
||||
}
|
||||
```
|
||||
|
||||
通过这个改动,`Stack` 类和客户端代码都可以干净地编译。
|
||||
|
||||
通过这个改动,`Stack` 类和客户端代码都可以干净地编译。
|
||||
|
||||
这个结论很清楚。 **为了获得最大的灵活性,对代表生产者或消费者的输入参数使用通配符类型。** 如果一个输入参数既是一个生产者又是一个消费者,那么通配符类型对你没有好处:你需要一个精确的类型匹配,这就是没有任何通配符的情况。
|
||||
|
||||
这里有一个助记符来帮助你记住使用哪种通配符类型: **PECS 代表: producer-extends,consumer-super。**
|
||||
|
||||
换句话说,如果一个参数化类型代表一个 `T` 生产者,使用 `<? extends T>`;如果它代表 `T` 消费者,则使用 `<? super T>`。 在我们的 `Stack` 示例中,`pushAll` 方法的 `src` 参数生成栈使用的 `E` 实例,因此 `src` 的合适类型为 `Iterable<? extends E>`;`popAll` 方法的 `dst` 参数消费 `Stack` 中的 `E` 实例,因此 `dst` 的合适类型是 C`ollection <? super E>`。 PECS 助记符抓住了使用通配符类型的基本原则。 Naftalin 和 Wadler 称之为获取和放置原则(Get and Put Principle)[Naftalin07,2.4]。
|
||||
换句话说,如果一个参数化类型代表一个 `T` 生产者,使用 `<? extends T>`;如果它代表 `T` 消费者,则使用 `<? super T>`。 在我们的 `Stack` 示例中,`pushAll` 方法的 `src` 参数生成栈使用的 `E` 实例,因此 `src` 的合适类型为 `Iterable<? extends E>`;`popAll` 方法的 `dst` 参数消费 `Stack` 中的 `E` 实例,因此 `dst` 的合适类型是 `Collection <? super E>`。 PECS 助记符抓住了使用通配符类型的基本原则。 Naftalin 和 Wadler 称之为获取和放置原则(Get and Put Principle)[Naftalin07,2.4]。
|
||||
|
||||
记住这个助记符之后,让我们来看看本章中以前项目的一些方法和构造方法声明。 条目 28 中的 `Chooser` 类构造方法有这样的声明:
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
# 34. 使用枚举类型替代整型常量
|
||||
|
||||
枚举是其合法值由一组固定的常量组成的一种类型,例如一年中的季节,太阳系中的行星或一副扑克牌中的套装。 在将枚举类型添加到该语言之前,表示枚举类型的常见模式是声明一组名为 `int` 的常量,每个类型的成员都有一个常量:
|
||||
枚举是其合法值由一组固定的常量组成的一种类型,例如一年中的季节,太阳系中的行星或一副扑克牌中的花色。 在将枚举类型添加到该语言之前,表示枚举类型的常见模式是声明一组名为 `int` 的常量,每个类型的成员都有一个常量:
|
||||
|
||||
```java
|
||||
// The int enum pattern - severely deficient!
|
||||
@@ -113,9 +113,9 @@ Weight on URANUS is 167.398264
|
||||
Weight on NEPTUNE is 210.208751
|
||||
```
|
||||
|
||||
直到 2006 年,在 Java 中加入枚举两年之后,冥王星不再是一颗行星。 这引发了一个问题:「当你从枚举类型中移除一个元素时会发生什么?」答案是,任何不引用移除元素的客户端程序都将继续正常工作。 所以,举例来说,我们的 `WeightTable` 程序只需要打印一行少一行的表格。 什么是客户端程序引用删除的元素(在这种情况下,`Planet.Pluto`)? 如果重新编译客户端程序,编译将会失败并在引用前一个星球的行处提供有用的错误消息; 如果无法重新编译客户端,它将在运行时从此行中引发有用的异常。 这是你所希望的最好的行为,远远好于你用 `int` 枚举模式得到的结果。
|
||||
直到 2006 年,在 Java 中加入枚举两年之后,冥王星不再是一颗行星。 这引发了一个问题:「当你从枚举类型中移除一个元素时会发生什么?」答案是,任何不引用移除元素的客户端程序都将继续正常工作。 所以,举例来说,我们的 `WeightTable` 程序将会打印一个少一行的表格。 那么客户端程序引用删除的元素(在本例中是`Planet.Pluto`)会如何? 如果重新编译客户端程序,编译将会失败并在引用删除的星球的行处提供有用的错误消息; 如果无法重新编译客户端,它将在运行时从此行中引发有用的异常。 这是你所希望的最好的行为,远远好于你用 `int` 枚举模式得到的结果。
|
||||
|
||||
一些与枚举常量相关的行为只需要在定义枚举的类或包中使用。 这些行为最好以私有或包级私有方式实现。 然后每个常量携带一个隐藏的行为集合,允许包含枚举的类或包在与常量一起呈现时作出适当的反应。 与其他类一样,除非你有一个令人信服的理由将枚举方法暴露给它的客户端,否则将其声明为私有的,如果需要的话将其声明为包级私有(详见第 15 条)。
|
||||
一些与枚举常量相关的行为只需要在定义枚举的类或包中使用。 这些行为最好以私有或包级私有方式实现。 然后每个常量携带一个隐藏的行为集合,这些行为允许包含枚举的类或包在呈现常量时作出适当的反应。 与其他类一样,除非你有一个令人信服的理由将枚举方法暴露给它的客户端,否则将其声明为私有的,如果需要的话将其声明为包级私有(详见第 15 条)。
|
||||
|
||||
如果一个枚举是广泛使用的,它应该是一个顶级类; 如果它的使用与特定的顶级类绑定,它应该是该顶级类的成员类(详见第 24 条)。 例如,`java.math.RoundingMode` 枚举表示小数部分的舍入模式。 `BigDecimal` 类使用了这些舍入模式,但它们提供了一种有用的抽象,它并不与 `BigDecimal` 有根本的联系。 通过将 `RoundingMode` 设置为顶层枚举,类库设计人员鼓励任何需要舍入模式的程序员重用此枚举,从而提高跨 `API` 的一致性。
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ public enum Ensemble {
|
||||
|
||||
虽然这个枚举能正常工作,但对于维护来说则是一场噩梦。如果常量被重新排序,`numberOfMusicians` 方法将会中断。 如果你想添加一个与你已经使用的 `int` 值相关的第二个枚举常量,则没有那么好运了。 例如,为双四重奏(double quartet)添加一个常量可能会很好,它就像八重奏一样,由 8 位演奏家组成,但是没有办法做到这一点。
|
||||
|
||||
此外,如果没有给所有这些 `int` 值添加常量,也不能为某个 `int` 值添加一个常量。例如,假设你想要添加一个常量,表示一个由 12 位演奏家组成的三重四重奏(triple quartet)。对于由 11 个演奏家组成的合奏曲,并没有标准的术语,因此你不得不为未使用的 `int` 值(11)添加一个虚拟常量(dummy constant)。最多看起来就是有些不好看。如果许多 `int` 值是未使用的,则是不切实际的。
|
||||
此外,如果没有给所有中间的 `int` 值添加常量,就不能为这个 `int` 值添加一个常量。例如,假设你想要添加一个常量,表示一个由 12 位演奏家组成的三重四重奏(triple quartet)。对于由 11 个演奏家组成的合奏曲,并没有标准的术语,因此你不得不为未使用的 `int` 值(11)添加一个虚拟常量(dummy constant)。最多看起来就是有些不好看。如果许多 `int` 值是未使用的,则是不切实际的。
|
||||
|
||||
幸运的是,这些问题有一个简单的解决方案。 **永远不要从枚举的序号中得出与它相关的值; 请将其保存在实例属性中:**
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 36. 使用 EnumSet 替代位属性
|
||||
|
||||
如果枚举类型的元素主要用于集合中,一般来说使用 int 枚举模式(详见第 34 条),下面将 2 的不同倍数赋值给每个常量:
|
||||
如果枚举类型的元素主要用于集合中,则传统上使用 int 枚举模式(详见第 34 条),将 2 的不同次幂赋值给每个常量:
|
||||
|
||||
```java
|
||||
// Bit field enumeration constants - OBSOLETE!
|
||||
@@ -15,17 +15,17 @@ public class Text {
|
||||
}
|
||||
```
|
||||
|
||||
这种表示方式允许你使用按位或(or)运算将几个常量合并到一个称为位属性(bit field)的集合中:
|
||||
这种表示方式允许你使用按位或(or)运算将几个常量合并到一个称为位域(bit field)的集合中:
|
||||
|
||||
```java
|
||||
text.applyStyles(STYLE_BOLD | STYLE_ITALIC);
|
||||
```
|
||||
|
||||
位属性表示还允许你使用按位算术有效地执行集合运算,如并集和交集。 但是位属性具有 `int` 枚举常量等的所有缺点。 当打印为数字时,解释位属性比简单的 `int` 枚举常量更难理解。 没有简单的方法遍历所有由位属性表示的元素。 最后,必须预测在编写 API 时需要的最大位数,并相应地为位属性(通常为 `int` 或 `long`)选择一种类型。 一旦你选择了一个类型,你就不能超过它的宽度(32 或 64 位)而不改变 API。
|
||||
位域表示还允许你使用按位算术有效地执行集合运算,如并集和交集。 但是位域具有 `int` 枚举常量等的所有缺点。 当打印为数字时,解释位域比简单的 `int` 枚举常量更难理解。 没有简单的方法遍历所有由位域表示的元素。 最后,必须预测在编写 API 时需要的最大位数,并相应地为位域(通常为 `int` 或 `long`)选择一种类型。 一旦你选择了一个类型,你就不能在不改变 API 的情况下超过它的宽度(32 或 64 位)。
|
||||
|
||||
一些程序员使用枚举优于 `int` 常量,当他们需要传递常量集合时仍然使用位属性。 没有理由这样做,因为存在更好的选择。 `java.util` 包提供了 `EnumSet` 类来有效地表示从单个枚举类型中提取的值集合。 这个类实现了 `Set` 接口,提供了所有其他 `Set` 实现的丰富性,类型安全性和互操作性。 但是在内部,每个 `EnumSet` 都表示为一个位矢量(bit vector)。 如果底层的枚举类型有 64 个或更少的元素,并且大多数情况下,整个 `EnumSet` 用单个 `long` 表示,所以它的性能与位属性的性能相当。 批量操作(如 `removeAll` 和 `retainAll`)是使用按位算术实现的,就像你为位属性手动操作一样。 但是完全避免了手动位混乱的丑陋和错误倾向:`EnumSet` 为你做了很大的努力。
|
||||
当需要传递一组常量时,一些优先使用枚举而不是 `int` 常量的程序员仍然坚持使用位域。 没有理由这样做,因为存在更好的选择。 `java.util` 包提供了 `EnumSet` 类来有效地表示从单个枚举类型中提取的值集合。 这个类实现了 `Set` 接口,提供了所有其他 `Set` 实现的丰富性,类型安全性和互操作性。 但是在内部,每个 `EnumSet` 都表示为一个位向量(bit vector)。 如果底层的枚举类型有 64 个或更少的元素,并且大多数情况下,整个 `EnumSet` 用单个 `long` 表示,所以它的性能与位域的性能相当。 批量操作(如 `removeAll` 和 `retainAll`)是使用按位算术实现的,就像你手动操作位域一样。 但是完全避免了手动进行位操作导致的丑陋和潜在错误:`EnumSet` 为你完成这一困难工作。
|
||||
|
||||
下面是前一个使用枚举和枚举集合替代位属性的示例。 它更短,更清晰,更安全:
|
||||
下面是前一个使用枚举和枚举集合替代位域的示例。 它更短,更清晰,更安全:
|
||||
|
||||
```java
|
||||
// EnumSet - a modern replacement for bit fields
|
||||
@@ -45,7 +45,7 @@ text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
|
||||
|
||||
请注意,`applyStyles` 方法采用`Set<Style>`而不是`EnumSet<Style>`参数。 尽管所有客户端都可能会将 `EnumSet` 传递给该方法,但接受接口类型而不是实现类型通常是很好的做法(详见第 64 条)。 这允许一个不寻常的客户端通过其他 Set 实现的可能性。
|
||||
|
||||
总之,仅仅因为枚举类型将被用于集合中,所以没有理由用位属性来表示它。 `EnumSet` 类将位属性的简洁性和性能与条目 34 中所述的枚举类型的所有优点相结合。`EnumSet` 的一个真正缺点是,它不像 Java 9 那样创建一个不可变的 `EnumSet`,但是在即将发布的版本中可能会得到补救。 同时,你可以用 `Collections.unmodifiableSet` 封装一个 `EnumSet`,但是简洁性和性能会受到影响。
|
||||
总之,**仅仅因为枚举类型将被用于集合中,就没有理由用位域来表示它。** `EnumSet` 类结合了位域的简洁性和性能以及条目 34 中所述的枚举类型的所有优点。截至 Java 9 ,`EnumSet` 的一个真正缺点是不能创建一个不可变的 `EnumSet`,但是在之后的发行版本中这可能会得到纠正。 同时,你可以用 `Collections.unmodifiableSet` 封装一个 `EnumSet`,但是简洁性和性能会受到影响。
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -277,4 +277,5 @@ if (m.isAnnotationPresent(ExceptionTest.class)
|
||||
|
||||
这个项目中的测试框架只是一个演示,但它清楚地表明了注解相对于命名模式的优越性,而且它仅仅描绘了你可以用它们做什么的外观。 如果编写的工具要求程序员将信息添加到源代码中,请定义适当的注解类型。 **当可以使用注解代替时,没有理由使用命名模式。**
|
||||
|
||||
这就是说,除了特定的开发者(toolsmith)之外,大多数程序员都不需要定义注解类型。 **但所有程序员都应该使用 Java 提供的预定义注解类型**(详见第 27 和 40 条)。 另外,请考虑使用 IDE 或静态分析工具提供的注解。 这些注解可以提高这些工具提供的诊断信息的质量。 但请注意,这些注解尚未标准化,因此如果切换工具或标准出现,可能额外需要做一些工作。
|
||||
这就是说,除了特定的开发者(toolsmith)之外,大多数程序员都不需要定义注解类型。 **但所有程序员都应该使用 Java 提供的预定义注解类型**(详见第 27 和 40 条)。 另外,请考虑使用 IDE 或静态分析工具提供的注解。 这些注解可以提高这些工具提供的诊断信息的质量。 但请注意,这些注解尚未标准化,因此如果切换工具或标准出现,可能额外需要做一些工作。
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ public class Bigram {
|
||||
|
||||
主程序重复添加二十六个双字母组合到集合中,每个双字母组合由两个相同的小写字母组成。 然后它会打印集合的大小。 你可能希望程序打印 26,因为集合不能包含重复项。 如果你尝试运行程序,你会发现它打印的不是 26,而是 260。它有什么问题?
|
||||
|
||||
显然,`Bigram` 类的作者打算重写 `equals` 方法(详见第 10 条),甚至记得重写 `hashCode`(详见第 11 条)。 不幸的是,我们倒霉的程序员没有重写 `equals`,而是重载它(详见第 52 条)。 要重写 `Object.equals`,必须定义一个 `equals` 方法,其参数的类型为 `Object`,但 `Bigram` 的 `equals` 方法的参数不是 `Object` 类型的,因此 `Bigram` 继承 `Object` 的 `equals` 方法,这个 `equals` 方法测试对象的引用是否是同一个,就像 `==` 运算符一样。 每个祖母组合的 10 个副本中的每一个都与其他 9 个副本不同,所以它们被 `Object.equals` 视为不相等,这就解释了程序打印 260 的原因。
|
||||
显然,`Bigram` 类的作者打算重写 `equals` 方法(详见第 10 条),甚至记得重写 `hashCode`(详见第 11 条)。 不幸的是,我们倒霉的程序员没有重写 `equals`,而是重载它(详见第 52 条)。 要重写 `Object.equals`,必须定义一个 `equals` 方法,其参数的类型为 `Object`,但 `Bigram` 的 `equals` 方法的参数不是 `Object` 类型的,因此 `Bigram` 继承 `Object` 的 `equals` 方法,这个 `equals` 方法测试对象的引用是否是同一个,就像 `==` 运算符一样。 每个字母组合的 10 个副本中的每一个都与其他 9 个副本不同,所以它们被 `Object.equals` 视为不相等,这就解释了程序打印 260 的原因。
|
||||
|
||||
幸运的是,编译器可以帮助你找到这个错误,但只有当你通过告诉它你打算重写 `Object.equals` 来帮助你。 要做到这一点,用 `@Override` 注解 `Bigram.equals` 方法,如下所示:
|
||||
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
# 41. 使用标记接口定义类型
|
||||
|
||||
标记接口(marker interface),不包含方法声明,只是指定(或「标记」)一个类实现了具有某些属性的接口。 例如,考虑 `Serializable` 接口(第 12 章)。通过实现这个接口,一个类表明它的实例可以写入 `ObjectOutputStream`(或「序列化」)。
|
||||
标记接口(marker interface),是不包含方法声明的接口,只是指定(或「标记」)一个类实现了具有某些属性的接口。 例如,考虑 `Serializable` 接口(第 12 章)。通过实现这个接口,一个类表明它的实例可以写入 `ObjectOutputStream`(或被「序列化」)。
|
||||
|
||||
你可能会听说过标记注解(详见第 39 条)标记一个接口是废弃过时的。 这个断言是不正确的。 标记接口与标记注解相比具有两个优点。
|
||||
你可能会听说过标记注解(详见第 39 条)使得标记接口过时了。 这个断言是不正确的。 标记接口与标记注解相比具有两个优点。 首先,也是最重要的一点,**标记接口定义了一个由标记类实例实现的类型;标记注解则没有定义这样的类型。** 标记接口类型的存在允许在编译时捕获错误,如果使用标记注解,则直到运行时才能捕获错误。
|
||||
|
||||
首先,**标记接口定义了一个由标记类实例实现的类型;标记注解则不会。** 标记接口类型的存在允许在编译时捕获错误,如果使用标记注解,则直到运行时才能捕获错误。
|
||||
Java 的序列化机制(第 6 章)使用 `Serializable` 标记接口来指示某个类型是可序列化的。 对传递给它的对象进行序列化的 `ObjectOutputStream.writeObject` 方法要求其参数可序列化。 如果此方法的参数是 `Serializable` 类型,则在编译时会检测到序列化不适当对象的尝试(通过类型检查)。 编译时错误检测是标记接口的意图,但不幸的是,`ObjectOutputStream.writeObject` API 没有利用 `Serializable` 接口:它的参数被声明为 `Object` 类型,所以尝试序列化一个不可序列化的对象直到运行时才会失败。
|
||||
|
||||
Java 的序列化机制(第 6 章)使用 `Serializable` 标记接口来指示某个类型是可序列化的。 对传递给它的对象进行序列化的 `ObjectOutputStream.writeObject` 方法要求其参数可序列化。 如果此方法的参数是 `Serializable` 类型,则在编译时会检测到序列化不适当对象的尝试(通过类型检查)。 编译时错误检测是标记接口的意图,但不幸的是,`ObjectOutputStream.write` API 没有利用 `Serializable` 接口:它的参数被声明为 `Object` 类型,所以尝试序列化一个不可序列化的对象直到运行时才会失败。
|
||||
|
||||
**标记接口对于标记注解的另一个优点是可以更精确地定位目标。** 如果使用目标 `ElementType.TYPE` 声明注解类型,它可以应用于任何类或接口。 假设有一个标记仅适用于特定接口的实现。 如果将其定义为标记接口,则可以扩展它适用的唯一接口,保证所有标记类型也是适用的唯一接口的子类型。
|
||||
**标记接口对于标记注解的另一个优点是可以更精确地定位目标。** 如果使用目标 `ElementType.TYPE` 声明注解类型,它就可以被应用于任何类或接口。 假设有一个标记仅适用于特定接口的实现。 如果将其定义为标记接口,则可以扩展它适用的唯一接口,保证所有标记类型也是适用的唯一接口的子类型。
|
||||
|
||||
可以说,`Set` 接口就是这样一个受限的标记接口。 它仅适用于 `Collection` 子类型,但不会添加超出 `Collection` 定义的方法。 它通常不被认为是标记接口,因为它改进了几个 `Collection` 方法的契约,包括 `add`,`equals` 和 `hashCode`。 但很容易想象一个标记接口,它仅适用于某些特定接口的子类型,并且不会改进任何接口方法的契约。 这样的标记接口可以描述整个对象的一些约束条件(invariant),或者说明实例有资格被某个其他类的方法处理(就像 `Serializable` 接口指示实例有资格被 `ObjectOutputStream` 处理的方式)。
|
||||
|
||||
标记注解优于标记接口的主要优点是它们是较大的注解工具的一部分。因此,标记注解允许在基于注解的框架中保持一致性。
|
||||
**标记注解优于标记接口的主要优点是它们是更大的注解工具的一部分。**因此,标记注解允许在基于注解的框架中保持一致性。
|
||||
|
||||
所以什么时候应该使用标记注解,什么时候应该使用标记接口?显然,如果标记适用于除类或接口以外的任何程序元素,则必须使用注解,因为只能使用类和接口来实现或扩展接口。如果标记仅适用于类和接口,那么问自己问题:“可能我想编写一个或多个只接受具有此标记的对象的方法呢?”如果是这样,则应该优先使用标记接口而不是注解。这将使你可以将接口用作所讨论方法的参数类型,这将带来编译时类型检查的好处。如果你能说服自己,永远不会想写一个只接受带有标记的对象的方法,那么最好使用标记注解。另外,如果标记是大量使用注解的框架的一部分,则标记注解是明确的选择。
|
||||
所以什么时候应该使用标记注解,什么时候应该使用标记接口?显然,如果标记是应用于除类或接口以外的任何程序元素,则必须使用注解,因为只能使用类和接口来实现或扩展接口。如果标记仅适用于类和接口,那么问自己问题:「可能我想编写一个或多个只接受具有此标记的对象的方法呢?」如果是这样,则应该优先使用标记接口而不是注解。这将使你可以将接口用作所讨论方法的参数类型,这将带来编译时类型检查的好处。如果你能说服自己,永远不会想写一个只接受带有标记的对象的方法,那么最好使用标记注解。另外,如果标记是大量使用注解的框架的一部分,则标记注解是明确的选择。
|
||||
|
||||
总之,标记接口和标记注释都有其用处。 如果你想定义一个没有任何关联的新方法的类型,一个标记接口是一种可行的方法。 如果要标记除类和接口以外的程序元素,或者将标记符合到已经大量使用注解类型的框架中,那么标记注解是正确的选择。 **如果发现自己正在编写目标为 `ElementType.TYPE` 的标记注解类型,请花点时间确定它是否应该是注释类型,是不是标记接口是否更合适。**
|
||||
总之,标记接口和标记注释都有其用处。 如果你想定义一个没有任何关联的新方法的类型,一个标记接口是一种可行的方法。 如果要标记除类和接口以外的程序元素,或者将标记符合到已经大量使用注解类型的框架中,那么标记注解是正确的选择。 **如果发现自己正在编写目标为 `ElementType.TYPE` 的标记注解类型,那么请花时间弄清楚究竟应该用注解类型,还是标记接口更合适。**
|
||||
|
||||
从某种意义来说,本条目与条目 22 的的意思正好相反,条目 22 的意思是:「如果你不想定义一个类型,不要使用接口」。本条目的意思是:如果想定义一个类型,一定要使用接口。
|
||||
从某种意义来说,本条目与条目 22 的的意思正好相反,条目 22 的意思是:「如果你不想定义一个类型,不要使用接口」。本条目的意思是:「如果想定义一个类型,一定要使用接口。」
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
# 45. 明智审慎地使用 Stream
|
||||
|
||||
在 Java 8 中添加了 Stream API,以简化顺序或并行执行批量操作的任务。 该 API 提供了两个关键的抽象:流 (Stream),表示有限或无限的数据元素序列,以及流管道 (stream pipeline),表示对这些元素的多级计算。 Stream 中的元素可以来自任何地方。 常见的源包括集合,数组,文件,正则表达式模式匹配器,伪随机数生成器和其他流。 流中的数据元素可以是对象引用或基本类型。 支持三种基本类型:int,long 和 double。
|
||||
在 Java 8 中添加了 Stream API,以简化串行或并行执行批量操作的任务。 该 API 提供了两个关键的抽象:流 (Stream),表示有限或无限的数据元素序列,以及流管道 (stream pipeline),表示对这些元素的多级计算。 Stream 中的元素可以来自任何地方。 常见的源包括集合、数组、文件、正则表达式模式匹配器、伪随机数生成器和其他流。 流中的数据元素可以是对象引用或基本类型。 支持三种基本类型:int,long 和 double。
|
||||
|
||||
流管道由源流(source stream)的零或多个中间操作和一个终结操作组成。每个中间操作都以某种方式转换流,例如将每个元素映射到该元素的函数或过滤掉所有不满足某些条件的元素。中间操作都将一个流转换为另一个流,其元素类型可能与输入流相同或不同。终结操作对流执行最后一次中间操作产生的最终计算,例如将其元素存储到集合中、返回某个元素或打印其所有元素。
|
||||
Stream pipeline 由 Source stream(源流) 的零或多个中间操作(intermediate operations)和一个终结操作( terminal operation)组成。每个中间操作都以某种方式转换流,例如将每个元素映射到该元素的函数,或过滤掉所有不满足某些条件的元素。中间操作都将一个流转换为另一个流,其元素类型可能与输入流相同或不同。终结操作对流执行最后一次中间操作产生的最终计算,例如将其元素存储到集合中、返回某个元素或打印其所有元素。
|
||||
|
||||
管道延迟(lazily)计算求值:计算直到终结操作被调用后才开始,而为了完成终结操作而不需要的数据元素永远不会被计算出来。 这种延迟计算求值的方式使得可以使用无限流。 请注意,没有终结操作的流管道是静默无操作的,所以不要忘记包含一个。
|
||||
Stream pipeline 通常是惰性(lazily)计算求值:直到终结操作被调用后才开始计算,而为了完成终结操作而不需要的数据元素永远不会被计算出来。 这种惰性计算求值的方式,使得无限流成为可能。 请注意,没有终结操作的 Stream pipine 是一个静默无操作的指令,所以不要忘记包含一个终止操作。
|
||||
|
||||
Stream API 流式的(fluent)::它设计允许所有组成管道的调用被链接到一个表达式中。事实上,多个管道可以链接在一起形成一个表达式。
|
||||
Stream API 流式的(fluent):它设计允许所有组成 pipeline 的调用被链接到一个表达式中。事实上,多个管道可以链接在一起形成一个表达式。
|
||||
|
||||
默认情况下,流管道按顺序(sequentially)运行。 使管道并行执行就像在管道中的任何流上调用并行方法一样简单,但很少这样做(详见第 48 条)。
|
||||
默认情况下,流管道会按顺序(sequentially)运行。 要使管道并行执行,只需要在管道中的任何流上调用 `parallel()`方法一样简单,但是通常不建议这么做(详见第 48 条)。
|
||||
|
||||
Stream API 具有足够的通用性,实际上任何计算都可以使用 Stream 执行,但仅仅因为可以,并不意味着应该这样做。如果使用得当,流可以使程序更短更清晰;如果使用不当,它们会使程序难以阅读和维护。对于何时使用流没有硬性的规则,但是有一些启发。
|
||||
Stream API 具有足够的通用性,实际上任何计算都可以使用 Stream 执行,但是「可以」,并不意味着应该这样做。如果使用得当,流可以使程序更短更清晰;如果使用不当,它们会使程序难以阅读和维护。对于何时使用流没有硬性的规则,但是有一些启发。
|
||||
|
||||
考虑以下程序,该程序从字典文件中读取单词并打印其大小符合用户指定的最小值的所有变位词(anagram)组。如果两个单词由长度相通,不同顺序的相同字母组成,则它们是变位词。程序从用户指定的字典文件中读取每个单词并将单词放入 map 对象中。map 对象的键是按照字母排序的单词,因此「staple」的键是「aelpst」,「petals」的键也是「aelpst」:这两个单词就是同位词,所有的同位词共享相同的依字母顺序排列的形式(或称之为 alphagram)。map 对象的值是包含共享字母顺序形式的所有单词的列表。 处理完字典文件后,每个列表都是一个完整的同位词组。然后程序遍历 map 对象的 `values()` 的视图并打印每个大小符合阈值的列表:
|
||||
|
||||
@@ -25,8 +25,8 @@ public class Anagrams {
|
||||
try (Scanner s = new Scanner(dictionary)) {
|
||||
while (s.hasNext()) {
|
||||
String word = s.next();
|
||||
groups.computeIfAbsent(alphabetize(word),
|
||||
(unused) -> new TreeSet<>()).add(word);
|
||||
groups.computeIfAbsent(alphabetize(word),
|
||||
(unused) -> new TreeSet<>()).add(word);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,24 +45,24 @@ public class Anagrams {
|
||||
|
||||
这个程序中的一个步骤值得注意。将每个单词插入到 map 中(以粗体显示)中使用了 `computeIfAbsent` 方法,该方法是在 Java 8 中添加的。这个方法在 map 中查找一个键:如果键存在,该方法只返回与其关联的值。如果没有,该方法通过将给定的函数对象应用于键来计算值,将该值与键关联,并返回计算值。`computeIfAbsent` 方法简化了将多个值与每个键关联的 map 的实现。
|
||||
|
||||
现在考虑以下程序,它解决了同样的问题,但大量过度使用了流。 请注意,整个程序(打开字典文件的代码除外)包含在单个表达式中。 在单独的表达式中打开字典文件的唯一原因是允许使用 `try-with-resources` 语句,该语句确保关闭字典文件:
|
||||
现在考虑以下程序,它也能解决同样的问题,但大量过度使用了流。 请注意,整个程序(打开字典文件的代码除外)包含在单个表达式中。 在单独的表达式中打开字典文件的唯一原因是允许使用 `try-with-resources` 语句,该语句确保关闭字典文件:
|
||||
|
||||
```java
|
||||
// Overuse of streams - don't do this!
|
||||
public class Anagrams {
|
||||
public static void main(String[] args) throws IOException {
|
||||
Path dictionary = Paths.get(args[0]);
|
||||
int minGroupSize = Integer.parseInt(args[1]);
|
||||
try (Stream<String> words = Files.lines(dictionary)) {
|
||||
words.collect(
|
||||
groupingBy(word -> word.chars().sorted()
|
||||
.collect(StringBuilder::new,
|
||||
(sb, c) -> sb.append((char) c),
|
||||
StringBuilder::append).toString()))
|
||||
.values().stream()
|
||||
.filter(group -> group.size() >= minGroupSize)
|
||||
.map(group -> group.size() + ": " + group)
|
||||
.forEach(System.out::println);
|
||||
public static void main(String[] args) throws IOException {
|
||||
Path dictionary = Paths.get(args[0]);
|
||||
int minGroupSize = Integer.parseInt(args[1]);
|
||||
try (Stream<String> words = Files.lines(dictionary)) {
|
||||
words.collect(
|
||||
groupingBy(word -> word.chars().sorted()
|
||||
.collect(StringBuilder::new,
|
||||
(sb, c) -> sb.append((char) c),
|
||||
StringBuilder::append).toString()))
|
||||
.values().stream()
|
||||
.filter(group -> group.size() >= minGroupSize)
|
||||
.map(group -> group.size() + ": " + group)
|
||||
.forEach(System.out::println);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,18 +76,18 @@ public class Anagrams {
|
||||
// Tasteful use of streams enhances clarity and conciseness
|
||||
public class Anagrams {
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
Path dictionary = Paths.get(args[0]);
|
||||
int minGroupSize = Integer.parseInt(args[1]);
|
||||
public static void main(String[] args) throws IOException {
|
||||
Path dictionary = Paths.get(args[0]);
|
||||
int minGroupSize = Integer.parseInt(args[1]);
|
||||
|
||||
try (Stream<String> words = Files.lines(dictionary)) {
|
||||
words.collect(groupingBy(word -> alphabetize(word)))
|
||||
.values().stream()
|
||||
.filter(group -> group.size() >= minGroupSize)
|
||||
.forEach(g -> System.out.println(g.size() + ": " + g));
|
||||
}
|
||||
}
|
||||
// alphabetize method is the same as in original version
|
||||
try (Stream<String> words = Files.lines(dictionary)) {
|
||||
words.collect(groupingBy(word -> alphabetize(word)))
|
||||
.values().stream()
|
||||
.filter(group -> group.size() >= minGroupSize)
|
||||
.forEach(g -> System.out.println(g.size() + ": " + g));
|
||||
}
|
||||
}
|
||||
// alphabetize method is the same as in original version
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -64,4 +64,5 @@ private static void sort(long a[], int offset, int length) {
|
||||
|
||||
不要从本条目中推断出对参数的任意限制都是一件好事。 相反,你应该设计一些方法,使其尽可能通用。 假设方法可以对它接受的所有参数值做一些合理的操作,那么对参数的限制越少越好。 但是,通常情况下,某些限制是正在实现的抽象所固有的。
|
||||
|
||||
总而言之,每次编写方法或构造方法时,都应该考虑对其参数存在哪些限制。 应该记在这些限制,并在方法体的开头使用显式检查来强制执行这些限制。 养成这样做的习惯很重要。 在第一次有效性检查失败时,它所需要的少量工作将会得到对应的回报。
|
||||
总而言之,每次编写方法或构造方法时,都应该考虑对其参数存在哪些限制。 应该记在这些限制,并在方法体的开头使用显式检查来强制执行这些限制。 养成这样做的习惯很重要。 在第一次有效性检查失败时,它所需要的少量工作将会得到对应的回报。
|
||||
|
||||
|
||||
@@ -107,4 +107,5 @@ public Date end() {
|
||||
|
||||
包含方法或构造方法的类,这些方法或构造方法的调用指示控制权的转移,这些类无法防御恶意客户端。 只有当一个类和它的客户之间存在相互信任,或者当对类的不变量造成损害时,除了客户之外,任何人都不会受到损害。 后一种情况的一个例子是包装类模式(详见第 18 条)。 根据包装类的性质,客户端可以通过在包装后直接访问对象来破坏类的不变性,但这通常只会损害客户端。
|
||||
|
||||
总之,如果一个类有从它的客户端获取或返回的可变组件,那么这个类必须防御性地拷贝这些组件。如果拷贝的成本太高,并且类信任它的客户端不会不适当地修改组件,则可以用文档替换防御性拷贝,该文档概述了客户端不得修改受影响组件的责任。
|
||||
总之,如果一个类有从它的客户端获取或返回的可变组件,那么这个类必须防御性地拷贝这些组件。如果拷贝的成本太高,并且类信任它的客户端不会不适当地修改组件,则可以用文档替换防御性拷贝,该文档概述了客户端不得修改受影响组件的责任。
|
||||
|
||||
|
||||
@@ -22,4 +22,5 @@
|
||||
public enum TemperatureScale { FAHRENHEIT, CELSIUS }
|
||||
```
|
||||
|
||||
`Thermometer.newInstance(TemperatureScale.CELSIUS)` 不仅比 `Thermometer.newInstance(true)` 更有意义,而且可以在将来的版本中将`KELVIN`添加到 `TemperatureScale` 中,而无需向 `Thermometer` 添加新的静态工厂。 此外,还可以将温度刻度(temperature-scale)依赖关系重构为枚举常量的方法(详见第 34 条)。 例如,每个刻度常量可以有一个采用 double 值并将其转换为 `Celsius` 的方法。
|
||||
`Thermometer.newInstance(TemperatureScale.CELSIUS)` 不仅比 `Thermometer.newInstance(true)` 更有意义,而且可以在将来的版本中将`KELVIN`添加到 `TemperatureScale` 中,而无需向 `Thermometer` 添加新的静态工厂。 此外,还可以将温度刻度(temperature-scale)依赖关系重构为枚举常量的方法(详见第 34 条)。 例如,每个刻度常量可以有一个采用 double 值并将其转换为 `Celsius` 的方法。
|
||||
|
||||
|
||||
@@ -148,4 +148,5 @@ public boolean contentEquals(StringBuffer sb) {
|
||||
|
||||
虽然 Java 类库在很大程度上遵循了这一条目中的建议,但是有一些类违反了它。例如,String 导出两个重载的静态工厂方法`valueOf(char[])`和`valueOf(Object)`,它们在传递相同的对象引用时执行完全不同的操作。对此没有任何正当的理由理由,它应该被视为一种异常现象,有可能造成真正的混乱。
|
||||
|
||||
总而言之,仅仅可以重载方法并不意味着应该这样做。通常,最好避免重载具有相同数量参数的多个签名的方法。在某些情况下,特别是涉及构造方法的情况下,可能无法遵循此建议。在这些情况下,至少应该避免通过添加强制转换将相同的参数集传递给不同的重载。如果这是无法避免的,例如,因为要对现有类进行改造以实现新接口,那么应该确保在传递相同的参数时,所有重载的行为都是相同的。如果做不到这一点,程序员将很难有效地使用重载方法或构造方法,也无法理解为什么它不能工作。
|
||||
总而言之,仅仅可以重载方法并不意味着应该这样做。通常,最好避免重载具有相同数量参数的多个签名的方法。在某些情况下,特别是涉及构造方法的情况下,可能无法遵循此建议。在这些情况下,至少应该避免通过添加强制转换将相同的参数集传递给不同的重载。如果这是无法避免的,例如,因为要对现有类进行改造以实现新接口,那么应该确保在传递相同的参数时,所有重载的行为都是相同的。如果做不到这一点,程序员将很难有效地使用重载方法或构造方法,也无法理解为什么它不能工作。
|
||||
|
||||
|
||||
@@ -64,4 +64,5 @@ public void foo(int a1, int a2, int a3, int... rest) { }
|
||||
|
||||
`EnumSet` 的静态工厂使用这种技术将创建枚举集合的成本降到最低。这是适当的,因为枚举集合为比特属性提供具有性能竞争力的替换(performance-competitive replacement for bit fields)是至关重要的 (详见第 36 条)。
|
||||
|
||||
总之,当需要使用可变数量的参数定义方法时,可变参数非常有用。 在使用可变参数前加上任何必需的参数,并注意使用可变参数的性能后果。
|
||||
总之,当需要使用可变数量的参数定义方法时,可变参数非常有用。 在使用可变参数前加上任何必需的参数,并注意使用可变参数的性能后果。
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ public List<Cheese> getCheeses() {
|
||||
}
|
||||
```
|
||||
|
||||
没有理由对没有奶酪(Cheese)可供购买的情况进行特殊处理。这样需要在客户端做额外的代码处理可能为 null 的返回值,例如:
|
||||
把没有奶酪(Cheese)可买的情况当做一种特例,这是不合常理的。这样需要在客户端中必须有额外的代码来处理 null 的返回值,如:
|
||||
|
||||
```java
|
||||
List<Cheese> cheeses = shop.getCheeses();
|
||||
@@ -24,7 +24,7 @@ if (cheeses != null && cheeses.contains(Cheese.STILTON))
|
||||
System.out.println("Jolly good, just the thing.");
|
||||
```
|
||||
|
||||
在几乎每次使用返回 null 来代替空集合或数组的方法时,都需要使用这种迂回的方式。 它容易出错,因为编写客户端的程序员可能忘记编写特殊情况代码来处理 null 返回。 多年来这种错误可能会被忽视,因为这种方法通常会返回一个或多个对象。 此外,返回 null 代替空容器会使返回容器的方法的实现变得复杂。
|
||||
在几乎每次使用返回 null 来代替空集合或数组的方法时,都需要使用这种迂回的方式。 这样做很容易出错,因为编写客户端的程序员可能忘记编写特殊情况代码来处理 null 返回。 多年来这种错误可能会被忽视,因为这种方法通常会返回一个或多个对象。 此外,返回 null 代替空容器会使返回容器的方法的实现变得复杂。
|
||||
|
||||
有时有人认为,null 返回值比空集合或数组更可取,因为它避免了分配空容器的开销。这个论点有两点是不成立的。首先,除非测量结果表明所讨论的分配是性能问题的真正原因,否则不宜担心此级别的性能(详见第 67 条)。第二,可以在不分配空集合和数组的情况下返回它们。下面是返回可能为空的集合的典型代码。通常,这就是你所需要的:
|
||||
|
||||
@@ -72,4 +72,5 @@ public Cheese[] getCheeses() {
|
||||
return cheesesInStock.toArray(new Cheese[cheesesInStock.size()]);
|
||||
```
|
||||
|
||||
总之,**永远不要返回 null 来代替空数组或集合**。它使你的 API 更难以使用,更容易出错,并且没有性能优势。
|
||||
总之,**永远不要返回 null 来代替空数组或集合**。它使你的 API 更难以使用,更容易出错,并且没有性能优势。
|
||||
|
||||
|
||||
@@ -118,4 +118,5 @@ streamOfOptionals.
|
||||
|
||||
这里留下了一个悬而未决的大问题。在实例中存储 Optional 属性是否合适吗?通常这是一种“不好的味道”:它建议你可能应该有一个包含 Optional 属性的子类。但有时这可能是合理的。考虑条目 2 中的 `NutritionFacts` 类的情况。`NutritionFacts` 实例包含许多不需要的属性。不可能为这些属性的每个可能组合都提供一个子类。此外,属性包含基本类型,这使得很难直接表示这种缺失。对于 `NutritionFacts` 最好的 API 将为每个 Optional 属性从 getter 方法返回一个 Optional,因此将这些 Optional 作为属性存储在对象中是很有意义的。
|
||||
|
||||
总之,如果发现自己编写的方法不能总是返回值,并且认为该方法的用户在每次调用时考虑这种可能性很重要,那么或许应该返回一个 Optional 的方法。但是,应该意识到,返回 Optional 会带来实际的性能后果;对于性能关键的方法,最好返回 null 或抛出异常。最后,除了作为返回值之外,不应该在任何其他地方中使用 Optional。
|
||||
总之,如果发现自己编写的方法不能总是返回值,并且认为该方法的用户在每次调用时考虑这种可能性很重要,那么或许应该返回一个 Optional 的方法。但是,应该意识到,返回 Optional 会带来实际的性能后果;对于性能关键的方法,最好返回 null 或抛出异常。最后,除了作为返回值之外,不应该在任何其他地方中使用 Optional。
|
||||
|
||||
|
||||
@@ -77,4 +77,5 @@ for (int i = 0, n = expensiveComputation(); i < n; i++) {
|
||||
|
||||
关于这个做法需要注意的重要一点是,它有两个循环变量,i 和 n,它们都具有完全相同的作用域。第二个变量 n 用于存储第一个变量的限定值,从而避免了每次迭代中冗余计算的代价。作为一个规则,如果循环测试涉及一个方法调用,并且保证在每次迭代中返回相同的结果,那么应该使用这种用法。
|
||||
|
||||
最小化局部变量作用域的最终技术是**保持方法小而集中**。 如果在同一方法中组合两个行为(activities),则与一个行为相关的局部变量可能会位于执行另一个行为的代码范围内。 为了防止这种情况发生,只需将方法分为两个:每个行为对应一个方法。
|
||||
最小化局部变量作用域的最终技术是**保持方法小而集中**。 如果在同一方法中组合两个行为(activities),则与一个行为相关的局部变量可能会位于执行另一个行为的代码范围内。 为了防止这种情况发生,只需将方法分为两个:每个行为对应一个方法。
|
||||
|
||||
|
||||
@@ -105,4 +105,5 @@ public interface Iterable<E> {
|
||||
|
||||
如果必须从头开始编写自己的 Iterator 实现,那么实现 Iterable 会有点棘手,但是如果你正在编写表示一组元素的类型,那么你应该强烈考虑让它实现 Iterable 接口,甚至可以选择不让它实现 Collection 接口。这允许用户使用 for-each 循环遍历类型,他们会永远感激不尽的。
|
||||
|
||||
总之,for-each 循环在清晰度,灵活性和错误预防方面提供了超越传统 for 循环的令人注目的优势,而且没有性能损失。 尽可能使用 for-each 循环优先于 for 循环。
|
||||
总之,for-each 循环在清晰度,灵活性和错误预防方面提供了超越传统 for 循环的令人注目的优势,而且没有性能损失。 尽可能使用 for-each 循环优先于 for 循环。
|
||||
|
||||
|
||||
@@ -48,10 +48,11 @@ public static void main(String[] args) throws IOException {
|
||||
}
|
||||
```
|
||||
|
||||
库太大,无法学习所有文档 [Java9-api],但是 **每个程序员都应该熟悉 java.lang、java.util 和 java.io 的基础知识及其子包。** 其他库的知识可以根据需要获得。概述库中的工具超出了本项目的范围,这些工具多年来已经发展得非常庞大。
|
||||
这些标准类库太庞大了,以致于不可能学完所有的文档 [Java9-api],但是 **每个程序员都应该熟悉 java.lang、java.util 和 java.io 的基础知识及其子包。** 其他库的知识可以根据需要获得。概述库中的工具超出了本条目的范围,这些工具多年来已经发展得非常庞大。
|
||||
|
||||
有几个图书馆值得一提。collections 框架和 streams 库(详见第 45 到 48 条)应该是每个程序员的基本工具包的一部分,`java.util.concurrent` 中的并发实用程序也应该是其中的一部分。这个包既包含高级的并发工具来简化多线程的编程任务,还包含低级别的并发基本类型,允许专家们自己编写更高级的并发抽象。`java.util.concurrent` 的高级部分,在第 80 条和第 81 条中讨论。
|
||||
其中有几个库值得一提。Collections 框架和 Streams 库(详见第 45 到 48 条)应该是每个程序员的基本工具包的一部分,`java.util.concurrent` 中的并发实用程序也应该是其中的一部分。这个包既包含高级的并发工具来简化多线程的编程任务,还包含低级别的并发基本类型,允许专家们自己编写更高级的并发抽象。`java.util.concurrent` 的高级部分,在第 80 条和第 81 条中讨论。
|
||||
|
||||
有时,类库工具可能无法满足你的需求。你的需求越专门化,发生这种情况的可能性就越大。虽然你的第一个思路应该是使用这些库,但是如果你已经了解了它们在某些领域提供的功能,而这些功能不能满足你的需求,那么可以使用另一种实现。任何有限的库集所提供的功能总是存在漏洞。如果你在 Java 平台库中找不到你需要的东西,你的下一个选择应该是寻找高质量的第三方库,比如谷歌的优秀的开源 Guava 库 [Guava]。如果你无法在任何适当的库中找到所需的功能,你可能别无选择,只能自己实现它。
|
||||
有时,类库工具可能无法满足你的需求。你的需求越特殊,发生这种情况的可能性就越大。虽然你的第一个思路应该是使用这些库,但是如果你已经了解了它们在某些领域提供的功能,而这些功能不能满足你的需求,那么可以使用另一种实现。任何有限的库集所提供的功能总是存在漏洞。如果你在 Java 平台库中找不到你需要的东西,你的下一个选择应该是寻找高质量的第三方库,比如谷歌的优秀的开源 Guava 库 [Guava]。如果你无法在任何适当的库中找到所需的功能,你可能别无选择,只能自己实现它。
|
||||
|
||||
总而言之,不要白费力气重新发明轮子。如果你需要做一些看起来相当常见的事情,那么库中可能已经有一个工具可以做你想做的事情。如果有,使用它;如果你不知道,检查一下。一般来说,库代码可能比你自己编写的代码更好,并且随着时间的推移可能会得到改进。这并不反映你作为一个程序员的能力。规模经济决定了库代码得到的关注要远远超过大多数开发人员所能承担的相同功能。
|
||||
|
||||
总而言之,不要白费力气重新发明轮子。如果你需要做一些看起来相当常见的事情,那么库中可能已经有一个工具可以做你想做的事情。如果有,使用它;如果你不知道,检查一下。一般来说,库代码可能比你自己编写的代码更好,并且随着时间的推移可能会得到改进。这并不反映你作为一个程序员的能力。规模经济决定了库代码得到的关注要远远超过大多数开发人员所能承担的相同功能。
|
||||
@@ -69,4 +69,5 @@ public static void main(String[] args) {
|
||||
}
|
||||
```
|
||||
|
||||
总之,对于任何需要精确答案的计算,不要使用 float 或 double 类型。如果希望系统来处理十进制小数点,并且不介意不使用基本类型带来的不便和成本,请使用 BigDecimal。使用 BigDecimal 的另一个好处是,它可以完全控制舍入,当执行需要舍入的操作时,可以从八种舍入模式中进行选择。如果你使用合法的舍入行为执行业务计算,这将非常方便。如果性能是最重要的,那么你不介意自己处理十进制小数点,而且数值不是太大,可以使用 int 或 long。如果数值不超过 9 位小数,可以使用 int;如果不超过 18 位,可以使用 long。如果数量可能超过 18 位,则使用 BigDecimal。
|
||||
总之,对于任何需要精确答案的计算,不要使用 float 或 double 类型。如果希望系统来处理十进制小数点,并且不介意不使用基本类型带来的不便和成本,请使用 BigDecimal。使用 BigDecimal 的另一个好处是,它可以完全控制舍入,当执行需要舍入的操作时,可以从八种舍入模式中进行选择。如果你使用合法的舍入行为执行业务计算,这将非常方便。如果性能是最重要的,那么你不介意自己处理十进制小数点,而且数值不是太大,可以使用 int 或 long。如果数值不超过 9 位小数,可以使用 int;如果不超过 18 位,可以使用 long。如果数量可能超过 18 位,则使用 BigDecimal。
|
||||
|
||||
|
||||
@@ -75,4 +75,5 @@ public final class ThreadLocal<T> {
|
||||
|
||||
粗略地说,这就是 `java.lang.ThreadLocal` 提供的 API,除了解决基于字符串的问题之外,它比任何基于键的 API 都更快、更优雅。
|
||||
|
||||
总之,当存在或可以编写更好的数据类型时,应避免将字符串用来表示对象。如果使用不当,字符串比其他类型更麻烦、灵活性更差、速度更慢、更容易出错。字符串经常被误用的类型包括基本类型、枚举和聚合类型。
|
||||
总之,当存在或可以编写更好的数据类型时,应避免将字符串用来表示对象。如果使用不当,字符串比其他类型更麻烦、灵活性更差、速度更慢、更容易出错。字符串经常被误用的类型包括基本类型、枚举和聚合类型。
|
||||
|
||||
|
||||
@@ -27,4 +27,5 @@ public String statement() {
|
||||
|
||||
自 Java 6 以来,为了使字符串连接更快,已经做了大量工作,但是这两个方法在性能上的差异仍然很大:如果 numItems 返回 100,lineForItem 返回 80 个字符串,那么第二个方法在我的机器上运行的速度是第一个方法的 6.5 倍。由于第一种方法在项目数量上是平方级的,而第二种方法是线性的,所以随着项目数量的增加,性能差异会变得越来越大。注意,第二个方法预先分配了一个足够大的 StringBuilder 来保存整个结果,从而消除了自动增长的需要。即使使用默认大小的 StringBuilder,它仍然比第一个方法快 5.5 倍。
|
||||
|
||||
道理很简单:**不要使用字符串连接操作符合并多个字符串**,除非性能无关紧要。否则使用 StringBuilder 的 append 方法。或者,使用字符数组,再或者一次只处理一个字符串,而不是组合它们。
|
||||
道理很简单:**不要使用字符串连接操作符合并多个字符串**,除非性能无关紧要。否则使用 StringBuilder 的 append 方法。或者,使用字符数组,再或者一次只处理一个字符串,而不是组合它们。
|
||||
|
||||
|
||||
@@ -34,4 +34,5 @@ Set<Son> sonSet = new HashSet<>();
|
||||
|
||||
没有合适接口类型的最后一种情况是,实现接口但同时提供接口中不存在的额外方法的类,例如,`PriorityQueue` 有一个在 `Queue` 接口上不存在的比较器方法。只有当程序依赖于额外的方法时,才应该使用这样的类来引用它的实例,这种情况应该非常少见。
|
||||
|
||||
这三种情况并不是面面俱到的,而仅仅是为了传达适合通过类引用对象的情况。在实际应用中,给定对象是否具有适当的接口应该是显而易见的。如果是这样,如果使用接口引用对象,程序将更加灵活和流行。**如果没有合适的接口,就使用类层次结构中提供所需功能的最底层的类**
|
||||
这三种情况并不是面面俱到的,而仅仅是为了传达适合通过类引用对象的情况。在实际应用中,给定对象是否具有适当的接口应该是显而易见的。如果是这样,如果使用接口引用对象,程序将更加灵活和流行。**如果没有合适的接口,就使用类层次结构中提供所需功能的最底层的类**
|
||||
|
||||
|
||||
@@ -68,4 +68,5 @@ private static void fatalError(String msg) {
|
||||
|
||||
反射的合法用途(很少)是管理类对运行时可能不存在的其他类、方法或字段的依赖关系。如果你正在编写一个包,并且必须针对其他包的多个版本运行,此时反射将非常有用。该技术是根据支持包所需的最小环境(通常是最老的版本)编译包,并反射性地访问任何较新的类或方法。如果你试图访问的新类或方法在运行时不存在,要使此工作正常进行,则必须采取适当的操作。适当的操作可能包括使用一些替代方法来完成相同的目标,或者使用简化的功能进行操作。
|
||||
|
||||
总之,反射是一种功能强大的工具,对于某些复杂的系统编程任务是必需的,但是它有很多缺点。如果编写的程序必须在编译时处理未知的类,则应该尽可能只使用反射实例化对象,并使用在编译时已知的接口或超类访问对象。
|
||||
总之,反射是一种功能强大的工具,对于某些复杂的系统编程任务是必需的,但是它有很多缺点。如果编写的程序必须在编译时处理未知的类,则应该尽可能只使用反射实例化对象,并使用在编译时已知的接口或超类访问对象。
|
||||
|
||||
|
||||
@@ -10,4 +10,5 @@
|
||||
|
||||
使用本地方法有严重的缺点。由于本地语言不安全(详见第 50 条),使用本地方法的应用程序不再能免受内存毁坏错误的影响。由于本地语言比 Java 更依赖于平台,因此使用本地方法的程序的可移植性较差。它们也更难调试。如果不小心,本地方法可能会降低性能,因为垃圾收集器无法自动跟踪本地内存使用情况(详见第 8 条),而且进出本地代码会产生相关的成本。最后,本地方法需要「粘合代码」,这很难阅读,而且编写起来很乏味。
|
||||
|
||||
总之,在使用本地方法之前要三思。一般很少需要使用它们来提高性能。如果必须使用本地方法来访问底层资源或本地库,请尽可能少地使用本地代码,并对其进行彻底的测试。本地代码中的一个错误就可以破坏整个应用程序。
|
||||
总之,在使用本地方法之前要三思。一般很少需要使用它们来提高性能。如果必须使用本地方法来访问底层资源或本地库,请尽可能少地使用本地代码,并对其进行彻底的测试。本地代码中的一个错误就可以破坏整个应用程序。
|
||||
|
||||
|
||||
@@ -46,4 +46,5 @@
|
||||
|
||||
自本条目首次编写以来的近 20 年里,Java 软件栈的每个组件都变得越来越复杂,从处理器到 vm 再到库,Java 运行的各种硬件都有了极大的增长。所有这些加在一起,使得 Java 程序的性能比 2001 年更难以预测,而对它进行度量的需求也相应增加。
|
||||
|
||||
总而言之,不要努力写快的程序,要努力写好程序;速度自然会提高。但是在设计系统时一定要考虑性能,特别是在设计API、线路层协议和持久数据格式时。当你完成了系统的构建之后,请度量它的性能。如果足够快,就完成了。如果没有,利用分析器找到问题的根源,并对系统的相关部分进行优化。第一步是检查算法的选择:再多的底层优化也不能弥补算法选择的不足。根据需要重复这个过程,在每次更改之后测量性能,直到你满意为止。
|
||||
总而言之,不要努力写快的程序,要努力写好程序;速度自然会提高。但是在设计系统时一定要考虑性能,特别是在设计API、线路层协议和持久数据格式时。当你完成了系统的构建之后,请度量它的性能。如果足够快,就完成了。如果没有,利用分析器找到问题的根源,并对系统的相关部分进行优化。第一步是检查算法的选择:再多的底层优化也不能弥补算法选择的不足。根据需要重复这个过程,在每次更改之后测量性能,直到你满意为止。
|
||||
|
||||
|
||||
@@ -48,4 +48,5 @@ if (car.speed() > 2 * SPEED_LIMIT)
|
||||
|
||||
字段名的语法约定没有类、接口和方法名的语法约定建立得好,也不那么重要,因为设计良好的 API 包含很少的公开字段。类型为 boolean 的字段的名称通常类似于 boolean 访问器方法,省略了初始值「is」,例如 initialized、composite。其他类型的字段通常用名词或名词短语来命名,如 height、digits 和 bodyStyle。局部变量的语法约定类似于字段的语法约定,但要求更少。
|
||||
|
||||
总之,将标准命名约定内在化,并将其作为第二性征来使用。排版习惯是直接的,而且在很大程度上是明确的;语法惯例更加复杂和松散。引用《The Java Language Specification》[JLS, 6.1] 中的话说,「如果长期以来的传统用法要求不遵循这些约定,就不应该盲目地遵循这些约定。」,应使用常识判断。
|
||||
总之,将标准命名约定内在化,并将其作为第二性征来使用。排版习惯是直接的,而且在很大程度上是明确的;语法惯例更加复杂和松散。引用《The Java Language Specification》[JLS, 6.1] 中的话说,「如果长期以来的传统用法要求不遵循这些约定,就不应该盲目地遵循这些约定。」,应使用常识判断。
|
||||
|
||||
|
||||
@@ -55,4 +55,5 @@ obj.action(args);
|
||||
|
||||
如果你怀疑这个简单的调用序列是否符合要求,这个 API 重构可能就是恰当的。这样重构之后的 API 在本质上等同于第 69 条中的「状态测试方法」,并且同样的告诫依然适用:如果对象将在缺少外部同步的情况下被并发访问,或者可被外界改变状态,这种重构就是不恰当的,因为在 actionPermitted 和 action 这两个调用的时间间隔之中,对象的状态有可能会发生变化。如果单独的 actionPermitted 方法必须重复 action 方法的工作,出于性能的考虑,这种 API 重构就不值得去做。
|
||||
|
||||
总而言之,在谨慎使用的前提之下,受检异常可以提升程序的可读性;如果过度使用,将会使 API 使用起来非常痛苦。如果调用者无法恢复失败,就应该抛出未受检异常。如果可以恢复,并且想要迫使调用者处理异常的条件,首选应该返回一个 optional 值。当且仅当万一失败时,这些无法提供足够的信息,才应该抛出受检异常。
|
||||
总而言之,在谨慎使用的前提之下,受检异常可以提升程序的可读性;如果过度使用,将会使 API 使用起来非常痛苦。如果调用者无法恢复失败,就应该抛出未受检异常。如果可以恢复,并且想要迫使调用者处理异常的条件,首选应该返回一个 optional 值。当且仅当万一失败时,这些无法提供足够的信息,才应该抛出受检异常。
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
最经常被重用的异常类型是 `IllegalArgumentException`(详见第 49 条)。当调用者传递的参数值不合适的时候,往往就会抛出这个异常。比如,假设某一个参数代表了“某个动作的重复次数”,如果程序员给这个参数传递了一个负数,就会抛出这个异常。
|
||||
|
||||
另一个经常被重用的异常是 `llegalStateException`。如果因为接收对象的状态而使调用非法,通常就会抛出这个异常。例如,如果在某个对象被正确地初始化之前,调用者就企图使用这个对象,就会抛出这个异常。
|
||||
另一个经常被重用的异常是 `IllegalStateException`。如果因为接收对象的状态而使调用非法,通常就会抛出这个异常。例如,如果在某个对象被正确地初始化之前,调用者就企图使用这个对象,就会抛出这个异常。
|
||||
|
||||
可以这么说,所有错误的方法调用都可以被归结为非法参数或者非法状态,但是,还有一些其他的标准异常也被用于某些特定情况下的非法参数和非法状态。如果调用者在某个不允许 null 值的参数中传递了 null,习惯的做法就是抛出 `NullPointerException` 异常,而不是 `IllegalArgumentException`。同样地,如果调用者在表示序列下标的参数中传递了越界的值,应该抛出的就是 `IndexOutOfBoundsException` 异常,而不是 `IllegalArgumentException`。
|
||||
|
||||
|
||||
@@ -59,4 +59,5 @@ class HigherLevelException extends Exception {
|
||||
|
||||
如果无法阻止来自低层的异常,其次的做法是,让更高层来悄悄地处理这些异常,从而将高层方法的调用者与低层的问题隔离开来。在这种情况下,可以用某种适当的记录机制(如 java.util.logging)将异常记录下来。这样有助于管理员调查问题,同时又将客户端代码和最终用户与问题隔离开来。
|
||||
|
||||
总而言之,如果不能阻止或者处理来自更低层的异常,一般的做法是使用异常转译,只有在低层方法的规范碰巧可以保证“它所抛出的所有异常对于更高层也是合适的”情况下,才可以将异常从低层传播到高层。异常链对高层和低层异常都提供了最佳的功能:它允许抛出适当的高层异常,同时又能捕获低层的原因进行失败分析(详见第 75 条) 。
|
||||
总而言之,如果不能阻止或者处理来自更低层的异常,一般的做法是使用异常转译,只有在低层方法的规范碰巧可以保证“它所抛出的所有异常对于更高层也是合适的”情况下,才可以将异常从低层传播到高层。异常链对高层和低层异常都提供了最佳的功能:它允许抛出适当的高层异常,同时又能捕获低层的原因进行失败分析(详见第 75 条) 。
|
||||
|
||||
|
||||
@@ -14,4 +14,5 @@
|
||||
|
||||
**如果一个类中的许多方法出于同样的原因而抛出同一个异常,在该类的文档注释中对这个异常建立文档,这是可以接受的,** 而不是为每个方法单独建立文档。一个常见的例子是 `NullPointerException`。 若类的文档注释中有这样的描述:「All methods in this class throw a NullPointerException if a null object reference is passed in any parameter 」(如果 null 对象引用被传递到任何一个参数中,这个类中的所有方法都会抛出 `NullPointerException`),或者有其他类似的语句,这是可以的。
|
||||
|
||||
总而言之,要为你编写的每个方法所能抛出的每个异常建立文档。对于未受检异常和受检异常,以及抽象的方法和具体的方法一概如此。这个文档在文档注释中应当采用 `@throws` 标签的形式。要在方法的 throws 子句中为每个受检异常提供单独的声明,但是不要声明未受检的异常。如果没有为可以抛出的异常建立文档,其他人就很难或者根本不可能有效地使用你的类和接口。
|
||||
总而言之,要为你编写的每个方法所能抛出的每个异常建立文档。对于未受检异常和受检异常,以及抽象的方法和具体的方法一概如此。这个文档在文档注释中应当采用 `@throws` 标签的形式。要在方法的 throws 子句中为每个受检异常提供单独的声明,但是不要声明未受检的异常。如果没有为可以抛出的异常建立文档,其他人就很难或者根本不可能有效地使用你的类和接口。
|
||||
|
||||
|
||||
@@ -28,4 +28,5 @@ public Object pop() {
|
||||
|
||||
即使在可以实现失败原子性的场合,它也并不总是人们所期望的。对于某些操作,它会显著地增加开销或者复杂性。也就是说, 一旦了解了这个问题,获得失败原子性往往既简单又容易。
|
||||
|
||||
总而言之,作为方法规范的一部分,它产生的任何异常都应该让对象保持在调用该方法之前的状态。如果违反这条规则, API 文档就应该清楚地指明对象将会处于什么样的状态。遗憾的是,大量现有的 API 文档都未能做到这一点。
|
||||
总而言之,作为方法规范的一部分,它产生的任何异常都应该让对象保持在调用该方法之前的状态。如果违反这条规则, API 文档就应该清楚地指明对象将会处于什么样的状态。遗憾的是,大量现有的 API 文档都未能做到这一点。
|
||||
|
||||
|
||||
@@ -24,4 +24,5 @@ try {
|
||||
}
|
||||
```
|
||||
|
||||
本条目中的建议同样适用于受检异常和未受检异常。不管异常代表了可预见的异常条件,还是编程错误,用空的 catch 块忽略它,都将导致程序在遇到错误的情况下悄然地执行下去。然后,有可能在将来的某个点上,当程序不能再容忍与错误源明显相关的问题时,它就会失败。正确地处理异常能够彻底避免失败。只要将异常传播给外界,至少会导致程序迅速失败,从而保留了有助于调试该失败条件的信息。
|
||||
本条目中的建议同样适用于受检异常和未受检异常。不管异常代表了可预见的异常条件,还是编程错误,用空的 catch 块忽略它,都将导致程序在遇到错误的情况下悄然地执行下去。然后,有可能在将来的某个点上,当程序不能再容忍与错误源明显相关的问题时,它就会失败。正确地处理异常能够彻底避免失败。只要将异常传播给外界,至少会导致程序迅速失败,从而保留了有助于调试该失败条件的信息。
|
||||
|
||||
|
||||
@@ -128,4 +128,5 @@ public static long generateSerialNumber() {
|
||||
|
||||
让一个线程在短时间内修改一个数据对象,然后与其他线程共享,这是可以接受的,它只同步共享对象引用的动作。然后其他线程没有进一步的同步也可以读取对象,只要它没有再被修改。这种对象被称作高效不可变( effectively immutable ) [Goetz06, 3.5.4] 。将这种对象引用从一个线程传递到其他的线程被称作安全发布( safe publication) [Goetz06, 3.5.3] 。安全发布对象引用有许多种方法:可以将它保存在静态字段巾,作为类初始化的一部分;可以将它保存在 volatile 字段、final 字段或者通过正常锁定访问的字段中;或者可以将它放到并发的集合中(详见第 81 条)。
|
||||
|
||||
总而言之, **当多个线程共享可变数据的时候,每个读或者写数据的线程都必须执行同步。** 如果没有同步,就无法保证一个线程所做的修改可以被另一个线程获知。未能同步共享可变数据会造成程序的活跃性失败( liveness failure )和安全性失败( safety failure ) 。这样的失败是最难调试的。它们可能是间歇性的,且与时间相关,程序的行为在不同的虚拟机上可能根本不同。如果只需要线程之间的交互通信,而不需要互斥, volatile 修饰符就是一种可以接受的同步形式,但要正确地使用它可能需要一些技巧。
|
||||
总而言之, **当多个线程共享可变数据的时候,每个读或者写数据的线程都必须执行同步。** 如果没有同步,就无法保证一个线程所做的修改可以被另一个线程获知。未能同步共享可变数据会造成程序的活跃性失败( liveness failure )和安全性失败( safety failure ) 。这样的失败是最难调试的。它们可能是间歇性的,且与时间相关,程序的行为在不同的虚拟机上可能根本不同。如果只需要线程之间的交互通信,而不需要互斥, volatile 修饰符就是一种可以接受的同步形式,但要正确地使用它可能需要一些技巧。
|
||||
|
||||
|
||||
@@ -172,4 +172,5 @@ private void notifyElementAdded(E element) {
|
||||
|
||||
如果方法修改了静态字段,并且该方法很可能要被多个线程调用,那么也必须在内部同步对这个字段的访问(除非这个类能够容忍不确定的行为) 。多线程的客户端要在这种方法上执行外部同步是不可能的,因为其他不相关的客户端不需要同步也能调用该方法。字段本质上就是一个全局变量,即使是私有的也一样,因为它可以被不相关的客户端读取和修改。第 78 条中的 `generateSerialNumber` 方法使用的 `nextSerialNumber` 字段就是这样的一个例子。
|
||||
|
||||
总而言之,为了避免死锁和数据破坏,千万不要从同步区字段内部调用外来方法。更通俗地讲,要尽量将同步区字段内部的工作量限制到最少。当你在设计一个可变类的时候,要考虑一下它们是否应该自己完成同步操作。在如今这个多核的时代,这比永远不要过度同步来得更重要。只有当你有足够的理由一定要在内部同步类的时候,才应该这么做,同时还应该将这个决定清楚地写到文档中(详见第 82 条) 。
|
||||
总而言之,为了避免死锁和数据破坏,千万不要从同步区字段内部调用外来方法。更通俗地讲,要尽量将同步区字段内部的工作量限制到最少。当你在设计一个可变类的时候,要考虑一下它们是否应该自己完成同步操作。在如今这个多核的时代,这比永远不要过度同步来得更重要。只有当你有足够的理由一定要在内部同步类的时候,才应该这么做,同时还应该将这个决定清楚地写到文档中(详见第 82 条) 。
|
||||
|
||||
|
||||
@@ -113,4 +113,5 @@ synchronized (obj) {
|
||||
|
||||
即使这些前提条件都满足,也许还是有理由使用 `notifyAll` 方法而不是 `notify` 方法。就好像把 `wait` 方法调用放在一个循环中,以避免在公有可访问对象上的意外或恶意的通知一样,与此类似,使用 `notifyAll` 方法代替 `notify` 方法可以避免来自不相关线程的意外或恶意的等待。否则,这样的等待会“吞掉”一个关键的通知,使真正的接收线程无限地等待下去。
|
||||
|
||||
简而言之,直接使用 `wait` 方法和 `notify` 方法就像用“并发汇编语言”进行编程一样,而 `java.util.concurrent` 则提供了更高级的语言。 **没有理由在新代码中使用 `wait` 方法和 `notify` 方法,即使有,也是极少的。** 如果你在维护使用 `wait` 方法和 `notify` 方法的代码,务必确保始终是利用标准的模式从 `while` 循环内部调用 `wait` 方法。一般情况下,应该优先使用 `notifyAll` 方法,而不是使用 `notify` 方法。如果使用 `notify` 方法,请一定要小心,以确保程序的活性。
|
||||
简而言之,直接使用 `wait` 方法和 `notify` 方法就像用“并发汇编语言”进行编程一样,而 `java.util.concurrent` 则提供了更高级的语言。 **没有理由在新代码中使用 `wait` 方法和 `notify` 方法,即使有,也是极少的。** 如果你在维护使用 `wait` 方法和 `notify` 方法的代码,务必确保始终是利用标准的模式从 `while` 循环内部调用 `wait` 方法。一般情况下,应该优先使用 `notifyAll` 方法,而不是使用 `notify` 方法。如果使用 `notify` 方法,请一定要小心,以确保程序的活性。
|
||||
|
||||
|
||||
@@ -46,4 +46,5 @@ synchronized(m) { // Synchronizing on m, not s!
|
||||
|
||||
私有锁对象用法特别适合为继承而设计的类(详见第 19 条)。如果这样一个类要使用它的实例进行锁定,那么子类很容易在无意中干扰基类的操作,反之亦然。通过为不同的目的使用相同的锁,子类和基类最终可能「踩到对方的脚趾头」。这不仅仅是一个理论问题,它就发生在 `Thread` 类中 [Bloch05, Puzzle 77]。
|
||||
|
||||
总之,每个类都应该措辞严谨的描述或使用线程安全注解清楚地记录其线程安全属性。`synchronized` 修饰符在文档中没有任何作用。有条件的线程安全类必须记录哪些方法调用序列需要外部同步,以及在执行这些序列时需要获取哪些锁。如果你编写一个无条件线程安全的类,请考虑使用一个私有锁对象来代替同步方法。这将保护你免受客户端和子类的同步干扰,并为你提供更大的灵活性,以便在后续的版本中采用复杂的并发控制方式。
|
||||
总之,每个类都应该措辞严谨的描述或使用线程安全注解清楚地记录其线程安全属性。`synchronized` 修饰符在文档中没有任何作用。有条件的线程安全类必须记录哪些方法调用序列需要外部同步,以及在执行这些序列时需要获取哪些锁。如果你编写一个无条件线程安全的类,请考虑使用一个私有锁对象来代替同步方法。这将保护你免受客户端和子类的同步干扰,并为你提供更大的灵活性,以便在后续的版本中采用复杂的并发控制方式。
|
||||
|
||||
|
||||
@@ -74,4 +74,5 @@ private FieldType getField() {
|
||||
|
||||
如果您不关心每个线程是否都会重新计算字段的值,并且字段的类型是 long 或 double 之外的基本类型,那么您可以选择在单检查模式中从字段声明中删除 volatile 修饰符。这种变体称为原生单检查模式。它加快了某些架构上的字段访问速度,代价是需要额外的初始化(每个访问该字段的线程最多需要一个初始化)。这绝对是一种奇特的技术,不是日常使用的。
|
||||
|
||||
总之,您应该正常初始化大多数字段,而不是延迟初始化。如果必须延迟初始化字段以实现性能目标或为了破坏友有害的初始化循环,则使用适当的延迟初始化技术。对于字段,使用双重检查模式;对于静态字段,则应该使用the lazy initialization holder class idiom。例如,可以容忍重复初始化的实例字段,您还可以考虑单检查模式。
|
||||
总之,您应该正常初始化大多数字段,而不是延迟初始化。如果必须延迟初始化字段以实现性能目标或为了破坏友有害的初始化循环,则使用适当的延迟初始化技术。对于字段,使用双重检查模式;对于静态字段,则应该使用the lazy initialization holder class idiom。例如,可以容忍重复初始化的实例字段,您还可以考虑单检查模式。
|
||||
|
||||
|
||||
@@ -42,4 +42,5 @@ public class SlowCountDownLatch {
|
||||
|
||||
一个相关的技术是调整线程优先级,类似的警告也适用于此技术,即,线程优先级是 Java 中最不可移植的特性之一。通过调整线程优先级来调优应用程序的响应性并非不合理,但很少情况下是必要的,而且不可移植。试图通过调整线程优先级来解决严重的活性问题是不合理的。在找到并修复潜在原因之前,问题很可能会再次出现。
|
||||
|
||||
总之,不要依赖线程调度器来判断程序的正确性。生成的程序既不健壮也不可移植。因此,不要依赖 `Thread.yield` 或线程优先级。这些工具只是对调度器的提示。线程优先级可以少量地用于提高已经工作的程序的服务质量,但绝不应该用于「修复」几乎不能工作的程序。
|
||||
总之,不要依赖线程调度器来判断程序的正确性。生成的程序既不健壮也不可移植。因此,不要依赖 `Thread.yield` 或线程优先级。这些工具只是对调度器的提示。线程优先级可以少量地用于提高已经工作的程序的服务质量,但绝不应该用于「修复」几乎不能工作的程序。
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
可序列化会使类的演变受到限制,施加这种约束的一个简单示例涉及流的唯一标识符,通常称其为串行版本 UID。每个可序列化的类都有一个与之关联的唯一标识符。如果你没有通过声明一个名为 `serialVersionUID` 的静态 final long 字段来指定这个标识符,那么系统将在运行时对类应用加密散列函数(SHA-1)自动生成它。这个值受到类的名称、实现的接口及其大多数成员(包括编译器生成的合成成员)的影响。如果你更改了其中任何一项,例如,通过添加一个临时的方法,生成的序列版本 UID 就会更改。如果你未能声明序列版本 UID,兼容性将被破坏,从而在运行时导致 `InvalidClassException`。
|
||||
|
||||
**实现 `Serializable` 接口的第二个代价是,增加了出现 bug 和安全漏洞的可能性(第85项)。** 通常,对象是用构造函数创建的;序列化是一种用于创建对象的超语言机制。无论你接受默认行为还是无视它,反序列化都是一个「隐藏构造函数」,其他构造函数具有的所有问题它都有。由于没有与反序列化关联的显式构造函数,因此很容易忘记必须让它能够保证所有的不变量都是由构造函数建立的,并且不允许攻击者访问正在构造的对象内部。依赖于默认的反序列化机制,会让对象轻易地遭受不变性破坏和非法访问(详见第 88 条)。
|
||||
**实现 `Serializable` 接口的第二个代价是,增加了出现 bug 和安全漏洞的可能性(详见第 85 条)。** 通常,对象是用构造函数创建的;序列化是一种用于创建对象的超语言机制。无论你接受默认行为还是无视它,反序列化都是一个「隐藏构造函数」,其他构造函数具有的所有问题它都有。由于没有与反序列化关联的显式构造函数,因此很容易忘记必须让它能够保证所有的不变量都是由构造函数建立的,并且不允许攻击者访问正在构造的对象内部。依赖于默认的反序列化机制,会让对象轻易地遭受不变性破坏和非法访问(详见第 88 条)。
|
||||
|
||||
**实现 `Serializable` 接口的第三个代价是,它增加了与发布类的新版本相关的测试负担。** 当一个可序列化的类被修改时,重要的是检查是否可以在新版本中序列化一个实例,并在旧版本中反序列化它,反之亦然。因此,所需的测试量与可序列化类的数量及版本的数量成正比,工作量可能很大。你必须确保「序列化-反序列化」过程成功,并确保它生成原始对象的无差错副本。如果在第一次编写类时精心设计了自定义序列化形式,那么测试的工作量就会减少(详见第 87 和 90 条)。
|
||||
|
||||
|
||||
@@ -145,4 +145,5 @@ private static final long serialVersionUID = randomLongValue;
|
||||
|
||||
如果你希望创建一个新版本的类,它与现有版本不兼容,如果更改序列版本 UID 声明中的值,这将导致反序列化旧版本的序列化实例的操作引发 `InvalidClassException`。**不要更改序列版本 UID,除非你想破坏与现有序列化所有实例的兼容性。**
|
||||
|
||||
总而言之,如果你已经决定一个类应该是可序列化的(详见第 86 条),那么请仔细考虑一下序列化的形式应该是什么。只有在合理描述对象的逻辑状态时,才使用默认的序列化形式;否则,设计一个适合描述对象的自定义序列化形式。设计类的序列化形式应该和设计导出方法花的时间应该一样多,都应该严谨对待(详见第 51 条)。正如不能从未来版本中删除导出的方法一样,也不能从序列化形式中删除字段;必须永远保存它们,以确保序列化兼容性。选择错误的序列化形式可能会对类的复杂性和性能产生永久性的负面影响。
|
||||
总而言之,如果你已经决定一个类应该是可序列化的(详见第 86 条),那么请仔细考虑一下序列化的形式应该是什么。只有在合理描述对象的逻辑状态时,才使用默认的序列化形式;否则,设计一个适合描述对象的自定义序列化形式。设计类的序列化形式应该和设计导出方法花的时间应该一样多,都应该严谨对待(详见第 51 条)。正如不能从未来版本中删除导出的方法一样,也不能从序列化形式中删除字段;必须永远保存它们,以确保序列化兼容性。选择错误的序列化形式可能会对类的复杂性和性能产生永久性的负面影响。
|
||||
|
||||
|
||||
@@ -32,11 +32,11 @@ public final class Period {
|
||||
}
|
||||
```
|
||||
|
||||
假设决定要把这个类左晨可序列化的。因为 Period 对象的物理表示法正好反映了它的逻辑数据内容,所以,使用默认的序列化形式并没有什么不合理的(详见 87 条)。因此,为了使这个类成为可序列化的,似乎你所需要做的也就是在类的声明中增加 implements Serializable 字样。然而,如果你真的这么做,那么这个类就不保证它的关键约束了。
|
||||
假设你决定要把这个类成为可序列化的。因为 Period 对象的物理表示法正好反映了它的逻辑数据内容,所以,使用默认的序列化形式是合理的(详见 87 条)。因此,为了使这个类成为可序列化的,似乎你所需要做的也就是在类的声明中增加 implements Serializable 字样。然而,如果你真的这么做,那么这个类就不保证它的关键约束了。
|
||||
|
||||
问题在于 `readObject` 方法实际上相当于另外一个公有的构造器,它要求同其他构造器一样警惕所有的注意事项。构造器必须检查其参数的有效性(详见 49 条),并且在必要的时候对参数进行保护性拷贝(详见 50 条),同样的,`readObject` 方法也需要这样做。如果 `readObject` 方法无法做到这两者之一,对于攻击者来说要违反这个类的约束条件就相对容易很多。
|
||||
|
||||
不严格的说, `readObject` 方法是一个“用字节流作为唯一参数”的构造器。在正常使用的情况下,对一个正常构造的实例进行序列化可以产生字节流。但是,当面对一个人工仿造的字节流时, `readObject` 产生的对象会违反它所属类的约束条件,这时问题就产生了。这种字节流可以用来创建一个不可能的对象(impossible object),这时利用普通构造器无法创建的。
|
||||
不严格的说, `readObject` 方法是一个「用字节流作为唯一参数」的构造器。在正常使用的情况下,对一个正常构造的实例进行序列化可以产生字节流。但是,当面对一个人工仿造的字节流时, `readObject` 产生的对象会违反它所属类的约束条件,这时问题就产生了。这种字节流可以用来创建一个不可能的对象(impossible object),这时利用普通构造器无法创建的。
|
||||
|
||||
假设我们仅仅在 Period 类的声明加上了 `implements Serializable` 字样。那么这个丑陋的程序代码将会产生一个 Period 实例,他的结束时间比起始时间还早。对于高位 byte 值进行强制类型转换是 Java 缺少 byte 并且做出 byte 类型签名的不幸决定的后果:
|
||||
|
||||
@@ -76,7 +76,7 @@ public class BogusPeriod {
|
||||
}
|
||||
```
|
||||
|
||||
被用来初始化 serializedForm 的 byte 常量数组是这样产生的:首先对一个正常的 Period 实例进行序列化,然后对得到的字节流进行手工编辑。对于这个例子而言,字节流的细节并不重要,如果你对此十分好奇,可以在《Java Object Serialization Specification》[Serialization, 6] 中查到有关序列化字节流格式的描述信息。如果运行这个程序,它会打印出“ Fri Jan 01 12:00:00 PST 1999 - Sun Jan 01 12:00:00 PST 1984”。主要把 Period 类声明成为可序列化的,这会使我们创建出其违反类约束条件的对象。
|
||||
被用来初始化 serializedForm 的 byte 常量数组是这样产生的:首先对一个正常的 Period 实例进行序列化,然后对得到的字节流进行手工编辑。对于这个例子而言,字节流的细节并不重要,如果你对此十分好奇,可以在《Java Object Serialization Specification》[Serialization, 6] 中查到有关序列化字节流格式的描述信息。如果运行这个程序,它会打印出「Fri Jan 01 12:00:00 PST 1999 - Sun Jan 01 12:00:00 PST 1984」。主要把 Period 类声明成为可序列化的,这会使我们创建出其违反类约束条件的对象。
|
||||
|
||||
为了修整这个问题,可以为 `Period` 提供一个 `readObject` 方法,该方法首先调用 `defaultReadObject`,然后检查被反序列化之后的对象有效性。如果有效性检查失败,`readObject` 方法就会抛出一个 `InvalidObjectException` 异常,这使得反序列化过程不能成功的完成:
|
||||
|
||||
@@ -91,7 +91,7 @@ private void readObject(ObjectInputStream s)
|
||||
}
|
||||
```
|
||||
|
||||
尽管这样的修成避免了攻击者创建无效的 `Period` 实例,但是这里依旧隐藏着一个更为微妙的问题。通过伪造字节流,要想创建可变的 `Period` 实例仍是有可能的,做法事:字节流以一个有效的 `Period` 实例开头,然后附加上两个额外的引用,指向 `Period` 实例中两个私有的 `Date` 字段。攻击者从 `ObjectInputStream` 读取 `Period` 实例,然后读取附加在其后面的“恶意编制的对线引用”。这些对象引用使得攻击者能够访问到 `Period` 对象内部的私有 `Date` 字段所引用的对象。通过改变这些 `Date` 实例,攻击者可以改变 `Period` 实例。如下的类演示了这种攻击方式:
|
||||
尽管这样的修成避免了攻击者创建无效的 `Period` 实例,但是这里依旧隐藏着一个更为微妙的问题。通过伪造字节流,要想创建可变的 `Period` 实例仍是有可能的,做法是:字节流以一个有效的 `Period` 实例开头,然后附加上两个额外的引用,指向 `Period` 实例中两个私有的 `Date` 字段。攻击者从 `ObjectInputStream` 读取 `Period` 实例,然后读取附加在其后面的「恶意编制的对线引用」。这些对象引用使得攻击者能够访问到 `Period` 对象内部的私有 `Date` 字段所引用的对象。通过改变这些 `Date` 实例,攻击者可以改变 `Period` 实例。如下的类演示了这种攻击方式:
|
||||
|
||||
```java
|
||||
public class MutablePeriod {
|
||||
@@ -160,9 +160,9 @@ Wed Nov 22 00:21:29 PST 2017 - Wed Nov 22 00:21:29 PST 1978
|
||||
Wed Nov 22 00:21:29 PST 2017 - Sat Nov 22 00:21:29 PST 1969
|
||||
```
|
||||
|
||||
虽然 Period 实例被创建之后,他的约束条件没有被破坏。但是要随意修改它的内部组件仍然是有可能的。一旦攻击者获得了一个可变的 `Period` 实例,就可以将这个实例传递给一个“安全性依赖于 Period 的不可变性”的类,从而造成更大的危害。这种推断并不牵强:实际上,有许多类的安全性就是依赖于 String 的不可变性。
|
||||
虽然 Period 实例被创建之后,他的约束条件没有被破坏。但是要随意修改它的内部组件仍然是有可能的。一旦攻击者获得了一个可变的 `Period` 实例,就可以将这个实例传递给一个「安全性依赖于 Period 的不可变性」的类,从而造成更大的危害。这种推断并不牵强:实际上,有许多类的安全性就是依赖于 String 的不可变性。
|
||||
|
||||
问题的根源在于,`Period` 的 `readObject` 方法并没有完成足够的保护性拷贝。 **当一个对象被反序列化的时候,对于客户端不应该拥有的对象引用,如果那个字段包含了这样的对象引用,就必须做保护性拷贝,这是非常重要的。** 因此,对于每个可序列化的不可变类,如果它好汉了私有的可变字段,那么在它的 `readObject` 方法中,必须要对这些字段进行保护性拷贝。下面的这些 `readObject` 方法可以确保 `Period` 类的约束条件不会遭到破坏,以保持它的不可变性:
|
||||
问题的根源在于,`Period` 的 `readObject` 方法并没有完成足够的保护性拷贝。 **当一个对象被反序列化的时候,对于客户端不应该拥有的对象引用,如果那个字段包含了这样的对象引用,就必须做保护性拷贝,这是非常重要的。** 因此,对于每个可序列化的不可变类,如果它包含了私有的可变字段,那么在它的 `readObject` 方法中,必须要对这些字段进行保护性拷贝。下面的这些 `readObject` 方法可以确保 `Period` 类的约束条件不会遭到破坏,以保持它的不可变性:
|
||||
|
||||
```java
|
||||
// readObject method with defensive copying and validity checking
|
||||
@@ -185,7 +185,7 @@ Wed Nov 22 00:23:41 PST 2017 - Wed Nov 22 00:23:41 PST 2017
|
||||
Wed Nov 22 00:23:41 PST 2017 - Wed Nov 22 00:23:41 PST 2017
|
||||
```
|
||||
|
||||
有一个简单的“石蕊”测试,可以用来确定默认的 `readObject` 方法是否可以被接受。测试方法:增加一个公有的构造器,其参数对应于该对象中每个非 transient 的字段,并且无论参数的值是什么,都是不进行检查就可以保存到相应的字段中。对于这样的做法,你是否会感到很舒适?如果你对这个问题的回答是否定的,就必须提供一个显式的 `readObject` 方法,并且它必须执行构造器所要求的所有有效性检查和保护性拷贝。另一种方法是,可以使用序列化代理模式(serialization proxy pattern),详见第 90 条。强烈建议使用这个模式,因为它分担了安全反序列化的部门工作。
|
||||
有一个简单的「石蕊」测试,可以用来确定默认的 `readObject` 方法是否可以被接受。测试方法:增加一个公有的构造器,其参数对应于该对象中每个非 transient 的字段,并且无论参数的值是什么,都是不进行检查就可以保存到相应的字段中。对于这样的做法,你是否会感到很舒适?如果你对这个问题的回答是否定的,就必须提供一个显式的 `readObject` 方法,并且它必须执行构造器所要求的所有有效性检查和保护性拷贝。另一种方法是,可以使用序列化代理模式(serialization proxy pattern),详见第 90 条。强烈建议使用这个模式,因为它分担了安全反序列化的部门工作。
|
||||
|
||||
对于非 final 的可序列化的类,在 `readObject` 方法和构造器之间还有其他类似的地方。与构造器一样,`readObject` 方法不可以调用可被覆盖的方法,无论是直接调用还是间接调用都不可以(详见 19 条)。如果违反了这条规则,并且覆盖了该方法,被覆盖的方法将在子类的状态被反序列化之前先运行。这个程序很可能会失败[Bloch05, Puzzle 91]。
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ public class ElvisImpersonator {
|
||||
|
||||
通过将 `favoriteSongs` 字段声明为 `transient`,可以修复这个问题,但是最好把 `Elvis` 做成一个单元素的枚举类型(详见第 3 条)。就如 `ElvisStealer` 攻击所示范的,用 `readResolve` 方法防止“临时”被反序列化的实例收到攻击者的访问,这种方法十分脆弱需要万分谨慎。
|
||||
|
||||
如果将一个可序列化的实例受控的类编写为枚举,Java 就可以绝对保证出了所声明的常量之外,不会有其他实例,除非攻击者恶意的使用了享受特权的方法。如 `AccessibleObject.setAccessible`。能够做到这一点的任何一位攻击者,已经拥有了足够的特权来执行任意的本地代码,后果不堪设想。将 Elvis 写成枚举的例子如下所示:
|
||||
如果将一个可序列化的实例受控的类编写为枚举,Java 就可以绝对保证除了所声明的常量之外,不会有其他实例,除非攻击者恶意的使用了享受特权的方法。如 `AccessibleObject.setAccessible`。能够做到这一点的任何一位攻击者,已经拥有了足够的特权来执行任意的本地代码,后果不堪设想。将 Elvis 写成枚举的例子如下所示:
|
||||
```java
|
||||
// Enum singleton - the preferred approach
|
||||
public enum Elvis {
|
||||
@@ -130,4 +130,5 @@ public enum Elvis {
|
||||
|
||||
**readResolve 的可访问性(accessibility)十分重要。** 如果把 `readResolve` 方法放在一个 `final` 类上面,它应该是私有的。如果把 `readResolve` 方法放在一个非 `final` 类上,就必须认真考虑它的的访问性。如果它是私有的,就不适用于任何一个子类。如果它是包级私有的,就适用于同一个包内的子类。如果它是受保护的或者是公开的,并且子类没有覆盖它,对序列化的子类进行反序列化,就会产生一个超类实例,这样可能会导致 `ClassCastException` 异常。
|
||||
|
||||
总而言之,应该尽可能的使用枚举类型来实施实例控制的约束条件。如果做不到,同时又需要一个即可序列化又可以实例受控的类,就必须提供一个 `readResolve` 方法,并确保该类的所有实例化字段都被基本类型,或者是 `transient` 的。
|
||||
总而言之,应该尽可能的使用枚举类型来实施实例控制的约束条件。如果做不到,同时又需要一个即可序列化又可以实例受控的类,就必须提供一个 `readResolve` 方法,并确保该类的所有实例化字段都被基本类型,或者是 `transient` 的。
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 90. 考虑用序列化代理代替序列化实例
|
||||
|
||||
正如 85 条和第 86 条提到的,以及本章一直在讨论的,决定实现 Serializable 接口,会增加出错和出现安全问题的可能性,因为它允许利用语言之外的机制来创建实例,而不是使用普通的构造器。然而,有一只方法可以极大的减少这些风险。就是序列化代理模式(seralization proxy pattern)。
|
||||
正如 85 条和第 86 条提到的,以及本章一直在讨论的,决定实现 Serializable 接口,会增加出错和出现安全问题的可能性,因为它允许利用语言之外的机制来创建实例,而不是使用普通的构造器。然而,有一种方法可以极大的减少这些风险。就是序列化代理模式(seralization proxy pattern)。
|
||||
|
||||
序列化代理模式相当简单。首先,为可序列化的类设计一个私有的静态嵌套类,精确地表示外围类的逻辑状态。这个嵌套类被称为序列化代理(seralization proxy),它应该有一个单独的构造器,其参数类型就是那个外围类。这个构造器只是从它的参数中复制数据:它不需要进行任何一致性检验或者保护性拷贝。从设计的角度看,序列化代理的默认序列化形式是外围类最好的序列化形式。外围类及其序列代理都必须声明实现 `Serializable` 接口。
|
||||
|
||||
@@ -94,4 +94,5 @@ private static class SerializationProxy<E extends Enum<E>>
|
||||
|
||||
最后一点,序列化代理模式所增强的功能和安全性不是没有代价。在我的机器上,通过序列化代理来序列化和反序列化 `Period` 实例的开销,比使用保护性拷贝增加了 14%。
|
||||
|
||||
总而言之,当你发现必须在一个不能被客户端拓展的类上面编写 `readObject` 或者 `writeObject` 方法时,就应该考虑使用序列化代理模式。想要稳健的将带有重要约束条件的对象序列化时,这种模式是最容易的方法。
|
||||
总而言之,当你发现必须在一个不能被客户端拓展的类上面编写 `readObject` 或者 `writeObject` 方法时,就应该考虑使用序列化代理模式。想要稳健的将带有重要约束条件的对象序列化时,这种模式是最容易的方法。
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
BIN
images/groupcode.png
Normal file
BIN
images/groupcode.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
BIN
images/shop.jpg
Normal file
BIN
images/shop.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
Reference in New Issue
Block a user