mirror of
https://github.com/sjsdfg/effective-java-3rd-chinese.git
synced 2026-08-21 11:13:28 +08:00
Compare commits
53 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 |
18
README.md
18
README.md
@@ -5,23 +5,33 @@
|
||||
现在全部章节已经更新完成
|
||||
- [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.2/effective-java20190527.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>
|
||||
|
||||
## 额外资源
|
||||
|
||||
|
||||
@@ -14,7 +14,17 @@
|
||||
|
||||
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>
|
||||
|
||||
|
||||
|
||||
@@ -24,6 +34,8 @@ Effective Java 第三版翻译校对群:[909059971](https://jq.qq.com/?_wv=102
|
||||
|
||||
## 📚 高效 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)
|
||||
@@ -33,11 +45,17 @@ Effective Java 第三版翻译校对群:[909059971](https://jq.qq.com/?_wv=102
|
||||
- [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)
|
||||
@@ -49,6 +67,9 @@ Effective Java 第三版翻译校对群:[909059971](https://jq.qq.com/?_wv=102
|
||||
- [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)
|
||||
@@ -57,6 +78,9 @@ Effective Java 第三版翻译校对群:[909059971](https://jq.qq.com/?_wv=102
|
||||
- [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)
|
||||
@@ -65,6 +89,9 @@ Effective Java 第三版翻译校对群:[909059971](https://jq.qq.com/?_wv=102
|
||||
- [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)
|
||||
@@ -72,6 +99,9 @@ Effective Java 第三版翻译校对群:[909059971](https://jq.qq.com/?_wv=102
|
||||
- [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)
|
||||
@@ -80,6 +110,9 @@ Effective Java 第三版翻译校对群:[909059971](https://jq.qq.com/?_wv=102
|
||||
- [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)
|
||||
@@ -92,6 +125,9 @@ Effective Java 第三版翻译校对群:[909059971](https://jq.qq.com/?_wv=102
|
||||
- [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)
|
||||
@@ -101,6 +137,9 @@ Effective Java 第三版翻译校对群:[909059971](https://jq.qq.com/?_wv=102
|
||||
- [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)
|
||||
@@ -108,6 +147,9 @@ Effective Java 第三版翻译校对群:[909059971](https://jq.qq.com/?_wv=102
|
||||
- [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)
|
||||
@@ -115,11 +157,6 @@ Effective Java 第三版翻译校对群:[909059971](https://jq.qq.com/?_wv=102
|
||||
- [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)
|
||||
|
||||
BIN
docs/assets/groupcode.png
Normal file
BIN
docs/assets/groupcode.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
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 |
@@ -40,7 +40,7 @@ public static Boolean valueOf(boolean b) {
|
||||
|
||||
这两个实现类的存在对于客户端而言是不可见的。 如果 `RegularEnumSet` 对于小的枚举类型不再具有性能优势,则可以在未来版本中将其淘汰,且不会产生任何不良影响。 同样,如果可以证明添加 `EnumSet` 的更多的实现可以提高性能,那么在未来的版本可能就会这样做。 客户既不知道也不关心他们从工厂返回的对象的类别; 他们只需要知道它是 `EnumSet` 的子类。
|
||||
|
||||
**静态工厂的第五个优点是,在编写包含该方法的类时,返回的对象的类不需要存在。** 这种灵活的静态工厂方法构成了服务提供者框架的基础,比如 Java 数据库连接 AP(JDBC)。服务提供者框架是提供者实现服务的系统,并且系统使得实现对客户端可用,从而将客户端从实现中分离出来。
|
||||
**静态工厂的第五个优点是,在编写包含该方法的类时,返回的对象的类不需要存在。** 这种灵活的静态工厂方法构成了服务提供者框架的基础,比如 Java 数据库连接 API(JDBC)。服务提供者框架是提供者实现服务的系统,并且系统使得实现对客户端可用,从而将客户端从实现中分离出来。
|
||||
|
||||
服务提供者框架中有三个基本组:服务接口,它表示实现;提供者注册 API,提供者用来注册实现;以及服务访问 API,客户端使用该 API 获取服务的实例。服务访问 API 允许客户指定选择实现的标准。在缺少这样的标准的情况下,API 返回一个默认实现的实例,或者允许客户通过所有可用的实现进行遍历。服务访问 API 是灵活的静态工厂,它构成了服务提供者框架的基础。
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);
|
||||
|
||||
通常情况下,这个构造方法的调用需要许多你不想设置的参数,但是你不得不为它们传递一个值。 在这种情况下,我们为 `fat` 属性传递了 0 值。「只有」六个参数可能看起来并不那么糟糕,但随着参数数量的增加,它很快就会失控。
|
||||
|
||||
简而言之,**可伸缩构造方法模式是有效的,但是当有很多参数时,很难编写客户端代码,而且很难读懂它。**读者不知道这些值是什么意思,并且必须仔细地去数参数才能找到答案。一长串相同类型的参数可能会导致一些细微的 bug。如果客户端不小心写反了两个这样的参数,编译器并不会报错,但是程序在运行时会出现错误行为 (详见第 51 条)。
|
||||
简而言之,**可伸缩构造方法模式是有效的,但是当有很多参数时,很难编写客户端代码,而且很难读懂它。** 读者不知道这些值是什么意思,并且必须仔细地去数参数才能找到答案。一长串相同类型的参数可能会导致一些细微的 bug。如果客户端不小心写反了两个这样的参数,编译器并不会报错,但是程序在运行时会出现错误行为 (详见第 51 条)。
|
||||
|
||||
当在构造方法中遇到许多可选参数时,另一种选择是 JavaBeans 模式,在这种模式中,调用一个无参的构造方法来创建对象,然后调用 `setter` 方法来设置每个必需的参数和可选参数:
|
||||
|
||||
|
||||
@@ -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 模式)。这种称为依赖注入的实践将极大地增强类的灵活性、可重用性和可测试性。
|
||||
总而言之,不要用单例和静态工具类来实现依赖一个或多个底层资源的类,且该资源的行为会影响到该类的行为;也不要直接用这个类来创建这些资源。而应该将这些资源或者工厂传给构造器(或者静态工厂,或者构建器),通过它们来创建类。这个实践就被称作依赖注人,它极大地提升了类的灵活性、可重用性和可测试性。
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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)的调试工具才会被发现。 因此,学习如何预见这些问题,并防止这些问题发生,是非常值得的。
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -227,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,7 +350,7 @@ public final class PhoneNumber {
|
||||
|
||||
1. **当重写 equals 方法时,同时也要重写 hashCode 方法(详见第 11 条)**。
|
||||
2. **不要让 equals 方法试图太聪明。** 如果只是简单地测试用于相等的属性,那么要遵守 equals 约定并不困难。如果你在寻找相等方面过于激进,那么很容易陷入麻烦。一般来说,考虑到任何形式的别名通常是一个坏主意。例如,File 类不应该试图将引用的符号链接等同于同一文件对象。幸好 File 类并没这么做。
|
||||
3. **在 equal 时方法声明中,不要将参数 Object 替换成其他类型。** 对于程序员来说,编写一个看起来像这样的 equals 方法并不少见,然后花上几个小时苦苦思索为什么它不能正常工作:在 equal 时方法声明中,不要将参数 Object 替换成其他类型。对于程序员来说,编写一个看起来像这样的 equals 方法并不少见,然后花上几个小时苦苦思索为什么它不能正常工作。
|
||||
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)的性能。
|
||||
|
||||
|
||||
@@ -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 的。**
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ 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` 框架中的时候,就修复了几个类似性质的安全漏洞。
|
||||
|
||||
@@ -181,7 +181,7 @@ static void walk(Set<Dog> dogs) {
|
||||
|
||||
包装类的缺点很少。 一个警告是包装类不适合在回调框架(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
|
||||
}
|
||||
|
||||
@@ -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|
|
||||
|
||||
@@ -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,5 +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`,但是简洁性和性能会受到影响。
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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,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
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
最经常被重用的异常类型是 `IllegalArgumentException`(详见第 49 条)。当调用者传递的参数值不合适的时候,往往就会抛出这个异常。比如,假设某一个参数代表了“某个动作的重复次数”,如果程序员给这个参数传递了一个负数,就会抛出这个异常。
|
||||
|
||||
另一个经常被重用的异常是 `llegalStateException`。如果因为接收对象的状态而使调用非法,通常就会抛出这个异常。例如,如果在某个对象被正确地初始化之前,调用者就企图使用这个对象,就会抛出这个异常。
|
||||
另一个经常被重用的异常是 `IllegalStateException`。如果因为接收对象的状态而使调用非法,通常就会抛出这个异常。例如,如果在某个对象被正确地初始化之前,调用者就企图使用这个对象,就会抛出这个异常。
|
||||
|
||||
可以这么说,所有错误的方法调用都可以被归结为非法参数或者非法状态,但是,还有一些其他的标准异常也被用于某些特定情况下的非法参数和非法状态。如果调用者在某个不允许 null 值的参数中传递了 null,习惯的做法就是抛出 `NullPointerException` 异常,而不是 `IllegalArgumentException`。同样地,如果调用者在表示序列下标的参数中传递了越界的值,应该抛出的就是 `IndexOutOfBoundsException` 异常,而不是 `IllegalArgumentException`。
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
可序列化会使类的演变受到限制,施加这种约束的一个简单示例涉及流的唯一标识符,通常称其为串行版本 UID。每个可序列化的类都有一个与之关联的唯一标识符。如果你没有通过声明一个名为 `serialVersionUID` 的静态 final long 字段来指定这个标识符,那么系统将在运行时对类应用加密散列函数(SHA-1)自动生成它。这个值受到类的名称、实现的接口及其大多数成员(包括编译器生成的合成成员)的影响。如果你更改了其中任何一项,例如,通过添加一个临时的方法,生成的序列版本 UID 就会更改。如果你未能声明序列版本 UID,兼容性将被破坏,从而在运行时导致 `InvalidClassException`。
|
||||
|
||||
**实现 `Serializable` 接口的第二个代价是,增加了出现 bug 和安全漏洞的可能性(第85项)。** 通常,对象是用构造函数创建的;序列化是一种用于创建对象的超语言机制。无论你接受默认行为还是无视它,反序列化都是一个「隐藏构造函数」,其他构造函数具有的所有问题它都有。由于没有与反序列化关联的显式构造函数,因此很容易忘记必须让它能够保证所有的不变量都是由构造函数建立的,并且不允许攻击者访问正在构造的对象内部。依赖于默认的反序列化机制,会让对象轻易地遭受不变性破坏和非法访问(详见第 88 条)。
|
||||
**实现 `Serializable` 接口的第二个代价是,增加了出现 bug 和安全漏洞的可能性(详见第 85 条)。** 通常,对象是用构造函数创建的;序列化是一种用于创建对象的超语言机制。无论你接受默认行为还是无视它,反序列化都是一个「隐藏构造函数」,其他构造函数具有的所有问题它都有。由于没有与反序列化关联的显式构造函数,因此很容易忘记必须让它能够保证所有的不变量都是由构造函数建立的,并且不允许攻击者访问正在构造的对象内部。依赖于默认的反序列化机制,会让对象轻易地遭受不变性破坏和非法访问(详见第 88 条)。
|
||||
|
||||
**实现 `Serializable` 接口的第三个代价是,它增加了与发布类的新版本相关的测试负担。** 当一个可序列化的类被修改时,重要的是检查是否可以在新版本中序列化一个实例,并在旧版本中反序列化它,反之亦然。因此,所需的测试量与可序列化类的数量及版本的数量成正比,工作量可能很大。你必须确保「序列化-反序列化」过程成功,并确保它生成原始对象的无差错副本。如果在第一次编写类时精心设计了自定义序列化形式,那么测试的工作量就会减少(详见第 87 和 90 条)。
|
||||
|
||||
|
||||
@@ -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 {
|
||||
@@ -162,7 +162,7 @@ Wed Nov 22 00:21:29 PST 2017 - Sat Nov 22 00:21:29 PST 1969
|
||||
|
||||
虽然 Period 实例被创建之后,他的约束条件没有被破坏。但是要随意修改它的内部组件仍然是有可能的。一旦攻击者获得了一个可变的 `Period` 实例,就可以将这个实例传递给一个「安全性依赖于 Period 的不可变性」的类,从而造成更大的危害。这种推断并不牵强:实际上,有许多类的安全性就是依赖于 String 的不可变性。
|
||||
|
||||
问题的根源在于,`Period` 的 `readObject` 方法并没有完成足够的保护性拷贝。 **当一个对象被反序列化的时候,对于客户端不应该拥有的对象引用,如果那个字段包含了这样的对象引用,就必须做保护性拷贝,这是非常重要的。** 因此,对于每个可序列化的不可变类,如果它好汉了私有的可变字段,那么在它的 `readObject` 方法中,必须要对这些字段进行保护性拷贝。下面的这些 `readObject` 方法可以确保 `Period` 类的约束条件不会遭到破坏,以保持它的不可变性:
|
||||
问题的根源在于,`Period` 的 `readObject` 方法并没有完成足够的保护性拷贝。 **当一个对象被反序列化的时候,对于客户端不应该拥有的对象引用,如果那个字段包含了这样的对象引用,就必须做保护性拷贝,这是非常重要的。** 因此,对于每个可序列化的不可变类,如果它包含了私有的可变字段,那么在它的 `readObject` 方法中,必须要对这些字段进行保护性拷贝。下面的这些 `readObject` 方法可以确保 `Period` 类的约束条件不会遭到破坏,以保持它的不可变性:
|
||||
|
||||
```java
|
||||
// readObject method with defensive copying and validity checking
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 90. 考虑用序列化代理代替序列化实例
|
||||
|
||||
正如 85 条和第 86 条提到的,以及本章一直在讨论的,决定实现 Serializable 接口,会增加出错和出现安全问题的可能性,因为它允许利用语言之外的机制来创建实例,而不是使用普通的构造器。然而,有一只方法可以极大的减少这些风险。就是序列化代理模式(seralization proxy pattern)。
|
||||
正如 85 条和第 86 条提到的,以及本章一直在讨论的,决定实现 Serializable 接口,会增加出错和出现安全问题的可能性,因为它允许利用语言之外的机制来创建实例,而不是使用普通的构造器。然而,有一种方法可以极大的减少这些风险。就是序列化代理模式(seralization proxy pattern)。
|
||||
|
||||
序列化代理模式相当简单。首先,为可序列化的类设计一个私有的静态嵌套类,精确地表示外围类的逻辑状态。这个嵌套类被称为序列化代理(seralization proxy),它应该有一个单独的构造器,其参数类型就是那个外围类。这个构造器只是从它的参数中复制数据:它不需要进行任何一致性检验或者保护性拷贝。从设计的角度看,序列化代理的默认序列化形式是外围类最好的序列化形式。外围类及其序列代理都必须声明实现 `Serializable` 接口。
|
||||
|
||||
|
||||
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