├── .gitignore
├── LICENSE
├── README.md
├── docs
├── .vuepress
│ └── config.js
├── README.md
├── appendices
│ ├── app-becoming-a-programmer.md
│ ├── app-benefits-and-costs-of-static-type-checking.md
│ ├── app-collection-topics.md
│ ├── app-data-compression.md
│ ├── app-io-streams.md
│ ├── app-javadoc.md
│ ├── app-low-level-concurrency.md
│ ├── app-new-io.md
│ ├── app-object-serialization.md
│ ├── app-passing-and-returning-objects.md
│ ├── app-programming-guidelines.md
│ ├── app-standard-io.md
│ ├── app-supplements.md
│ ├── app-the-positive-legacy-of-c-plus-plus-and-java.md
│ └── app-understanding-equals-and-hashcode.md
├── ch1.md
├── ch10.md
├── ch11.md
├── ch12.md
├── ch13.md
├── ch14.md
├── ch15.md
├── ch16.md
├── ch17.md
├── ch18.md
├── ch19.md
├── ch2.md
├── ch20.md
├── ch21.md
├── ch22.md
├── ch23.md
├── ch24.md
├── ch25.md
├── ch3.md
├── ch4.md
├── ch5.md
├── ch6.md
├── ch7.md
├── ch8.md
├── ch9.md
├── cover.png
├── figures
│ ├── On-Java-8_En-176.png
│ ├── On-Java-8_En-204.png
│ ├── On-Java-8_En-206.png
│ ├── On-Java-8_En-216.png
│ ├── On-Java-8_En-217.png
│ ├── On-Java-8_En-218.png
│ ├── On-Java-8_En-220.png
│ ├── On-Java-8_En-224.png
│ ├── On-Java-8_En-230.png
│ ├── On-Java-8_En-234.png
│ ├── On-Java-8_En-26.png
│ ├── On-Java-8_En-279.png
│ ├── On-Java-8_En-31.png
│ ├── On-Java-8_En-32.png
│ ├── On-Java-8_En-33.png
│ ├── On-Java-8_En-34.png
│ ├── On-Java-8_En-36.png
│ ├── On-Java-8_En-37.png
│ ├── On-Java-8_En-39.png
│ ├── On-Java-8_En-40.png
│ ├── On-Java-8_En-429.png
│ ├── On-Java-8_En-612.png
│ ├── On-Java-8_En-639.png
│ ├── On-Java-8_En-640.png
│ ├── On-Java-8_En-643.png
│ ├── On-Java-8_En-645.png
│ ├── On-Java-8_En-713.png
│ ├── On-Java-8_En-714.png
│ ├── On-Java-8_En-716.png
│ ├── On-Java-8_En-718.png
│ ├── On-Java-8_En-719.png
│ ├── On-Java-8_En-720.png
│ ├── On-Java-8_En-721.png
│ ├── On-Java-8_En-722.png
│ ├── On-Java-8_En-723.png
│ └── On-Java-8_En-725.png
├── images
│ ├── 1545758268350.png
│ ├── 1545763399825.png
│ ├── 1545764724202.png
│ ├── 1545764780795.png
│ ├── 1545764820176.png
│ ├── 1545839316314.png
│ ├── 1545841270997.png
│ ├── 1554546258113.png
│ ├── 1554546378822.png
│ ├── 1554546452861.png
│ ├── 1554546627710.png
│ ├── 1554546666685.png
│ ├── 1554546693664.png
│ ├── 1554546847181.png
│ ├── 1554546861836.png
│ ├── 1554546881189.png
│ ├── 1554546890132.png
│ ├── 1561774164644.png
│ ├── 1562204648023.png
│ ├── 1562252767216.png
│ ├── 1562406479787.png
│ ├── 1562409366637.png
│ ├── 1562409926765.png
│ ├── 1562653648586.png
│ ├── 1562737974623.png
│ ├── 1562999314238.png
│ ├── QQGroupQRCode.png
│ ├── collection.png
│ ├── cover.jpg
│ ├── cover_small.jpg
│ ├── designproxy.png
│ ├── image-20190409114913825-4781754.png
│ ├── level_1_title.png
│ ├── level_2_title.png
│ ├── map.png
│ ├── qqgroup.png
│ ├── reader.png
│ └── simple-collection-taxonomy.png
├── introduction.md
├── preface.md
└── yarn.lock
├── gitee-deploy.sh
├── images
├── 1545758268350.png
├── 1545763399825.png
├── 1545764724202.png
├── 1545764780795.png
├── 1545764820176.png
├── 1545839316314.png
├── 1545841270997.png
├── 1554546258113.png
├── 1554546378822.png
├── 1554546452861.png
├── 1554546627710.png
├── 1554546666685.png
├── 1554546693664.png
├── 1554546847181.png
├── 1554546861836.png
├── 1554546881189.png
├── 1554546890132.png
├── 1561774164644.png
├── 1562204648023.png
├── 1562252767216.png
├── 1562406479787.png
├── 1562409366637.png
├── 1562409926765.png
├── 1562653648586.png
├── 1562737974623.png
├── 1562999314238.png
├── QQGroupQRCode.png
├── collection.png
├── cover.jpg
├── cover_small.jpg
├── designproxy.png
├── image-20190409114913825-4781754.png
├── level_1_title.png
├── level_2_title.png
├── map.png
├── qqgroup.png
├── reader.png
└── simple-collection-taxonomy.png
└── package.json
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | docs/.vuepress/dist/
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018-present, Yuxi (Evan) You
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # OnJava8-zh
2 |
3 | OnJava8 中文翻译
4 |
5 | 在线阅读:[http://gdut_yy.gitee.io/doc-onjava8/](http://gdut_yy.gitee.io/doc-onjava8/)
6 |
7 |
8 |
9 | ## 前言
10 |
11 | ## Index
12 |
13 | - [前言](docs/preface.md)
14 | - [简介](docs/introduction.md)
15 |
16 | ---
17 |
18 | - [第 1 章 对象的概念](docs/ch1.md)
19 | - [第 2 章 安装 Java 和本书用例](docs/ch2.md)
20 | - [第 3 章 万物皆对象](docs/ch3.md)
21 | - [第 4 章 运算符](docs/ch4.md)
22 | - [第 5 章 控制流](docs/ch5.md)
23 | - [第 6 章 初始化和清理](docs/ch6.md)
24 | - [第 7 章 封装](docs/ch7.md)
25 | - [第 8 章 复用](docs/ch8.md)
26 | - [第 9 章 多态](docs/ch9.md)
27 | - [第 10 章 接口](docs/ch10.md)
28 | - [第 11 章 内部类](docs/ch11.md)
29 | - [第 12 章 集合](docs/ch12.md)
30 | - [第 13 章 函数式编程](docs/ch13.md)
31 | - [第 14 章 流式编程](docs/ch14.md)
32 | - [第 15 章 异常](docs/ch15.md)
33 | - [第 16 章 代码校验](docs/ch16.md)
34 | - [第 17 章 文件](docs/ch17.md)
35 | - [第 18 章 字符串](docs/ch18.md)
36 | - [第 19 章 类型信息](docs/ch19.md)
37 | - [第 20 章 泛型](docs/ch20.md)
38 | - [第 21 章 数组](docs/ch21.md)
39 | - [第 22 章 枚举](docs/ch22.md)
40 | - [第 23 章 注解](docs/ch23.md)
41 | - [第 24 章 并发编程](docs/ch24.md)
42 | - [第 25 章 设计模式](docs/ch25.md)
43 |
44 | ---
45 |
46 | - [附录:补充](docs/appendices/app-supplements.md)
47 | - [附录:编程指南](docs/appendices/app-programming-guidelines.md)
48 | - [附录:文档注释](docs/appendices/app-javadoc.md)
49 | - [附录:对象传递和返回](docs/appendices/app-passing-and-returning-objects.md)
50 | - [附录:流式 IO](docs/appendices/app-io-streams.md)
51 | - [附录:标准 IO](docs/appendices/app-standard-io.md)
52 | - [附录:新 IO](docs/appendices/app-new-io.md)
53 | - [附录:理解 equals 和 hashCode 方法](docs/appendices/app-understanding-equals-and-hashcode.md)
54 | - [附录:集合主题](docs/appendices/app-collection-topics.md)
55 | - [附录:并发底层原理](docs/appendices/app-low-level-concurrency.md)
56 | - [附录:数据压缩](docs/appendices/app-data-compression.md)
57 | - [附录:对象序列化](docs/appendices/app-object-serialization.md)
58 | - [附录:静态语言类型检查](docs/appendices/app-benefits-and-costs-of-static-type-checking.md)
59 | - [附录:C++和 Java 的优良传统](docs/appendices/app-the-positive-legacy-of-c-plus-plus-and-java.md)
60 | - [附录:成为一名程序员](docs/appendices/app-becoming-a-programmer.md)
61 |
62 | ## 本地开发 & 阅读
63 |
64 | 本项目基于 vuepress 进行开发,以提供比 github mardown 更佳的阅读体验
65 |
66 | 依赖于 `node.js`、`yarn`、`vuepress` 等环境
67 |
68 | ```sh
69 | # node
70 | node -v
71 | > v10.14.1
72 | # yarn
73 | yarn -v
74 | > 1.13.0
75 | # vuepress
76 | yarn global add vuepress
77 |
78 | # 本地开发
79 | git clone https://github.com/gdut-yy/OnJava8-zh.git
80 | cd OnJava8-zh
81 | yarn docs:dev
82 |
83 | # 本地阅读
84 | http://localhost:8080/doc-onjava8/
85 | ```
86 |
87 | ## License
88 |
89 | [MIT](https://github.com/gdut-yy/OnJava8-zh/blob/master/LICENSE)
90 |
--------------------------------------------------------------------------------
/docs/.vuepress/config.js:
--------------------------------------------------------------------------------
1 | // .vuepress/config.js
2 | module.exports = {
3 | // 网站的标题
4 | title: "OnJava8 中文",
5 | // 上下文根
6 | base: "/doc-onjava8/",
7 | themeConfig: {
8 | // 假定是 GitHub. 同时也可以是一个完整的 GitLab URL
9 | repo: "gdut-yy/OnJava8-zh",
10 | // 自定义仓库链接文字。默认从 `themeConfig.repo` 中自动推断为
11 | // "GitHub"/"GitLab"/"Bitbucket" 其中之一,或是 "Source"。
12 | repoLabel: "Github",
13 | // 以下为可选的编辑链接选项
14 | // 假如你的文档仓库和项目本身不在一个仓库:
15 | docsRepo: "gdut-yy/OnJava8-zh",
16 | // 假如文档放在一个特定的分支下:
17 | docsBranch: "master/docs",
18 | // 默认是 false, 设置为 true 来启用
19 | editLinks: true,
20 | // 默认为 "Edit this page"
21 | editLinkText: "帮助我们改善此页面!",
22 | // 最后更新时间
23 | lastUpdated: "Last Updated",
24 | // 最大深度
25 | sidebarDepth: 4,
26 | // 导航栏
27 | nav: [],
28 | // 侧边栏
29 | sidebar: {
30 | "/": [
31 | "",
32 | "preface.md",
33 | "introduction.md",
34 | "ch1.md",
35 | "ch2.md",
36 | "ch3.md",
37 | "ch4.md",
38 | "ch5.md",
39 | "ch6.md",
40 | "ch7.md",
41 | "ch8.md",
42 | "ch9.md",
43 | "ch10.md",
44 | "ch11.md",
45 | "ch12.md",
46 | "ch13.md",
47 | "ch14.md",
48 | "ch15.md",
49 | "ch16.md",
50 | "ch17.md",
51 | "ch18.md",
52 | "ch19.md",
53 | "ch20.md",
54 | "ch21.md",
55 | "ch22.md",
56 | "ch23.md",
57 | "ch24.md",
58 | "ch25.md",
59 | "appendices/app-supplements.md",
60 | "appendices/app-programming-guidelines.md",
61 | "appendices/app-javadoc.md",
62 | "appendices/app-passing-and-returning-objects.md",
63 | "appendices/app-io-streams.md",
64 | "appendices/app-standard-io.md",
65 | "appendices/app-new-io.md",
66 | "appendices/app-understanding-equals-and-hashcode.md",
67 | "appendices/app-collection-topics.md",
68 | "appendices/app-low-level-concurrency.md",
69 | "appendices/app-data-compression.md",
70 | "appendices/app-object-serialization.md",
71 | "appendices/app-benefits-and-costs-of-static-type-checking.md",
72 | "appendices/app-the-positive-legacy-of-c-plus-plus-and-java.md",
73 | "appendices/app-becoming-a-programmer.md"
74 | ]
75 | }
76 | }
77 | };
78 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # OnJava8 中文
2 |
3 |
4 |

5 |
6 |
7 | ## 序
8 |
9 | - 本书原作者为 [美] Bruce Eckel,即《Java 编程思想》的作者。
10 | - 本书是事实上的 《Java 编程思想》第五版。
11 | - 《Java 编程思想》第四版基于 JAVA 5 版本;《On Java 8》 基于 JAVA 8 版本。
12 |
13 | ## 目录
14 |
15 | - [前言](preface.md)
16 | - [简介](introduction.md)
17 |
18 | ---
19 |
20 | - [第 1 章 对象的概念](ch1.md)
21 | - [第 2 章 安装 Java 和本书用例](ch2.md)
22 | - [第 3 章 万物皆对象](ch3.md)
23 | - [第 4 章 运算符](ch4.md)
24 | - [第 5 章 控制流](ch5.md)
25 | - [第 6 章 初始化和清理](ch6.md)
26 | - [第 7 章 封装](ch7.md)
27 | - [第 8 章 复用](ch8.md)
28 | - [第 9 章 多态](ch9.md)
29 | - [第 10 章 接口](ch10.md)
30 | - [第 11 章 内部类](ch11.md)
31 | - [第 12 章 集合](ch12.md)
32 | - [第 13 章 函数式编程](ch13.md)
33 | - [第 14 章 流式编程](ch14.md)
34 | - [第 15 章 异常](ch15.md)
35 | - [第 16 章 代码校验](ch16.md)
36 | - [第 17 章 文件](ch17.md)
37 | - [第 18 章 字符串](ch18.md)
38 | - [第 19 章 类型信息](ch19.md)
39 | - [第 20 章 泛型](ch20.md)
40 | - [第 21 章 数组](ch21.md)
41 | - [第 22 章 枚举](ch22.md)
42 | - [第 23 章 注解](ch23.md)
43 | - [第 24 章 并发编程](ch24.md)
44 | - [第 25 章 设计模式](ch25.md)
45 |
46 | ---
47 |
48 | - [附录:补充](appendices/app-supplements.md)
49 | - [附录:编程指南](appendices/app-programming-guidelines.md)
50 | - [附录:文档注释](appendices/app-javadoc.md)
51 | - [附录:对象传递和返回](appendices/app-passing-and-returning-objects.md)
52 | - [附录:流式 IO](appendices/app-io-streams.md)
53 | - [附录:标准 IO](appendices/app-standard-io.md)
54 | - [附录:新 IO](appendices/app-new-io.md)
55 | - [附录:理解 equals 和 hashCode 方法](appendices/app-understanding-equals-and-hashcode.md)
56 | - [附录:集合主题](appendices/app-collection-topics.md)
57 | - [附录:并发底层原理](appendices/app-low-level-concurrency.md)
58 | - [附录:数据压缩](appendices/app-data-compression.md)
59 | - [附录:对象序列化](appendices/app-object-serialization.md)
60 | - [附录:静态语言类型检查](appendices/app-benefits-and-costs-of-static-type-checking.md)
61 | - [附录:C++和 Java 的优良传统](appendices/app-the-positive-legacy-of-c-plus-plus-and-java.md)
62 | - [附录:成为一名程序员](appendices/app-becoming-a-programmer.md)
63 |
--------------------------------------------------------------------------------
/docs/appendices/app-becoming-a-programmer.md:
--------------------------------------------------------------------------------
1 | # 附录:成为一名程序员
2 |
3 | >我分别于2003,2006,2007和2009年撰写的博客文章混搭
4 |
5 | ## 如何开始
6 |
7 | 这是一条相当漫长和曲折的道路。我在高一学代数时(1971年),有个非常古怪的老师有一台计算机,还弄到了一台配有一个300波特的音频电话耦合器的ASR-33电传打字机,我学会了如何执行命令并得到响应,以及一个可以在高中区使用的HP-1000计算机上的帐户。我们能够创建和运行BASIC程序并将它们保存在打孔磁带上。我对此非常着迷,所以尽可能地把它带回家后在晚上写程序。我写了一个赛马模拟游戏--HOSRAC.BAS,用星号来代表马的移动,由于是在纸上打印输出,所以需要一点想象力。
8 |
9 | 我的朋友丹尼尔(就是设计我的书封面的人)有一个兄弟,他有段时间通过向酒吧和餐馆提供弹球机来赚钱。他有一台投币式街机(老虎机),最早的《乓》游戏之一,我对此全然不知,到现在我还忍受不了这东西(现在我几乎不玩电脑游戏,这样看来我可能是个没有幽默的人,但似乎编程比玩电脑游戏更有趣、更具挑战性。)
10 |
11 | 后来我在高中参与了摄影和新闻工作,在大学的第一年就主修新闻学。我觉得自己已经从学校学到了足够多的东西,又转修了物理学。后来我在加州大学欧文分校完成了物理学位,如果我当时选择了一个特定的工程领域,修了足够的工程课就能拿到双专业,但我试图走得更远一些,所以最后我获得的本科学位是 "应用物理"。作为一名本科生,我多多少少学习了一些可以自娱自乐,但又没有任何深度的计算机编程课程。我个人认为在这些课程细细熏陶下,帮我打下了一定的基础,但事实我理解的这些东西没有任何深度。我不知道计算机、编译器或解释器有什么区别(只是对编译器和解释器一点点的理解)。对我来说计算机是绝对可靠的,而且我从来没有想过在程序语言和操作系统中会有出现错误的可能。
12 |
13 | 后来我去了在加州州立理工大学攻读研究生,主要有三点原因
14 |
15 | 1. 我真的非常喜欢物理学这个领域
16 |
17 | 2. 他们接受了我,甚至给了我一份教学工作和奖学金
18 |
19 | 3. 出乎意料的是他们给我的工作时间不止一个夏天
20 |
21 | 而我完全没做好上班的准备。
22 |
23 | 作为一名物理专业的学生,我学习的是太阳能发电系统,当时太阳能发电系统很大 (如果你的房子上装了太阳能或生意上是关于太阳能系统,加州就会给予税收抵免,因此也兴起很多生意),加州理工大学也承诺会在工程系开设相应的课程。然而因为学校没有提供必要的课程,要想获得在太阳能工程的学位得花好几年时间。所以我学习了研究生其他的工程课,包括介绍机械,太阳能,电气和电子工程。我上的课是非电气工程专业的电气工程导论。最常见的研究生工程课程是计算机工程专业,所以最后我拿了那个学位。我还上了艺术课,几门舞蹈课,还有一些计算机科学课程 (Pascal和数据结构),在计算机工程中,我终于弄清楚了处理器的工作流程,从那以后我一直带着一个处理器在身上。这些就是我学的计算机基础知识。
24 |
25 | 刚开始工作的时候,凭借着一堆硬件和相对简单低水平的编程,做了一名计算机工程师。因为C语言似乎是理想的嵌入式系统语言,于是我开始自学,并慢慢开始了解更多关于编程语言的东西。我们在这家公司从源代码构建编译器,这让我大开眼界。 (想象一下一个编译器只是另一个软件的一部分!)
26 |
27 | 当我去华盛顿大学海洋学院为Tom Keffer后来创建了“疯狗浪”)工作时,我们决定使用C++。我只有一本Stroustrup写的非初学者书可以参考,最终不得不通过检查C++预处理器生成的中间C代码来了解语言的功能。这个过程非常痛苦,但学习的效果很好。从那以后我就用相同的方式学习,因为它让我学习了如何剖析一种语言,并看到它本质的能力,与此同时开始有了批判性思维。
28 |
29 | 我并没有理解清楚所有的概念。只是在之后的日子里不断反复,我所知道的一切需要时间才能消化吸收。如果我现在能很容易地理解一个新概念,那只是因为它是我已经知道的积累概念的一个变种。在加州理工大学招收非计算机本科学历的计算机科学研究生项目中,学生们曾经说他们花了一年的时间才弄清楚他们对计算机的困惑(他们正在沉浸程序之中)。当人们学习计算机时,他们往往会对自己抱有不切实际的期望,通常是他们听说学计算机编程的好处,就希望在几周内找到一份高薪的工作。但是,最好的学习过程是先对计算机感兴趣,随着时间的推移,学习的越来越多,自然的就开始自学。
30 |
31 | 这些就是我主要做的事,尽管我通过学计算机工程有还算扎实的基础,但我没上过编程课,而是通过自学。在此期间我也在不断地学习新事物,在这个行业里,不断学习是非常重要的一部分。
32 |
33 | ## 码农生涯
34 |
35 | 我会定期收到有关职业建议的请求,所以我尝试在这里回答一下这个问题。
36 |
37 | 人们提出的问题通常是错误的问题:“我应该学习 C++ 还是 Java ?”在本文中,我将尝试阐述我对选择计算机职业所涉及的真正问题的看法。
38 |
39 | 请注意,我在这里并不是和那些已经知道自己使命的人聊(译者注:指计划成为程序员或者已经从业的程序员,暗指这里是讲给外行的小白的)。因为无论别人怎么说,你都要去做,因为它已经渗入你的血液,并且你将无法摆脱它。你已经知道答案了:你当然会学到 C++ ,Java ,shell 脚本,Python 和许多其他语言和技术。即使你只有14岁,你也已经知道其中几种语言。
40 |
41 | 问我这个问题的人可能来自另一职业。也许他们来自 Web 开发等领域,他们已经发现 HTML 只是一种类似编程,他们想尝试构建更实质的内容。但是,我特别希望,如果你提出这个问题,你就已经意识到,要在计算机领域取得成功,你必须教自己如何学习,并且永不停止学习。
42 |
43 | 随着我做的越来越多,在我看来,软件越发比其他任何东西都更像写作。而且我们还没有弄清怎样成为一个好的作家,我们只知道何时我们喜欢别人写的东西。这不是像一些工程那样,我们要做的只是将某些东西放到一端,然后转动曲柄。诱人的是将软件视为确定性的,这就是我们想要的,这就是我们不断推出工具来帮助我们实现所需行为的原因。但是我的经验不断表明事实是相反的:它更多地是关于人而不是过程,并且它在确定性机器上运行的事实变得越来越没有影响力(指运行环境受机器影响,与机器相关这个事实),就像海森堡原理(不确定性原理:不可能同时知道一个粒子的位置和它的速度)不会在人类规模上影响事物一样。
44 |
45 | 在我青年时期,父亲是建造民居的,我偶尔会为他工作,大部分时间都从事艰苦的工作,有时还得悬挂石膏板。他和他的木匠会告诉我说,他们是为了我才把这些工作交给了我 —— 为了不让我从事这项工作。这确实是有效的。
46 |
47 | 因此,我也可以用比喻说,建造软件就像盖房子一样。我们并不是指每个在房屋上工作的人都一样。有混凝土泥瓦匠,屋顶工,水管工,电工,石膏板工人,抹灰工,瓷砖铺砌工,普通劳工,粗木匠,精整木匠,当然还有总承包商。这些中的每一个都需要一套不同的技能,这需要花费不同的时间和精力 房屋建造也受制于繁荣和萧条的周期,例如编程。为了快速起步,你可能需要当普通劳工或石膏板工人工作,在那里你可以在没有太多学习曲线的情况下开始获得报酬。只要需求旺盛,你就可以稳定工作,而且如果没有足够的人来工作,你的薪水甚至可能会上涨。但是一旦经济低迷,木匠甚至总承包商就可以自己将石膏板挂起来。
48 |
49 | 当 Internet 刚兴起时,你所要做的就是花一些时间学习 HTML ,就可以找到一份工作并赚到很多钱。但是,当情况恶化时,你很快就会发现需要的技能层次结构很深,HTML 程序员(例如劳工和石膏板工)排在第一位,而高技能的码农和木匠则被保留。
50 |
51 | 我想在这里说的是:除非你准备致力于终身学习,否则请不要从事这项业务。有时,编程似乎是一份报酬丰厚,值得信赖的工作,但确保这一点的唯一方法是,始终使自己变得更有价值。
52 |
53 | 当然,也可以找到例外。总会有一些人只学习一种语言,并且足够精通,甚至足够聪明,那么可以在不用多学很多其他知识的情况下继续工作。但是他们靠运气生存,最终很脆弱。为了减少自身的脆弱性,必须通过阅读,参加用户组,会议和研讨会来不断提高自己的能力。你在该领域的走得越深,你的价值就越大,这意味着你的工作前景更稳定,并且可以获得更高的薪水。
54 |
55 | 另一种方法是从总体上看待该领域,并找到一个你能成为专家的点。例如,我的兄弟对软件感兴趣,并且涉足软件,但是他的业务是安装计算机,维修计算机和升级计算机。他一直都很细致,因此,当他安装或修理计算机时,你会知道计算机状态良好。不仅是软件,而且一直到电缆,电缆都整齐地捆扎在一起,并且不成束。他的工作多到做不完,而且他从不关心网络泡沫破灭。毋庸置疑,他是不可能失业的。
56 |
57 | 我在大学待了很长时间,并以各种方式设法度过了难关。我甚至开始在加州大学洛杉矶分校攻读博士学位。这里的课程很短,我欣慰地说是因为我不再爱上大学了,而我在大学待了这么长时间的原因是因为我非常喜欢。但是我喜欢的通常是跑偏的东西。例如艺术,舞蹈课程,在大学报社工作,以及我参加的少数计算机编程课程(由于我是物理本科生和计算机工程专业的研究生,所以也算跑偏)。尽管我在学业上还算是出色的(具有讽刺意味的是,当时许多不接受我作为学生的大学现在都在课程中使用我的书),但我确实很享受大学生的生活,并且完成了博士学位。我可能会走上简单的道路,最终成为一名教授。
58 |
59 | 但是事实证明,我从大学获得的最大价值一部分来自那些跑偏的课程,这些课程使我的思维超出了“我们已经知道的东西”。我认为在计算机领域尤其如此,因为你总是通过编程来实现其他目标,而你对该目标越了解,你的表现就会越好(我学习了一些欧洲研究生课程,这些课程要求结合其他一些专业研究计算,通过解决这个领域相关的问题,你就会形成一种新的理论体系并可以将它用在别处)。
60 |
61 | 我还认为,不仅编程,多了解一些其它的知识,还可以大大提高你的解决问题的能力(就像了解一种以上的编程语言可以极大地提高你的编程能力一样)。在很多情况下,我遇到过仅接受过计算机科学训练的人,他们的思维似乎比其他背景(例如数学或物理学)的人更受限制,但其实这些人(数学或物理学领域的人)才更需要严格的思维。
62 |
63 | 在我组织的一次会议上,主题之一是为理想的求职者提供一系列功能:
64 |
65 | - 将学习作为一种生活方式。例如,学习一种以上的语言;没有什么比学习另一种语言更能吸引你的眼球。
66 | - 知道在哪里以及如何获得新知识。
67 | - 研究现有技术。
68 | - 我们是工具使用者,即要善于利用工具。
69 | - 学习做最简单的事情。
70 | - 了解业务(阅读杂志。从 *fast company*(国外一家商业杂志)开始,该公司的文章非常简短有趣。然后你就会知道是否要阅读其他的)
71 | - 应对错误负责。 “我用着没事”是不可接受的策略。查找自己的错误。
72 | - 成为领导者:那些沟通和鼓舞别人的人。
73 | - 你在为谁服务?
74 | - 没有正确的答案……但总是更好的方法。展示和讨论你的代码,不要有情感上的依恋。你不是你的代码。
75 | - 这是通往完美的渐进旅程。
76 |
77 | 承担一切可能的风险,最好的风险是那些可怕的风险,但是在尝试时你会比想象中的更加活跃。最好不要刻意去预测某个特定的结果,因为如果你过于重视某个结果,就会经常错过真正的可能性。应该“让我们做一点实验,看看会把我们带到哪里”。这些实验是我最好的冒险。
78 |
79 | 有些人对这个答案感到失望,然后回答“是的,这都是非常有趣和有用的。但是实际上,我应该学习什么? C++ 还是 Java ?”,以防这些问题,我将在这里重复一遍:我知道似乎所有的 1 和 0 都应该使一切具有确定性因此此类问题应该有一个简单的答案,但事实并非如此。这与做出选择并完成选择无关,这是有关持续学习和有时需要大胆的选择。相信我,这样你的生活会更加令人兴奋。
80 |
81 | ### 延伸阅读
82 | * [Teach Yourself Programming In Ten Years](http://norvig.com/21-days.html), by Peter Norvig.
83 | * [How To Be A Programmer](http://samizdat.mines.edu/howto/HowToBeAProgrammer.html), by Robert Read.
84 | * A [speech by Steve Jobs](http://news.stanford.edu/news/2005/june15/jobs-061505.html) to inspire a group of graduating college students.
85 | * Kathy Sierra: [Does College Matter](https://headrush.typepad.com/creating_passionate_users/2005/07/does_college_ma.html)?
86 | * Paul Graham [on College](http://www.paulgraham.com/college.html).
87 | * Joel Spolsky: [Advice for Computer Science College Students](https://www.joelonsoftware.com/2005/01/02/advice-for-computer-science-college-students/).
88 | * James Shore: [Five Design Skills Every Programmer Should Have](https://www.jamesshore.com/Blog/Five-Design-Skills.html).
89 | * Steve Yegge: [The Truth About Interviewing](http://steve-yegge.blogspot.com/2006/03/truth-about-interviewing.html).
90 |
91 | ## 百分之五的神话
92 |
93 |
94 | ## 重在动手
95 |
96 |
97 | ## 像打字般编程
98 |
99 |
100 | ## 做你喜欢的事
101 |
102 | *“1960年,一位研究人员对1500名商学院学生进行了访谈,并将他们分为两类:那些为了钱财来这里上学的人,1245人,以及那些打算利用学位做他们非常关心的事情的人,255人。二十年后,研究人员再次访谈了这些毕业生,发现其中有101位百万富翁,除了其中一位,所有百万富翁都来自追求他们喜欢做的事的那255人!”*
103 |
104 | “现在你可能觉得你对巴洛克时期的冰岛诗歌,或者蝴蝶收集,或者高尔夫,抑或是对社会正义的热情,会因为要养家糊口而让你和你喜欢做的事分道扬镳,并非一定要如此。弗拉基米尔·纳博科夫(Vladimir Nabokov)是本世纪最伟大的小说家之一,他对蝴蝶收藏的热情远远超过写作。事实上,他的第一个大学教学工作是关于鳞翅类昆虫。在过去40年里,对40万美国群众的研究表明,即使是部分的、零散的追求培养你的激情,也可以帮助你充分利用你目前的能力,激励你培养新的能力。”--摘自《The Other 90%》 Robert K.Cooper
105 |
106 | 当然你可以看Po Bronson写的《 What Should I Do With My Life?》这本书,对这些想法进行更多的探索。
107 |
108 |
--------------------------------------------------------------------------------
/docs/appendices/app-benefits-and-costs-of-static-type-checking.md:
--------------------------------------------------------------------------------
1 | # 附录:静态语言类型检查
2 |
3 | > 这是一本我多年来撰写的经过编辑过的论文集,论文集试图将静态检查语言和动态语言之间的争论放到一个正确的角度。还有一个前言部分,描述了我最近对这个话题的思考和见解。
4 |
5 | ## 前言
6 |
7 | ## 静态类型检查和测试
8 |
9 | ## 如何提升打字
10 |
11 | ## 生产力的成本
12 |
13 | ## 静态和动态
14 |
--------------------------------------------------------------------------------
/docs/appendices/app-data-compression.md:
--------------------------------------------------------------------------------
1 | # 附录:数据压缩
2 |
3 | Java I/O 类库提供了可以读写压缩格式流的类。你可以将其他 I/O 类包装起来用于提供压缩功能。
4 |
5 | 这些类不是从 **Reader** 和 **Writer** 类派生的,而是 **InputStream** 和 **OutputStream** 层级结构的一部分。这是由于压缩库处理的是字节,而不是字符。但是,你可能会被迫混合使用两种类型的流(请记住,你可以使用 **InputStreamReader** 和 **OutputStreamWriter**,这两个类可以在字节类型和字符类型之间轻松转换)。
6 |
7 | | 压缩类 | 功能 |
8 | | ------------------------ | ---------------------------------------------------------------------- |
9 | | **CheckedInputStream** | `getCheckSum()` 可以对任意 **InputStream** 计算校验和(而不只是解压) |
10 | | **CheckedOutputStream** | `getCheckSum()` 可以对任意 **OutputStream** 计算校验和(而不只是压缩) |
11 | | **DeflaterOutputStream** | 压缩类的基类 |
12 | | **ZipOutputStream** | **DeflaterOutputStream** 类的一种,用于压缩数据到 Zip 文件结构 |
13 | | **GZIPOutputStream** | **DeflaterOutputStream** 类的一种,用于压缩数据到 GZIP 文件结构 |
14 | | **InflaterInputStream** | 解压类的基类 |
15 | | **ZipInputStream** | **InflaterInputStream** 类的一种,用于解压 Zip 文件结构的数据 |
16 | | **GZIPInputStream** | **InflaterInputStream** 类的一种,用于解压 GZIP 文件结构的数据 |
17 |
18 | 尽管存在很多压缩算法,但是 Zip 和 GZIP 可能是最常见的。你可以使用许多用于读取和写入这些格式的工具,来轻松操作压缩数据。
19 |
20 | ## 使用 Gzip 简单压缩
21 |
22 | GZIP 接口十分简单,因此当你有一个需要压缩的数据流(而不是一个包含不同数据分片的容器)时,使用 GZIP 更为合适。如下是一个压缩单个文件的示例:
23 |
24 | ```java
25 | // compression/GZIPcompress.java
26 | // (c)2017 MindView LLC: see Copyright.txt
27 | // We make no guarantees that this code is fit for any purpose.
28 | // Visit http://OnJava8.com for more book information.
29 | // {java GZIPcompress GZIPcompress.java}
30 | // {VisuallyInspectOutput}
31 |
32 | public class GZIPcompress {
33 | public static void main(String[] args) {
34 | if (args.length == 0) {
35 | System.out.println(
36 | "Usage: \nGZIPcompress file\n" +
37 | "\tUses GZIP compression to compress " +
38 | "the file to test.gz");
39 | System.exit(1);
40 | }
41 | try (
42 | InputStream in = new BufferedInputStream(
43 | new FileInputStream(args[0]));
44 | BufferedOutputStream out =
45 | new BufferedOutputStream(
46 | new GZIPOutputStream(
47 | new FileOutputStream("test.gz")))
48 | ) {
49 | System.out.println("Writing file");
50 | int c;
51 | while ((c = in.read()) != -1)
52 | out.write(c);
53 | } catch (IOException e) {
54 | throw new RuntimeException(e);
55 | }
56 | System.out.println("Reading file");
57 | try (
58 | BufferedReader in2 = new BufferedReader(
59 | new InputStreamReader(new GZIPInputStream(
60 | new FileInputStream("test.gz"))))
61 | ) {
62 | in2.lines().forEach(System.out::println);
63 | } catch (IOException e) {
64 | throw new RuntimeException(e);
65 | }
66 | }
67 | }
68 |
69 | ```
70 |
71 | 使用压缩类非常简单,你只需要把你的输出流包装在 **GZIPOutputStream** 或 **ZipOutputStream** 中,将输入流包装在 **GZIPInputStream** 或 **ZipInputStream**。其他的一切就只是普通的 I/O 读写。这是面向字符流和面向字节流的混合示例;in 使用 Reader 类,而 **GZIPOutputStreams** 构造函数只能接受 **OutputStream** 对象,而不能接受 **Writer** 对象。当打开文件的时候,**GZIPInputStream** 会转换成为 **Reader**。
72 |
73 | ## 使用 zip 多文件存储
74 |
75 | 支持 Zip 格式的库比 GZIP 库更广泛。有了它,你可以轻松存储多个文件,甚至还有一个单独的类可以轻松地读取 Zip 文件。该库使用标准 Zip 格式,因此它可以与当前可在 Internet 上下载的所有 Zip 工具无缝协作。以下示例与前一个示例具有相同的形式,但它可以根据需要处理任意数量的命令行参数。此外,它还显示了 **Checksum** 类计算和验证文件的校验和。有两种校验和类型:Adler32(更快)和 CRC32(更慢但更准确)。
76 |
77 | ```java
78 | // compression/ZipCompress.java
79 | // (c)2017 MindView LLC: see Copyright.txt
80 | // We make no guarantees that this code is fit for any purpose.
81 | // Visit http://OnJava8.com for more book information.
82 | // Uses Zip compression to compress any
83 | // number of files given on the command line
84 | // {java ZipCompress ZipCompress.java}
85 | // {VisuallyInspectOutput}
86 | public class ZipCompress {
87 | public static void main(String[] args) {
88 | try (
89 | FileOutputStream f =
90 | new FileOutputStream("test.zip");
91 | CheckedOutputStream csum =
92 | new CheckedOutputStream(f, new Adler32());
93 | ZipOutputStream zos = new ZipOutputStream(csum);
94 | BufferedOutputStream out =
95 | new BufferedOutputStream(zos)
96 | ) {
97 | zos.setComment("A test of Java Zipping");
98 | // No corresponding getComment(), though.
99 | for (String arg : args) {
100 | System.out.println("Writing file " + arg);
101 | try (
102 | InputStream in = new BufferedInputStream(
103 | new FileInputStream(arg))
104 | ) {
105 | zos.putNextEntry(new ZipEntry(arg));
106 | int c;
107 | while ((c = in.read()) != -1)
108 | out.write(c);
109 | }
110 | out.flush();
111 | }
112 | // Checksum valid only after the file is closed!
113 | System.out.println(
114 | "Checksum: " + csum.getChecksum().getValue());
115 | } catch (IOException e) {
116 | throw new RuntimeException(e);
117 | }
118 | // Now extract the files:
119 | System.out.println("Reading file");
120 | try (
121 | FileInputStream fi =
122 | new FileInputStream("test.zip");
123 | CheckedInputStream csumi =
124 | new CheckedInputStream(fi, new Adler32());
125 | ZipInputStream in2 = new ZipInputStream(csumi);
126 | BufferedInputStream bis =
127 | new BufferedInputStream(in2)
128 | ) {
129 | ZipEntry ze;
130 | while ((ze = in2.getNextEntry()) != null) {
131 | System.out.println("Reading file " + ze);
132 | int x;
133 | while ((x = bis.read()) != -1)
134 | System.out.write(x);
135 | }
136 | if (args.length == 1)
137 | System.out.println(
138 | "Checksum: " + csumi.getChecksum().getValue());
139 | } catch (IOException e) {
140 | throw new RuntimeException(e);
141 | }
142 | // Alternative way to open and read Zip files:
143 | try (
144 | ZipFile zf = new ZipFile("test.zip")
145 | ) {
146 | Enumeration e = zf.entries();
147 | while (e.hasMoreElements()) {
148 | ZipEntry ze2 = (ZipEntry) e.nextElement();
149 | System.out.println("File: " + ze2);
150 | // ... and extract the data as before
151 | }
152 | } catch (IOException e) {
153 | throw new RuntimeException(e);
154 | }
155 | }
156 | }
157 | ```
158 |
159 | 对于要添加到存档的每个文件,必须调用 `putNextEntry()` 并传递 **ZipEntry** 对象。 **ZipEntry** 对象包含一个扩展接口,用于获取和设置 Zip 文件中该特定条目的所有可用数据:名称,压缩和未压缩大小,日期,CRC 校验和,额外字段数据,注释,压缩方法以及它是否是目录条目。但是,即使 Zip 格式有设置密码的方法,Java 的 Zip 库也不支持。虽然 **CheckedInputStream** 和 **CheckedOutputStream** 都支持 Adler32 和 CRC32 校验和,但 **ZipEntry** 类仅支持 CRC 接口。这是对基础 Zip 格式的限制,但它可能会限制你使用更快的 Adler32。
160 |
161 | 要提取文件,**ZipInputStream** 有一个 `getNextEntry()` 方法,这个方法在有文件存在的情况下调用,会返回下一个 **ZipEntry**。作为一个更简洁的替代方法,你可以使用 **ZipFile** 对象读取该文件,该对象具有方法 entries() 返回一个包裹 **ZipEntries** 的 **Enumeration**。
162 |
163 | 要读取校验和,你必须以某种方式访问关联的 **Checksum** 对象。这里保留了对 **CheckedOutputStream** 和 **CheckedInputStream** 对象的引用,但你也可以保持对 **Checksum** 对象的引用。 Zip 流中的一个令人困惑的方法是 `setComment()`。如 **ZipCompress** 所示。在 Java 中,你可以在编写文件时设置注释,但是没有办法恢复 **ZipInputStream** 中的注释。注释似乎仅通过 **ZipEntry** 在逐个条目的基础上完全支持。
164 |
165 | 使用 GZIP 或 Zip 库时,你不仅被限制于文件——你可以压缩任何内容,包括通过网络连接发送的数据。
166 |
167 | ## Java 的 jar
168 |
169 | Zip 格式也用于 JAR(Java ARchive)文件格式,这是一种将一组文件收集到单个压缩文件中的方法,就像 Zip 一样。但是,与 Java 中的其他所有内容一样,JAR 文件是跨平台的,因此你不必担心平台问题。你还可以将音频和图像文件像类文件一样包含在其中。
170 |
171 | JAR 文件由一个包含压缩文件集合的文件和一个描述它们的“清单(manifest)”组成。(你可以创建自己的清单文件;否则,jar 程序将为你执行此操作。)你可以在 JDK 文档中,找到更多关于 JAR 清单的信息。
172 |
173 | JDK 附带的 jar 工具会自动压缩你选择的文件。你可以在命令行上调用它:
174 |
175 | ```shell
176 | jar [options] destination [manifest] inputfile(s)
177 | ```
178 |
179 | 选项是一组字母(不需要连字符或任何其他指示符)。 Unix / Linux 用户会注意到这些选项与 tar 命令选项的相似性。这些是:
180 |
181 | | 选项 | 功能 |
182 | | ---------- | ---------------------------------------------------------------------------------------------------------------------------------- |
183 | | **c** | 创建一个新的或者空的归档文件 |
184 | | **t** | 列出内容目录 |
185 | | **x** | 提取所有文件 |
186 | | **x** file | 提取指定的文件 |
187 | | **f** | 这代表着,“传递文件的名称。”如果你不使用它,jar 假定它的输入将来自标准输入,或者,如果它正在创建一个文件,它的输出将转到标准输出。 |
188 | | **m** | 代表第一个参数是用户创建的清单文件的名称。 |
189 | | **v** | 生成详细的输出用于表述 jar 所作的事情 |
190 | | **0** | 仅存储文件;不压缩文件(用于创建放在类路径中的 JAR 文件)。 |
191 | | **M** | 不要自动创建清单文件 |
192 |
193 | 如果放入 JAR 文件的文件中包含子目录,则会自动添加该子目录,包括其所有子目录等。还会保留路径信息。
194 |
195 | 以下是一些调用 jar 的典型方法。以下命令创建名为 myJarFile 的 JAR 文件。 jar 包含当前目录中的所有类文件,以及自动生成的清单文件:
196 |
197 | ```shell
198 | jar cf myJarFile.jar *.class
199 | ```
200 |
201 | 下一个命令与前面的示例类似,但它添加了一个名为 myManifestFile.mf 的用户创建的清单文件。 :
202 |
203 | ```shell
204 | jar cmf myJarFile.jar myManifestFile.mf *.class
205 | ```
206 |
207 | 这个命令输出了 myJarFile.jar 中的文件目录:
208 |
209 | ```shell
210 | jar tf myJarFile.jar
211 | ```
212 |
213 | 如下添加了一个“verbose”的标志,用于生成更多关于 myJarFile.jar 中文件的详细信息:
214 |
215 | ```shell
216 | jar tvf myJarFile.jar
217 | ```
218 |
219 | 假设 audio,classes 和 image 都是子目录,它将所有子目录组合到文件 myApp.jar 中。还包括“verbose”标志,以便在 jar 程序工作时提供额外的反馈:
220 |
221 | ```shell
222 | jar cvf myApp.jar audio classes image
223 | ```
224 |
225 | 如果你在创建 JAR 文件时使用了 0(零) 选项,该文件将会被替换在你的类路径(CLASSPATH)中:
226 |
227 | ```shell
228 | CLASSPATH="lib1.jar;lib2.jar;"
229 | ```
230 |
231 | 然后 Java 可以搜索到 lib1.jar 和 lib2.jar 的类文件。
232 |
233 | jar 工具不像 Zip 实用程序那样通用。例如,你无法将文件添加或更新到现有 JAR 文件;只能从头开始创建 JAR 文件。
234 |
235 | 此外,你无法将文件移动到 JAR 文件中,在移动文件时将其删除。
236 |
237 | 但是,在一个平台上创建的 JAR 文件可以通过任何其他平台上的 jar 工具透明地读取(这个问题有时会困扰 Zip 实用程序)。
238 |
239 |
--------------------------------------------------------------------------------
/docs/appendices/app-io-streams.md:
--------------------------------------------------------------------------------
1 | # 附录:流式 IO
2 |
3 | > Java 7 引入了一种简单明了的方式来读写文件和操作目录。大多情况下,[文件](./17-Files.md)这一章所介绍的那些库和技术就足够你用了。但是,如果你必须面对一些特殊的需求和比较底层的操作,或者处理一些老版本的代码,那么你就必须了解本附录中的内容。
4 |
5 | 对于编程语言的设计者来说,实现良好的输入/输出(I/O)系统是一项比较艰难的任务,不同实现方案的数量就可以证明这点。其中的挑战似乎在于要涵盖所有的可能性,你不仅要覆盖到不同的 I/O 源和 I/O 接收器(如文件、控制台、网络连接等),还要实现多种与它们进行通信的方式(如顺序、随机访问、缓冲、二进制、字符、按行和按字等)。
6 |
7 | Java 类库的设计者通过创建大量的类来解决这一难题。一开始,你可能会对 Java I/O 系统提供了如此多的类而感到不知所措。Java 1.0 之后,Java 的 I/O 类库发生了明显的改变,在原来面向字节的类中添加了面向字符和基于 Unicode 的类。在 Java 1.4 中,为了改进性能和功能,又添加了 `nio` 类(全称是 “new I/O”,Java 1.4 引入,到现在已经很多年了)。这部分在[附录:新 I/O](./Appendix-New-IO.md) 中介绍。
8 |
9 | 因此,要想充分理解 Java I/O 系统以便正确运用它,我们需要学习一定数量的类。另外,理解 I/O 类库的演化过程也很有必要,因为如果缺乏历史的眼光,很快我们就会对什么时候该使用哪些类,以及什么时候不该使用它们而感到困惑。
10 |
11 | 编程语言的 I/O 类库经常使用**流**这个抽象概念,它将所有数据源或者数据接收器表示为能够产生或者接收数据片的对象。
12 |
13 | > **注意**:Java 8 函数式编程中的 `Stream` 类和这里的 I/O stream 没有任何关系。这又是另一个例子,如果再给设计者一次重来的机会,他们将使用不同的术语。
14 |
15 | I/O 流屏蔽了实际的 I/O 设备中处理数据的细节:
16 |
17 | 1. 字节流对应原生的二进制数据;
18 | 2. 字符流对应字符数据,它会自动处理与本地字符集之间的转换;
19 | 3. 缓冲流可以提高性能,通过减少底层 API 的调用次数来优化 I/O。
20 |
21 | 从 JDK 文档的类层次结构中可以看到,Java 类库中的 I/O 类分成了输入和输出两部分。在设计 Java 1.0 时,类库的设计者们就决定让所有与输入有关系的类都继承自 `InputStream`,所有与输出有关系的类都继承自 `OutputStream`。所有从 `InputStream` 或 `Reader` 派生而来的类都含有名为 `read()` 的基本方法,用于读取单个字节或者字节数组。同样,所有从 `OutputStream` 或 `Writer` 派生而来的类都含有名为 `write()` 的基本方法,用于写单个字节或者字节数组。但是,我们通常不会用到这些方法,它们之所以存在是因为别的类可以使用它们,以便提供更有用的接口。
22 |
23 | 我们很少使用单一的类来创建流对象,而是通过叠合多个对象来提供所期望的功能(这是**装饰器设计模式**)。为了创建一个流,你却要创建多个对象,这也是 Java I/O 类库让人困惑的主要原因。
24 |
25 | 这里我只会提供这些类的概述,并假定你会使用 JDK 文档来获取它们的详细信息(比如某个类的所以方法的详细列表)。
26 |
27 | ## 输入流类型
28 |
29 | `InputStream` 表示那些从不同数据源产生输入的类,如[表 I/O-1](#table-io-1) 所示,这些数据源包括:
30 |
31 | 1. 字节数组;
32 | 2. `String` 对象;
33 | 3. 文件;
34 | 4. “管道”,工作方式与实际生活中的管道类似:从一端输入,从另一端输出;
35 | 5. 一个由其它种类的流组成的序列,然后我们可以把它们汇聚成一个流;
36 | 6. 其它数据源,如 Internet 连接。
37 |
38 | 每种数据源都有相应的 `InputStream` 子类。另外,`FilterInputStream` 也属于一种 `InputStream`,它的作用是为“装饰器”类提供基类。其中,“装饰器”类可以把属性或有用的接口与输入流连接在一起,这个我们稍后再讨论。
39 |
40 | **表 I/O-1 `InputStream` 类型**
41 |
42 | | 类 | 功能 | 构造器参数 | 如何使用 |
43 | | :-----------------------: | :----------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------- | :---------------------------------------------------------------------- |
44 | | `ByteArrayInputStream` | 允许将内存的缓冲区当做 `InputStream` 使用 | 缓冲区,字节将从中取出 | 作为一种数据源:将其与 `FilterInputStream` 对象相连以提供有用接口 |
45 | | `StringBufferInputStream` | 将 `String` 转换成 `InputStream` | 字符串。底层实现实际使用 `StringBuffer` | 作为一种数据源:将其与 `FilterInputStream` 对象相连以提供有用接口 |
46 | | `FileInputStream` | 用于从文件中读取信息 | 字符串,表示文件名、文件或 `FileDescriptor` 对象 | 作为一种数据源:将其与 `FilterInputStream` 对象相连以提供有用接口 |
47 | | `PipedInputStream` | 产生用于写入相关 `PipedOutputStream` 的数据。实现“管道化”概念 | `PipedOutputSteam` | 作为多线程中的数据源:将其与 `FilterInputStream` 对象相连以提供有用接口 |
48 | | `SequenceInputStream` | 将两个或多个 `InputStream` 对象转换成一个 `InputStream` | 两个 `InputStream` 对象或一个容纳 `InputStream` 对象的容器 `Enumeration` | 作为一种数据源:将其与 `FilterInputStream` 对象相连以提供有用接口 |
49 | | `FilterInputStream` | 抽象类,作为“装饰器”的接口。其中,“装饰器”为其它的 `InputStream` 类提供有用的功能。见[表 I/O-3](#table-io-3) | 见[表 I/O-3](#table-io-3) | 见[表 I/O-3](#table-io-3) |
50 |
51 | ## 输出流类型
52 |
53 | 如[表 I/O-2](#table-io-2) 所示,该类别的类决定了输出所要去往的目标:字节数组(但不是 `String`,当然,你也可以用字节数组自己创建)、文件或管道。
54 |
55 | 另外,`FilterOutputStream` 为“装饰器”类提供了一个基类,“装饰器”类把属性或者有用的接口与输出流连接了起来,这些稍后会讨论。
56 |
57 | **表 I/O-2:`OutputStream` 类型**
58 |
59 | | 类 | 功能 | 构造器参数 | 如何使用 |
60 | | :---------------------: | :------------------------------------------------------------------------------------------------------ | :----------------------------------------------- | :------------------------------------------------------------------------------- |
61 | | `ByteArrayOutputStream` | 在内存中创建缓冲区。所有送往“流”的数据都要放置在此缓冲区 | 缓冲区初始大小(可选) | 用于指定数据的目的地:将其与 `FilterOutputStream` 对象相连以提供有用接口 |
62 | | `FileOutputStream` | 用于将信息写入文件 | 字符串,表示文件名、文件或 `FileDescriptor` 对象 | 用于指定数据的目的地:将其与 `FilterOutputStream` 对象相连以提供有用接口 |
63 | | `PipedOutputStream` | 任何写入其中的信息都会自动作为相关 `PipedInputStream` 的输出。实现“管道化”概念 | `PipedInputStream` | 指定用于多线程的数据的目的地:将其与 `FilterOutputStream` 对象相连以提供有用接口 |
64 | | `FilterOutputStream` | 抽象类,作为“装饰器”的接口。其中,“装饰器”为其它 `OutputStream` 提供有用功能。见[表 I/O-4](#table-io-4) | 见[表 I/O-4](#table-io-4) | 见[表 I/O-4](#table-io-4) |
65 |
66 | ## 添加属性和有用的接口
67 |
68 | 装饰器在[泛型](./20-Generics.md)这一章引入。Java I/O 类库需要多种不同功能的组合,这正是使用装饰器模式的原因所在[^1]。而之所以存在 **filter**(过滤器)类,是因为让抽象类 **filter** 作为所有装饰器类的基类。装饰器必须具有和它所装饰对象相同的接口,但它也可以扩展接口,不过这种情况只发生在个别 **filter** 类中。
69 |
70 | 但是,装饰器模式也有一个缺点:在编写程序的时候,它给我们带来了相当多的灵活性(因为我们可以很容易地对属性进行混搭),但它同时也增加了代码的复杂性。Java I/O 类库操作不便的原因在于:我们必须创建许多类(“核心” I/O 类型加上所有的装饰器)才能得到我们所希望的单个 I/O 对象。
71 |
72 | `FilterInputStream` 和 `FilterOutputStream` 是用来提供装饰器类接口以控制特定输入流 `InputStream` 和 输出流 `OutputStream` 的两个类,但它们的名字并不是很直观。`FilterInputStream` 和 `FilterOutputStream` 分别从 I/O 类库中的基类 `InputStream` 和 `OutputStream` 派生而来,这两个类是创建装饰器的必要条件(这样它们才能为所有被装饰的对象提供统一接口)。
73 |
74 | ### 通过 `FilterInputStream` 从 `InputStream` 读取
75 |
76 | `FilterInputStream` 类能够完成两件截然不同的事情。其中,`DataInputStream` 允许我们读取不同的基本数据类型和 `String` 类型的对象(所有方法都以 “read” 开头,例如 `readByte()`、`readFloat()`等等)。搭配其对应的 `DataOutputStream`,我们就可以通过数据“流”将基本数据类型的数据从一个地方迁移到另一个地方。具体是那些“地方”是由[表 I/O-1](#table-io-1) 中的那些类决定的。
77 |
78 | 其它 `FilterInputStream` 类则在内部修改 `InputStream` 的行为方式:是否缓冲,是否保留它所读过的行(允许我们查询行数或设置行数),以及是否允许把单个字符推回输入流等等。最后两个类看起来就像是为了创建编译器提供的(它们被添加进来可能是为了对“用 Java 构建编译器”实现提供支持),因此我们在一般编程中不会用到它们。
79 |
80 | 在实际应用中,不管连接的是什么 I/O 设备,我们基本上都会对输入进行缓冲。所以当初 I/O 类库如果能默认都让输入进行缓冲,同时将无缓冲输入作为一种特殊情况(或者只是简单地提供一个方法调用),这样会更加合理,而不是像现在这样迫使我们基本上每次都得手动添加缓冲。
81 |
82 | **表 I/O-3:`FilterInputStream` 类型**
83 |
84 | | 类 | 功能 | 构造器参数 | 如何使用 |
85 | | :---------------------: | :------------------------------------------------------------------------------------------- | :---------------------------------------- | :------------------------------------------------------- |
86 | | `DataInputStream` | 与 `DataOutputStream` 搭配使用,按照移植方式从流读取基本数据类型(`int`、`char`、`long` 等) | `InputStream` | 包含用于读取基本数据类型的全部接口 |
87 | | `BufferedInputStream` | 使用它可以防止每次读取时都得进行实际写操作。代表“使用缓冲区” | `InputStream`,可以指定缓冲区大小(可选) | 本质上不提供接口,只是向进程添加缓冲功能。与接口对象搭配 |
88 | | `LineNumberInputStream` | 跟踪输入流中的行号,可调用 `getLineNumber()` 和 `setLineNumber(int)` | `InputStream` | 仅增加了行号,因此可能要与接口对象搭配使用 |
89 | | `PushbackInputStream` | 具有能弹出一个字节的缓冲区,因此可以将读到的最后一个字符回退 | `InputStream` | 通常作为编译器的扫描器,我们可能永远也不会用到 |
90 |
91 | ### 通过 `FilterOutputStream` 向 `OutputStream` 写入
92 |
93 | 与 `DataInputStream` 对应的是 `DataOutputStream`,它可以将各种基本数据类型和 `String` 类型的对象格式化输出到“流”中,。这样一来,任何机器上的任何 `DataInputStream` 都可以读出它们。所有方法都以 “write” 开头,例如 `writeByte()`、`writeFloat()` 等等。
94 |
95 | `PrintStream` 最初的目的就是为了以可视化格式打印所有基本数据类型和 `String` 类型的对象。这和 `DataOutputStream` 不同,后者的目的是将数据元素置入“流”中,使 `DataInputStream` 能够可移植地重构它们。
96 |
97 | `PrintStream` 内有两个重要方法:`print()` 和 `println()`。它们都被重载了,可以打印各种各种数据类型。`print()` 和 `println()` 之间的差异是,后者在操作完毕后会添加一个换行符。
98 |
99 | `PrintStream` 可能会造成一些问题,因为它捕获了所有 `IOException`(因此,我们必须使用 `checkError()` 自行测试错误状态,如果出现错误它会返回 `true`)。另外,`PrintStream` 没有处理好国际化问题。这些问题都在 `PrintWriter` 中得到了解决,这在后面会讲到。
100 |
101 | `BufferedOutputStream` 是一个修饰符,表明这个“流”使用了缓冲技术,因此每次向流写入的时候,不是每次都会执行物理写操作。我们在进行输出操作的时候可能会经常用到它。
102 |
103 | **表 I/O-4:`FilterOutputStream` 类型**
104 |
105 | | 类 | 功能 | 构造器参数 | 如何使用 |
106 | | :--------------------: | :---------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------- | :------------------------------------------------------------ |
107 | | `DataOutputStream` | 与 `DataInputStream` 搭配使用,因此可以按照移植方式向流中写入基本数据类型(`int`、`char`、`long` 等) | `OutputStream` | 包含用于写入基本数据类型的全部接口 |
108 | | `PrintStream` | 用于产生格式化输出。其中 `DataOutputStream` 处理数据的存储,`PrintStream` 处理显示 | `OutputStream`,可以用 `boolean` 值指示是否每次换行时清空缓冲区(可选) | 应该是对 `OutputStream` 对象的 `final` 封装。可能会经常用到它 |
109 | | `BufferedOutputStream` | 使用它以避免每次发送数据时都进行实际的写操作。代表“使用缓冲区”。可以调用 `flush()` 清空缓冲区 | `OutputStream`,可以指定缓冲区大小(可选) | 本质上并不提供接口,只是向进程添加缓冲功能。与接口对象搭配 |
110 |
111 | ## Reader 和 Writer
112 |
113 | Java 1.1 对基本的 I/O 流类库做了重大的修改。你初次遇到 `Reader` 和 `Writer` 时,可能会以为这两个类是用来替代 `InputStream` 和 `OutputStream` 的,但实际上并不是这样。尽管一些原始的“流”类库已经过时了(如果使用它们,编译器会发出警告),但是 `InputStream` 和 `OutputStream` 在面向字节 I/O 这方面仍然发挥着极其重要的作用,而 `Reader` 和 `Writer` 则提供兼容 Unicode 和面向字符 I/O 的功能。另外:
114 |
115 | 1. Java 1.1 往 `InputStream` 和 `OutputStream` 的继承体系中又添加了一些新类,所以这两个类显然是不会被取代的;
116 |
117 | 2. 有时我们必须把来自“字节”层级结构中的类和来自“字符”层次结构中的类结合起来使用。为了达到这个目的,需要用到“适配器(adapter)类”:`InputStreamReader` 可以把 `InputStream` 转换为 `Reader`,而 `OutputStreamWriter` 可以把 `OutputStream` 转换为 `Writer`。
118 |
119 | 设计 `Reader` 和 `Writer` 继承体系主要是为了国际化。老的 I/O 流继承体系仅支持 8 比特的字节流,并且不能很好地处理 16 比特的 Unicode 字符。由于 Unicode 用于字符国际化(Java 本身的 `char` 也是 16 比特的 Unicode),所以添加 `Reader` 和 `Writer` 继承体系就是为了让所有的 I/O 操作都支持 Unicode。另外,新类库的设计使得它的操作比旧类库要快。
120 |
121 | ### 数据的来源和去处
122 |
123 | 几乎所有原始的 Java I/O 流类都有相应的 `Reader` 和 `Writer` 类来提供原生的 Unicode 操作。但是在某些场合,面向字节的 `InputStream` 和 `OutputStream` 才是正确的解决方案。特别是 `java.util.zip` 类库就是面向字节而不是面向字符的。因此,最明智的做法是尽量**尝试**使用 `Reader` 和 `Writer`,一旦代码没法成功编译,你就会发现此时应该使用面向字节的类库了。
124 |
125 | 下表展示了在两个继承体系中,信息的来源和去处(即数据物理上来自哪里又去向哪里)之间的对应关系:
126 |
127 | | 来源与去处:Java 1.0 类 | 相应的 Java 1.1 类 |
128 | | :---------------------------------: | :-----------------------------------------: |
129 | | `InputStream` | `Reader`
适配器:`InputStreamReader` |
130 | | `OutputStream` | `Writer`
适配器:`OutputStreamWriter` |
131 | | `FileInputStream` | `FileReader` |
132 | | `FileOutputStream` | `FileWriter` |
133 | | `StringBufferInputStream`(已弃用) | `StringReader` |
134 | | (无相应的类) | `StringWriter` |
135 | | `ByteArrayInputStream` | `CharArrayReader` |
136 | | `ByteArrayOutputStream` | `CharArrayWriter` |
137 | | `PipedInputStream` | `PipedReader` |
138 | | `PipedOutputStream` | `PipedWriter` |
139 |
140 | 总的来说,这两个不同的继承体系中的接口即便不能说完全相同,但也是非常相似的。
141 |
142 | ### 更改流的行为
143 |
144 | 对于 `InputStream` 和 `OutputStream` 来说,我们会使用 `FilterInputStream` 和 `FilterOutputStream` 的装饰器子类来修改“流”以满足特殊需要。`Reader` 和 `Writer` 的类继承体系沿用了相同的思想——但是并不完全相同。
145 |
146 | 在下表中,左右之间对应关系的近似程度现比上一个表格更加粗略一些。造成这种差别的原因是类的组织形式不同,`BufferedOutputStream` 是 `FilterOutputStream` 的子类,但 `BufferedWriter` 却不是 `FilterWriter` 的子类(尽管 `FilterWriter` 是抽象类,但却没有任何子类,把它放在表格里只是占个位置,不然你可能奇怪 `FilterWriter` 上哪去了)。然而,这些类的接口却又十分相似。
147 |
148 | | 过滤器:Java 1.0 类 | 相应 Java 1.1 类 |
149 | | :---------------------- | :------------------------------------------------------------------------------------------------------------------------ |
150 | | `FilterInputStream` | `FilterReader` |
151 | | `FilterOutputStream` | `FilterWriter` (抽象类,没有子类) |
152 | | `BufferedInputStream` | `BufferedReader`(也有 `readLine()`) |
153 | | `BufferedOutputStream` | `BufferedWriter` |
154 | | `DataInputStream` | 使用 `DataInputStream`( 如果必须用到 `readLine()`,那你就得使用 `BufferedReader`。否则,一般情况下就用 `DataInputStream` |
155 | | `PrintStream` | `PrintWriter` |
156 | | `LineNumberInputStream` | `LineNumberReader` |
157 | | `StreamTokenizer` | `StreamTokenizer`(使用具有 `Reader` 参数的构造器) |
158 | | `PushbackInputStream` | `PushbackReader` |
159 |
160 | 有一条限制需要明确:一旦要使用 `readLine()`,我们就不应该用 `DataInputStream`(否则,编译时会得到使用了过时方法的警告),而应该使用 `BufferedReader`。除了这种情况之外的情形中,`DataInputStream` 仍是 I/O 类库的首选成员。
161 |
162 | 为了使用时更容易过渡到 `PrintWriter`,它提供了一个既能接受 `Writer` 对象又能接受任何 `OutputStream` 对象的构造器。`PrintWriter` 的格式化接口实际上与 `PrintStream` 相同。
163 |
164 | Java 5 添加了几种 `PrintWriter` 构造器,以便在将输出写入时简化文件的创建过程,你马上就会见到它们。
165 |
166 | 其中一种 `PrintWriter` 构造器还有一个执行**自动 flush**[^2] 的选项。如果构造器设置了该选项,就会在每个 `println()` 调用之后,自动执行 flush。
167 |
168 | ### 未发生改变的类
169 |
170 | 有一些类在 Java 1.0 和 Java 1.1 之间未做改变。
171 |
172 | | 以下这些 Java 1.0 类在 Java 1.1 中没有相应类 |
173 | | -------------------------------------------- |
174 | | `DataOutputStream` |
175 | | `File` |
176 | | `RandomAccessFile` |
177 | | `SequenceInputStream` |
178 |
179 | 特别是 `DataOutputStream`,在使用时没有任何变化;因此如果想以可传输的格式存储和检索数据,请用 `InputStream` 和 `OutputStream` 继承体系。
180 |
181 | ## RandomAccessFile 类
182 |
183 | `RandomAccessFile` 适用于由大小已知的记录组成的文件,所以我们可以使用 `seek()` 将文件指针从一条记录移动到另一条记录,然后对记录进行读取和修改。文件中记录的大小不一定都相同,只要我们能确定那些记录有多大以及它们在文件中的位置即可。
184 |
185 | 最初,我们可能难以相信 `RandomAccessFile` 不是 `InputStream` 或者 `OutputStream` 继承体系中的一部分。除了实现了 `DataInput` 和 `DataOutput` 接口(`DataInputStream` 和 `DataOutputStream` 也实现了这两个接口)之外,它和这两个继承体系没有任何关系。它甚至都不使用 `InputStream` 和 `OutputStream` 类中已有的任何功能。它是一个完全独立的类,其所有的方法(大多数都是 `native` 方法)都是从头开始编写的。这么做是因为 `RandomAccessFile` 拥有和别的 I/O 类型本质上不同的行为,因为我们可以在一个文件内向前和向后移动。在任何情况下,它都是自我独立的,直接继承自 `Object`。
186 |
187 | 从本质上来讲,`RandomAccessFile` 的工作方式类似于把 `DataIunputStream` 和 `DataOutputStream` 组合起来使用。另外它还有一些额外的方法,比如使用 `getFilePointer()` 可以得到当前文件指针在文件中的位置,使用 `seek()` 可以移动文件指针,使用 `length()` 可以得到文件的长度。另外,其构造器还需要传入第二个参数(和 C 语言中的 `fopen()` 相同)用来表示我们是准备对文件进行 “随机读”(r)还是“读写”(rw)。它并不支持只写文件,从这点来看,如果当初 `RandomAccessFile` 能设计成继承自 `DataInputStream`,可能也是个不错的实现方式。
188 |
189 | 在 Java 1.4 中,`RandomAccessFile` 的大多数功能(但不是全部)都被 nio 中的**内存映射文件**(mmap)取代,详见[附录:新 I/O](./Appendix-New-IO.md)。
190 |
191 | ## IO 流典型用途
192 |
193 | 尽管我们可以用不同的方式来组合 I/O 流类,但常用的也就其中几种。你可以下面的例子可以作为 I/O 典型用法的基本参照(在你确定无法使用[文件](./17-Files.md)这一章所述的库之后)。
194 |
195 | 在这些示例中,异常处理都被简化为将异常传递给控制台,但是这样做只适用于小型的示例和工具。在你自己的代码中,你需要考虑更加复杂的错误处理方式。
196 |
197 | ### 缓冲输入文件
198 |
199 | 如果想要打开一个文件进行字符输入,我们可以使用一个 `FileInputReader` 对象,然后传入一个 `String` 或者 `File` 对象作为文件名。为了提高速度,我们希望对那个文件进行缓冲,那么我们可以将所产生的引用传递给一个 `BufferedReader` 构造器。`BufferedReader` 提供了 `line()` 方法,它会产生一个 `Stream` 对象:
200 |
201 | ```java
202 | // iostreams/BufferedInputFile.java
203 | // {VisuallyInspectOutput}
204 | import java.io.*;
205 | import java.util.stream.*;
206 |
207 | public class BufferedInputFile {
208 | public static String read(String filename) {
209 | try (BufferedReader in = new BufferedReader(
210 | new FileReader(filename))) {
211 | return in.lines()
212 | .collect(Collectors.joining("\n"));
213 | } catch (IOException e) {
214 | throw new RuntimeException(e);
215 | }
216 | }
217 |
218 | public static void main(String[] args) {
219 | System.out.print(
220 | read("BufferedInputFile.java"));
221 | }
222 | }
223 | ```
224 |
225 | `Collectors.joining()` 在其内部使用了一个 `StringBuilder` 来累加其运行结果。该文件会通过 `try-with-resources` 子句自动关闭。
226 |
227 | ### 从内存输入
228 |
229 | 下面示例中,从 `BufferedInputFile.read()` 读入的 `String` 被用来创建一个 `StringReader` 对象。然后调用其 `read()` 方法,每次读取一个字符,并把它显示在控制台上:
230 |
231 | ```java
232 | // iostreams/MemoryInput.java
233 | // {VisuallyInspectOutput}
234 | import java.io.*;
235 |
236 | public class MemoryInput {
237 | public static void
238 | main(String[] args) throws IOException {
239 | StringReader in = new StringReader(
240 | BufferedInputFile.read("MemoryInput.java"));
241 | int c;
242 | while ((c = in.read()) != -1)
243 | System.out.print((char) c);
244 | }
245 | }
246 | ```
247 |
248 | 注意 `read()` 是以 `int` 形式返回下一个字节,所以必须类型转换为 `char` 才能正确打印。
249 |
250 | ### 格式化内存输入
251 |
252 | 要读取格式化数据,我们可以使用 `DataInputStream`,它是一个面向字节的 I/O 类(不是面向字符的)。这样我们就必须使用 `InputStream` 类而不是 `Reader` 类。我们可以使用 `InputStream` 以字节形式读取任何数据(比如一个文件),但这里使用的是字符串。
253 |
254 | ```java
255 | // iostreams/FormattedMemoryInput.java
256 | // {VisuallyInspectOutput}
257 | import java.io.*;
258 |
259 | public class FormattedMemoryInput {
260 | public static void main(String[] args) {
261 | try (
262 | DataInputStream in = new DataInputStream(
263 | new ByteArrayInputStream(
264 | BufferedInputFile.read(
265 | "FormattedMemoryInput.java")
266 | .getBytes()))
267 | ) {
268 | while (true)
269 | System.out.write((char) in.readByte());
270 | } catch (EOFException e) {
271 | System.out.println("\nEnd of stream");
272 | } catch (IOException e) {
273 | throw new RuntimeException(e);
274 | }
275 | }
276 | }
277 | ```
278 |
279 | `ByteArrayInputStream` 必须接收一个字节数组,所以这里我们调用了 `String.getBytes()` 方法。所产生的的 `ByteArrayInputStream` 是一个适合传递给 `DataInputStream` 的 `InputStream`。
280 |
281 | 如果我们用 `readByte()` 从 `DataInputStream` 一次一个字节地读取字符,那么任何字节的值都是合法结果,因此返回值不能用来检测输入是否结束。取而代之的是,我们可以使用 `available()` 方法得到剩余可用字符的数量。下面例子演示了怎么一次一个字节地读取文件:
282 |
283 | ```java
284 | // iostreams/TestEOF.java
285 | // Testing for end of file
286 | // {VisuallyInspectOutput}
287 | import java.io.*;
288 |
289 | public class TestEOF {
290 | public static void main(String[] args) {
291 | try (
292 | DataInputStream in = new DataInputStream(
293 | new BufferedInputStream(
294 | new FileInputStream("TestEOF.java")))
295 | ) {
296 | while (in.available() != 0)
297 | System.out.write(in.readByte());
298 | } catch (IOException e) {
299 | throw new RuntimeException(e);
300 | }
301 | }
302 | }
303 | ```
304 |
305 | 注意,`available()` 的工作方式会随着所读取媒介类型的不同而有所差异,它的字面意思就是“在没有阻塞的情况下所能读取的字节数”。对于文件,能够读取的是整个文件;但是对于其它类型的“流”,可能就不是这样,所以要谨慎使用。
306 |
307 | 我们也可以通过捕获异常来检测输入的末尾。但是,用异常作为控制流是对异常的一种错误使用方式。
308 |
309 | ### 基本文件的输出
310 |
311 | `FileWriter` 对象用于向文件写入数据。实际使用时,我们通常会用 `BufferedWriter` 将其包装起来以增加缓冲的功能(可以试试移除此包装来感受一下它对性能的影响——缓冲往往能显著地增加 I/O 操作的性能)。在本例中,为了提供格式化功能,它又被装饰成了 `PrintWriter`。按照这种方式创建的数据文件可作为普通文本文件来读取。
312 |
313 | ```java
314 | // iostreams/BasicFileOutput.java
315 | // {VisuallyInspectOutput}
316 | import java.io.*;
317 |
318 | public class BasicFileOutput {
319 | static String file = "BasicFileOutput.dat";
320 |
321 | public static void main(String[] args) {
322 | try (
323 | BufferedReader in = new BufferedReader(
324 | new StringReader(
325 | BufferedInputFile.read(
326 | "BasicFileOutput.java")));
327 | PrintWriter out = new PrintWriter(
328 | new BufferedWriter(new FileWriter(file)))
329 | ) {
330 | in.lines().forEach(out::println);
331 | } catch (IOException e) {
332 | throw new RuntimeException(e);
333 | }
334 | // Show the stored file:
335 | System.out.println(BufferedInputFile.read(file));
336 | }
337 | }
338 | ```
339 |
340 | `try-with-resources` 语句会自动 flush 并关闭文件。
341 |
342 | ### 文本文件输出快捷方式
343 |
344 | Java 5 在 `PrintWriter` 中添加了一个辅助构造器,有了它,你在创建并写入文件时,就不必每次都手动执行一些装饰的工作。下面的代码使用这种快捷方式重写了 `BasicFileOutput.java`:
345 |
346 | ```java
347 | // iostreams/FileOutputShortcut.java
348 | // {VisuallyInspectOutput}
349 | import java.io.*;
350 |
351 | public class FileOutputShortcut {
352 | static String file = "FileOutputShortcut.dat";
353 |
354 | public static void main(String[] args) {
355 | try (
356 | BufferedReader in = new BufferedReader(
357 | new StringReader(BufferedInputFile.read(
358 | "FileOutputShortcut.java")));
359 | // Here's the shortcut:
360 | PrintWriter out = new PrintWriter(file)
361 | ) {
362 | in.lines().forEach(out::println);
363 | } catch (IOException e) {
364 | throw new RuntimeException(e);
365 | }
366 | System.out.println(BufferedInputFile.read(file));
367 | }
368 | }
369 | ```
370 |
371 | 使用这种方式仍具备了缓冲的功能,只是现在不必自己手动添加缓冲了。但遗憾的是,其它常见的写入任务都没有快捷方式,因此典型的 I/O 流依旧涉及大量冗余的代码。本书[文件](./17-Files.md)一章中介绍的另一种方式,对此类任务进行了极大的简化。
372 |
373 | ### 存储和恢复数据
374 |
375 | `PrintWriter` 是用来对可读的数据进行格式化。但如果要输出可供另一个“流”恢复的数据,我们可以用 `DataOutputStream` 写入数据,然后用 `DataInputStream` 恢复数据。当然,这些流可能是任何形式,在下面的示例中使用的是一个文件,并且对读写都进行了缓冲。注意 `DataOutputStream` 和 `DataInputStream` 是面向字节的,因此要使用 `InputStream` 和 `OutputStream` 体系的类。
376 |
377 | ```java
378 | // iostreams/StoringAndRecoveringData.java
379 | import java.io.*;
380 |
381 | public class StoringAndRecoveringData {
382 | public static void main(String[] args) {
383 | try (
384 | DataOutputStream out = new DataOutputStream(
385 | new BufferedOutputStream(
386 | new FileOutputStream("Data.txt")))
387 | ) {
388 | out.writeDouble(3.14159);
389 | out.writeUTF("That was pi");
390 | out.writeDouble(1.41413);
391 | out.writeUTF("Square root of 2");
392 | } catch (IOException e) {
393 | throw new RuntimeException(e);
394 | }
395 | try (
396 | DataInputStream in = new DataInputStream(
397 | new BufferedInputStream(
398 | new FileInputStream("Data.txt")))
399 | ) {
400 | System.out.println(in.readDouble());
401 | // Only readUTF() will recover the
402 | // Java-UTF String properly:
403 | System.out.println(in.readUTF());
404 | System.out.println(in.readDouble());
405 | System.out.println(in.readUTF());
406 | } catch (IOException e) {
407 | throw new RuntimeException(e);
408 | }
409 | }
410 | }
411 | ```
412 |
413 | 输出结果:
414 |
415 | ```
416 | 3.14159
417 | That was pi
418 | 1.41413
419 | Square root of 2
420 | ```
421 |
422 | 如果我们使用 `DataOutputStream` 进行数据写入,那么 Java 就保证了即便读和写数据的平台多么不同,我们仍可以使用 `DataInputStream` 准确地读取数据。这一点很有价值,众所周知,人们曾把大量精力耗费在数据的平台相关性问题上。但现在,只要两个平台上都有 Java,就不会存在这样的问题[^3]。
423 |
424 | 当我们使用 `DastaOutputStream` 时,写字符串并且让 `DataInputStream` 能够恢复它的唯一可靠方式就是使用 UTF-8 编码,在这个示例中是用 `writeUTF()` 和 `readUTF()` 来实现的。UTF-8 是一种多字节格式,其编码长度根据实际使用的字符集会有所变化。如果我们使用的只是 ASCII 或者几乎都是 ASCII 字符(只占 7 比特),那么就显得及其浪费空间和带宽,所以 UTF-8 将 ASCII 字符编码成一个字节的形式,而非 ASCII 字符则编码成两到三个字节的形式。另外,字符串的长度保存在 UTF-8 字符串的前两个字节中。但是,`writeUTF()` 和 `readUTF()` 使用的是一种适用于 Java 的 UTF-8 变体(JDK 文档中有这些方法的详尽描述),因此如果我们用一个非 Java 程序读取用 `writeUTF()` 所写的字符串时,必须编写一些特殊的代码才能正确读取。
425 |
426 | 有了 `writeUTF()` 和 `readUTF()`,我们就可以在 `DataOutputStream` 中把字符串和其它数据类型混合使用。因为字符串完全可以作为 Unicode 格式存储,并且可以很容易地使用 `DataInputStream` 来恢复它。
427 |
428 | `writeDouble()` 将 `double` 类型的数字存储在流中,并用相应的 `readDouble()` 恢复它(对于其它的书类型,也有类似的方法用于读写)。但是为了保证所有的读方法都能够正常工作,我们必须知道流中数据项所在的确切位置,因为极有可能将保存的 `double` 数据作为一个简单的字节序列、`char` 或其它类型读入。因此,我们必须:要么为文件中的数据采用固定的格式;要么将额外的信息保存到文件中,通过解析额外信息来确定数据的存放位置。注意,对象序列化和 XML (二者都在[附录:对象序列化](Appendix-Object-Serialization.md)中介绍)是存储和读取复杂数据结构的更简单的方式。
429 |
430 | ### 读写随机访问文件
431 |
432 | 使用 `RandomAccessFile` 就像是使用了一个 `DataInputStream` 和 `DataOutputStream` 的结合体(因为它实现了相同的接口:`DataInput` 和 `DataOutput`)。另外,我们还可以使用 `seek()` 方法移动文件指针并修改对应位置的值。
433 |
434 | 在使用 `RandomAccessFile` 时,你必须清楚文件的结构,否则没法正确使用它。`RandomAccessFile` 有一套专门的方法来读写基本数据类型的数据和 UTF-8 编码的字符串:
435 |
436 | ```java
437 | // iostreams/UsingRandomAccessFile.java
438 | import java.io.*;
439 |
440 | public class UsingRandomAccessFile {
441 | static String file = "rtest.dat";
442 |
443 | public static void display() {
444 | try (
445 | RandomAccessFile rf =
446 | new RandomAccessFile(file, "r")
447 | ) {
448 | for (int i = 0; i < 7; i++)
449 | System.out.println(
450 | "Value " + i + ": " + rf.readDouble());
451 | System.out.println(rf.readUTF());
452 | } catch (IOException e) {
453 | throw new RuntimeException(e);
454 | }
455 | }
456 |
457 | public static void main(String[] args) {
458 | try (
459 | RandomAccessFile rf =
460 | new RandomAccessFile(file, "rw")
461 | ) {
462 | for (int i = 0; i < 7; i++)
463 | rf.writeDouble(i * 1.414);
464 | rf.writeUTF("The end of the file");
465 | rf.close();
466 | display();
467 | } catch (IOException e) {
468 | throw new RuntimeException(e);
469 | }
470 | try (
471 | RandomAccessFile rf =
472 | new RandomAccessFile(file, "rw")
473 | ) {
474 | rf.seek(5 * 8);
475 | rf.writeDouble(47.0001);
476 | rf.close();
477 | display();
478 | } catch (IOException e) {
479 | throw new RuntimeException(e);
480 | }
481 | }
482 | }
483 | ```
484 |
485 | 输出结果:
486 |
487 | ```
488 | Value 0: 0.0
489 | Value 1: 1.414
490 | Value 2: 2.828
491 | Value 3: 4.242
492 | Value 4: 5.656
493 | Value 5: 7.069999999999999
494 | Value 6: 8.484
495 | The end of the file
496 | Value 0: 0.0
497 | Value 1: 1.414
498 | Value 2: 2.828
499 | Value 3: 4.242
500 | Value 4: 5.656
501 | Value 5: 47.0001
502 | Value 6: 8.484
503 | The end of the file
504 | ```
505 |
506 | `display()` 方法打开了一个文件,并以 `double` 值的形式显示了其中的七个元素。在 `main()` 中,首先创建了文件,然后打开并修改了它。因为 `double` 总是 8 字节长,所以如果要用 `seek()` 定位到第 5 个(从 0 开始计数) `double` 值,则要传入的地址值应该为 `5*8`。
507 |
508 | 正如前面所诉,虽然 `RandomAccess` 实现了 `DataInput` 和 `DataOutput` 接口,但实际上它和 I/O 继承体系中的其它部分是分离的。它不支持装饰,故而不能将其与 `InputStream` 及 `OutputStream` 子类中的任何一个组合起来,所以我们也没法给它添加缓冲的功能。
509 |
510 | 该类的构造器还有第二个必选参数:我们可以指定让 `RandomAccessFile` 以“只读”(r)方式或“读写”
511 | (rw)方式打开文件。
512 |
513 | 除此之外,还可以使用 `nio` 中的“内存映射文件”代替 `RandomAccessFile`,这在[附录:新 I/O](Appendix-New-IO.md)中有介绍。
514 |
515 | ## 本章小结
516 |
517 | Java 的 I/O 流类库的确能够满足我们的基本需求:我们可以通过控制台、文件、内存块,甚至因特网进行读写。通过继承,我们可以创建新类型的输入和输出对象。并且我们甚至可以通过重新定义“流”所接受对象类型的 `toString()` 方法,进行简单的扩展。当我们向一个期望收到字符串的方法传送一个非字符串对象时,会自动调用对象的 `toString()` 方法(这是 Java 中有限的“自动类型转换”功能之一)。
518 |
519 | 在 I/O 流类库的文档和设计中,仍留有一些没有解决的问题。例如,我们打开一个文件用于输出,如果在我们试图覆盖这个文件时能抛出一个异常,这样会比较好(有的编程系统只有当该文件不存在时,才允许你将其作为输出文件打开)。在 Java 中,我们应该使用一个 `File` 对象来判断文件是否存在,因为如果我们用 `FileOutputStream` 或者 `FileWriter` 打开,那么这个文件肯定会被覆盖。
520 |
521 | I/O 流类库让我们喜忧参半。它确实挺有用的,而且还具有可移植性。但是如果我们没有理解“装饰器”模式,那么这种设计就会显得不是很直观。所以,它的学习成本相对较高。而且它并不完善,比如说在过去,我不得不编写相当数量的代码去实现一个读取文本文件的工具——所幸的是,Java 7 中的 nio 消除了此类需求。
522 |
523 | 一旦你理解了装饰器模式,并且开始在某些需要这种灵活性的场景中使用该类库,那么你就开始能从这种设计中受益了。到那时候,为此额外多写几行代码的开销应该不至于让人觉得太麻烦。但还是请务必检查一下,确保使用[文件](./17-Files.md)一章中的库和技术没法解决问题后,再考虑使用本章的 I/O 流库。
524 |
525 | [^1]: 很难说这就是一个很好的设计选择,尤其是与其它编程语言中简单的 I/O 类库相比较。但它确实是如此选择的一个正当理由。
526 | [^2]: 译者注:“flush” 直译是“清空”,意思是把缓冲中的数据清空,输送到对应的目的地(如文件和屏幕)。
527 | [^3]: XML 是另一种方式,可以解决在不同计算平台之间移动数据,而不依赖于所有平台上都有 Java 这一问题。XML 将在[附录:对象序列化](./Appendix-Object-Serialization.md)一章中进行介绍。
528 |
--------------------------------------------------------------------------------
/docs/appendices/app-javadoc.md:
--------------------------------------------------------------------------------
1 | # 附录:文档注释
2 |
3 | 编写代码文档的最大问题可能是维护该文档。如果文档和代码是分开的,那么每次更改代码时更改文档都会变得很繁琐。解决方案似乎很简单:将代码链接到文档。最简单的方法是将所有内容放在同一个文件中。然而,要完成这完整的画面,您需要一个特殊的注释语法来标记文档,以及一个工具来将这些注释提取为有用的表单中。这就是 Java 所做的。
4 |
5 | 提取注释的工具称为 Javadoc,它是 JDK 安装的一部分。它使用 Java 编译器中的一些技术来寻找特殊的注释标记。它不仅提取由这些标记所标记的信息,还提取与注释相邻的类名或方法名。通过这种方式,您就可以用最少的工作量来生成合适的程序文档。
6 |
7 | Javadoc 输出为一个 html 文件,您可以使用 web 浏览器查看它。对于 Javadoc,您有一个简单的标准来创建文档,因此您可以期望所有 Java libraries 都有文档。
8 |
9 | 此外,您可以编写自己的 Javadoc 处理程序 doclet,对于 Javadoc(例如,以不同的格式生成输出)。
10 |
11 | 以下是对 Javadoc 基础知识的介绍和概述。在 JDK 文档中可以找到完整的描述。
12 |
13 | ## 句法规则
14 |
15 | 所有 Javadoc 指令都发生在以 **/\*\*** 开头(但仍然以 **\*/** 结尾)的注释中。
16 |
17 | 使用 Javadoc 有两种主要方法:
18 |
19 | 嵌入 HTML 或使用“doc 标签”。独立的 doc 标签是指令它以 **@** 开头,放在注释行的开头。(然而,前面的 **\*** 将被忽略。)可能会出现内联 doc 标签
20 |
21 | Javadoc 注释中的任何位置,也可以,以一个 **@** 开头,但是被花括号包围。
22 |
23 | 有三种类型的注释文档,它们对应于注释前面的元素:类、字段或方法。也就是说,类注释出现在类定义之前,字段注释出现在字段定义之前,方法注释出现在方法定义之前。举个简单的例子:
24 |
25 | ```java
26 | // javadoc/Documentation1.java
27 | /** 一个类注释 */
28 | public class Documentation1 {
29 | /** 一个属性注释 */
30 | public int i;
31 | /** 一个方法注释 */
32 | public void f() {}
33 | }
34 |
35 | ```
36 |
37 | Javadoc 处理注释文档仅适用于 **公共** 和 **受保护** 的成员。
38 |
39 | 默认情况下,将忽略对 **私有成员** 和包访问成员的注释(请参阅["隐藏实现"](/docs/book/07-Implementation-Hiding.md)一章),并且您将看不到任何输出。
40 |
41 | 这是有道理的,因为仅客户端程序员的观点是,在文件外部可以使用 **公共成员** 和 **受保护成员** 。 您可以使用 **-private** 标志和包含 **私人** 成员。
42 |
43 | 要通过 Javadoc 处理前面的代码,命令是:
44 |
45 | ```cmd
46 | javadoc Documentation1.java
47 | ```
48 |
49 | 这将产生一组 HTML 文件。 如果您在浏览器中打开 index.html,您将看到结果与所有其他 Java 文档具有相同的标准格式,因此用户对这种格式很熟悉,并可以轻松地浏览你的类。
50 |
51 | ## 内嵌 HTML
52 |
53 | Javadoc 传递未修改的 HTML 代码,用以生成的 HTML 文档。这使你可以充分利用 HTML。但是,这样做的主要目的是让你格式化代码,例如:
54 |
55 | ```java
56 | // javadoc/Documentation2.java
57 | /**
58 | * System.out.println(new Date());
59 | *
60 | */
61 | public class Documentation2 {}
62 | ```
63 |
64 | 您你也可以像在其他任何 Web 文档中一样使用 HTML 来格式化说明中的文字:
65 |
66 | ```java
67 | // javadoc/Documentation3.java
68 | /** You can even insert a list:
69 | *
70 | * - Item one
71 | *
- Item two
72 | *
- Item three
73 | *
74 | */
75 | public class Documentation3 {}
76 | ```
77 |
78 | 请注意,在文档注释中,Javadoc 删除了行首的星号以及前导空格。 Javadoc 重新格式化了所有内容,使其符合标准文档的外观。不要将诸如 \或 \
之类的标题用作嵌入式 HTML,因为 Javadoc 会插入自己的标题,后插入的标题将对其生成的文档产生干扰。
79 |
80 | 所有类型的注释文档(类,字段和方法)都可以支持嵌入式 HTML。
81 |
82 | ## 示例标签
83 |
84 | 以下是一些可用于代码文档的 Javadoc 标记。在尝试使用 Javadoc 进行任何认真的操作之前,请查阅 JDK 文档中的 Javadoc 参考,以了解使用 Javadoc 的所有不同方法。
85 |
86 | ### @see
87 |
88 | 这个标签可以将其他的类连接到文档中,Javadoc 将使用 @see 标记超链接到其他文档中,形式为:
89 |
90 | ```java
91 | @see classname
92 | @see fully-qualified-classname
93 | @see fully-qualified-classname#method-name
94 | ```
95 |
96 | 每个都向生成的文档中添加超链接的“另请参阅”条目。 Javadoc 不会检查超链接的有效性。
97 |
98 | ### {@link package.class#member label}
99 |
100 | 和 @see 非常相似,不同之处在于它可以内联使用,并使用标签作为超链接文本,而不是“另请参阅”。
101 |
102 | ### {@docRoot}
103 |
104 | 生成文档根目录的相对路径。对于显式超链接到文档树中的页面很有用。
105 |
106 | ### {@inheritDoc}
107 |
108 | 将文档从此类的最近基类继承到当前文档注释中。
109 |
110 | ### @version
111 |
112 | 其形式为:
113 |
114 | ```java
115 | @version version-information
116 | ```
117 |
118 | 其中 version-information 是你认为适合包含的任何重要信息。当在 Javadoc 命令行上放置 -version 标志时,特别在生成的 HTML 文档中用于生成 version 信息。
119 |
120 | ### @author
121 |
122 | 其形式为:
123 |
124 | ```
125 | @author author-information
126 | ```
127 |
128 | author-information 大概率是你的名字,但是一样可以包含你的 email 地址或者其他合适的信息。当在 Javadoc 命令行上放置 -author 标志的时候,在生成的 HTML 文档中特别注明了作者信息。
129 |
130 | 你可以对作者列表使用多个作者标签,但是必须连续放置它们。所有作者信息都集中在生成的 HTML 中的单个段落中。
131 |
132 | ### @since
133 |
134 | 此标记指示此代码的版本开始使用特定功能。例如,它出现在 HTML Java 文档中,以指示功能首次出现的 JDK 版本。
135 |
136 | ### @param
137 |
138 | 这将生成有关方法参数的文档:
139 |
140 | ```java
141 | @param parameter-name description
142 | ```
143 |
144 | 其中 parameter-name 是方法参数列表中的标识符,description 是可以在后续行中继续的文本。当遇到新的文档标签时,说明被视为完成。@param 标签的可以任意使用,大概每个参数一个。
145 |
146 | ### @return
147 |
148 | 这记录了返回值:
149 |
150 | ```java
151 | @return description
152 | ```
153 |
154 | 其中 description 给出了返回值的含义。它可延续到后面的行内。
155 |
156 | ### @throws
157 |
158 | 一个方法可以产生许多不同类型的异常,所有这些异常都需要描述。异常标记的形式为:
159 |
160 | ```java
161 | @throws fully-qualified-class-name description
162 | ```
163 |
164 | fully-qualified-class-name 给出明确的异常分类名称,并且 description (可延续到后面的行内)告诉你为什么这特定类型的异常会在方法调用后出现。
165 |
166 | ### @deprecated
167 |
168 | 这表示已被改进的功能取代的功能。deprecated 标记表明你不再使用此特定功能,因为将来有可能将其删除。标记为@不赞成使用的方法会导致编译器在使用时发出警告。在 Java 5 中,@deprecated Javadoc 标记已被 @Deprecated 注解取代(在[注解]()一章中进行了描述)。
169 |
170 | ## 文档示例
171 |
172 | **objects/HelloDate.java** 是带有文档注释的例子。
173 |
174 | ```java
175 | // javadoc/HelloDateDoc.java
176 | import java.util.*;
177 | /** The first On Java 8 example program.
178 | * Displays a String and today's date.
179 | * @author Bruce Eckel
180 | * @author www.MindviewInc.com
181 | * @version 5.0
182 | */
183 | public class HelloDateDoc {
184 | /** Entry point to class & application.
185 | * @param args array of String arguments
186 | * @throws exceptions No exceptions thrown
187 | */
188 | public static void main(String[] args) {
189 | System.out.println("Hello, it's: ");
190 | System.out.println(new Date());
191 | }
192 | }
193 | /* Output:
194 | Hello, it's:
195 | Tue May 09 06:07:27 MDT 2017
196 | */
197 | ```
198 |
199 | 你可以在 Java 标准库的源代码中找到许多 Javadoc 注释文档的示例。
200 |
--------------------------------------------------------------------------------
/docs/appendices/app-passing-and-returning-objects.md:
--------------------------------------------------------------------------------
1 | # 附录:对象传递和返回
2 |
3 | > 到现在为止,你已经对“传递”对象实际上是传递引用这一想法想法感到满意。
4 |
5 | 在许多编程语言中,你可以使用该语言的“常规”方式来传递对象,并且大多数情况下一切正常。 但是通常会出现这种情况,你必须做一些不平常的事情,突然事情变得更加复杂。 Java 也不例外,当您传递对象并对其进行操作时,准确了解正在发生的事情很重要。 本附录提供了这种见解。
6 |
7 | 提出本附录问题的另一种方法是,如果你之前使用类似 C++的编程语言,则是“ Java 是否有指针?” Java 中的每个对象标识符(除原语外)都是这些指针之一,但它们的用法是不仅受编译器的约束,而且受运行时系统的约束。 换一种说法,Java 有指针,但没有指针算法。 这些就是我一直所说的“引用”,您可以将它们视为“安全指针”,与小学的安全剪刀不同-它们不敏锐,因此您不费吹灰之力就无法伤害自己,但是它们有时可能很乏味。
8 |
9 | ## 传递引用
10 |
11 | 当你将引用传递给方法时,它仍指向同一对象。 一个简单的实验演示了这一点:
12 |
13 | ```java
14 | // references/PassReferences.java
15 | public class PassReferences {
16 | public static void f(PassReferences h) {
17 | System.out.println("h inside f(): " + h);
18 | }
19 | public static void main(String[] args) {
20 | PassReferences p = new PassReferences();
21 | System.out.println("p inside main(): " + p);
22 | f(p);
23 | }
24 | }
25 | /* Output:
26 | p inside main(): PassReferences@15db9742
27 | h inside f(): PassReferences@15db9742
28 | */
29 | ```
30 |
31 | 方法 `toString()` 在打印语句中自动调用,并且 `PassReferences` 直接从 `Object` 继承而无需重新定义 `toString()` 。 因此,使用的是 `Object` 的 `toString()` 版本,它打印出对象的类,然后打印出该对象所在的地址(不是引用,而是实际的对象存储)。
32 |
33 | ## 本地拷贝
34 |
35 | ## 控制克隆
36 |
37 | ## 不可变类
38 |
39 | ## 本章小结
40 |
--------------------------------------------------------------------------------
/docs/appendices/app-programming-guidelines.md:
--------------------------------------------------------------------------------
1 | # 附录:编程指南
2 |
3 | > 本附录包含了有助于指导你进行低级程序设计和编写代码的建议。
4 |
5 | 当然,这些只是指导方针,而不是规则。我们的想法是将它们用作灵感,并记住偶尔会违反这些指导方针的特殊情况。
6 |
7 | ## 设计
8 |
9 | 1. **优雅总是会有回报**。从短期来看,似乎需要更长的时间才能找到一个真正优雅的问题解决方案,但是当该解决方案第一次应用并能轻松适应新情况,而不需要数小时,数天或数月的挣扎时,你会看到奖励(即使没有人可以测量它们)。它不仅为你提供了一个更容易构建和调试的程序,而且它也更容易理解和维护,这也正是经济价值所在。这一点可以通过一些经验来理解,因为当你想要使一段代码变得优雅时,你可能看起来效率不是很高。抵制急于求成的冲动,它只会减慢你的速度。
10 |
11 | 2. **先让它工作,然后再让它变快**。即使你确定一段代码非常重要并且它是你系统中的主要瓶颈\*\*,也是如此。不要这样做。使用尽可能简单的设计使系统首先运行。然后如果速度不够快,请对其进行分析。你几乎总会发现“你的”瓶颈不是问题。节省时间,才是真正重要的东西。
12 |
13 | 3. **记住“分而治之”的原则**。如果所面临的问题太过混乱\*\*,就去想象一下程序的基本操作,因为存在一个处理困难部分的神奇“片段”(piece)。该“片段”是一个对象,编写使用该对象的代码,然后查看该对象并将其困难部分封装到其他对象中,等等。
14 |
15 | 4. **将类创建者与类用户(客户端程序员)分开**。类用户是“客户”,不需要也不想知道类幕后发生了什么。类创建者必须是设计类的专家,他们编写类,以便新手程序员都可以使用它,并仍然可以在应用程序中稳健地工作。将该类视为其他类的*服务提供者*(service provider)。只有对其它类透明,才能很容易地使用这个类。
16 |
17 | 5. **创建类时,给类起个清晰的名字,就算不需要注释也能理解这个类**。你的目标应该是使客户端程序员的接口在概念上变得简单。为此,在适当时使用方法重载来创建直观,易用的接口。
18 |
19 | 6. **你的分析和设计必须至少能够产生系统中的类、它们的公共接口以及它们与其他类的关系,尤其是基类**。 如果你的设计方法产生的不止于此,就该问问自己,该方法生成的所有部分是否在程序的生命周期内都具有价值。如果不是,那么维护它们会很耗费精力。对于那些不会影响他们生产力的东西,开发团队的成员往往不会去维护,这是许多设计方法都没有考虑的生活现实。
20 |
21 | 7. **让一切自动化**。首先在编写类之前,编写测试代码,并将其与类保持一致。通过构建工具自动运行测试。你可能会使用事实上的标准 Java 构建工具 Gradle。这样,通过运行测试代码可以自动验证任何更改,将能够立即发现错误。因为你知道自己拥有测试框架的安全网,所以当发现需要时,可以更大胆地进行彻底的更改。请记住,语言的巨大改进来自内置的测试,包括类型检查,异常处理等,但这些内置功能很有限,你必须完成剩下的工作,针对具体的类或程序,去完善这些测试内容,从而创建一个强大的系统。
22 |
23 | 8. **在编写类之前,先编写测试代码,以验证类的设计是完善的**。如果不编写测试代码,那么就不知道类是什么样的。此外,通过编写测试代码,往往能够激发出类中所需的其他功能或约束。而这些功能或约束并不总是出现在分析和设计过程中。测试还会提供示例代码,显示了如何使用这个类。
24 |
25 | 9. **所有的软件设计问题,都可以通过引入一个额外的间接概念层次(extra level of conceptual indirection)来解决**。这个软件工程的基本规则[^1]是抽象的基础,是面向对象编程的主要特征。在面向对象编程中,我们也可以这样说:“如果你的代码太复杂,就要生成更多的对象。”
26 |
27 | 10. **间接(indirection)应具有意义(与准则 9 一致)**。这个含义可以简单到“将常用代码放在单个方法中。”如果添加没有意义的间接(抽象,封装等)级别,那么它就像没有足够的间接性那样糟糕。
28 |
29 | 11. **使类尽可能原子化**。 为每个类提供一个明确的目的,它为其他类提供一致的服务。如果你的类或系统设计变得过于复杂,请将复杂类分解为更简单的类。最直观的指标是尺寸大小,如果一个类很大,那么它可能是做的事太多了,应该被拆分。建议重新设计类的线索是:
30 |
31 | - 一个复杂的*switch*语句:考虑使用多态。
32 | - 大量方法涵盖了很多不同类型的操作:考虑使用多个类。
33 | - 大量成员变量涉及很多不同的特征:考虑使用多个类。
34 | - 其他建议可以参见 Martin Fowler 的*Refactoring: Improving the Design of Existing Code*(重构:改善既有代码的设计)(Addison-Wesley 1999)。
35 |
36 | 12. **注意长参数列表**。那样方法调用会变得难以编写,读取和维护。相反,尝试将方法移动到更合适的类,并且(或者)将对象作为参数传递。
37 |
38 | 13. **不要重复自己**。如果一段代码出现在派生类的许多方法中,则将该代码放入基类中的单个方法中,并从派生类方法中调用它。这样不仅可以节省代码空间,而且可以轻松地传播更改。有时,发现这个通用代码会为接口添加有价值的功能。此指南的更简单版本也可以在没有继承的情况下发生:如果类具有重复代码的方法,则将该重复代码放入一个公共方,法并在其他方法中调用它。
39 |
40 | 14. **注意*switch*语句或链式*if-else*子句**。一个*类型检查编码*(type-check coding)的指示器意味着需要根据某种类型信息选择要执行的代码(确切的类型最初可能不明显)。很多时候可以用继承和多态替换这种代码,多态方法调用将会执行类型检查,并提供了更可靠和更容易的可扩展性。
41 |
42 | 15. **从设计的角度,寻找和分离那些因不变的事物而改变的事物**。也就是说,在不强制重新设计的情况下搜索可能想要更改的系统中的元素,然后将这些元素封装在类中。
43 |
44 | 16. **不要通过子类扩展基本功能**。如果一个接口元素对于类来说是必不可少的,则它应该在基类中,而不是在派生期间添加。如果要在继承期间添加方法,请考虑重新设计。
45 |
46 | 17. **少即是多**。从一个类的最小接口开始,尽可能小而简单,以解决手头的问题,但不要试图预测类的所有使用方式。在使用该类时,就将会了解如何扩展接口。但是,一旦这个类已经在使用了,就无法在不破坏客户端代码的情况下缩小接口。如果必须添加更多方法,那很好,它不会破坏代码。但即使新方法取代旧方法的功能,也只能是保留现有接口(如果需要,可以结合底层实现中的功能)。如果必须通过添加更多参数来扩展现有方法的接口,请使用新参数创建重载方法,这样,就不会影响到对现有方法的任何调用。
47 |
48 | 18. **大声读出你的类以确保它们合乎逻辑**。将基类和派生类之间的关系称为“is-a”,将成员对象称为“has-a”。
49 |
50 | 19. **在需要在继承和组合之间作决定时,问一下自己是否必须向上转换为基类型**。如果不是,则使用组合(成员对象)更好。这可以消除对多种基类型的感知需求(perceived need)。如果使用继承,则用户会认为他们应该向上转型。
51 |
52 | 20. **注意重载**。方法不应该基于参数的值而有条件地执行代码。在这里,应该创建两个或多个重载方法。
53 |
54 | 21. **使用异常层次结构**,最好是从标准 Java 异常层次结构中的特定适当类派生。然后,捕获异常的人可以为特定类型的异常编写处理程序,然后为基类型编写处理程序。如果添加新的派生异常,现有客户端代码仍将通过基类型捕获异常。
55 |
56 | 22. **有时简单的聚合可以完成工作**。航空公司的“乘客舒适系统”由独立的元素组成:座位,空调,影视等,但必须在飞机上创建许多这样的元素。你创建私有成员并建立一个全新的接口了吗?如果不是,在这种情况下,组件也应该是公共接口的一部分,因此应该创建公共成员对象。这些对象有自己的私有实现,这些实现仍然是安全的。请注意,简单聚合不是经常使用的解决方案,但确实会有时候会用到。
57 |
58 | 23. **考虑客户程序员和维护代码的人的观点**。设计类以便尽可能直观地被使用。预测要进行的更改,并精心设计类,以便轻松地进行更改。
59 |
60 | 24. **注意“巨型对象综合症”**(giant object syndrome)。这通常是程序员的痛苦,他们是面向对象编程的新手,总是编写面向过程程序并将其粘贴在一个或两个巨型对象中。除应用程序框架外,对象代表应用程序中的概念,而不是应用程序本身。
61 |
62 | 25. **如果你必须做一些丑陋的事情,至少要把类内的丑陋本地化**。
63 |
64 | 26. **如果必须做一些不可移植的事情,那就对这个事情做一个抽象,并在一个类中进行本地化**。这种额外的间接级别可防止在整个程序中扩散这种不可移植性。 (这个原则也体现在*桥接*模式中,等等)。
65 |
66 | 27. **对象不应该仅仅只是持有一些数据**。它们也应该有明确的行为。有时候,“数据传输对象”(data transfer objects)是合适的,但只有在泛型集合不合适时,才被明确用于打包和传输一组元素。
67 |
68 | 28. **在从现有类创建新类时首先选择组合**。仅在设计需要时才使用继承。如果在可以使用组合的地方使用继承,那么设计将会变得很复杂,这是没必要的。
69 |
70 | 29. **使用继承和覆盖方法来表达行为的差异,而不是使用字段来表示状态的变化**。如果发现一个类使用了状态变量,并且有一些方法是基于这些变量切换行为的,那么请重新设计它,以表示子类和覆盖方法中的行为差异。一个极端的反例是继承不同的类来表示颜色,而不是使用“颜色”字段。
71 |
72 | 30. **注意*协变*(variance)**。两个语义不同的对象可能具有相同的操作或职责。为了从继承中受益,会试图让其中一个成为另一个的子类,这是一种很自然的诱惑。这称为协变,但没有真正的理由去强制声明一个并不存在的父子类关系。更好的解决方案是创建一个通用基类,并为两者生成一个接口,使其成为这个通用基类的派生类。这仍然可以从继承中受益,并且这可能是关于设计的一个重要发现。
73 |
74 | 31. **在继承期间注意*限定*(limitation)**。最明确的设计为继承的类增加了新的功能。含糊的设计在继承期间删除旧功能而不添加新功能。但是规则是用来打破的,如果是通过调用一个旧的类库来工作,那么将一个现有类限制在其子类型中,可能比重构层次结构更有效,因此新类适合在旧类的上层。
75 |
76 | 32. **使用设计模式来消除“裸功能”(naked functionality)**。也就是说,如果类只需要创建一个对象,请不要推进应用程序并写下注释“只生成一个。”应该将其包装成一个单例(singleton)。如果主程序中有很多乱七八糟的代码去创建对象,那么找一个像工厂方法一样的创建模式,可以在其中封装创建过程。消除“裸功能”不仅会使代码更易于理解和维护,而且还会使其能够更加防范应对后面的善意维护者(well-intentioned maintainers)。
77 |
78 | 33. **注意“分析瘫痪”(analysis paralysis)**。记住,不得不经常在不了解整个项目的情况下推进项目,并且通常了解那些未知因素的最好、最快的方式是进入下一步而不是尝试在脑海中弄清楚。在获得解决方案之前,往往无法知道解决方案。Java 有内置的防火墙,让它们为你工作。你在一个类或一组类中的错误不会破坏整个系统的完整性。
79 |
80 | 34. **如果认为自己有很好的分析,设计或实施,请做一个演练**。从团队外部带来一些人,不一定是顾问,但可以是公司内其他团体的人。用一双新眼睛评审你的工作,可以在一个更容易修复它们的阶段发现问题,而不仅仅是把大量时间和金钱全扔到演练过程中。
81 |
82 | ## 实现
83 |
84 | 36. **遵循编码惯例**。有很多不同的约定,例如,[谷歌使用的约定](https://google.github.io/styleguide/javaguide.html)(本书中的代码尽可能地遵循这些约定)。如果坚持使用其他语言的编码风格,那么读者就会很难去阅读。无论决定采用何种编码约定,都要确保它们在整个项目中保持一致。集成开发环境通常包含内置的重新格式化(reformatter)和检查器(checker)。
85 |
86 | 37. **无论使用何种编码风格,如果你的团队(甚至更好是公司)对其进行标准化,它就确实会产生重大影响**。这意味着,如果不符合这个标准,那么每个人都认为修复别人的编码风格是公平的游戏。标准化的价值在于解析代码可以花费较少的脑力,因此可以更专注于代码的含义。
87 |
88 | 38. **遵循标准的大写规则**。类名的第一个字母大写。字段,方法和对象(引用)的第一个字母应为小写。所有标识符应该将各个单词组合在一起,并将所有中间单词的首字母大写。例如:
89 |
90 | - **ThisIsAClassName**
91 | - **thisIsAMethodOrFieldName**
92 |
93 | 将 **static final** 类型的标识符的所有字母全部大写,并用下划线分隔各个单词,这些标识符在其定义中具有常量初始值。这表明它们是编译时常量。
94 |
95 | - **包是一个特例**,它们都是小写的字母,即使是中间词。域扩展(com,org,net,edu 等)也应该是小写的。这是 Java 1.1 和 Java 2 之间的变化。
96 |
97 | 39. **不要创建自己的“装饰”私有字段名称**。这通常以前置下划线和字符的形式出现。匈牙利命名法(译者注:一种命名规范,基本原则是:变量名=属性+类型+对象描述。Win32 程序风格采用这种命名法,如`WORD wParam1;LONG lParam2;HANDLE hInstance`)是最糟糕的例子,你可以在其中附加额外字符用于指示数据类型,用途,位置等,就好像你正在编写汇编语言一样,编译器根本没有提供额外的帮助。这些符号令人困惑,难以阅读,并且难以执行和维护。让类和包来指定名称范围。如果认为必须装饰名称以防止混淆,那么代码就可能过于混乱,这应该被简化。
98 |
99 | 40. 在创建一般用途的类时,**遵循“规范形式”**。包括**equals()**,**hashCode()**,**toString()**,**clone()**的定义(实现**Cloneable**,或选择其他一些对象复制方法,如序列化),并实现**Comparable**和**Serializable**。
100 |
101 | 41. **对读取和更改私有字段的方法使用“get”,“set”和“is”命名约定**。这种做法不仅使类易于使用,而且也是命名这些方法的标准方法,因此读者更容易理解。
102 |
103 | 42. **对于所创建的每个类,请包含该类的 JUnit 测试**(请参阅*junit.org*以及[第十六章:代码校验]()中的示例)。无需删除测试代码即可在项目中使用该类,如果进行更改,则可以轻松地重新运行测试。测试代码也能成为如何使用这个类的示例。
104 |
105 | 43. **有时需要继承才能访问基类的 protected 成员**。这可能导致对多种基类型的感知需求(perceived need)。如果不需要向上转型,则可以首先派生一个新类来执行受保护的访问。然后把该新类作为使用它的任何类中的成员对象,以此来代替直接继承。
106 |
107 | 44. **为了提高效率,避免使用*final*方法**。只有在分析后发现方法调用是瓶颈时,才将**final**用于此目的。
108 |
109 | 45. **如果两个类以某种功能方式相互关联(例如集合和迭代器),则尝试使一个类成为另一个类的内部类**。这不仅强调了类之间的关联,而且通过将类嵌套在另一个类中,可以允许在单个包中重用类名。Java 集合库通过在每个集合类中定义内部**Iterator**类来实现此目的,从而为集合提供通用接口。使用内部类的另一个原因是作为**私有**实现的一部分。这里,内部类将有利于实现隐藏,而不是上面提到的类关联和防止命名空间污染。
110 |
111 | 46. **只要你注意到类似乎彼此之间具有高耦合,请考虑如果使用内部类可能获得的编码和维护改进**。内部类的使用不会解耦类,而是明确耦合关系,并且更方便。
112 |
113 | 47. **不要成为过早优化的牺牲品**。过早优化是很疯狂的行为。特别是,不要担心编写(或避免)本机方法(native methods),将某些方法设置为**final**,或者在首次构建系统时调整代码以使其高效。你的主要目标应该是验证设计。即使设计需要一定的效率,也*先让它工作,然后再让它变快*。
114 |
115 | 48. **保持作用域尽可能小,以便能见度和对象的寿命尽可能小**。这减少了在错误的上下文中使用对象并隐藏了难以发现的 bug 的机会。例如,假设有一个集合和一段迭代它的代码。如果复制该代码以用于一个新集合,那么可能会意外地将旧集合的大小用作新集合的迭代上限。但是,如果旧集合比较大,则会在编译时捕获错误。
116 |
117 | 49. **使用标准 Java 库中的集合**。熟练使用它们,将会大大提高工作效率。首选**ArrayList**用于序列,**HashSet**用于集合,**HashMap**用于关联数组,**LinkedList**用于堆栈(而不是**Stack**,尽管也可以创建一个适配器来提供堆栈接口)和队列(也可以使用适配器,如本书所示)。当使用前三个时,将其分别向上转型为**List**,**Set**和**Map**,那么就可以根据需要轻松更改为其他实现。
118 |
119 | 50. **为使整个程序健壮,每个组件必须健壮**。在所创建的每个类中,使用 Java 所提供的所有工具,如访问控制,异常,类型检查,同步等。这样,就可以在构建系统时安全地进入下一级抽象。
120 |
121 | 51. **编译时错误优于运行时错误**。尝试尽可能在错误发生点处理错误。在最近的处理程序中尽其所能地捕获它能处理的所有异常。在当前层面处理所能处理的所有异常,如果解决不了,就重新抛出异常。
122 |
123 | 52. **注意长方法定义**。方法应该是简短的功能单元,用于描述和实现类接口的离散部分。维护一个冗长而复杂的方法是很困难的,而且代价很大,并且这个方法可能是试图做了太多事情。如果看到这样的方法,这表明,至少应该将它分解为多种方法。也可能建议去创建一个新类。小的方法也可以促进类重用。(有时方法必须很大,但它们应该只做一件事。)
124 |
125 | 53. **保持“尽可能私有”**。一旦公开了你的类库中的一个方面(一个方法,一个类,一个字段),你就永远无法把它拿回来。如果这样做,就将破坏某些人的现有代码,迫使他们重写和重新设计。如果你只公开了必须公开的内容,就可以轻易地改变其他一切,而不会对其他人造成影响,而且由于设计趋于发展,这是一个重要的自由。通过这种方式,更改具体实现将对派生类造成的影响最小。在处理多线程时,私有尤其重要,只有**私有**字段可以防止不同步使用。具有包访问权限的类应该仍然具有**私有**字段,但通常有必要提供包访问权限的方法而不是将它们**公开**。
126 |
127 | 54. **大量使用注释,并使用*Javadoc commentdocumentation*语法生成程序文档**。但是,注释应该为代码增加真正的意义,如果注释只是重申了代码已经清楚表达的内容,这是令人讨厌的。请注意,Java 类和方法名称的典型详细信息减少了对某些注释的需求。
128 |
129 | 55. **避免使用“魔法数字”**。这些是指硬编码到代码中的数字。如果后续必须要更改它们,那将是一场噩梦,因为你永远不知道“100”是指“数组大小”还是“完全不同的东西”。相反,创建一个带有描述性名称的常量并在整个程序中使用常量标识符。这使程序更易于理解,更易于维护。
130 |
131 | 56. **在创建构造方法时,请考虑异常**。最好的情况是,构造方法不会做任何抛出异常的事情。次一级的最佳方案是,该类仅由健壮的类组成或继承自健壮的类,因此如果抛出异常则不需要处理。否则,必须清除**finally**子句中的组合类。如果构造方法必然失败,则适当的操作是抛出异常,因此调用者不会认为该对象是正确创建的而盲目地继续下去。
132 |
133 | 57. **在构造方法内部,只需要将对象设置为正确的状态**。主动避免调用其他方法(**final**方法除外),因为这些方法可以被其他人覆盖,从而在构造期间产生意外结果。(有关详细信息,请参阅[第六章:初始化和清理]()章节。)较小,较简单的构造方法不太可能抛出异常或导致问题。
134 |
135 | 58. **如果类在客户端程序员用完对象时需要进行任何清理,请将清理代码放在一个明确定义的方法中**,并使用像 **dispose()** 这样的名称来清楚地表明其目的。另外,在类中放置一个 **boolean** 标志来指示是否调用了 **dispose()** ,因此 **finalize()** 可以检查“终止条件”(参见[第六章:初始化和清理]()章节)。
136 |
137 | 59. **_finalize()_ 的职责只能是验证对象的“终止条件”以进行调试**。(参见[第六章:初始化和清理]()一章)在特殊情况下,可能需要释放垃圾收集器无法释放的内存。因为可能无法为对象调用垃圾收集器,所以无法使用 **finalize()** 执行必要的清理。为此,必须创建自己的 **dispose()** 方法。在类的 **finalize()** 方法中,检查以确保对象已被清理,如果没有被清理,则抛出一个派生自**RuntimeException**的异常,以指示编程错误。在依赖这样的计划之前,请确保 **finalize()** 适用于你的系统。(可能需要调用 **System.gc()** 来确保此行为。)
138 |
139 | 60. **如果必须在特定范围内清理对象(除了通过垃圾收集器),请使用以下准则:** 初始化对象,如果成功,立即进入一个带有 **finally** 子句的 **try** 块,并在 **finally**中执行清理操作。
140 |
141 | 61. **在继承期间覆盖 _finalize()_ 时,记得调用 _super.finalize()_**。(如果是直接继承自 **Object** 则不需要这样做。)调用 **super.finalize()** 作为重写的 **finalize()** 的最终行为而不是在第一行调用它,这样可以确保基类组件在需要时仍然有效。
142 |
143 | 62. **创建固定大小的对象集合时,将它们转换为数组,** 尤其是在从方法中返回此集合时。这样就可以获得数组编译时类型检查的好处,并且数组的接收者可能不需要在数组中强制转换对象来使用它们。请注意,集合库的基类 **java.util.Collection** 有两个 **toArray()** 方法来完成此任务。
144 |
145 | 63. **优先选择 _接口_ 而不是 _抽象类_**。如果知道某些东西应该是基类,那么第一选择应该是使其成为一个接口,并且只有在需要方法定义或成员变量时才将其更改为抽象类。一个接口关心客户端想要做什么,而一个类倾向于关注(或允许)实现细节。
146 |
147 | 64. **为了避免非常令人沮丧的经历,请确保类路径中的每个名称只对应一个不在包中的类**。否则,编译器可以首先找到具有相同名称的其他类,并报告没有意义的错误消息。如果你怀疑自己有类路径问题,请尝试在类路径的每个起始点查找具有相同名称的 **.class** 文件。理想情况下,应该将所有类放在包中。
148 |
149 | 65. **注意意外重载**。如果尝试覆盖基类方法但是拼写错误,则最终会添加新方法而不是覆盖现有方法。但是,这是完全合法的,因此在编译时或运行时不会获得任何错误消息,但代码将无法正常工作。始终使用 **@Override** 注释来防止这种情况。
150 |
151 | 66. **注意过早优化**。先让它工作,然后再让它变快。除非发现代码的特定部分存在性能瓶颈。除非是使用分析器发现瓶颈,否则过早优化会浪费时间。性能调整所隐藏的额外成本是代码将变得难以理解和维护。
152 |
153 | 67. **请注意,相比于编写代码,代码被阅读的机会更多**。清晰的设计可能产生易于理解的程序,但注释,详细解释,测试和示例是非常宝贵的,它们可以帮助你和你的所有后继者。如果不出意外,试图从 JDK 文档中找出有用信息的挫败感应该可以说服你。
154 |
155 | [^1]: Andrew Koenig 向我解释了它。
156 |
157 |
--------------------------------------------------------------------------------
/docs/appendices/app-standard-io.md:
--------------------------------------------------------------------------------
1 | # 附录:标准IO
2 |
3 | >*标准 I/O*这个术语参考Unix中的概念,指程序所使用的单一信息流(这种思想在大多数操作系统中,也有相似形式的实现)。
4 |
5 | 程序的所有输入都可以来自于*标准输入*,其所有输出都可以流向*标准输出*,并且其所有错误信息均可以发送到*标准错误*。*标准 I/O* 的意义在于程序之间可以很容易地连接起来,一个程序的标准输出可以作为另一个程序的标准输入。这是一个非常强大的工具。
6 |
7 | ## 从标准输入中读取
8 |
9 | 遵循标准 I/O 模型,Java 提供了标准输入流 `System.in`、标准输出流 `System.out` 和标准错误流 `System.err`。在本书中,你已经了解到如何使用 `System.out`将数据写到标准输出。 `System.out` 已经预先包装[^1]成了 `PrintStream` 对象。标准错误流 `System.err` 也预先包装为 `PrintStream` 对象,但是标准输入流 `System.in` 是原生的没有经过包装的 `InputStream`。这意味着尽管可以直接使用标准输出流 `System.in` 和标准错误流 `System.err`,但是在读取 `System.in` 之前必须先对其进行包装。
10 |
11 | 我们通常一次一行地读取输入。为了实现这个功能,将 `System.in` 包装成 `BufferedReader` 来使用,这要求我们用 `InputStreamReader` 把 `System.in` 转换[^2]成 `Reader` 。下面这个例子将键入的每一行显示出来:
12 |
13 | ```java
14 | // standardio/Echo.java
15 | // How to read from standard input
16 | import java.io.*;
17 | import onjava.TimedAbort;
18 |
19 | public class Echo {
20 | public static void main(String[] args) {
21 | TimedAbort abort = new TimedAbort(2);
22 | new BufferedReader(
23 | new InputStreamReader(System.in))
24 | .lines()
25 | .peek(ln -> abort.restart())
26 | .forEach(System.out::println);
27 | // Ctrl-Z or two seconds inactivity
28 | // terminates the program
29 | }
30 | }
31 | ```
32 |
33 | `BufferedReader` 提供了 `lines()` 方法,返回类型是 `Stream` 。这显示出流模型的的灵活性:仅使用标准输入就能很好地工作。 `peek()` 方法重启 `TimeAbort`,只要保证至少每隔两秒有输入就能够使程序保持开启状态。
34 |
35 | ## 将`System.out` 转换成 `PrintWriter`
36 |
37 | `System.out` 是一个 `PrintStream`,而 `PrintStream` 是一个`OutputStream`。 `PrintWriter` 有一个把 `OutputStream` 作为参数的构造器。因此,如果你需要的话,可以使用这个构造器把 `System.out` 转换成 `PrintWriter` 。
38 |
39 | ```java
40 | // standardio/ChangeSystemOut.java
41 | // Turn System.out into a PrintWriter
42 |
43 | import java.io.*;
44 |
45 | public class ChangeSystemOut {
46 | public static void main(String[] args) {
47 | PrintWriter out =
48 | new PrintWriter(System.out, true);
49 | out.println("Hello, world");
50 | }
51 | }
52 | ```
53 |
54 | 输出结果:
55 |
56 | ```
57 | Hello, world
58 | ```
59 |
60 | 要使用 `PrintWriter` 带有两个参数的构造器,并设置第二个参数为 `true`,从而使能自动刷新到输出缓冲区的功能;否则,可能无法看到打印输出。
61 |
62 | ## 重定向标准 I/O
63 |
64 | Java的 `System` 类提供了简单的 `static` 方法调用,从而能够重定向标准输入流、标准输出流和标准错误流:
65 | - setIn(InputStream)
66 | - setOut(PrintStream)
67 | - setErr(PrintStream)
68 |
69 | 如果我们突然需要在显示器上创建大量的输出,而这些输出滚动的速度太快以至于无法阅读时,重定向输出就显得格外有用,可把输出内容重定向到文件中供后续查看。对于我们想重复测试特定的用户输入序列的命令行程序来说,重定向输入就很有价值。下例简单演示了这些方法的使用:
70 |
71 | ```java
72 | // standardio/Redirecting.java
73 | // Demonstrates standard I/O redirection
74 | import java.io.*;
75 |
76 | public class Redirecting {
77 | public static void main(String[] args) {
78 | PrintStream console = System.out;
79 | try (
80 | BufferedInputStream in = new BufferedInputStream(
81 | new FileInputStream("Redirecting.java"));
82 | PrintStream out = new PrintStream(
83 | new BufferedOutputStream(
84 | new FileOutputStream("Redirecting.txt")))
85 | ) {
86 | System.setIn(in);
87 | System.setOut(out);
88 | System.setErr(out);
89 | new BufferedReader(
90 | new InputStreamReader(System.in))
91 | .lines()
92 | .forEach(System.out::println);
93 | } catch (IOException e) {
94 | throw new RuntimeException(e);
95 | } finally {
96 | System.setOut(console);
97 | }
98 | }
99 | }
100 | ```
101 |
102 | 该程序将文件中内容载入到标准输入,并把标准输出和标准错误重定向到另一个文件。它在程序的开始保存了最初对 `System.out` 对象的引用,并且在程序结束时将系统输出恢复到了该对象上。
103 |
104 | I/O重定向操作的是字节流而不是字符流,因此使用 `InputStream` 和 `OutputStream`,而不是 `Reader` 和 `Writer`。
105 |
106 | ## 执行控制
107 |
108 | 你经常需要在Java内部直接执行操作系统的程序,并控制这些程序的输入输出,Java类库提供了执行这些操作的类。
109 |
110 | 一项常见的任务是运行程序并将输出结果发送到控制台。本节包含了一个可以简化此任务的实用工具。
111 |
112 | 在使用这个工具时可能会产生两种类型的错误:导致异常的普通错误——对于这些错误我们只需要重新抛出一个 `RuntimeException` 即可,以及进程自身的执行过程中导致的错误——我们需要用单独的异常来报告这些错误:
113 |
114 | ```java
115 | // onjava/OSExecuteException.java
116 | package onjava;
117 |
118 | public class OSExecuteException extends RuntimeException {
119 | public OSExecuteException(String why) {
120 | super(why);
121 | }
122 | }
123 | ```
124 |
125 | 为了运行程序,我们需要传递给 `OSExecute.command()` 一个 `String command`,我们可以在控制台键入同样的指令运行程序。该命令传递给 `java.lang.ProcessBuilder` 的构造器(需要将其作为 `String` 对象的序列),然后启动生成的 `ProcessBuilder` 对象。
126 |
127 | ```java
128 | // onjava/OSExecute.java
129 | // Run an operating system command
130 | // and send the output to the console
131 | package onjava;
132 | import java.io.*;
133 |
134 | public class OSExecute {
135 | public static void command(String command) {
136 | boolean err = false;
137 | try {
138 | Process process = new ProcessBuilder(
139 | command.split(" ")).start();
140 | try (
141 | BufferedReader results = new BufferedReader(
142 | new InputStreamReader(
143 | process.getInputStream()));
144 | BufferedReader errors = new BufferedReader(
145 | new InputStreamReader(
146 | process.getErrorStream()))
147 | ) {
148 | results.lines()
149 | .forEach(System.out::println);
150 | err = errors.lines()
151 | .peek(System.err::println)
152 | .count() > 0;
153 | }
154 | } catch (IOException e) {
155 | throw new RuntimeException(e);
156 | }
157 | if (err)
158 | throw new OSExecuteException(
159 | "Errors executing " + command);
160 | }
161 | }
162 | ```
163 |
164 | 为了捕获在程序执行时产生的标准输出流,我们可以调用 `getInputStream()`。这是因为 `InputStream` 是我们可以从中读取信息的流。
165 |
166 | 这里这些行只是被打印了出来,但是你也可以从 `command()` 捕获和返回它们。
167 |
168 | 该程序的错误被发送到了标准错误流,可以调用 `getErrorStream()` 捕获。如果存在任何错误,它们都会被打印并且抛出 `OSExcuteException` ,以便调用程序处理这个问题。
169 |
170 | 下面是展示如何使用 `OSExecute` 的示例:
171 |
172 | ```java
173 | // standardio/OSExecuteDemo.java
174 | // Demonstrates standard I/O redirection
175 | // {javap -cp build/classes/main OSExecuteDemo}
176 | import onjava.*;
177 |
178 | public class OSExecuteDemo {}
179 | ```
180 |
181 | 这里使用 `javap` 反编译器(随JDK发布)来反编译程序,编译结果:
182 |
183 | ```
184 | Compiled from "OSExecuteDemo.java"
185 | public class OSExecuteDemo {
186 | public OSExecuteDemo();
187 | }
188 | ```
189 |
190 | [^1]: 译者注:这里用到了**装饰器模式**。
191 |
192 | [^2]: 译者注:这里用到了**适配器模式**。
193 |
--------------------------------------------------------------------------------
/docs/appendices/app-supplements.md:
--------------------------------------------------------------------------------
1 | # 附录:补充
2 |
3 | > 本书有许多补充内容,包括 MindView 网站提供的项目和服务。
4 |
5 | 本附录介绍了这些补充内容,你可以自行决定它们是否对你有所帮助。
6 |
7 | ## 可下载的补充
8 |
9 | 可以从 [https://github.com/BruceEckel/OnJava8-examples](https://github.com/BruceEckel/OnJava8-examples) 免费下载本书的代码。这里包括 Gradle 构建文件和其它一些必要的支持文件,以便成功构建和执行本书中所有的示例代码。
10 |
11 | ## 通过 Thinking-in-C 来巩固 Java 基础
12 |
13 | 在 [www.OnJava8.com](www.OnJava8.com) 上,可以免费下载*Thinking in C*的演示文稿。 此演示文稿由 Chuck Allison 创建,由 MindView 有限责任公司开发。这是一个电子演示文稿,介绍了 Java 语法所基于的 C 语法,运算符和函数。
14 |
15 | ## Hand-On Java 电子演示文稿
16 |
17 | _Hand-On Java 电子演示文稿_(Hands-On Java eSeminar)是基于*Thinking in Java*第 2 版。对应于该书中的每一章,它附带有一个音频讲解和相应的幻灯片。我创建了这个电子演示文稿,并讲述了这些材料。这个资料是 HTML5 格式的,所以它应该可以在大多数现代浏览器上运行。该演示文稿将在[www.OnJava8.com](www.OnJava8.com)上发售,你可以在该网站上找到该产品的试用版演示。
18 |
19 |
--------------------------------------------------------------------------------
/docs/appendices/app-the-positive-legacy-of-c-plus-plus-and-java.md:
--------------------------------------------------------------------------------
1 | # 附录:C++和 Java 的优良传统
2 |
3 | > 在各种讨论声中,有一些人认为 C++是一种设计糟糕的语言。 我认为理解 C++和 Java 语言的选择有助于了解更大的视角。
4 |
5 | 也就是说,我几乎不再使用 C++了。当我使用它的时候,要么是用来检查遗留代码,要么是编写性能关键(performance-critical)部分,程序通常尽可能小,以便用其他语言编写的其他程序来调用。
6 |
7 | 因为我在最初的 8 年里一直在 C++标准委员会工作,所以我见证了那些被做出的决定。它们都经过了极其谨慎的考虑,远远超过了许多在 Java 中做出的决定。
8 |
9 | 然而,正如人们正确地指出的那样,由此产生的语言使用起来既复杂又痛苦,而且只要我一段时间不使用它,我就会忘记那些古怪的规则。在我写书的时候,我是从第一原理(first principles)处了解这些规则的,而不是记住了它们。
10 |
11 | 为了理解 C++语言为何既令人不愉快且复杂,同时又是精心设计的,必须要牢记 C++中所有内容的主要设计决策:与 C. Bjarne Stroustrup(该语言的创造者,即“C++之父”)的兼容性决定。这样的设计似乎是为了可以让大量的 C 程序员透明地转移到对象(代指 C++)上:允许他们在 C++下编译他们的 C 代码。这是一个巨大的限制,一直是 C++最大的优势......而且也是它的祸根。这就是使得 C++成功的原因,也是使它复杂的原因。
12 |
13 | 它也欺骗了那些不太了解 C++的 Java 设计师。例如,他们认为运算符重载对于程序员来说很难正确使用。这在 C++中基本上是正确的,因为 C++既有栈分配又有堆分配,你必须重载运算符来处理所有情况而且不要造成内存泄漏。这确实很困难。然而,Java 有单一的内存分配机制和一个垃圾收集器,这使得运算符重载变得微不足道,正如 C#中那样(但在早于 Java 的 Python 中已经可以看到)。但多年来,来自 Java 团队的一贯态度是“运算符重载太复杂了”。这里还有许多决策,所做的事明显不应该是他们做的。正是由于这些原因,让我有了蔑视 Gosling(即“Java 之父”)和 Java 团队决策的名声。(Java 7 和 8 由于某种原因包含了更好的决策。但是向后兼容性这个约束总是会阻碍真正的改进。语言永远不会是它本来的样子。)
14 |
15 | 还有很多其他的例子。“为了提高效率,必须包含基本类型”;坚持“万物皆对象”是正确的;当对性能有要求的时候,提供一个陷阱门(trap door)来做低级别的活动(lower-level activities)(这里也可以使用 hotspot 技术透明地提高性能,正如他们最终做的那样);不能直接使用浮点处理器去计算超越函数,它用软件来完成。我已经尽可能多地提出了这样的问题,但我得到的却一直是类似“这是 Java 方式”这样的回复。
16 |
17 | 当我提出关于泛型的设计有多糟糕的时候,我得到了相同的回复,以及“我们必须向后兼容那样以前用 Java 做出的决策”(即使它们是糟糕的决策)。最近越来越多的人已经获得了足够的泛型经验,可以发现泛型真的很难用。事实上,C++模板更强大、更一致(现在更容易使用,因为编译器的错误消息是可以容忍的)。人们一直在认真对待物化(reification),这可能是有用的东西,但是在那种被严格约束所削弱的设计中并没有多大影响。
18 |
19 | 这样的例子还有很多很多。这是否意味着 Java 失败了?绝对不。Java 将程序员的主流带入了垃圾收集、虚拟机和一致的错误处理模型的世界。由于它的所有缺陷,它将我们提升到了一个水平,现在我们已经准备好使用更高级别的语言了。
20 |
21 | 有一点,C++是领先的语言,人们认为它总是如此。许多人对 Java 有同样的看法,但由于 JVM,Java 使得取代自己变得更加容易。现在有可能会有人创建一种新语言,并使其在短时间内像 Java 一样高效运行。以前,为新语言开发一个正确有效的编译器需要花费大部分开发时间。
22 |
23 | 这种情况已经发生了,包括像 Scala 这样的高级静态语言,以及动态语言,新的且可移植的,如 Groovy,Clojure,JRuby 和 Jython。这是未来,并且过渡很顺畅,因为可以很轻易地将这些新语言与现有 Java 代码结合使用,并且必要时可以重写那些在 Java 中的瓶颈。
24 |
25 | 在撰写本文时,Java 是世界上的头号编程语言。然而,Java 最终将会减弱,就像 C++一样,沦只在特殊情况下使用(或者只是用来支持传统的代码,因为它不能像 C++那样和硬件连接)。但是无意中的好处,也是 Java 真正意外的光彩之处在于它为自己的替代品创造了一条非常畅通的道路,即使 Java 本身已经达到了无法再发展的程度。未来所有的语言都应该从中学习:要么创建一个可以重构的文化(像 Python 和 Ruby 做的那样),要么就让竞争者茁壮成长。
26 |
27 |
--------------------------------------------------------------------------------
/docs/ch1.md:
--------------------------------------------------------------------------------
1 | # 第 1 章 对象的概念
2 |
3 | > “我们没有意识到惯用语言的结构有多大的力量。可以毫不夸张地说,它通过语义反应机制奴役我们。语言表现出来并在无意识中给我们留下深刻印象的结构会自动投射到我们周围的世界。” -- Alfred Korzybski (1930)
4 |
5 | 计算机革命的起源来自机器。编程语言就像是那台机器。它不仅是我们思维放大的工具与另一种表达媒介,更像是我们思想的一部分。语言的灵感来自其他形式的表达,如写作,绘画,雕塑,动画和电影制作。编程语言就是创建应用程序的思想结构。
6 |
7 | 面向对象编程(Object-Oriented Programming OOP)是一种编程思维方式和编码架构。本章讲述 OOP 的基本概述。如果读者对此不太理解,可先行跳过本章。等你具备一定编程基础后,请务必再回头看。只有这样你才能深刻理解面向对象编程的重要性及设计方式。
8 |
9 | ## 抽象
10 |
11 | 所有编程语言都提供抽象机制。从某种程度上来说,问题的复杂度直接取决于抽象的类型和质量。这里的“类型”意思是:抽象的内容是什么?汇编语言是对底层机器的轻微抽象。接着出现的“命令式”语言(如 FORTRAN,BASIC 和 C)是对汇编语言的抽象。与汇编相比,这类语言已有了长足的改进,但它们的抽象原理依然要求我们着重考虑计算机的结构,而非问题本身的结构。
12 |
13 | 程序员必须要在机器模型(“解决方案空间”)和实际解决的问题模型(“问题空间”)之间建立起一种关联。这个过程既费精力,又脱离编程语言本身的范畴。这使得程序代码很难编写,维护代价高昂。同时还造就了一门副产业的“编程方法”学科。
14 |
15 | 为机器建模的另一个方法是为要解决的问题制作模型。对一些早期语言来说,如 LISP 和 APL,它们的做法是“从不同的角度观察世界”——“所有问题都归纳为列表”或“所有问题都归纳为算法”。PROLOG 则将所有
16 | 问题都归纳为决策链。对于这些语言,我们认为它们一部分是“基于约束”的编程,另一部分则是专为
17 | 处理图形符号设计的(后者被证明限制性太强)。每种方法都有自己特殊的用途,适合解决某一类的问题。只要超出了它们力所能及的范围,就会显得非常笨拙。
18 |
19 | 面向对象的程序设计在此基础上跨出了一大步,程序员可利用一些工具表达“问题空间”内的元素。由于这种表达非常具有普遍性,所以不必受限于特定类型的问题。我们将问题空间中的元素以及它们在解决方案空间的表示称作“对象”(**Object**)。当然,还有一些在问题空间没有对应的对象体。通过添加新的对象类型,程序可进行灵活的调整,以便与特定的问题配合。所以当你在阅读描述解决方案的代码时,也是在阅读问题的表述。与我们以前见过的相比,这无疑是一种更加灵活、更加强大的语言抽象方法。总之,OOP 允许我们根据问题来描述问题,而不是根据运行解决方案的计算机。然而,它仍然与计算机有联系,每个对象都类似一台小计算机:它们有自己的状态并且可以进行特定的操作。这与现实世界的“对象”或者“物体”相似:它们都有自己的特征和行为。
20 |
21 | Smalltalk 作为第一个成功的面向对象并影响了 Java 的程序设计语言 ,_Alan Kay_ 总结了其五大基本特征。通过这些特征,我们可理解“纯粹”的面向对象程序设计方法是什么样的:
22 |
23 | > 1. **万物皆对象**。你可以将对象想象成一种特殊的变量。它存储数据,但可以在你对其“发出请求”时执行本身的操作。理论上讲,你总是可以从要解决的问题身上抽象出概念性的组件,然后在程序中将其表示为一个对象。
24 | > 2. **程序是一组对象,通过消息传递来告知彼此该做什么**。要请求调用一个对象的方法,你需要向该对象发送消息。
25 | > 3. **每个对象都有自己的存储空间,可容纳其他对象**。或者说,通过封装现有对象,可制作出新型对象。所以,尽管对象的概念非常简单,但在程序中却可达到任意高的复杂程度。
26 | > 4. **每个对象都有一种类型**。根据语法,每个对象都是某个“类”的一个“实例”。其中,“类”(Class)是“类型”(Type)的同义词。一个类最重要的特征就是“能将什么消息发给它?”。
27 | > 5. **同一类所有对象都能接收相同的消息**。这实际是别有含义的一种说法,大家不久便能理解。由于类型为“圆”(Circle)的一个对象也属于类型为“形状”(Shape)的一个对象,所以一个圆完全能接收发送给"形状”的消息。这意味着可让程序代码统一指挥“形状”,令其自动控制所有符合“形状”描述的对象,其中自然包括“圆”。这一特性称为对象的“可替换性”,是 OOP 最重要的概念之一。
28 |
29 | _Grady Booch_ 提供了对对象更简洁的描述:一个对象具有自己的状态,行为和标识。这意味着对象有自己的内部数据(提供状态)、方法 (产生行为),并彼此区分(每个对象在内存中都有唯一的地址)。
30 |
31 | ## 接口
32 |
33 | 亚里士多德(_Aristotle_)大概是第一个认真研究“类型”的哲学家,他曾提出过“鱼类和鸟类”这样的概念。所有对象都是唯一的,但同时也是具有相同的特性和行为的对象所归属的类的一部分。这种思想被首次应用于第一个面向对象编程语言 Simula-67,它在程序中使用基本关键字 **class** 来引入新的类型(class 和 type 通常可互换使用,有些人对它们进行了进一步区分,他们强调 type 决定了接口,而 class 是那个接口的一种特殊实现方式)。
34 |
35 | Simula 是一个很好的例子。正如这个名字所暗示的,它的作用是“模拟”(Simulate)类似“银行出纳员”这样的经典问题。在这个例子里,我们有一系列出纳员、客户、帐号、交易和货币单位等许多"对象”。每类成员(元素)都具有一些通用的特征:每个帐号都有一定的余额;每名出纳都能接收客户的存款;等等。与此同时,每个成员都有自己的状态;每个帐号都有不同的余额;每名出纳都有一个名字。所以在计算机程序中,能用独一无二的实体分别表示出纳员、客户、帐号以及交易。这个实体便是“对象”,而且每个对象都隶属一个特定的“类”,那个类具有自己的通用特征与行为。
36 |
37 | 因此,在面向对象的程序设计中,尽管我们真正要做的是新建各种各样的数据“类型”(Type),但几乎所有面向对象的程序设计语言都采用了 `class` 关键字。当你看到 “type” 这个词的时候,请同时想到 `class`;反之亦然。
38 |
39 | 创建好一个类后,可根据情况生成许多对象。随后,可将那些对象作为要解决问题中存在的元素进行处理。事实上,当我们进行面向对象的程序设计时,面临的最大一项挑战性就是:如何在“问题空间”(问题实际存在的地方)的元素与“方案空间”(对实际问题进行建模的地方,如计算机)的元素之间建立理想的“一对一”的映射关系。
40 |
41 | 那么如何利用对象完成真正有用的工作呢?必须有一种办法能向对象发出请求,令其解决一些实际的问题,比如完成一次交易、在屏幕上画一些东西或者打开一个开关等等。每个对象仅能接受特定的请求。我们向对象发出的请求是通过它的“接口”(Interface)定义的,对象的“类型”或“类”则规定了它的接口形式。“类型”与“接口”的对应关系是面向对象程序设计的基础。
42 |
43 | 下面让我们以电灯泡为例:
44 |
45 | 
46 |
47 | ```java
48 | Light lt = new Light();
49 | lt.on();
50 | ```
51 |
52 | 在这个例子中,类型/类的名称是 **Light**,可向 **Light** 对象发出的请求包括包括打开 `on`、关闭 `off`、变得更明亮 `brighten` 或者变得更暗淡 `dim`。通过声明一个引用,如 `lt` 和 `new` 关键字,我们创建了一个 **Light** 类型的对象,再用等号将其赋给引用。
53 |
54 | 为了向对象发送消息,我们使用句点符号 `.` 将 `lt` 和消息名称 `on` 连接起来。可以看出,使用一些预先定义好的类时,我们在程序里采用的代码是非常简单直观的。
55 |
56 | 上图遵循 **UML**(Unified Modeling Language,统一建模语言)的格式。每个类由一个框表示,框的顶部有类型名称,框中间部分是要描述的任何数据成员,方法(属于此对象的方法,它们接收任何发送到该对象的消息)在框的底部。通常,只有类的名称和公共方法在 **UML** 设计图中显示,因此中间部分未显示,如本例所示。如果你只对类名感兴趣,则也不需要显示方法信息。
57 |
58 | ## 服务提供
59 |
60 | 在开发或理解程序设计时,我们可以将对象看成是“服务提供者”。你的程序本身将为用户提供服务,并且它能通过调用其他对象提供的服务来实现这一点。我们的最终目标是开发或调用工具库中已有的一些对象,提供理想的服务来解决问题。
61 |
62 | 那么问题来了:我们该选择哪个对象来解决问题呢?例如,你正在开发一个记事本程序。_你可能会想到在屏幕输入默认的记事本对象_,一个用于检测不同类型打印机并执行打印的对象。这些对象中的某些已经有了。那对于还没有的对象,我们该设计成啥样呢?这些对象需要提供哪些服务,以及还需要调用其他哪些对象?
63 |
64 | 我们可以将这些问题一一分解,抽象成一组服务。软件设计的基本原则是高内聚:每个组件的内部作用明确,功能紧密相关。然而经常有人将太多功能塞进一个对象中。例如:在支票打印模块中,你需要设计一个可以同时读取文本格式又能正确识别不同打印机型号的对象。正确的做法是提供三个或更多对象:一个对象检查所有排版布局的目录;一个或一组可以识别不同打印机型号的对象展示通用的打印界面;第三个对象组合上述两个服务来完成任务。这样,每个对象都提供了一组紧密的服务。在良好的面向对象设计中,每个对象功能单一且高效。这样的程序设计可以提高我们代码的复用性,同时也方便别人阅读和理解我们的代码。只有让人知道你提供什么服务,别人才能更好地将其应用到其他模块或程序中。
65 |
66 | ## 封装
67 |
68 | 我们可以把编程的侧重领域划分为研发和应用。应用程序员调用研发程序员构建的基础工具类来做快速开发。研发程序员开发一个工具类,该工具类仅向应用程序员公开必要的内容,并隐藏内部实现的细节。这样可以有效地避免该工具类被错误的使用和更改,从而减少程序出错的可能。彼此职责划分清晰,相互协作。当应用程序员调用研发程序员开发的工具类时,双方建立了关系。应用程序员通过使用现成的工具类组装应用程序或者构建更大的工具库。如果工具类的创建者将类的内部所有信息都公开给调用者,那么有些使用规则就不容易被遵守。因为前者无法保证后者是否会按照正确的规则来使用,甚至是改变该工具类。只有设定访问控制,才能从根本上阻止这种情况的发生。
69 |
70 | 因此,使用访问控制的原因有以下两点:
71 |
72 | 1. 让应用程序员不要触摸他们不应该触摸的部分。(请注意,这也是一个哲学决策。部分编程语言认为如果程序员有需要,则应该让他们访问细节部分。);
73 |
74 | 2. 使类库的创建者(研发程序员)在不影响后者使用的情况下完善更新工具库。例如,我们开发了一个功能简单的工具类,后来发现可以通过优化代码来提高执行速度。假如工具类的接口和实现部分明确分开并受到保护,那我们就可以轻松地完成改造。
75 |
76 | Java 有三个显式关键字来设置类中的访问权限:`public`(公开),`private`(私有)和`protected`(受保护)。这些访问修饰符决定了谁能使用它们修饰的方法、变量或类。
77 |
78 | 1. `public`(公开)表示任何人都可以访问和使用该元素;
79 |
80 | 2. `private`(私有)除了类本身和类内部的方法,外界无法直接访问该元素。`private` 是类和调用者之间的屏障。任何试图访问私有成员的行为都会报编译时错误;
81 |
82 | 3. `protected`(受保护)类似于 `private`,区别是子类(下一节就会引入继承的概念)可以访问 `protected` 的成员,但不能访问 `private` 成员;
83 |
84 | 4. `default`(默认)如果你不使用前面的三者,默认就是 `default` 访问权限。`default` 被称为包访问,因为该权限下的资源可以被同一包(库组件)中其他类的成员访问。
85 |
86 | ## 复用
87 |
88 | 一个类经创建和测试后,理应是可复用的。然而很多时候,由于程序员没有足够的编程经验和远见,我们的代码复用性并不强。
89 |
90 | 代码和设计方案的复用性是面向对象程序设计的优点之一。我们可以通过重复使用某个类的对象来达到这种复用性。同时,我们也可以将一个类的对象作为另一个类的成员变量使用。新的类可以是由任意数量和任意类型的其他对象构成。这里涉及到“组合”和“聚合”的概念:
91 |
92 | - **组合**(Composition)经常用来表示“拥有”关系(has-a relationship)。例如,“汽车拥有引擎”。
93 |
94 | - **聚合**(Aggregation)动态的**组合**。
95 |
96 | 
97 |
98 | 上图中实心三角形指向“ **Car** ”表示 **组合** 的关系;如果是 **聚合** 关系,可以使用空心三角形。
99 |
100 | (**译者注**:组合和聚合都属于关联关系的一种,只是额外具有整体-部分的意义。至于是聚合还是组合,需要根据实际的业务需求来判断。可能相同超类和子类,在不同的业务场景,关联关系会发生变化。只看代码是无法区分聚合和组合的,具体是哪一种关系,只能从语义级别来区分。聚合关系中,整件不会拥有部件的生命周期,所以整件删除时,部件不会被删除。再者,多个整件可以共享同一个部件。组合关系中,整件拥有部件的生命周期,所以整件删除时,部件一定会跟着删除。而且,多个整件不可以同时共享同一个部件。这个区别可以用来区分某个关联关系到底是组合还是聚合。两个类生命周期不同步,则是聚合关系,生命周期同步就是组合关系。)
101 |
102 | 使用“组合”关系给我们的程序带来极大的灵活性。通常新建的类中,成员对象会使用 `private` 访问权限,这样应用程序员则无法对其直接访问。我们就可以在不影响客户代码的前提下,从容地修改那些成员。我们也可以在“运行时"改变成员对象从而动态地改变程序的行为,这进一步增大了灵活性。下面一节要讲到的“继承”并不具备这种灵活性,因为编译器对通过继承创建的类进行了限制。
103 |
104 | 在面向对象编程中经常重点强调“继承”。在新手程序员的印象里,或许先入为主地认为“继承应当随处可见”。沿着这种思路产生的程序设计通常拙劣又复杂。相反,在创建新类时首先要考虑“组合”,因为它更简单灵活,而且设计更加清晰。等我们有一些编程经验后,一旦需要用到继承,就会明显意识到这一点。
105 |
106 | ## 继承
107 |
108 | “继承”给面向对象编程带来极大的便利。它在概念上允许我们将各式各样的数据和功能封装到一起,这样便可恰当表达“问题空间”的概念,而不用受制于必须使用底层机器语言。
109 |
110 | 通过使用 `class` 关键字,这些概念形成了编程语言中的基本单元。遗憾的是,这么做还是有很多麻烦:在创建了一个类之后,即使另一个新类与其具有相似的功能,你还是得重新创建一个新类。但我们若能利用现成的数据类型,对其进行“克隆”,再根据情况进行添加和修改,情况就显得理想多了。“继承”正是针对这个目标而设计的。但继承并不完全等价于克隆。在继承过程中,若原始类(正式名称叫作基类、超类或父类)发生了变化,修改过的“克隆”类(正式名称叫作继承类或者子类)也会反映出这种变化。
111 |
112 | 
113 |
114 | 这个图中的箭头从派生类指向基类。正如你将看到的,通常有多个派生类。类型不仅仅描述一组对象的约束,它还涉及其他类型。两种类型可以具有共同的特征和行为,但是一种类型可能包含比另一种类型更多的特征,并且还可以处理更多的消息(或者以不同的方式处理它们)。继承通过基类和派生类的概念来表达这种相似性。基类包含派生自它的类型之间共享的所有特征和行为。创建基类以表示思想的核心。从基类中派生出其他类型来表示实现该核心的不同方式。
115 |
116 | 
117 |
118 | 例如,垃圾回收机对垃圾进行分类。基类是“垃圾”。每块垃圾都有重量、价值等特性,它们可以被切碎、熔化或分解。在此基础上,可以通过添加额外的特性(瓶子有颜色,钢罐有磁性)或行为(铝罐可以被压碎)派生出更具体的垃圾类型。此外,一些行为可以不同(纸张的价值取决于它的类型和状态)。使用继承,你将构建一个类型层次结构,来表示你试图解决的某种类型的问题。第二个例子是常见的“形状”例子,可能用于计算机辅助设计系统或游戏模拟。基类是“形状”,每个形状都有大小、颜色、位置等等。每个形状可以绘制、擦除、移动、着色等。由此,可以派生出(继承出)具体类型的形状——圆形、正方形、三角形等等——每个形状可以具有附加的特征和行为。
119 |
120 | 
121 |
122 | 例如,某些形状可以翻转。有些行为可能不同,比如计算形状的面积。类型层次结构体现了形状之间的相似性和差异性。以相同的术语将解决方案转换成问题是有用的,因为你不需要在问题描述和解决方案描述之间建立许多中间模型。通过使用对象,类型层次结构成为了主要模型,因此你可以直接从真实世界中对系统的描述过渡到用代码对系统进行描述。事实上,有时候,那些善于寻找复杂解决方案的人会被面向对象设计的简单性难倒。从现有类型继承创建新类型。这种新类型不仅包含现有类型的所有成员(尽管私有成员被隐藏起来并且不可访问),而且更重要的是它复制了基类的接口。也就是说,基类对象接收的所有消息也能被派生类对象接收。根据类接收的消息,我们知道类的类型,因此派生类与基类是相同的类型。
123 |
124 | 在前面的例子中,“圆是形状”。这种通过继承的类型等价性是理解面向对象编程含义的基本门槛之一。因为基类和派生类都具有相同的基本接口,所以伴随此接口的必定有某些具体实现。也就是说,当对象接收到特定消息时,必须有可执行代码。如果继承一个类而不做其他任何事,则来自基类接口的方法直接进入派生类。这意味着派生类和基类不仅具有相同的类型,而且具有相同的行为,这么做没什么特别意义。
125 |
126 | 有两种方法可以区分新的派生类与原始的基类。第一种方法很简单:在派生类中添加新方法。这些新方法不是基类接口的一部分。这意味着基类不能满足你的所有需求,所以你添加了更多的方法。继承的这种简单而原始的用途有时是解决问题的完美解决方案。然而,还是要仔细考虑是否在基类中也要有这些额外的方法。这种设计的发现与迭代过程在面向对象程序设计中会经常发生。
127 |
128 | 尽管继承有时意味着你要在接口中添加新方法(尤其是在以 **extends** 关键字表示继承的 Java 中),但并非总需如此。第二种也是更重要地区分派生类和基类的方法是改变现有基类方法的行为,这被称为覆盖 (overriding)。要想覆盖一个方法,只需要在派生类中重新定义这个方法即可。
129 |
130 | ### "是一个"与"像是一个"的关系
131 |
132 | 对于继承可能会引发争论:继承应该只覆盖基类的方法(不应该添加基类中没有的方法)吗?如果这样的话,基类和派生类就是相同的类型了,因为它们具有相同的接口。这会造成,你可以用一个派生类对象完全替代基类对象,这叫作"纯粹替代",也经常被称作"替代原则"。在某种意义上,这是一种处理继承的理想方式。我们经常把这种基类和派生类的关系称为是一个(is-a)关系,因为可以说"圆是一个形状"。判断是否继承,就看在你的类之间有无这种 is-a 关系。
133 |
134 | 有时你在派生类添加了新的接口元素,从而扩展接口。虽然新类型仍然可以替代基类,但是这种替代不完美,原因在于基类无法访问新添加的方法。这种关系称为像是一个(is-like-a)关系。新类型不但拥有旧类型的接口,而且包含其他方法,所以不能说新旧类型完全相同。
135 |
136 | 
137 |
138 | 以空调为例,假设房间里已经安装好了制冷设备的控制器,即你有了控制制冷设备的接口。想象一下,现在空调坏了,你重新安装了一个既制冷又制热的热力泵。热力泵就像是一个(is-like-a)空调,但它可以做更多。因为当初房间的控制系统被设计成只能控制制冷设备,所以它只能与新对象(热力泵)的制冷部分通信。新对象的接口已经扩展了,现有控制系统却只知道原来的接口,一旦看到这个设计,你就会发现,作为基类的制冷系统不够一般化,应该被重新命名为"温度控制系统",也应该包含制热功能,这样的话,我们就可以使用替代原则了。上图反映了在现实世界中进行设计时可能会发生的事情。
139 |
140 | 当你看到替代原则时,很容易会认为纯粹替代是唯一可行的方式,并且使用纯粹替代的设计是很好的。但有些时候,你会发现必须得在派生(扩展)类中添加新方法(提供新的接口)。只要仔细审视,你可以很明显地区分两种设计方式的使用场合。
141 |
142 | ## 多态
143 |
144 | 我们在处理类的层次结构时,通常把一个对象看成是它所属的基类,而不是把它当成具体类。通过这种方式,我们可以编写出不局限于特定类型的代码。在上个“形状”的例子中,“方法”(method)操纵的是通用“形状”,而不关心它们是“圆”、“正方形”、“三角形”还是某种尚未定义的形状。所有的形状都可以被绘制、擦除和移动,因此“方法”向其中的任何代表“形状”的对象发送消息都不必担心对象如何处理信息。
145 |
146 | 这样的代码不会受添加的新类型影响,并且添加新类型是扩展面向对象程序以处理新情况的常用方法。 例如,你可以通过通用的“形状”基类派生出新的“五角形”形状的子类,而不需要修改通用"形状"基类的方法。通过派生新的子类来扩展设计的这种能力是封装变化的基本方法之一。
147 |
148 | 这种能力改善了我们的设计,且减少了软件的维护代价。如果我们把派生的对象类型统一看成是它本身的基类(“圆”当作“形状”,“自行车”当作“车”,“鸬鹚”当作“鸟”等等),编译器(compiler)在编译时期就无法准确地知道什么“形状”被擦除,哪一种“车”在行驶,或者是哪种“鸟”在飞行。这就是关键所在:当程序接收这种消息时,程序员并不想知道哪段代码会被执行。“绘图”的方法可以平等地应用到每种可能的“形状”上,形状会依据自身的具体类型执行恰当的代码。
149 |
150 | 如果不需要知道执行了哪部分代码,那我们就能添加一个新的不同执行方式的子类而不需要更改调用它的方法。那么编译器在不确定该执行哪部分代码时是怎么做的呢?举个例子,下图的 **BirdController** 对象和通用 **Bird** 对象中,**BirdController** 不知道 **Bird** 的确切类型却还能一起工作。从 **BirdController** 的角度来看,这是很方便的,因为它不需要编写特别的代码来确定 **Bird** 对象的确切类型或行为。那么,在调用 **move()** 方法时是如何保证发生正确的行为(鹅走路、飞或游泳、企鹅走路或游泳)的呢?
151 |
152 | 
153 |
154 | 这个问题的答案,是面向对象程序设计的妙诀:在传统意义上,编译器不能进行函数调用。由非 OOP 编译器产生的函数调用会引起所谓的**早期绑定**,这个术语你可能从未听说过,不会想过其他的函数调用方式。这意味着编译器生成对特定函数名的调用,该调用会被解析为将执行的代码的绝对地址。
155 |
156 | 通过继承,程序直到运行时才能确定代码的地址,因此发送消息给对象时,还需要其他一些方案。为了解决这个问题,面向对象语言使用**后期绑定**的概念。当向对象发送信息时,被调用的代码直到运行时才确定。编译器确保方法存在,并对参数和返回值执行类型检查,但是它不知道要执行的确切代码。
157 |
158 | 为了执行后期绑定,Java 使用一个特殊的代码位来代替绝对调用。这段代码使用对象中存储的信息来计算方法主体的地址(此过程在多态性章节中有详细介绍)。因此,每个对象的行为根据特定代码位的内容而不同。当你向对象发送消息时,对象知道该如何处理这条消息。在某些语言中,必须显式地授予方法后期绑定属性的灵活性。例如,C++ 使用 **virtual** 关键字。在这些语言中,默认情况下方法不是动态绑定的。在 Java 中,动态绑定是默认行为,不需要额外的关键字来实现多态性。
159 |
160 | 为了演示多态性,我们编写了一段代码,它忽略了类型的具体细节,只与基类对话。该代码与具体类型信息分离,因此更易于编写和理解。而且,如果通过继承添加了一个新类型(例如,一个六边形),那么代码对于新类型的 Shape 就像对现有类型一样有效。因此,该程序是可扩展的。
161 |
162 | 代码示例:
163 |
164 | ```java
165 | void doSomething(Shape shape) {
166 | shape.erase();
167 | // ...
168 | shape.draw();
169 | }
170 | ```
171 |
172 | 此方法与任何 **Shape** 对话,因此它与所绘制和擦除的对象的具体类型无关。如果程序的其他部分使用 `doSomething()` 方法:
173 |
174 | ```java
175 | Circle circle = new Circle();
176 | Triangle triangle = new Triangle();
177 | Line line = new Line();
178 | doSomething(circle);
179 | doSomething(triangle);
180 | doSomething(line);
181 |
182 | ```
183 |
184 | 可以看到无论传入的“形状”是什么,程序都正确的执行了。
185 |
186 | 
187 |
188 | 这是一个非常令人惊奇的编程技巧。分析下面这行代码:
189 |
190 | ```java
191 | doSomething(circle);
192 | ```
193 |
194 | 当预期接收 **Shape** 的方法被传入了 **Circle**,会发生什么。由于 **Circle** 也是一种 **Shape**,所
195 | 以 `doSomething(circle)` 能正确地执行。也就是说,`doSomething()` 能接收任意发送给 **Shape** 的消息。这是完全安全和合乎逻辑的事情。
196 |
197 | 这种把子类当成其基类来处理的过程叫做“向上转型”(**upcasting**)。在面向对象的编程里,经常利用这种方法来给程序解耦。再看下面的 `doSomething()` 代码示例:
198 |
199 | ```java
200 | shape.erase();
201 | // ...
202 | shape.draw();
203 | ```
204 |
205 | 我们可以看到程序并未这样表达:“如果你是一个 Circle ,就这样做;如果你是一个 Square,就那样做...”。若那样编写代码,就需检查 Shape 所有可能的类型,如圆、矩形等等。这显然是非常麻烦的,而且每次添加了一种新的 Shape 类型后,都要相应地进行修改。在这里,我们只需说:“你是一种几何形状,我知道你能删掉 `erase()` 和绘制 `draw()`,你自己去做吧,注意细节。”
206 |
207 | 尽管我们没作出任何特殊指示,程序的操作也是完全正确和恰当的。我们知道,为 Circle 调用`draw()` 时执行的代码与为一个 Square 或 Line 调用 `draw()` 时执行的代码是不同的。但在将 `draw()` 信息发给一个匿名 Shape 时,根据 Shape 句柄当时连接的实际类型,会相应地采取正确的操作。这非常神奇,因为当 Java 编译器为 `doSomething()` 编译代码时,它并不知道自己要操作的准确类型是什么。
208 |
209 | 尽管我们确实可以保证最终会为 Shape 调用 `erase()` 和 `draw()`,但并不能确定特定的 Circle,Square 或者 Line 调用什么。最后,程序执行的操作却依然是正确的,这是怎么做到的呢?
210 |
211 | 发送消息给对象时,如果程序不知道接收的具体类型是什么,但最终执行是正确的,这就是对象的“多态性”(Polymorphism)。面向对象的程序设计语言是通过“动态绑定”的方式来实现对象的多态性的。编译器和运行时系统会负责对所有细节的控制;我们只需知道要做什么,以及如何利用多态性来更好地设计程序。
212 |
213 | ## 单继承结构
214 |
215 | 自从 C++ 引入以来,一个 OOP 问题变得尤为突出:是否所有的类都应该默认从一个基类继承呢?这个答案在 Java 中是肯定的(实际上,除 C++ 以外的几乎所有 OOP 语言中也是这样)。在 Java 中,这个最终基类的名字就是 `Object`。
216 |
217 | Java 的单继承结构有很多好处。由于所有对象都具有一个公共接口,因此它们最终都属于同一个基类。相反的,对于 C++ 所使用的多继承的方案则是不保证所有的对象都属于同一个基类。从向后兼容的角度看,多继承的方案更符合 C 的模型,而且受限较少。
218 |
219 | 对于完全面向对象编程,我们必须要构建自己的层次结构,以提供与其他 OOP 语言同样的便利。我们经常会使用到新的类库和不兼容的接口。为了整合它们而花费大气力(有可能还要用上多继承)以获得 C++ 样的“灵活性”值得吗?如果从零开始,Java 这样的替代方案会是更好的选择。
220 |
221 | 另外,单继承的结构使得垃圾收集器的实现更为容易。这也是 Java 在 C++ 基础上的根本改进之一。
222 |
223 | 由于运行期的类型信息会存在于所有对象中,所以我们永远不会遇到判断不了对象类型的情况。这对于系统级操作尤其重要,例如[异常处理](#异常处理)。同时,这也让我们的编程具有更大的灵活性。
224 |
225 | ## 集合
226 |
227 | 通常,我们并不知道解决某个具体问题需要的对象数量和持续时间,以及对象的存储方式。那么我们如何知悉程序在运行时需要分配的内存空间呢?
228 |
229 | 在面向对象的设计中,问题的解决方案有些过于轻率:创建一个新类型的对象来引用、容纳其他的对象。当然,我们也可以使用多数编程语言都支持的“数组”(array)。在 Java 中“集合”(Collection)的使用率更高。(也可称之为“容器”,但“集合”这个称呼更通用。)
230 |
231 | “集合”这种类型的对象可以存储任意类型、数量的其他对象。它能根据需要自动扩容,我们不用关心过程是如何实现的。
232 |
233 | 还好,一般优秀的 OOP 语言都会将“集合”作为其基础包。在 C++ 中,“集合”是其标准库的一部分,通常被称为 STL(Standard Template Library,标准模板库)。SmallTalk 有一套非常完整的集合库。同样,Java 的标准库中也提供许多现成的集合类。
234 |
235 | 在一些库中,一两个泛型集合就能满足我们所有的需求了,而在其他一些类库(Java)中,不同类型的集合对应不同的需求:常见的有 List,常用于保存序列;Map,也称为关联数组,常用于将对象与其他对象关联);Set,只能保存非重复的值;其他还包括如队列(Queue)、树(Tree)、栈(Stack)、堆(Heap)等等。从设计的角度来看,我们真正想要的是一个能够解决某个问题的集合。如果一种集合就满足所有需求,那么我们就不需要剩下的了。之所以选择集合有以下两个原因:
236 |
237 | 1. 集合可以提供不同类型的接口和外部行为。堆栈、队列的应用场景和集合、列表不同,它们中的一种提供的解决方案可能比其他灵活得多。
238 |
239 | 2. 不同的集合对某些操作有不同的效率。例如,List 的两种基本类型:ArrayList 和 LinkedList。虽然两者具有相同接口和外部行为,但是在某些操作中它们的效率差别很大。在 ArrayList 中随机查找元素是很高效的,而 LinkedList 随机查找效率低下。反之,在 LinkedList 中插入元素的效率要比在 ArrayList 中高。由于底层数据结构的不同,每种集合类型在执行相同的操作时会表现出效率上的差异。
240 |
241 | 我们可以一开始使用 LinkedList 构建程序,在优化系统性能时改用 ArrayList。通过对 List 接口的抽象,我们可以很容易地将 LinkedList 改为 ArrayList。
242 |
243 | 在 Java 5 泛型出来之前,集合中保存的是通用类型 `Object`。Java 单继承的结构意味着所有元素都基于 `Object` 类,所以在集合中可以保存任何类型的数据,易于重用。要使用这样的集合,我们先要往集合添加元素。由于 Java 5 版本前的集合只保存 `Object`,当我们往集合中添加元素时,元素便向上转型成了 `Object`,从而丢失自己原有的类型特性。这时我们再从集合中取出该元素时,元素的类型变成了 `Object`。那么我们该怎么将其转回原先具体的类型呢?这里,我们使用了强制类型转换将其转为更具体的类型,这个过程称为对象的“向下转型”。通过“向上转型”,我们知道“圆形”也是一种“形状”,这个过程是安全的。可是我们不能从“Object”看出其就是“圆形”或“形状”,所以除非我们能确定元素的具体类型信息,否则“向下转型”就是不安全的。也不能说这样的错误就是完全危险的,因为一旦我们转化了错误的类型,程序就会运行出错,抛出“运行时异常”(RuntimeException)。(后面的章节会提到) 无论如何,我们要寻找一种在取出集合元素时确定其具体类型的方法。另外,每次取出元素都要做额外的“向下转型”对程序和程序员都是一种开销。以某种方式创建集合,以确认保存元素的具体类型,减少集合元素“向下转型”的开销和可能出现的错误难道不好吗?这种解决方案就是:参数化类型机制(Parameterized Type Mechanism)。
244 |
245 | 参数化类型机制可以使得编译器能够自动识别某个 `class` 的具体类型并正确地执行。举个例子,对集合的参数化类型机制可以让集合仅接受“形状”这种类型的元素,并以“形状”类型取出元素。Java 5 版本支持了参数化类型机制,称之为“泛型”(Generic)。泛型是 Java 5 的主要特性之一。你可以按以下方式向 ArrayList 中添加 Shape(形状):
246 |
247 | ```java
248 | ArrayList shapes = new ArrayList<>();
249 | ```
250 |
251 | 泛型的应用,让 Java 的许多标准库和组件都发生了改变。在本书的代码示例中,你也会经常看到泛型的身影。
252 |
253 | ## 对象创建与生命周期
254 |
255 | 我们在使用对象时要注意的一个关键问题就是对象的创建和销毁方式。每个对象的生存都需要资源,尤其是内存。为了资源的重复利用,当对象不再被使用时,我们应该及时释放资源,清理内存。
256 |
257 | 在简单的编程场景下,对象的清理并不是问题。我们创建对象,按需使用,最后销毁它。然而,情况往往要比这更复杂:
258 |
259 | 假设,我们正在为机场设计一个空中交通管制的系统(该例也适用于仓库货柜管理、影带出租或者宠物寄养仓库系统)。第一步比较简单:创建一个用来保存飞机的集合,每当有飞机进入交通管制区域时,我们就创建一个“飞机”对象并将其加入到集合中,等到飞机离开时将其从这个集合中清除。与此同时,我们还需要一个记录飞机信息的系统,也许这些数据不像主要控制功能那样引人注意。比如,我们要记录所有飞机中的小型飞机的的信息(比如飞行计划)。此时,我们又创建了第二个集合来记录所有小型飞机。 每当创建一个“飞机”对象的时候,将其放入第一个集合;若它属于小型飞机,也必须同时将其放入第二个集合里。
260 |
261 | 现在问题开始棘手了:我们怎么知道何时该清理这些对象呢?当某一个系统处理完成,而其他系统可能还没有处理完成。这样的问题在其他的场景下也可能发生。在 C++ 程序设计中,当使用完一个对象后,必须明确将其删除,这就让问题变复杂了。
262 |
263 | 对象的数据在哪?它的生命周期是怎么被控制的? 在 C++ 设计中采用的观点是效率第一,因此它将选择权交给了程序员。为了获得最大的运行时速度,程序员可以在编写程序时,通过将对象放在栈(Stack,有时称为自动变量或作用域变量)或静态存储区域(static storage area)中来确定内存占用和生存时间。这些区域的对象会被优先分配内存和释放。这种控制在某些情况下非常有用。
264 |
265 | 然而相对的,我们也牺牲了程序的灵活性。因为在编写代码时,我们必须要弄清楚对象的数量、生存时间还有类型。如果我们要用它来解决一个相当普遍的问题时(如计算机辅助设计、仓库管理或空中交通管制等),限制就太大了。
266 |
267 | 第二种方法是在堆内存(Heap)中动态地创建对象。在这种方式下,直到程序运行我们才能确定需要创建的对象数量、生存时间和类型。什么时候需要,什么时候在堆内存中创建。 因为内存的占用是动态管理的,所以在运行时,在堆内存上开辟空间所需的时间可能比在栈内存上要长(但也不一定)。在栈内存开辟和释放空间通常是一条将栈指针向下移动和一条将栈指针向上移动的汇编指令。开辟堆内存空间的时间取决于内存机制的设计。
268 |
269 | 动态方法有这样一个一般性的逻辑假设:对象趋向于变得复杂,因此额外的内存查找和释放对对象的创建影响不大。(原文:_The dynamic approach makes the generally logical assumption that objects tend to be complicated, so the extra overhead of finding storage and releasing that storage will not have an important impact on the creation of an object._)此外,更好的灵活性对于问题的解决至关重要。
270 |
271 | Java 使用动态内存分配。每次创建对象时,使用 `new` 关键字构建该对象的动态实例。这又带来另一个问题:对象的生命周期。较之堆内存,在栈内存中创建对象,编译器能够确定该对象的生命周期并自动销毁它;然而如果你在堆内存创建对象的话,编译器是不知道它的生命周期的。在 C++ 中你必须以编程方式确定何时销毁对象,否则可能导致内存泄漏。Java 的内存管理是建立在垃圾收集器上的,它能自动发现对象不再被使用并释放内存。垃圾收集器的存在带来了极大的便利,它减少了我们之前必须要跟踪的问题和编写相关代码的数量。因此,垃圾收集器提供了更高级别的保险,以防止潜在的内存泄漏问题,这个问题使得许多 C++ 项目没落。
272 |
273 | Java 的垃圾收集器被设计用来解决内存释放的问题(虽然这不包括对象清理的其他方面)。垃圾收集器知道对象什么时候不再被使用并且自动释放内存。结合单继承和仅可在堆中创建对象的机制,Java 的编码过程比用 C++ 要简单得多。我们所要做的决定和要克服的障碍也会少很多!
274 |
275 | ## 异常处理
276 |
277 | 自编程语言被发明以来,程序的错误处理一直都是个难题。因为很难设计出一个好的错误处理方案,所以许多编程语言都忽略了这个问题,把这个问题丢给了程序类库的设计者。他们提出了在许多情况下都可以工作但很容易被规避的半途而废的措施,通常只需忽略错误。多数错误处理方案的主要问题是:它们依赖程序员之间的约定俗成而不是语言层面的限制。换句话说,如果程序员赶时间或没想起来,这些方案就很容易被忘记。
278 |
279 | 异常处理机制将程序错误直接交给编程语言甚至是操作系统。“异常”(Exception)是一个从出错点“抛出”(thrown)后能被特定类型的异常处理程序捕获(catch)的一个对象。它不会干扰程序的正常运行,仅当程序出错的时候才被执行。这让我们的编码更简单:不用再反复检查错误了。另外,异常不像方法返回的错误值和方法设置用来表示发生错误的标志位那样可以被忽略。异常的发生是不会被忽略的,它终究会在某一时刻被处理。
280 |
281 | 最后,“异常机制”提供了一种可靠地从错误状况中恢复的方法,使得我们可以编写出更健壮的程序。有时你只要处理好抛出的异常情况并恢复程序的运行即可,无需退出。
282 |
283 | Java 的异常处理机制在编程语言中脱颖而出。Java 从一开始就内置了异常处理,因此你不得不使用它。这是 Java 语言唯一接受的错误报告方法。如果没有编写适当的异常处理代码,你将会收到一条编译时错误消息。这种有保障的一致性有时会让程序的错误处理变得更容易。值得注意的是,异常处理并不是面向对象的特性。尽管在面向对象的语言中异常通常由对象表示,但是在面向对象语言之前也存在异常处理。
284 |
285 | ## 本章小结
286 |
287 | 面向过程程序包含数据定义和函数调用。要找到程序的意图,你必须要在脑中建立一个模型,弄清函数调用和更底层的概念。这些程序令人困扰,因为它们的表示更多地面向计算机而不是我们要解决的问题,这就是我们在设计程序时需要中间表示的原因。OOP 在面向过程编程的基础上增加了许多新的概念,所以有人会认为使用 Java 来编程会比同等的面向过程编程要更复杂。在这里,我想给大家一个惊喜:通常按照 Java 规范编写的程序会比面向过程程序更容易被理解。
288 |
289 | 你看到的是对象的概念,这些概念是站在“问题空间”的(而不是站在计算机角度的“解决方案空间”),以及发送消息给对象以指示该空间中的活动。面向对象编程的一个优点是:设计良好的 Java 程序代码更容易被人阅读理解。由于 Java 类库的复用性,通常程序要写的代码也会少得多。
290 |
291 | OOP 和 Java 不一定适合每个人。评估自己的需求以及与现有方案作比较是很重要的。请充分考虑后再决定是不是选择 Java。如果在可预见的未来,Java 并不能很好的满足你的特定需求,那么你应该去寻找其他替代方案(特别是,我推荐看 Python)。如果你依然选择 Java 作为你的开发语言,我希望你至少应该清楚你选择的是什么,以及为什么选择这个方向。
292 |
--------------------------------------------------------------------------------
/docs/ch2.md:
--------------------------------------------------------------------------------
1 | # 第 2 章 安装 Java 和本书用例
2 |
3 | 现在,我们来为这次阅读之旅做些准备吧!
4 |
5 | 在开始学习 Java 之前,你必须要先安装好 Java 和本书的源代码示例。因为考虑到可能有“专门的初学者”从本书开始学习编程,所以我会详细地教你如何使用命令行。 如果你已经有此方面的经验了,可以跳过这段安装说明。如果你对此处描述的任何术语或过程仍不清楚,还可以通过 [Google](https://google.com/) 搜索找到答案。具体的问题或困难请试着在 [StackOverflow](https://stackoverflow.com/) 上提问。或者去 [YouTube](https://youtube.com) 看有没有相关的安装说明。
6 |
7 | ## 编辑器
8 |
9 | 首先你需要安装一个编辑器来创建和修改本书用例里的 Java 代码。有可能你还需要使用编辑器来更改系统配置文件。
10 |
11 | 相比一些重量级的 IDE(Integrated Development Environments,集成开发环境),如 Eclipse、NetBeans 和 IntelliJ IDEA (译者注:做项目强烈推荐 IDEA),编辑器是一种更纯粹的文本编辑器。如果你已经有了一个用着顺手的 IDE,那就可以直接用了。为了方便后面的学习和统一下教学环境,我推荐大家使用 Atom 这个编辑器。大家可以在 [atom.io](https://atom.io) 上下载。
12 |
13 | Atom 是一个免费开源、易于安装且跨平台(支持 Window、Mac 和 Linux)的文本编辑器。内置支持 Java 文件。相比 IDE 的厚重,它比较轻量级,是学习本书的理想工具。Atom 包含了许多方便的编辑功能,相信你一定会爱上它!更多关于 Atom 使用的细节问题可以到它的网站上寻找。
14 |
15 | 还有很多其他的编辑器。有一种亚文化的群体,他们热衷于争论哪个更好用!如果你找到一个你更喜欢的编辑器,换一种使用也没什么难度。重要的是,你要找一个用着舒服的。
16 |
17 | ## Shell
18 |
19 | 如果你之前没有接触过编程,那么有可能对 Shell(命令行窗口) 不太熟悉。shell 的历史可以追溯到早期的计算时代,当时在计算机上的操作是都通过输入命令进行的,计算机通过回显响应。所有的操作都是基于文本的。
20 |
21 | 尽管和现在的图形用户界面相比,Shell 操作方式很原始。但是同时 shell 也为我们提供了许多有用的功能特性。在学习本书的过程中,我们会经常使用到 Shell,包括现在这部分的安装,还有运行 Java 程序。
22 |
23 | Mac:单击聚光灯(屏幕右上角的放大镜图标),然后键入 `terminal`。单击看起来像小电视屏幕的应用程序(你也可以单击“return”)。这就启动了你的用户下的 shell 窗口。
24 |
25 | windows:首先,通过目录打开 windows 资源管理器:
26 |
27 | - Windows 7: 单击屏幕左下角的“开始”图标,输入“explorer”后按回车键。
28 | - Windows 8: 按 Windows+Q,输入 “explorer” 后按回车键。
29 | - Windows 10: 按 Windows+E 打开资源管理器,导航到所需目录,单击窗口左上角的“文件“选项卡,选择“打开 Window PowerShell”启动 Shell。
30 |
31 | Linux: 在 home 目录打开 Shell。
32 |
33 | - Debian: 按 Alt+F2, 在弹出的对话框中输入“gnome-terminal”
34 | - Ubuntu: 在屏幕中鼠标右击,选择 “打开终端”,或者按住 Ctrl+Alt+T
35 | - Redhat: 在屏幕中鼠标右击,选择 “打开终端”
36 | - Fedora: 按 Alt+F2,在弹出的对话框中输入“gnome-terminal”
37 |
38 | **目录**
39 |
40 | 目录是 Shell 的基础元素之一。目录用来保存文件和其他目录。目录就好比树的分支。如果书籍是你系统上的一个目录,并且它有两个其他目录作为分支,例如数学和艺术,那么我们就可以说你有一个书籍目录,它包含数学和艺术两个子目录。注意:Windows 使用 `\` 而不是 `/` 来分隔路径。
41 |
42 | **Shell 基本操作**
43 |
44 | 我在这展示的 Shell 操作和系统中大体相同。出于本书的原因,下面列举一些在 Shell 中的基本操作:
45 |
46 | ```bash
47 | 更改目录: cd <路径>
48 | cd .. 移动到上级目录
49 | pushd <路径> 记住来源的同时移动到其他目录,popd 返回来源
50 |
51 | 目录列举: ls 列举出当前目录下所有的文件和子目录名(不包含隐藏文件),
52 | 可以选择使用通配符 * 来缩小搜索范围。
53 | 示例(1): 列举所有以“.java”结尾的文件,输入 ls *.java (Windows: dir *.java)
54 | 示例(2): 列举所有以“F”开头,“.java”结尾的文件,输入ls F*.java (Windows: dir F*.java)
55 |
56 | 创建目录:
57 | Mac/Linux 系统:mkdir
58 | 示例:mkdir books
59 | Windows 系统:md
60 | 示例:md books
61 |
62 | 移除文件:
63 | Mac/Linux 系统:rm
64 | 示例:rm somefile.java
65 | Windows 系统:del
66 | 示例:del somefile.java
67 |
68 | 移除目录:
69 | Mac/Linux 系统:rm -r
70 | 示例:rm -r books
71 | Windows 系统:deltree
72 | 示例:deltree books
73 |
74 | 重复命令: !! 重复上条命令
75 | 示例:!n 重复倒数第n条命令
76 |
77 | 命令历史:
78 | Mac/Linux 系统:history
79 | Windows 系统:按 F7 键
80 |
81 | 文件解压:
82 | Linux/Mac 都有命令行解压程序 unzip,你可以通过互联网为 Windows 安装命令行解压程序 unzip。
83 | 图形界面下(Windows 资源管理器,Mac Finder,Linux Nautilus 或其他等效软件)右键单击该文件,
84 | 在 Mac 上选择“open”,在 Linux 上选择“extract here”,或在 Windows 上选择“extract all…”。
85 | 要了解关于 shell 的更多信息,请在维基百科中搜索 Windows shell,Mac/Linux用户可搜索 bash shell。
86 |
87 | ```
88 |
89 | ## Java 安装
90 |
91 | 为了编译和运行代码示例,首先你必须安装 JDK(Java Development Kit,JAVA 软件开发工具包)。本书中采用的是 JDK 8。
92 |
93 | **Windows**
94 |
95 | 1. 以下为 Chocolatey 的[安装说明](https://chocolatey.org/)。
96 | 2. 在命令行提示符下输入下面的命令,等待片刻,结束后 Java 安装完成并自动完成环境变量设置。
97 |
98 | ```bash
99 | choco install jdk8
100 | ```
101 |
102 | **Macintosh**
103 |
104 | Mac 系统自带的 Java 版本太老,为了确保本书的代码示例能被正确执行,你必须将它先更新到 Java 8。我们需要管理员权限来运行下面的步骤:
105 |
106 | 1. 以下为 HomeBrew 的[安装说明](https://brew.sh/)。安装完成后执行命令 `brew update` 更新到最新版本
107 | 2. 在命令行下执行下面的命令来安装 Java。
108 |
109 | ```bash
110 | brew cask install java
111 | ```
112 |
113 | 当以上安装都完成后,如果你有需要,可以使用游客账户来运行本书中的代码示例。
114 |
115 | **Linux**
116 |
117 | - **Ubuntu/Debian**:
118 |
119 | ```bash
120 | sudo apt-get update
121 | sudo apt-get install default-jdk
122 | ```
123 |
124 | - **Fedora/Redhat**:
125 |
126 | ```bash
127 | su-c "yum install java-1.8.0-openjdk"(注:执行引号内的内容就可以安装)
128 | ```
129 |
130 | ## 校验安装
131 |
132 | 打开新的命令行输入:
133 |
134 | ```bash
135 | java -version
136 | ```
137 |
138 | 正常情况下 你应该看到以下类似信息(版本号信息可能不一样):
139 |
140 | ```bash
141 | java version "1.8.0_112"
142 | Java(TM) SE Runtime Environment (build 1.8.0_112-b15)
143 | Java HotSpot(TM) 64-Bit Server VM (build 25.112-b15, mixed mode)
144 | ```
145 |
146 | 如果提示命令找不到或者无法被识别,请根据安装说明重试;如果还不行,尝试到 [StackOverflow](https://stackoverflow.com/search?q=installing+java) 寻找答案。
147 |
148 | ## 安装和运行代码示例
149 |
150 | 当 Java 安装完毕,下一步就是安装本书的代码示例了。安装步骤所有平台一致:
151 |
152 | 1. 从 [GitHub 仓库](https://github.com/BruceEckel/OnJava8-Examples/archive/master.zip)中下载本书代码示例
153 | 2. 解压到你所选目录里。
154 | 3. 使用 Windows 资源管理器,Mac Finder,Linux 的 Nautilus 或其他等效工具浏览,在该目录下打开 Shell。
155 | 4. 如果你在正确的目录中,你应该看到该目录中名为 gradlew 和 gradlew.bat 的文件,以及许多其他文件和目录。目录与书中的章节相对应。
156 | 5. 在 shell 中输入下面的命令运行:
157 |
158 | ```bash
159 | Windows 系统:
160 | gradlew run
161 |
162 | Mac/Linux 系统:
163 | ./gradlew run
164 | ```
165 |
166 | 第一次安装时 Gradle 需要安装自身和其他的相关的包,请稍等片刻。安装完成后,后续的安装将会快很多。
167 |
168 | **注意**: 第一次运行 gradlew 命令时必须连接互联网。
169 |
170 | **Gradle 基础任务**
171 |
172 | 本书构建的大量 Gradle 任务都可以自动运行。Gradle 使用约定大于配置的方式,简单设置即可具备高可用性。本书中“一起去骑行”的某些任务不适用于此或无法执行成功。以下是你通常会使用上的 Gradle 任务列表:
173 |
174 | ```bash
175 | 编译本书中的所有 java 文件,除了部分错误示范的
176 | gradlew compileJava
177 |
178 | 编译并执行 java 文件(某些文件是库组件)
179 | gradlew run
180 |
181 | 执行所有的单元测试(在本书第16章会有详细介绍)
182 | gradlew test
183 |
184 | 编译并运行一个具体的示例程序
185 | gradlew <本书章节>:<示例名称>
186 | 示例:gradlew objects:HelloDate
187 | ```
188 |
--------------------------------------------------------------------------------
/docs/ch25.md:
--------------------------------------------------------------------------------
1 | # 第 25 章 设计模式
2 |
3 | ## 概念
4 |
5 | 最初,你可以将模式视为解决特定类问题的一种特别巧妙且有深刻见解的方法。这就像前辈已经从所有角度去解决问题,并提出了最通用,最灵活的解决方案。问题可能是你之前看到并解决过的问题,但你的解决方案可能没有你在模式中体现的那种完整性。
6 |
7 | 虽然它们被称为“设计模式”,但它们实际上并不与设计领域相关联。模式似乎与传统的分析、设计和实现的思维方式不同。相反,模式在程序中体现了一个完整的思想,因此它有时会出现在分析阶段或高级设计阶段。因为模式在代码中有一个直接的实现,所以你可能不会期望模式在低级设计或实现之前出现(而且通常在到达这些阶段之前,你不会意识到需要一个特定的模式)。
8 |
9 | 模式的基本概念也可以看作是程序设计的基本概念:添加抽象层。当你抽象一些东西的时候,就像在剥离特定的细节,而这背后最重要的动机之一是:
10 |
11 | > **将易变的事物与不变的事物分开**
12 |
13 | 另一种方法是,一旦你发现程序的某些部分可能因某种原因而发生变化,你要保持这些变化不会引起整个代码中其他变化。 如果代码更容易理解,那么维护起来会更容易。
14 |
15 | 通常,开发一个优雅且易维护设计中最困难的部分是发现我称之为变化的载体(也就是最易改变的地方)。这意味着找到系统中最重要的变化,换而言之,找到变化会导致最严重后果的地方。一旦发现变化载体,就可以围绕构建设计的焦点。
16 |
17 | 因此,设计模式的目标是隔离代码中的更改。 如果以这种方式去看,你已经在本书中看到了设计模式。 例如,继承可以被认为是一种设计模式(虽然是由编译器实现的)。它允许你表达所有具有相同接口的对象(即保持相同的行为)中的行为差异(这就是变化的部分)。组合也可以被视为一种模式,因为它允许你动态或静态地更改实现类的对象,从而改变类的工作方式。
18 |
19 | 你还看到了设计模式中出现的另一种模式:迭代器(Java 1.0 和 1.1 随意地将其称为枚举; Java 2 集合才使用 Iterator)。当你逐个选择元素时并逐步处理,这会隐藏集合的特定实现。迭代器允许你编写通用代码,该代码对序列中的所有元素执行操作,而不考虑序列的构建方式。因此,你的通用代码可以与任何可以生成迭代器的集合一起使用。
20 |
21 | 即使模式是非常有用的,但有些人断言:
22 |
23 | > **设计模式代表语言的失败。**
24 |
25 | 这是一个非常重要的见解,因为一个模式在 C++ 有意义,可能在 JAVA 或者其他语言中就没有意义。出于这个原因,所以一个模式可能出现在设计模式书上,不意味着应用于你的编程语言是有用的。
26 |
27 | 我认为“语言失败”这个观点是有道理的,但是我也认为这个观点过于简单化。如果你试图解决一个特定的问题,而你使用的语言没有直接提供支持你使用的技巧,你可以说这个是语言的失败。但是,你使用特定的技巧的频率的是多少呢?也许平衡是对的:当你使用特定的技巧的时候,你必须付出更多的努力,但是你又没有足够的理由去使得语言支持这个技术。另一方面,没有语言的支持,使用这种技术常常会很混乱,但是在语言支持下,你可能会改变编程方式(例如,Java 8 流实现此目的)。
28 |
29 | ### 单例模式
30 |
31 | 也许单例模式是最简单的设计模式,它是一种提供一个且只有一个对象实例的方法。这在 java 库中使用,但是这有个更直接的示例:
32 |
33 | ```java
34 | // patterns/SingletonPattern.java
35 | interface Resource {
36 | int getValue();
37 | void setValue(int x);
38 | }
39 |
40 | /*
41 | * 由于这不是从Cloneable基类继承而且没有添加可克隆性,
42 | * 因此将其设置为final可防止通过继承添加可克隆性。
43 | * 这也实现了线程安全的延迟初始化:
44 | */
45 | final class Singleton {
46 | private static final class ResourceImpl implements Resource {
47 | private int i;
48 | private ResourceImpl(int i) {
49 | this.i = i;
50 | }
51 | public synchronized int getValue() {
52 | return i;
53 | }
54 | public synchronized void setValue(int x) {
55 | i = x;
56 | }
57 | }
58 |
59 | private static class ResourceHolder {
60 | private static Resource resource = new ResourceImpl(47);
61 | }
62 | public static Resource getResource() {
63 | return ResourceHolder.resource;
64 | }
65 | }
66 |
67 | public class SingletonPattern {
68 | public static void main(String[] args) {
69 | Resource r = Singleton.getResource();
70 | System.out.println(r.getValue());
71 | Resource s2 = Singleton.getResource();
72 | s2.setValue(9);
73 | System.out.println(r.getValue());
74 | try {
75 | // 不能这么做,会发生:compile-time error(编译时错误).
76 | // Singleton s3 = (Singleton)s2.clone();
77 | } catch(Exception e) {
78 | throw new RuntimeException(e);
79 | }
80 | }
81 | } /* Output: 47 9 */
82 | ```
83 |
84 | 创建单例的关键是防止客户端程序员直接创建对象。 在这里,这是通过在 Singleton 类中将 Resource 的实现作为私有类来实现的。
85 |
86 | 此时,你将决定如何创建对象。在这里,它是按需创建的,在第一次访问的时候创建。 该对象是私有的,只能通过 public getResource()方法访问。
87 |
88 | 懒惰地创建对象的原因是它嵌套的私有类 resourceHolder 在首次引用之前不会加载(在 getResource()中)。当 Resource 对象加载的时候,静态初始化块将被调用。由于 JVM 的工作方式,这种静态初始化是线程安全的。为保证线程安全,Resource 中的 getter 和 setter 是同步的。
89 |
90 | ### 模式分类
91 |
92 | “设计模式”一书讨论了 23 种不同的模式,分为以下三种类别(所有这些模式都围绕着可能变化的特定方面)。
93 |
94 | 1. **创建型**:如何创建对象。 这通常涉及隔离对象创建的细节,这样你的代码就不依赖于具体的对象的类型,因此在添加新类型的对象时不会更改。单例模式(Singleton)被归类为创作模式,本章稍后你将看到 Factory Method 的示例。
95 |
96 | 2. **构造型**:设计对象以满足特定的项目约束。它们处理对象与其他对象连接的方式,以确保系统中的更改不需要更改这些连接。
97 |
98 | 3. **行为型**:处理程序中特定类型的操作的对象。这些封装要执行的过程,例如解释语言、实现请求、遍历序列(如在迭代器中)或实现算法。本章包含观察者和访问者模式的例子。
99 |
100 | 《设计模式》一书中每个设计模式都有单独的一个章节,每个章节都有一个或者多个例子,通常使用 C++,但有时也使用 SmallTalk。 本章不重复设计模式中显示的所有模式,因为该书独立存在,应单独研究。 相反,你会看到一些示例,可以为你提供关于模式的理解以及它们如此重要的原因。
101 |
102 | ## 构建应用程序框架
103 |
104 | 应用程序框架允许您从一个类或一组类开始,创建一个新的应用程序,重用现有类中的大部分代码,并根据需要覆盖一个或多个方法来定制应用程序。
105 |
106 | **模板方法模式**
107 |
108 | 应用程序框架中的一个基本概念是模板方法模式,它通常隐藏在底层,通过调用基类中的各种方法来驱动应用程序(为了创建应用程序,您已经覆盖了其中的一些方法)。
109 |
110 | 模板方法模式的一个重要特性是它是在基类中定义的,并且不能更改。它有时是一个 **private** 方法,但实际上总是 **final**。它调用其他基类方法(您覆盖的那些)来完成它的工作,但是它通常只作为初始化过程的一部分被调用(因此框架使用者不一定能够直接调用它)。
111 |
112 | ```Java
113 | // patterns/TemplateMethod.java
114 | // Simple demonstration of Template Method
115 |
116 | abstract class ApplicationFramework {
117 | ApplicationFramework() {
118 | templateMethod();
119 | }
120 |
121 | abstract void customize1();
122 |
123 | abstract void customize2(); // "private" means automatically "final": private void templateMethod() { IntStream.range(0, 5).forEach( n -> { customize1(); customize2(); }); }}// Create a new "application": class MyApp extends ApplicationFramework { @Override void customize1() { System.out.print("Hello "); }@Override
124 |
125 | void customize2() {
126 | System.out.println("World!");
127 | }
128 | }
129 |
130 | public class TemplateMethod {
131 | public static void main(String[] args) {
132 | new MyApp();
133 | }
134 | }
135 | /*
136 | Output:
137 | Hello World!
138 | Hello World!
139 | Hello World!
140 | Hello World!
141 | Hello World!
142 | */
143 | ```
144 |
145 | 基类构造函数负责执行必要的初始化,然后启动运行应用程序的“engine”(模板方法模式)(在 GUI 应用程序中,这个“engine”是主事件循环)。框架使用者只提供
146 | **customize1()** 和 **customize2()** 的定义,然后“应用程序”已经就绪运行。
147 |
148 | 
149 |
150 | ## 面向实现
151 |
152 | 代理模式和桥接模式都提供了在代码中使用的代理类;完成工作的真正类隐藏在这个代理类的后面。当您在代理中调用一个方法时,它只是反过来调用实现类中的方法。这两种模式非常相似,所以代理模式只是桥接模式的一种特殊情况。人们倾向于将两者合并,称为代理模式,但是术语“代理”有一个长期的和专门的含义,这可能解释了这两种模式不同的原因。基本思想很简单:从基类派生代理,同时派生一个或多个提供实现的类:创建代理对象时,给它一个可以调用实际工作类的方法的实现。
153 |
154 | 在结构上,代理模式和桥接模式的区别很简单:代理模式只有一个实现,而桥接模式有多个实现。在设计模式中被认为是不同的:代理模式用于控制对其实现的访问,而桥接模式允许您动态更改实现。但是,如果您扩展了“控制对实现的访问”的概念,那么这两者就可以完美地结合在一起
155 |
156 | **代理模式**
157 |
158 | 如果我们按照上面的关系图实现,它看起来是这样的:
159 |
160 | ```Java
161 | // patterns/ProxyDemo.java
162 | // Simple demonstration of the Proxy pattern
163 | interface ProxyBase {
164 | void f();
165 |
166 | void g();
167 |
168 | void h();
169 | }
170 |
171 | class Proxy implements ProxyBase {
172 | private ProxyBase implementation;
173 |
174 | Proxy() {
175 | implementation = new Implementation();
176 | }
177 | // Pass method calls to the implementation:
178 | @Override
179 | public void f() { implementation.f(); }
180 | @Override
181 | public void g() { implementation.g(); }
182 | @Override
183 | public void h() { implementation.h(); }
184 | }
185 |
186 | class Implementation implements ProxyBase {
187 | public void f() {
188 | System.out.println("Implementation.f()");
189 | }
190 |
191 | public void g() {
192 | System.out.println("Implementation.g()");
193 | }
194 |
195 | public void h() {
196 | System.out.println("Implementation.h()");
197 | }
198 | }
199 |
200 | public class ProxyDemo {
201 | public static void main(String[] args) {
202 | Proxy p = new Proxy();
203 | p.f();
204 | p.g();
205 | p.h();
206 | }
207 | }
208 | /*
209 | Output:
210 | Implementation.f()
211 | Implementation.g()
212 | Implementation.h()
213 | */
214 | ```
215 |
216 | 具体实现不需要与代理对象具有相同的接口;只要代理对象以某种方式“代表具体实现的方法调用,那么基本思想就算实现了。然而,拥有一个公共接口是很方便的,因此具体实现必须实现代理对象调用的所有方法。
217 |
218 | **状态模式**
219 |
220 | 状态模式向代理对象添加了更多的实现,以及在代理对象的生命周期内从一个实现切换到另一种实现的方法:
221 |
222 | ```Java
223 | // patterns/StateDemo.java // Simple demonstration of the State pattern
224 | interface StateBase {
225 | void f();
226 |
227 | void g();
228 |
229 | void h();
230 |
231 | void changeImp(StateBase newImp);
232 | }
233 |
234 | class State implements StateBase {
235 | private StateBase implementation;
236 |
237 | State(StateBase imp) {
238 | implementation = imp;
239 | }
240 |
241 | @Override
242 | public void changeImp(StateBase newImp) {
243 | implementation = newImp;
244 | }// Pass method calls to the implementation: @Override public void f() { implementation.f(); } @Override public void g() { implementation.g(); } @Override
245 |
246 | public void h() {
247 | implementation.h();
248 | }
249 | }
250 |
251 | class Implementation1 implements StateBase {
252 | @Override
253 | public void f() {
254 | System.out.println("Implementation1.f()");
255 | }
256 |
257 | @Override
258 | public void g() {
259 | System.out.println("Implementation1.g()");
260 | }
261 |
262 | @Override
263 | public void h() {
264 | System.out.println("Implementation1.h()");
265 | }
266 |
267 | @Override
268 | public void changeImp(StateBase newImp) {
269 | }
270 | }
271 |
272 | class Implementation2 implements StateBase {
273 | @Override
274 | public void f() {
275 | System.out.println("Implementation2.f()");
276 | }
277 |
278 | @Override
279 | public void g() {
280 | System.out.println("Implementation2.g()");
281 | }
282 |
283 | @Override
284 | public void h() {
285 | System.out.println("Implementation2.h()");
286 | }
287 |
288 | @Override
289 | public void changeImp(StateBase newImp) {
290 | }
291 | }
292 |
293 | public class StateDemo {
294 | static void test(StateBase b) {
295 | b.f();
296 | b.g();
297 | b.h();
298 | }
299 |
300 | public static void main(String[] args) {
301 | StateBase b = new State(new Implementation1());
302 | test(b);
303 | b.changeImp(new Implementation2());
304 | test(b);
305 | }
306 | }
307 | /* Output:
308 | Implementation1.f()
309 | Implementation1.g()
310 | Implementation1.h()
311 | Implementation2.f()
312 | Implementation2.g()
313 | Implementation2.h()
314 | */
315 | ```
316 |
317 | 在 main()中,首先使用第一个实现,然后改变成第二个实现。代理模式和状态模式的区别在于它们解决的问题。设计模式中描述的代理模式的常见用途如下:
318 |
319 | 1. 远程代理。它在不同的地址空间中代理对象。远程方法调用(RMI)编译器 rmic 会自动为您创建一个远程代理。
320 |
321 | 2. 虚拟代理。这提供了“懒加载”来根据需要创建“昂贵”的对象。
322 |
323 | 3. 保护代理。当您希望对代理对象有权限访问控制时使用。
324 |
325 | 4. 智能引用。要在被代理的对象被访问时添加其他操作。例如,跟踪特定对象的引用数量,来实现写时复制用法,和防止对象别名。一个更简单的例子是跟踪特定方法的调用数量。您可以将 Java 引用视为一种保护代理,因为它控制在堆上实例对象的访问(例如,确保不使用空引用)。
326 |
327 | 在设计模式中,代理模式和桥接模式并不是相互关联的,因为它们被赋予(我认为是任意的)不同的结构。桥接模式,特别是使用一个单独的实现,但这似乎对我来说是不必要的,除非你确定该实现是你无法控制的(当然有可能,但是如果您编写所有代码,那么没有理由不从单基类的优雅中受益)。此外,只要代理对象控制对其“前置”对象的访问,代模式理就不需要为其实现使用相同的基类。不管具体情况如何,在代理模式和桥接模式中,代理对象都将方法调用传递给具体实现对象。
328 |
329 | **状态机**
330 |
331 | 桥接模式允许程序员更改实现,状态机利用一个结构来自动地将实现更改到下一个。当前实现表示系统所处的状态,系统在不同状态下的行为不同(因为它使用桥接模式)。基本上,这是一个利用对象的“状态机”。将系统从一种状态移动到另一种状态的代码通常是模板方法模式,如下例所示:
332 |
333 | ```Java
334 | // patterns/state/StateMachineDemo.java
335 | // The StateMachine pattern and Template method
336 | // {java patterns.state.StateMachineDemo}
337 | package patterns.state;
338 |
339 | import onjava.Nap;
340 |
341 | interface State {
342 | void run();
343 | }
344 |
345 | abstract class StateMachine {
346 | protected State currentState;
347 |
348 | Nap(0.5);
349 | System.out.println("Washing"); new
350 |
351 | protected abstract boolean changeState();
352 |
353 | // Template method:
354 | protected final void runAll() {
355 | while (changeState()) // Customizable
356 | currentState.run();
357 | }
358 | }
359 |
360 | // A different subclass for each state:
361 | class Wash implements State {
362 | @Override
363 | public void run() {
364 | }
365 | }
366 |
367 | class Spin implements State {
368 | @Override
369 | public void run() {
370 | System.out.println("Spinning");
371 | new Nap(0.5);
372 | }
373 | }
374 |
375 | class Rinse implements State {
376 | @Override
377 | public void run() {
378 | System.out.println("Rinsing");
379 | new Nap(0.5);
380 | }
381 | }
382 |
383 | class Washer extends StateMachine {
384 | private int i = 0;
385 |
386 | // The state table:
387 | private State[] states = {new Wash(), new Spin(), new Rinse(), new Spin(),};
388 |
389 | Washer() {
390 | runAll();
391 | }
392 |
393 | @Override
394 | public boolean changeState() {
395 | if (i < states.length) {
396 | // Change the state by setting the
397 | // surrogate reference to a new object:
398 | currentState = states[i++];
399 | return true;
400 | } else return false;
401 | }
402 | }
403 |
404 | public class StateMachineDemo {
405 | public static void main(String[] args) {
406 | new Washer();
407 | }
408 | }
409 | /*
410 | Output:
411 | Washing
412 | Spinning
413 | Rinsing
414 | Spinning
415 | */
416 | ```
417 |
418 | 在这里,控制状态的类(本例中是状态机)负责决定下一个状态。然而,状态对象本身也可以决定下一步移动到什么状态,通常基于系统的某种输入。这是更灵活的解决方案。
419 |
420 | ## 工厂模式
421 |
422 | ## 函数对象
423 |
424 | ## 接口改变
425 |
426 | ## 解释器
427 |
428 | ## 回调
429 |
430 | ## 多次调度
431 |
432 | ## 模式重构
433 |
434 | ## 抽象用法
435 |
436 | ## 多次派遣
437 |
438 | ## 访问者模式
439 |
440 | ## RTTI 的优劣
441 |
442 | ## 本章小结
443 |
--------------------------------------------------------------------------------
/docs/ch3.md:
--------------------------------------------------------------------------------
1 | # 第 3 章 万物皆对象
2 |
3 | > 如果我们说另外一种不同的语言,我们会发觉一个不同的世界!— Ludwig Wittgenstein (1889-1951)
4 |
5 | 相比 C++ ,Java 是一种更纯粹的面向对象编程语言。虽然它们都是混合语言,但在 Java 中,设计者们认为混合的作用并非像在 C++ 中那般重要。混合语言允许多种编程风格,这也是 C++ 支持向后兼容 C 的原因。正因为 C++ 是 C 语言的超集,所以它也同时包含了许多 C 语言不具备的特性,这使得 C++ 在某些方面过于复杂。
6 |
7 | Java 语言假设你只进行面向对象编程。开始学习之前,我们需要将思维置于面向对象的世界。本章你将了解到 Java 程序的基本组成,学习在 Java 中万物(几乎)皆对象的思想。
8 |
9 | ## 对象操纵
10 |
11 | “名字代表什么?玫瑰即使不叫玫瑰,也依旧芬芳”。(引用自 莎士比亚,《罗密欧与朱丽叶》)。
12 |
13 | 所有的编程语言都会操纵内存中的元素。有时程序员必须要有意识地直接或间接地操纵它们。在 C/C++ 中,对象的操纵是通过指针来完成的。
14 |
15 | Java 利用万物皆对象的思想和单一一致的语法方式来简化问题。虽万物皆可为对象,但我们所操纵的标识符实际上只是对对象的“引用” [^1]。 举例:我们可以用遥控器(引用)去操纵电视(对象)。只要拥有对象的“引用”,就可以操纵该“对象”。换句话说,我们无需直接接触电视,就可通过遥控器(引用)自由地控制电视(对象)的频道和音量。此外,没有电视,遥控器也可以单独存在。就是说,你仅仅有一个“引用”并不意味着你必然有一个与之关联的“对象”。
16 |
17 | 下面来创建一个 **String** 引用,用于保存单词或语句。代码示例:
18 |
19 | ```java
20 | String s;
21 | ```
22 |
23 | 这里我们只是创建了一个 **String** 对象的引用,而非对象。直接拿来使用会出现错误:因为此时你并没有给变量 `s` 赋值--指向任何对象。通常更安全的做法是:创建一个引用的同时进行初始化。代码示例:
24 |
25 | ```java
26 | String s = "asdf";
27 | ```
28 |
29 | Java 语法允许我们使用带双引号的文本内容来初始化字符串。同样,其他类型的对象也有相应的初始化方式。
30 |
31 | ## 对象创建
32 |
33 | “引用”用来关联“对象”。在 Java 中,通常我们使用`new`操作符来创建一个新对象。`new` 关键字代表:创建一个新的对象实例。所以,我们也可以这样来表示前面的代码示例:
34 |
35 | ```java
36 | String s = new String("asdf");
37 | ```
38 |
39 | 以上展示了字符串对象的创建过程,以及如何初始化生成字符串。除了 **String** 类型以外,Java 本身自带了许多现成的数据类型。除此之外,我们还可以创建自己的数据类型。事实上,这是 Java 程序设计中的一项基本行为。在本书后面的学习中将会接触到。
40 |
41 | ### 数据存储
42 |
43 | 那么,程序在运行时是如何存储的呢?尤其是内存是怎么分配的。有 5 个不同的地方可以存储数据:
44 |
45 | 1. **寄存器**(Registers)最快的存储区域,位于 CPU 内部 [^2]。然而,寄存器的数量十分有限,所以寄存器根据需求进行分配。我们对其没有直接的控制权,也无法在自己的程序里找到寄存器存在的踪迹(另一方面,C/C++ 允许开发者向编译器建议寄存器的分配)。
46 |
47 | 2. **栈内存**(Stack)存在于常规内存 RAM(随机访问存储器,Random Access Memory)区域中,可通过栈指针获得处理器的直接支持。栈指针下移分配内存,上移释放内存,这是一种快速有效的内存分配方法,速度仅次于寄存器。创建程序时,Java 系统必须准确地知道栈内保存的所有项的生命周期。这种约束限制了程序的灵活性。因此,虽然在栈内存上存在一些 Java 数据,特别是对象引用,但 Java 对象却是保存在堆内存的。
48 |
49 | 3. **堆内存**(Heap)这是一种通用的内存池(也在 RAM 区域),所有 Java 对象都存在于其中。与栈内存不同,编译器不需要知道对象必须在堆内存上停留多长时间。因此,用堆内存保存数据更具灵活性。创建一个对象时,只需用 `new` 命令实例化对象即可,当执行代码时,会自动在堆中进行内存分配。这种灵活性是有代价的:分配和清理堆内存要比栈内存需要更多的时间(如果可以用 Java 在栈内存上创建对象,就像在 C++ 中那样的话)。随着时间的推移,Java 的堆内存分配机制现在已经非常快,因此这不是一个值得关心的问题了。
50 |
51 | 4. **常量存储**(Constant storage)常量值通常直接放在程序代码中,因为它们永远不会改变。如需严格保护,可考虑将它们置于只读存储器 ROM (只读存储器,Read Only Memory)中 [^3]。
52 |
53 | 5. **非 RAM 存储**(Non-RAM storage)数据完全存在于程序之外,在程序未运行以及脱离程序控制后依然存在。两个主要的例子:(1)序列化对象:对象被转换为字节流,通常被发送到另一台机器;(2)持久化对象:对象被放置在磁盘上,即使程序终止,数据依然存在。这些存储的方式都是将对象转存于另一个介质中,并在需要时恢复成常规的、基于 RAM 的对象。Java 为轻量级持久化提供了支持。而诸如 JDBC 和 Hibernate 这些类库为使用数据库存储和检索对象信息提供了更复杂的支持。
54 |
55 | ### 基本类型的存储
56 |
57 | 有一组类型在 Java 中使用频率很高,它们需要特殊对待,这就是 Java 的基本类型。之所以这么说,是因为它们的创建并不是通过 `new` 关键字来产生。通常 `new` 出来的对象都是保存在堆内存中的,以此方式创建小而简单的变量往往是不划算的。所以对于这些基本类型的创建方法,Java 使用了和 C/C++ 一样的策略。也就是说,不是使用 `new` 创建变量,而是使用一个“自动”变量。 这个变量直接存储"值",并置于栈内存中,因此更加高效。
58 |
59 | Java 确定了每种基本类型的内存占用大小。 这些大小不会像其他一些语言那样随着机器环境的变化而变化。这种不变性也是 Java 更具可移植性的一个原因。
60 |
61 | | 基本类型 | 大小 | 最小值 | 最大值 | 包装类型 |
62 | | :------: | :-----: | :--------------: | :-----------------------: | :-------: |
63 | | boolean | — | — | — | Boolean |
64 | | char | 16 bits | Unicode 0 | Unicode 216 -1 | Character |
65 | | byte | 8 bits | -128 | +127 | Byte |
66 | | short | 16 bits | - 215 | + 215 -1 | Short |
67 | | int | 32 bits | - 231 | + 231 -1 | Integer |
68 | | long | 64 bits | - 263 | + 263 -1 | Long |
69 | | float | 32 bits | IEEE754 | IEEE754 | Float |
70 | | double | 64 bits | IEEE754 | IEEE754 | Double |
71 | | void | — | — | — | Void |
72 |
73 | 所有的数值类型都是有正/负符号的。布尔(boolean)类型的大小没有明确的规定,通常定义为取字面值 “true” 或 “false” 。基本类型有自己对应的包装类型,如果你希望在堆内存里表示基本类型的数据,就需要用到它们的包装类。代码示例:
74 |
75 | ```java
76 | char c = 'x';
77 | Character ch = new Character(c);
78 | ```
79 |
80 | 或者你也可以使用下面的形式,基本类型自动转换成包装类型(自动装箱):
81 |
82 | ```java
83 | Character ch = new Character('x');
84 | ```
85 |
86 | 相对的,包装类型转化为基本类型(自动拆箱):
87 |
88 | ```java
89 | char c = ch;
90 | ```
91 |
92 | 个中原因将在以后的章节里解释。
93 |
94 | ### 高精度数值
95 |
96 | 在 Java 中有两种类型的数据可用于高精度的计算。它们是 `BigInteger` 和 `BigDecimal`。尽管它们大致可以划归为“包装类型”,但是它们并没有对应的基本类型。
97 |
98 | 这两个类包含的方法提供的操作,与对基本类型执行的操作相似。也就是说,能对 int 或 float 做的运算,在 BigInteger 和 BigDecimal 这里也同样可以,只不过必须要通过调用它们的方法来实现而非运算符。此外,由于涉及到的计算量更多,所以运算速度会慢一些。诚然,我们牺牲了速度,但换来了精度。
99 |
100 | BigInteger 支持任意精度的整数。可用于精确表示任意大小的整数值,同时在运算过程中不会丢失精度。
101 | BigDecimal 支持任意精度的定点数字。例如,可用它进行精确的货币计算。
102 |
103 | 关于这两个类的详细信息,请参考 JDK 官方文档。
104 |
105 | ### 数组的存储
106 |
107 | 许多编程语言都支持数组类型。在 C 和 C++ 中使用数组是危险的,因为那些数组只是内存块。如果程序访问了内存块之外的数组或在初始化之前使用该段内存(常见编程错误),则结果是不可预测的。
108 |
109 | Java 的设计主要目标之一是安全性,因此许多困扰 C 和 C++ 程序员的问题不会在 Java 中再现。在 Java 中,数组使用前需要被初始化,并且不能访问数组长度以外的数据。这种范围检查,是以每个数组上少量的内存开销及运行时检查下标的额外时间为代价的,但由此换来的安全性和效率的提高是值得的。(并且 Java 经常可以优化这些操作)。
110 |
111 | 当我们创建对象数组时,实际上是创建了一个引用数组,并且每个引用的初始值都为 **null** 。在使用该数组之前,我们必须为每个引用指定一个对象 。如果我们尝试使用为 **null** 的引用,则会在运行时报错。因此,在 Java 中就防止了数组操作的常规错误。
112 |
113 | 我们还可创建基本类型的数组。编译器通过将该数组的内存全部置零来保证初始化。本书稍后将详细介绍数组,特别是在数组章节中。
114 |
115 | ## 代码注释
116 |
117 | Java 中有两种类型的注释。第一种是传统的 C 风格的注释,以 `/*` 开头,可以跨越多行,到 `*/` 结束。注意,许多程序员在多行注释的每一行开头添加 `*`,所以你经常会看到:
118 |
119 | ```java
120 | /* 这是
121 | * 跨越多行的
122 | * 注释
123 | */
124 | ```
125 |
126 | 但请记住,`/*` 和 `*/` 之间的内容都是被忽略的。所以你将其改为下面这样也是没有区别的。
127 |
128 | ```java
129 | /* 这是跨越多
130 | 行的注释 */
131 | ```
132 |
133 | 第二种注释形式来自 C++ 。它是单行注释,以 `//` 开头并一直持续到行结束。这种注释方便且常用,因为直观简单。所以你经常看到:
134 |
135 | ```java
136 | // 这是单行注释
137 | ```
138 |
139 | ## 对象清理
140 |
141 | 在一些编程语言中,管理变量的生命周期需要大量的工作。一个变量需要存活多久?如果我们想销毁它,应该什么时候去做呢?变量生命周期的混乱会导致许多 bug,本小结向你介绍 Java 是如何通过释放存储来简化这个问题的。
142 |
143 | ### 作用域
144 |
145 | 大多数程序语言都有作用域的概念。作用域决定了在该范围内定义的变量名的可见性和生存周期。在 C、 C++ 和 Java 中,作用域是由大括号 `{}` 的位置决定的。例如:
146 |
147 | ```java
148 | {
149 | int x = 12;
150 | // 仅 x 变量可用
151 | {
152 | int q = 96;
153 | // x 和 q 变量皆可用
154 | }
155 | // 仅 x 变量可用
156 | // 变量 q 不在作用域内
157 | }
158 | ```
159 |
160 | Java 的变量只有在其作用域内才可用。缩进使得 Java 代码更易于阅读。由于 Java 是一种自由格式的语言,额外的空格、制表符和回车并不会影响程序的执行结果。在 Java 中,你不能执行以下操作,即使这在 C 和 C++ 中是合法的:
161 |
162 | ```java
163 | {
164 | int x = 12;
165 | {
166 | int x = 96; // Illegal
167 | }
168 | }
169 | ```
170 |
171 | 在上例中, Java 编译器会在提示变量 x 已经被定义过了。因此,在 C/C++ 中将一个较大作用域的变量"隐藏"起来的做法,在 Java 中是不被允许的。 因为 Java 的设计者认为这样做会导致程序混乱。
172 |
173 | ### 对象作用域
174 |
175 | Java 对象与基本类型具有不同的生命周期。当我们使用 `new` 关键字来创建 Java 对象时,它的生命周期将会超出作用域。因此,下面这段代码示例:
176 |
177 | ```java
178 | {
179 | String s = new String("a string");
180 | }
181 | // 作用域终点
182 | ```
183 |
184 | 上例中,引用 s 在作用域终点就结束了。但是,引用 s 指向的字符串对象依然还在占用内存。在这段代码中,我们无法在这个作用域之后访问这个对象,因为唯一对它的引用 s 已超出了作用域的范围。在后面的章节中,我们还会学习怎么在编程中传递和复制对象的引用。
185 |
186 | 只要你需要,`new` 出来的对象就会一直存活下去。 相比在 C++ 编码中操作内存可能会出现的诸多问题,这些困扰在 Java 中都不复存在了。在 C++ 中你不仅要确保对象的内存在你操作的范围内存在,还必须在使用完它们之后,将其销毁。
187 |
188 | 那么问题来了:我们在 Java 中并没有主动清理这些对象,那么它是如何避免 C++ 中出现的内存被填满从而阻塞程序的问题呢?答案是:Java 的垃圾收集器会检查所有 `new` 出来的对象并判断哪些不再可达,继而释放那些被占用的内存,供其他新的对象使用。也就是说,我们不必担心内存回收的问题了。你只需简单创建对象即可。当其不再被需要时,能自行被垃圾收集器释放。垃圾回收机制有效防止了因程序员忘记释放内存而造成的“内存泄漏”问题。
189 |
190 | ## 类的创建
191 |
192 | ### 类型
193 |
194 | 如果一切都是对象,那么是什么决定了某一类对象的外观和行为呢?换句话说,是什么确定了对象的类型?你可能很自然地想到 `type` 关键字。但是,事实上大多数面向对象的语言都使用 `class` 关键字类来描述一种新的对象。 通常在 `class` 关键字的后面的紧跟类的的名称。如下代码示例:
195 |
196 | ```java
197 | class ATypeName {
198 | // 这里是类的内部
199 | }
200 | ```
201 |
202 | 在上例中,我们引入了一个新的类型,尽管这个类里只有一行注释。但是我们一样可以通过 `new` 关键字来创建一个这种类型的对象。如下:
203 |
204 | ```java
205 | ATypeName a = new ATypeName();
206 | ```
207 |
208 | 到现在为止,我们还不能用这个对象来做什么事(即不能向它发送任何有意义的消息),除非我们在这个类里定义一些方法。
209 |
210 | ### 字段
211 |
212 | 当我们创建好一个类之后,我们可以往类里存放两种类型的元素:方法(method)和字段(field)。类的字段可以是基本类型,也可以是引用类型。如果类的字段是对某个对象的引用,那么必须要初始化该引用将其关联到一个实际的对象上(通过之前介绍的创建对象的方法)。每个对象都有用来存储其字段的空间。通常,字段不在对象间共享。下面是一个具有某些字段的类的代码示例:
213 |
214 | ```java
215 | class DataOnly {
216 | int i;
217 | double d;
218 | boolean b;
219 | }
220 | ```
221 |
222 | 这个类除了存储数据之外什么也不能做。但是,我们仍然可以通过下面的代码来创建它的一个对象:
223 |
224 | ```java
225 | DataOnly data = new DataOnly();
226 | ```
227 |
228 | 我们必须通过这个对象的引用来指定字段值。格式:对象名称.方法名称或字段名称。代码示例:
229 |
230 | ```java
231 | data.i = 47;
232 | data.d = 1.1;
233 | data.b = false;
234 | ```
235 |
236 | 如果你想修改对象内部包含的另一个对象的数据,可以通过这样的格式修改。代码示例:
237 |
238 | ```java
239 | myPlane.leftTank.capacity = 100;
240 | ```
241 |
242 | 你可以用这种方式嵌套许多对象(尽管这样的设计会带来混乱)。
243 |
244 | ### 基本类型默认值
245 |
246 | 如果类的成员变量(字段)是基本类型,那么在类初始化时,这些类型将会被赋予一个初始值。
247 |
248 | | 基本类型 | 初始值 |
249 | | :------: | :-----------: |
250 | | boolean | false |
251 | | char | \u0000 (null) |
252 | | byte | (byte) 0 |
253 | | short | (short) 0 |
254 | | int | 0 |
255 | | long | 0L |
256 | | float | 0.0f |
257 | | double | 0.0d |
258 |
259 | 这些默认值仅在 Java 初始化类的时候才会被赋予。这种方式确保了基本类型的字段始终能被初始化(在 C++ 中不会),从而减少了 bug 的来源。但是,这些初始值对于程序来说并不一定是合法或者正确的。 所以,为了安全,我们最好始终显式地初始化变量。
260 |
261 | 这种默认值的赋予并不适用于局部变量 —— 那些不属于类的字段的变量。 因此,若在方法中定义的基本类型数据,如下:
262 |
263 | ```java
264 | int x;
265 | ```
266 |
267 | 这里的变量 x 不会自动初始化为 0,因而在使用变量 x 之前,程序员有责任主动地为其赋值(和 C 、C++ 一致)。如果我们忘记了这一步, Java 将会提示我们“编译时错误,该变量可能尚未被初始化”。 这一点做的比 C++ 更好,在后者中,编译器只是提示警告,而在 Java 中则直接报错。
268 |
269 | ### 方法使用
270 |
271 | 在许多语言(如 C 和 C++)中,使用术语 _函数_ (function) 用来命名子程序。在 Java 中,我们使用术语 _方法_(method)来表示“做某事的方式”。
272 |
273 | 在 Java 中,方法决定对象能接收哪些消息。方法的基本组成部分包括名称、参数、返回类型、方法体。格式如:
274 |
275 | ```java
276 | [返回类型] [方法名](/*参数列表*/){
277 | // 方法体
278 | }
279 | ```
280 |
281 | #### 返回类型
282 |
283 | 方法的返回类型表明了当你调用它时会返回的结果类型。参数列表则显示了可被传递到方法内部的参数类型及名称。方法名和参数列表统称为**方法签名**(signature of the method)。签名作为方法的唯一标识。
284 |
285 | Java 中的方法只能作为类的一部分创建。它只能被对象所调用 [^4],并且该对象必须有权限来执行调用。若对象调用错误的方法,则程序将在编译时报错。
286 |
287 | 我们可以像下面这样调用一个对象的方法:
288 |
289 | ```java
290 | [对象引用].[方法名](参数1, 参数2, 参数3);
291 | ```
292 |
293 | 若方法不带参数,例如一个对象引用 `a` 的方法 `f` 不带参数并返回 **int** 型结果,我们可以如下表示:
294 |
295 | ```java
296 | int x = a.f();
297 | ```
298 |
299 | 上例中方法 `f` 的返回值类型必须和变量 `x` 的类型兼容 。调用方法的行为有时被称为向对象发送消息。面向对象编程可以总结为:向对象发送消息。
300 |
301 | #### 参数列表
302 |
303 | 方法参数列表指定了传递给方法的信息。正如你可能猜到的,这些信息就像 Java 中的其他所有信息 ,以对象的形式传递。参数列表必须指定每个对象的类型和名称。同样,我们并没有直接处理对象,而是在传递对象引用 [^5] 。但是引用的类型必须是正确的。如果方法需要 String 参数,则必须传入 String,否则编译器将报错。
304 |
305 | ```java
306 | int storage(String s) {
307 | return s.length() * 2;
308 | }
309 | ```
310 |
311 | 此方法计算并返回某个字符串所占的字节数。参数 `s` 的类型为 **String** 。将 s 传递给 `storage()` 后,我们可以把它看作和任何其他对象一样,可以向它发送消息。在这里,我们调用 `length()` 方法,它是一个 String 方法,返回字符串中的字符数。字符串中每个字符的大小为 16 位或 2 个字节。你还看到了 **return** 关键字,它执行两项操作。首先,它意味着“方法执行结束”。其次,如果方法有返回值,那么该值就紧跟 **return** 语句之后。这里,返回值是通过计算
312 |
313 | ```java
314 | s.length() * 2
315 | ```
316 |
317 | 产生的。在方法中,我们可以返回任何类型的数据。如果我们不想方法返回数据,则可以通过给方法标识 `void` 来表明这是一个无需返回值的方法。 代码示例:
318 |
319 | ```java
320 | boolean flag() {
321 | return true;
322 | }
323 |
324 | double naturalLogBase() {
325 | return 2.718;
326 | }
327 |
328 | void nothing() {
329 | return;
330 | }
331 |
332 | void nothing2() {
333 |
334 | }
335 | ```
336 |
337 | 当返回类型为 **void** 时, **return** 关键字仅用于退出方法,因此在方法结束处的 **return** 可被省略。我们可以随时从方法中返回,但若方法返回类型为非 `void`,则编译器会强制我们返回相应类型的值。
338 |
339 | 上面的描述可能会让你感觉程序只不过是一堆包含各种方法的对象,在这些方法中,将对象作为参数并发送消息给其他对象。大部分情况下确实如此。但在下一章的运算符中我们将会学习如何在方法中做出决策来完成更底层、详细的工作。对于本章,知道如何发送消息就够了。
340 |
341 | ## 程序编写
342 |
343 | 在看到第一个 Java 程序之前,我们还必须理解其他几个问题。
344 |
345 | ### 命名可见性
346 |
347 | 命名控制在任何一门编程语言中都是一个问题。如果你在两个模块中使用相同的命名,那么如何区分这两个名称,并防止两个名称发生“冲突”呢?在 C 语言编程中这是很具有挑战性的,因为程序通常是一个无法管理的名称海洋。C++ 将函数嵌套在类中,所以它们不会和嵌套在其他类中的函数名冲突。然而,C++ 还是允许全局数据和全局函数,因此仍有可能发生冲突。为了解决这个问题,C++ 使用附加的关键字引入了*命名空间*。
348 |
349 | Java 采取了一种新的方法避免了以上这些问题:为一个类库生成一个明确的名称,Java 创建者希望我们反向使用自己的网络域名,因为域名通常是唯一的。因此我的域名是 MindviewInc.com,所以我将我的 foibles 类库命名为 com.mindviewinc.utility.foibles。反转域名后,`.` 用来代表子目录的划分。
350 |
351 | 在 Java 1.0 和 Java 1.1 中,域扩展名 com、 edu、 org 和 net 等按惯例大写,因此类库中会出现这样类似的名称:com.mindviewinc.utility.foibles。然而,在 Java 2 的开发过程中,他们发现这会导致问题,所以现在整个包名都是小写的。此机制意味着所有文件都自动存在于自己的命名空间中,文件中的每个类都具有唯一标识符。这样,Java 语言可以防止名称冲突。
352 |
353 | 使用反向 URL 是一种新的命名空间方法,在此之前尚未有其他语言这么做过。Java 中有许多这些“创造性”地解决问题的方法。正如你想象,如果我们未经测试就添加一个功能并用于生产,那么在将来发现该功能的问题再想纠正,通常为时已晚(有些错误太严重了就得从语言中删除新功能。)
354 |
355 | 使用反向 URL 将命名空间与文件路径相关联不会导致 BUG,但它却给源代码管理带来麻烦。例如在 `com.mindviewinc.utility.foibles` 这样的目录结构中,我们创建了 `com` 和 `mindviewinc` 空目录。它们存在的唯一目的就是用来表示这个反向的 URL。
356 |
357 | 这种方式似乎为我们在编写 Java 程序中的某个问题打开了大门。空目录填充了深层次结构,它们不仅用于表示反向 URL,还用于捕获其他信息。这些长路径基本上用于存储有关目录中的内容的数据。如果你希望以最初设计的方式使用目录,这种方法可以从“令人沮丧”到“令人抓狂”,对于生产级的 Java 代码,你必须使用专门为此设计的 IDE 来管理代码。例如 NetBeans,Eclipse 或 IntelliJ IDEA。实际上,这些 IDE 都为我们管理和创建深层次空目录结构。
358 |
359 | 对于这本书中的例子,我不想让深层次结构给你的学习带来额外的麻烦,这实际上需要你在开始之前学习熟悉一种重量级的 IDE。所以,我们的每个章节的示例都位于一个浅的子目录中,以章节标题为名。这导致我偶尔会与遵循深层次方法的工具发生冲突。
360 |
361 | ### 使用其他组件
362 |
363 | 无论何时在程序中使用预先定义好的类,编译器都必须找到该类。最简单的情况下,该类存在于被调用的源代码文件中。此时我们使用该类 —— 即使该类在文件的后面才会被定义(Java 消除了所谓的“前向引用”问题)。而如果一个类位于其他文件中,又会怎样呢?你可能认为编译器应该足够智能去找到它,但这样是有问题的。想象一下,假如你要使用某个类,但目录中存在多个同名的类(可能用途不同)。或者更糟糕的是,假设你正在编写程序,在构建过程中,你想将某个新类添加到类库中,但却与已有的类名称冲突。
364 |
365 | 要解决此问题,你必须通过使用 **import** 关键字来告诉 Java 编译器具体要使用的类。**import** 指示编译器导入一个包,也就是一个类库(在其他语言中,一个库不仅包含类,还可能包括函数和数据,但请记住 Java 中的所有代码都必须写在类里)。大多数时候,我们都在使用 Java 标准库中的组件。有了这些构件,你就不必写一长串的反转域名。例如:
366 |
367 | ```java
368 | import java.util.ArrayList;
369 | ```
370 |
371 | 上例可以告诉编译器使用位于标准库 **util** 下的 ArrayList 类。但是,**util** 中包含许多类,我们可以使用通配符 `*` 来导入其中部分类,而无需显式得逐一声明这些类。代码示例:
372 |
373 | ```java
374 | import java.util.*;
375 | ```
376 |
377 | 本书中的示例很小,为简单起见,我们通常会使用 `.*` 形式略过导入。然而,许多教程书籍都会要求程序员逐一导入每个类。
378 |
379 | ### static 关键字
380 |
381 | 类是对象的外观及行为方式的描述。通常只有在使用 `new` 创建那个类的对象后,数据存储空间才被分配,对象的方法才能供外界调用。这种方式在两种情况下是不足的。
382 |
383 | 1. 有时你只想为特定字段(注:也称为属性、域)分配一个共享存储空间,而不去考虑究竟要创建多少对象,甚至根本就不创建对象。
384 |
385 | 2. 创建一个与此类的任何对象无关的方法。也就是说,即使没有创建对象,也能调用该方法。
386 |
387 | **static** 关键字(从 C++ 采用)就符合上述两点要求。当我们说某个事物是静态时,就意味着该字段或方法不依赖于任何特定的对象实例 。 即使我们从未创建过该类的对象,也可以调用其静态方法或访问其静态字段。相反,对于普通的非静态字段和方法,我们必须要先创建一个对象并使用该对象来访问字段或方法,因为非静态字段和方法必须与特定对象关联 [^6] 。
388 |
389 | 一些面向对象的语言使用类数据(class data)和类方法(class method),表示静态数据和方法只是作为类,而不是类的某个特定对象而存在的。有时 Java 文献也使用这些术语。
390 |
391 | 我们可以在类的字段或方法前添加 `static` 关键字来表示这是一个静态字段或静态方法。 代码示例:
392 |
393 | ```java
394 | class StaticTest {
395 | static int i = 47;
396 | }
397 | ```
398 |
399 | 现在,即使你创建了两个 `StaticTest` 对象,但是静态变量 `i` 仍只占一份存储空间。两个对象都会共享相同的变量 `i`。 代码示例:
400 |
401 | ```java
402 | StaticTest st1 = new StaticTest();
403 | StaticTest st2 = new StaticTest();
404 | ```
405 |
406 | `st1.i` 和 `st2.i` 指向同一块存储空间,因此它们的值都是 47。引用静态变量有两种方法。在前面的示例中,我们通过一个对象来定位它,例如 `st2.i`。我们也可以通过类名直接引用它,这种方式对于非静态成员则不可行:
407 |
408 | ```java
409 | StaticTest.i++;
410 | ```
411 |
412 | `++` 运算符将会使变量结果 + 1。此时 `st1.i` 和 `st2.i` 的值都变成了 48。
413 |
414 | 使用类名直接引用静态变量是首选方法,因为它强调了变量的静态属性。类似的逻辑也适用于静态方法。我们可以通过对象引用静态方法,就像使用任何方法一样,也可以通过特殊的语法方式 `Classname.method()` 来直接调用静态字段或方法 [^7]。 代码示例:
415 |
416 | ```java
417 | class Incrementable {
418 | static void increment() {
419 | StaticTest.i++;
420 | }
421 | }
422 | ```
423 |
424 | 上例中,`Incrementable` 的 `increment()` 方法通过 `++` 运算符将静态数据 `i` 加 1。我们依然可以先实例化对象再调用该方法。 代码示例:
425 |
426 | ```java
427 | Incrementable sf = new Incrementable();
428 | sf.increment();
429 | ```
430 |
431 | 当然了,首选的方法是直接通过类来调用它。代码示例:
432 |
433 | ```java
434 | Incrementable.increment();
435 | ```
436 |
437 | 相比非静态的对象,`static` 属性改变了数据创建的方式。同样,当 `static` 关键字修饰方法时,它允许我们无需创建对象就可以直接通过类的引用来调用该方法。正如我们所知,`static` 关键字的这些特性对于应用程序入口点的 `main()` 方法尤为重要。
438 |
439 | ## 小试牛刀
440 |
441 | 最后,我们开始编写第一个完整的程序。我们使用 Java 标准库中的 **Date** 类来展示一个字符串和日期。
442 |
443 | ```java
444 |
445 | // objects/HelloDate.java
446 | import java.util.*;
447 |
448 | public class HelloDate {
449 | public static void main(String[] args) {
450 | System.out.println("Hello, it's: ");
451 | System.out.println(new Date());
452 | }
453 | }
454 |
455 | ```
456 |
457 | 在本书中,所有代码示例的第一行都是注释行,其中包含文件的路径信息(比如本章的目录名是 **objects**),后跟文件名。我的工具可以根据这些信息自动提取和测试书籍的代码,你也可以通过参考第一行注释轻松地在 Github 库中找到对应的代码示例。
458 |
459 | 如果你想在代码中使用一些额外的类库,那么就必须在程序文件的开始处使用 **import** 关键字来导入它们。之所以说是额外的,因为有一些类库已经默认自动导入到每个文件里了。例如:`java.lang` 包。
460 |
461 | 现在打开你的浏览器在 [Oracle](https://www.oracle.com/) 上查看文档。如果你还没有从 [Oracle](https://www.oracle.com/) 网站上下载 JDK 文档,那现在就去 [^8] 。查看包列表,你会看到 Java 附带的所有不同的类库。
462 |
463 | 选择 `java.lang`,你会看到该库中所有类的列表。由于 `java.lang` 隐式包含在每个 Java 代码文件中,因此这些类是自动可用的。`java.lang` 类库中没有 **Date** 类,所以我们必须导入其他的类库(即 Date 所在的类库)。如果你不清楚某个类所在的类库或者想查看类库中所有的类,那么可以在 Java 文档中选择 “Tree” 查看。
464 |
465 | 现在,我们可以找到 Java 附带的每个类。使用浏览器的“查找”功能查找 **Date**,搜索结果中将会列出 **java.util.Date**,我们就知道了 **Date** 在 **util** 库中,所以必须导入 **java.util.\*** 才能使用 **Date**。
466 |
467 | 如果你在文档中选择 **java.lang**,然后选择 **System**,你会看到 **System** 类中有几个字段,如果你选择了 **out**,你会发现它是一个静态的 **PrintStream** 对象。 所以,即使我们不使用 **new** 创建, **out** 对象就已经存在并可以使用。 **out** 对象可以执行的操作取决于它的类型: **PrintStream** ,其在文档中是一个超链接,如果单击该链接,我们将可以看到 **PrintStream** 对应的方法列表(更多详情,将在本书后面介绍)。 现在我们重点说的是 **println()** 这个方法。 它的作用是 “将信息输出到控制台,并以换行符结束”。既然如此,我们可以这样编码来输出信息到控制台。 代码示例:
468 |
469 | ```java
470 | System.out.println("A String of things");
471 | ```
472 |
473 | 每个 java 源文件中允许有多个类。同时,源文件的名称必须要和其中一个类名相同,否则编译器将会报错。每个独立的程序应该包含一个 `main()` 方法作为程序运行的入口。其方法签名和返回类型如下。代码示例:
474 |
475 | ```java
476 | public static void main(String[] args) {
477 | }
478 | ```
479 |
480 | 关键字 **public** 表示方法可以被外界访问到。( 更多详情将在 **隐藏实现** 章节讲到)
481 | **main()** 方法的参数是一个 字符串(**String**) 数组。 参数 **args** 并没有在当前的程序中使用到,但是 Java 编译器强制要求必须要有, 这是因为它们被用于接收从命令行输入的参数。
482 |
483 | 下面我们来看一段有趣的代码:
484 |
485 | ```java
486 | System.out.println(new Date());
487 | ```
488 |
489 | 上面的示例中,我们创建了一个日期(**Date**)类型的对象并将其转化为字符串类型,输出到控制台中。 一旦这一行语句执行完毕,我们就不再需要该日期对象了。这时,Java 垃圾回收器就可以将其占用的内存回收,我们无需去主动清除它们。
490 |
491 | 查看 JDK 文档时,我们可以看到在 **System** 类下还有很多其他有用的方法( Java 的牛逼之处还在于,它拥有一个庞大的标准库资源)。代码示例:
492 |
493 | ```java
494 | // objects/ShowProperties.java
495 | public class ShowProperties {
496 | public static void main(String[] args) {
497 | System.getProperties().list(System.out);
498 | System.out.println(System.getProperty("user.name"));
499 | System.out.println(System.getProperty("java.library.path"));
500 | }
501 | }
502 | ```
503 |
504 | 输出结果(前 20 行):
505 |
506 | ```text
507 | java.runtime.name=Java(TM) SE Runtime Environment
508 | sun.boot.library.path=C:\Program
509 | Files\Java\jdk1.8.0_112\jr...
510 | java.vm.version=25.112-b15
511 | java.vm.vendor=Oracle Corporation
512 | java.vendor.url=http://java.oracle.com/
513 | path.separator=;
514 | java.vm.name=Java HotSpot(TM) 64-Bit Server VM
515 | file.encoding.pkg=sun.io
516 | user.script=
517 | user.country=US
518 | sun.java.launcher=SUN_STANDARD
519 | sun.os.patch.level=
520 | java.vm.specification.name=Java Virtual Machine
521 | Specification
522 | user.dir=C:\Users\Bruce\Documents\GitHub\on-ja...
523 | java.runtime.version=1.8.0_112-b15
524 | java.awt.graphicsenv=sun.awt.Win32GraphicsEnvironment
525 | java.endorsed.dirs=C:\Program
526 | Files\Java\jdk1.8.0_112\jr...
527 | os.arch=amd64
528 | java.io.tmpdir=C:\Users\Bruce\AppData\Local\Temp\
529 | ```
530 |
531 | `main()` 方法中的第一行会输出所有的系统字段,也就是环境信息。 **list()** 方法将结果发送给它的参数 **System.out**。在本书的后面,我们还会接触到将结果输出到其他地方,例如文件中。另外,我们还可以请求特定的字段。该例中我们使用到了 **user.name** 和 **java.library.path**。
532 |
533 | ### 编译和运行
534 |
535 | 要编译和运行本书中的代码示例,首先必须具有 Java 编程环境。 第二章的示例中描述了安装过程。如果你遵循这些说明,那么你将会在不受 Oracle 的限制的条件下用到 Java 开发工具包(JDK)。如果你使用其他开发系统,请查看该系统的文档以确定如何编译和运行程序。 第二章还介绍了如何安装本书的示例。
536 |
537 | 移动到子目录 **objects** 下并键入:
538 |
539 | ```bash
540 | javac HelloDate.java
541 | ```
542 |
543 | 此命令不应产生任何响应。如果我们收到任何类型的错误消息,则表示未正确安装 JDK,那就得检查这些问题。
544 |
545 | 若执行不报错的话,此时可以键入:
546 |
547 | ```java
548 | java HelloDate
549 | ```
550 |
551 | 我们将会得到正确的日期输出。这是我们编译和运行本书中每个程序(包含 `main()` 方法)的过程 [^9]。此外,本书的源代码在根目录中也有一个名为 **build.gradle** 的文件,其中包含用于自动构建,测试和运行本书文件的 **Gradle** 配置。当你第一次运行 `gradlew` 命令时,**Gradle** 将自动安装(前提是已安装 Java)。
552 |
553 | ## 编码风格
554 |
555 | Java 编程语言编码规范(Code Conventions for the Java Programming Language)[^10] 要求类名的首字母大写。 如果类名是由多个单词构成的,则每个单词的首字母都应大写(不采用下划线来分隔)例如:
556 |
557 | ```java
558 | class AllTheColorsOfTheRainbow {
559 | // ...
560 | }
561 | ```
562 |
563 | 有时称这种命名风格叫“驼峰命名法”。对于几乎所有其他方法,字段(成员变量)和对象引用名都采用驼峰命名的方式,但是它们的首字母不需要大写。代码示例:
564 |
565 | ```java
566 | class AllTheColorsOfTheRainbow {
567 | int anIntegerRepresentingColors;
568 | void changeTheHueOfTheColor(int newHue) {
569 | // ...
570 | }
571 | // ...
572 | }
573 |
574 | ```
575 |
576 | 在 Oracle 的官方类库中,花括号的位置同样遵循和本书中上述示例相同的规范。
577 |
578 | ## 本章小结
579 |
580 | 本章向你展示了简单的 Java 程序编写以及该语言相关的基本概念。到目前为止,我们的示例都只是些简单的顺序执行。在接下来的两章里,我们将会接触到 Java 的一些基本操作符,以及如何去控制程序执行的流程。
581 |
582 | [^1]: 这里可能有争议。有人说这是一个指针,但这假定了一个潜在的实现。此外,Java 引用的语法更类似于 C++ 引用而非指针。在 《Thinking in Java》 的第 1 版中,我发明了一个新术语叫“句柄”(handle),因为 C++ 引用和 Java 引用有一些重要的区别。作为一个从 C++ 的过来人,我不想混淆 Java 可能的最大受众 —— C++ 程序员。在《Thinking in Java》的第 2 版中,我认为“引用”(reference)是更常用的术语,从 C++ 转过来的人除了引用的术语之外,还有很多东西需要处理,所以他们不妨双脚都跳进去。但是,也有些人甚至不同意“引用”。在某书中我读到一个观点:Java 支持引用传递的说法是完全错误的,因为 Java 对象标识符(根据该作者)实际上是“对象引用”(object references),并且一切都是值传递。所以你不是通过引用传递,而是“通过值传递对象引用。人们可以质疑我的这种解释的准确性,但我认为我的方法简化了对概念的理解而又没对语言造成伤害(嗯,语言专家可能会说我骗你,但我会说我只是对此进行了适当的抽象。)
583 | [^2]: 大多数微处理器芯片都有额外的高速缓冲存储器,但这是按照传统存储器而不是寄存器。
584 | [^3]: 一个例子是字符串常量池。所有文字字符串和字符串值常量表达式都会自动放入特殊的静态存储中。
585 | [^4]: 静态方法,我们很快就能接触到,它可以在没有对象的情况下直接被类调用。
586 | [^5]: 通常除了前面提到的“特殊”数据类型 boolean、 char、 byte、 short、 int、 long、 float 和 double。通常来说,传递对象就意味者传递对象的引用。
587 | [^6]: 静态方法在使用之前不需要创建对象,因此它们不能直接调用非静态的成员或方法(因为非静态成员和方法必须要先实例化为对象才可以被使用)。
588 | [^7]: 在某些情况下,它还为编译器提供了更好的优化可能。
589 | [^8]: 请注意,此文档未包含在 JDK 中;你必须单独下载才能获得它。
590 | [^9]: 对于本书中编译和运行命令行的每个程序,你可能还需要设置 CLASSPATH 。
591 | [^10]: 为了保持本书的代码排版紧凑,我并没完全遵守规范,但我尽量会做到符合 Java 标准。
592 |
--------------------------------------------------------------------------------
/docs/ch5.md:
--------------------------------------------------------------------------------
1 | # 第 5 章 控制流
2 |
3 | > 程序必须在执行过程中控制它的世界并做出选择。 在 Java 中,你需要执行控制语句来做出选择。
4 |
5 | Java 使用了 C 的所有执行控制语句,因此对于熟悉 C/C++ 编程的人来说,这部分内容轻车熟路。大多数面向过程编程语言都有共通的某种控制语句。在 Java 中,涉及的关键字包括 **if-else,while,do-while,for,return,break** 和选择语句 **switch**。 Java 并不支持备受诟病的 **goto**(尽管它在某些特殊场景中依然是最行之有效的方法)。 尽管如此,在 Java 中我们仍旧可以进行类似的逻辑跳转,但较之典型的 **goto** 用法限制更多。
6 |
7 | ## true 和 false
8 |
9 | 所有的条件语句都利用条件表达式的“真”或“假”来决定执行路径。举例:
10 | `a == b`。它利用了条件表达式 `==` 来比较 `a` 与 `b` 的值是否相等。 该表达式返回 `true` 或 `false`。代码示例:
11 |
12 | ```java
13 | // control/TrueFalse.java
14 | public class TrueFalse {
15 | public static void main(String[] args) {
16 | System.out.println(1 == 1);
17 | System.out.println(1 == 2);
18 | }
19 | }
20 | ```
21 |
22 | 输出结果:
23 |
24 | ```
25 | true false
26 | ```
27 |
28 | 通过上一章的学习,我们知道任何关系运算符都可以产生条件语句。 **注意**:在 Java 中使用数值作为布尔值是非法的,即便这种操作在 C/C++ 中是被允许的(在这些语言中,“真”为非零,而“假”是零)。如果想在布尔测试中使用一个非布尔值,那么首先需要使用条件表达式来产生 **boolean** 类型的结果,例如 `if(a != 0)`。
29 |
30 | ## if-else
31 |
32 | **if-else** 语句是控制程序执行流程最基本的形式。 其中 `else` 是可选的,因此可以有两种形式的 `if`。代码示例:
33 |
34 | ```java
35 | if(Boolean-expression)
36 | “statement”
37 | ```
38 |
39 | 或
40 |
41 | ```java
42 | if(Boolean-expression)
43 | “statement”
44 | else
45 | “statement”
46 | ```
47 |
48 | 布尔表达式(Boolean-expression)必须生成 **boolean** 类型的结果,执行语句 `statement` 既可以是以分号 `;` 结尾的一条简单语句,也可以是包含在大括号 `{}` 内的的复合语句 —— 封闭在大括号内的一组简单语句。 凡本书中提及“statement”一词,皆表示类似的执行语句。
49 |
50 | 下面是一个有关 **if-else** 语句的例子。`test()` 方法可以告知你两个数值之间的大小关系。代码示例:
51 |
52 | ```java
53 | // control/IfElse.java
54 | public class IfElse {
55 | static int result = 0;
56 | static void test(int testval, int target) {
57 | if(testval > target)
58 | result = +1;
59 | else if(testval < target) // [1]
60 | result = -1;
61 | else
62 | result = 0; // Match
63 | }
64 |
65 | public static void main(String[] args) {
66 | test(10, 5);
67 | System.out.println(result);
68 | test(5, 10);
69 | System.out.println(result);
70 | test(5, 5);
71 | System.out.println(result);
72 | }
73 | }
74 | ```
75 |
76 | 输出结果:
77 |
78 | ```
79 | 1
80 | -1
81 | 0
82 | ```
83 |
84 | **注解**:`else if` 并非新关键字,它仅是 `else` 后紧跟的一条新 `if` 语句。
85 |
86 | Java 和 C/C++ 同属“自由格式”的编程语言,但通常我们会在 Java 控制流程语句中采用首部缩进的规范,以便代码更具可读性。
87 |
88 |
89 | ## 迭代语句
90 |
91 | **while**,**do-while** 和 **for** 用来控制循环语句(有时也称迭代语句)。只有控制循环的布尔表达式计算结果为 `false`,循环语句才会停止。
92 |
93 | ### while
94 |
95 | **while** 循环的形式是:
96 |
97 | ```java
98 | while(Boolean-expression)
99 | statement
100 | ```
101 |
102 | 执行语句会在每一次循环前,判断布尔表达式返回值是否为 `true`。下例可产生随机数,直到满足特定条件。代码示例:
103 |
104 | ```java
105 | // control/WhileTest.java
106 | // 演示 while 循环
107 | public class WhileTest {
108 | static boolean condition() {
109 | boolean result = Math.random() < 0.99;
110 | System.out.print(result + ", ");
111 | return result;
112 | }
113 | public static void main(String[] args) {
114 | while(condition())
115 | System.out.println("Inside 'while'");
116 | System.out.println("Exited 'while'");
117 | }
118 | }
119 | ```
120 |
121 | 输出结果:
122 |
123 | ```
124 | true, Inside 'while'
125 | true, Inside 'while'
126 | true, Inside 'while'
127 | true, Inside 'while'
128 | true, Inside 'while'
129 | ...________...________...________...________...
130 | true, Inside 'while'
131 | true, Inside 'while'
132 | true, Inside 'while'
133 | true, Inside 'while'
134 | false, Exited 'while'
135 | ```
136 |
137 | `condition()` 方法使用到了 **Math** 库的**静态**方法 `random()`。该方法的作用是产生 0 和 1 之间 (包括 0,但不包括 1) 的一个 **double** 值。
138 |
139 | **result** 的值是通过比较运算符 `<` 产生的 **boolean** 类型的结果。当控制台输出 **boolean** 型值时,会自动将其转换为对应的文字形式 `true` 或 `false`。此处 `while` 条件表达式代表:“仅在 `condition()` 返回 `false` 时停止循环”。
140 |
141 | ### do-while
142 |
143 | **do-while** 的格式如下:
144 |
145 | ```java
146 | do
147 | statement
148 | while(Boolean-expression);
149 | ```
150 |
151 | **while** 和 **do-while** 之间的唯一区别是:即使条件表达式返回结果为 `false`, **do-while** 语句也至少会执行一次。 在 **while** 循环体中,如布尔表达式首次返回的结果就为 `false`,那么循环体内的语句不会被执行。实际应用中,**while** 形式比 **do-while** 更为常用。
152 |
153 | ### for
154 |
155 | **for** 循环可能是最常用的迭代形式。 该循环在第一次迭代之前执行初始化。随后,它会执行布尔表达式,并在每次迭代结束时,进行某种形式的步进。**for** 循环的形式是:
156 |
157 | ```java
158 | for(initialization; Boolean-expression; step)
159 | statement
160 | ```
161 |
162 | 初始化 (initialization) 表达式、布尔表达式 (Boolean-expression) ,或者步进 (step) 运算,都可以为空。每次迭代之前都会判断布尔表达式的结果是否成立。一旦计算结果为 `false`,则跳出 **for** 循环体并继续执行后面代码。 每次循环结束时,都会执行一次步进。
163 |
164 | **for** 循环通常用于“计数”任务。代码示例:
165 |
166 | ```java
167 | // control/ListCharacters.java
168 |
169 | public class ListCharacters {
170 | public static void main(String[] args) {
171 | for(char c = 0; c < 128; c++)
172 | if(Character.isLowerCase(c))
173 | System.out.println("value: " + (int)c +
174 | " character: " + c);
175 | }
176 | }
177 | ```
178 |
179 | 输出结果(前 10 行):
180 |
181 | ```
182 | value: 97 character: a
183 | value: 98 character: b
184 | value: 99 character: c
185 | value: 100 character: d
186 | value: 101 character: e
187 | value: 102 character: f
188 | value: 103 character: g
189 | value: 104 character: h
190 | value: 105 character: i
191 | value: 106 character: j
192 | ...
193 | ```
194 |
195 | **注意**:变量 **c** 是在 **for** 循环执行时才被定义的,并不是在主方法的开头。**c** 的作用域范围仅在 **for** 循环体内。
196 |
197 | 传统的面向过程语言如 C 需要先在代码块(block)前定义好所有变量才能够使用。这样编译器才能在创建块时,为这些变量分配内存空间。在 Java 和 C++ 中,我们可以在整个块使用变量声明,并且可以在需要时才定义变量。 这种自然的编码风格使我们的代码更容易被人理解 [^1]。
198 |
199 | 上例使用了 **java.lang.Character** 包装类,该类不仅包含了基本类型 `char` 的值,还封装了一些有用的方法。例如这里就用到了静态方法 `isLowerCase()` 来判断字符是否为小写。
200 |
201 |
202 | #### 逗号操作符
203 |
204 | 在 Java 中逗号运算符(这里并非指我们平常用于分隔定义和方法参数的逗号分隔符)仅有一种用法:在 **for** 循环的初始化和步进控制中定义多个变量。我们可以使用逗号分隔多个语句,并按顺序计算这些语句。**注意**:要求定义的变量类型相同。代码示例:
205 |
206 | ```java
207 | // control/CommaOperator.java
208 |
209 | public class CommaOperator {
210 | public static void main(String[] args) {
211 | for(int i = 1, j = i + 10; i < 5; i++, j = i * 2) {
212 | System.out.println("i = " + i + " j = " + j);
213 | }
214 | }
215 | }
216 | ```
217 |
218 | 输出结果:
219 |
220 | ```
221 | i = 1 j = 11
222 | i = 2 j = 4
223 | i = 3 j = 6
224 | i = 4 j = 8
225 | ```
226 |
227 | 上例中 **int** 类型声明包含了 `i` 和 `j`。实际上,在初始化部分我们可以定义任意数量的同类型变量。**注意**:在 Java 中,仅允许 **for** 循环在控制表达式中定义变量。 我们不能将此方法与其他的循环语句和选择语句中一起使用。同时,我们可以看到:无论在初始化还是在步进部分,语句都是顺序执行的。
228 |
229 | ## for-in 语法
230 |
231 | Java 5 引入了更为简洁的“增强版 **for** 循环”语法来操纵数组和集合。(更多细节,可参考 [数组](./21-Arrays.md) 和 [集合](./12-Collections.md) 章节内容)。大部分文档也称其为 **for-each** 语法,但因为了不与 Java 8 新添的 `forEach()` 产生混淆,因此我称之为 **for-in** 循环。 (Python 已有类似的先例,如:**for x in sequence**)。**注意**:你可能会在其他地方看到不同叫法。
232 |
233 | **for-in** 无需你去创建 **int** 变量和步进来控制循环计数。 下面我们来遍历获取 **float** 数组中的元素。代码示例:
234 |
235 | ```java
236 | // control/ForInFloat.java
237 |
238 | import java.util.*;
239 |
240 | public class ForInFloat {
241 | public static void main(String[] args) {
242 | Random rand = new Random(47);
243 | float[] f = new float[10];
244 | for(int i = 0; i < 10; i++)
245 | f[i] = rand.nextFloat();
246 | for(float x : f)
247 | System.out.println(x);
248 | }
249 | }
250 | ```
251 |
252 | 输出结果:
253 |
254 | ```
255 | 0.72711575
256 | 0.39982635
257 | 0.5309454
258 | 0.0534122
259 | 0.16020656
260 | 0.57799757
261 | 0.18847865
262 | 0.4170137
263 | 0.51660204
264 | 0.73734957
265 | ```
266 |
267 | 上例中我们展示了传统 **for** 循环的用法。接下来再来看下 **for-in** 的用法。代码示例:
268 |
269 | ```java
270 | for(float x : f) {
271 | ```
272 |
273 | 这条语句定义了一个 **float** 类型的变量 `x`,继而将每一个 `f` 的元素赋值给它。
274 |
275 | 任何一个返回数组的方法都可以使用 **for-in** 循环语法来遍历元素。例如 **String** 类有一个方法 `toCharArray()`,返回值类型为 **char** 数组,我们可以很容易地在 **for-in** 循环中遍历它。代码示例:
276 |
277 | ```java
278 | // control/ForInString.java
279 |
280 | public class ForInString {
281 | public static void main(String[] args) {
282 | for(char c: "An African Swallow".toCharArray())
283 | System.out.print(c + " ");
284 | }
285 | }
286 | ```
287 |
288 | 输出结果:
289 |
290 | ```
291 | A n A f r i c a n S w a l l o w
292 | ```
293 |
294 | 很快我们能在 [集合](./12-Collections.md) 章节里学习到,**for-in** 循环适用于任何可迭代(_iterable_)的 对象。
295 |
296 | 通常,**for** 循环语句都会在一个整型数值序列中步进。代码示例:
297 |
298 | ```java
299 | for(int i = 0; i < 100; i++)
300 | ```
301 |
302 | 正因如此,除非先创建一个 **int** 数组,否则我们无法使用 **for-in** 循环来操作。为简化测试过程,我已在 `onjava` 包中封装了 **Range** 类,利用其 `range()` 方法可自动生成恰当的数组。
303 |
304 | 在 [封装](./07-Implementation-Hiding.md)(Implementation Hiding)这一章里我们介绍了静态导入(static import),无需了解细节就可以直接使用。 有关静态导入的语法,可以在 **import** 语句中看到:
305 |
306 | ```java
307 | // control/ForInInt.java
308 |
309 | import static onjava.Range.*;
310 |
311 | public class ForInInt {
312 | public static void main(String[] args) {
313 | for(int i : range(10)) // 0..9
314 | System.out.print(i + " ");
315 | System.out.println();
316 | for(int i : range(5, 10)) // 5..9
317 | System.out.print(i + " ");
318 | System.out.println();
319 | for(int i : range(5, 20, 3)) // 5..20 step 3
320 | System.out.print(i + " ");
321 | System.out.println();
322 | for(int i : range(20, 5, -3)) // Count down
323 | System.out.print(i + " ");
324 | System.out.println();
325 | }
326 | }
327 | ```
328 |
329 | 输出结果:
330 |
331 | ```
332 | 0 1 2 3 4 5 6 7 8 9
333 | 5 6 7 8 9
334 | 5 8 11 14 17
335 | 20 17 14 11 8
336 | ```
337 |
338 | `range()` 方法已被 [重载](./06-Housekeeping.md#方法重载)(重载:同名方法,参数列表或类型不同)。上例中 `range()` 方法有多种重载形式:第一种产生从 0 至范围上限(不包含)的值;第二种产生参数一至参数二(不包含)范围内的整数值;第三种形式有一个步进值,因此它每次的增量为该值;第四种 `range()` 表明还可以递减。`range()` 无参方法是该生成器最简单的版本。有关内容会在本书稍后介绍。
339 |
340 | `range()` 的使用提高了代码可读性,让 **for-in** 循环在本书中适应更多的代码示例场景。
341 |
342 | 请注意,`System.out.print()` 不会输出换行符,所以我们可以分段输出同一行。
343 |
344 | _for-in_ 语法可以节省我们编写代码的时间。 更重要的是,它提高了代码可读性以及更好地描述代码意图(获取数组的每个元素)而不是详细说明这操作细节(创建索引,并用它来选择数组元素) 本书推荐使用 _for-in_ 语法。
345 |
346 | ## return
347 |
348 | 在 Java 中有几个关键字代表无条件分支,这意味无需任何测试即可发生。这些关键字包括 **return**,**break**,**continue** 和跳转到带标签语句的方法,类似于其他语言中的 **goto**。
349 |
350 | **return** 关键字有两方面的作用:1.指定一个方法返回值 (在方法返回类型非 **void** 的情况下);2.退出当前方法,并返回作用 1 中值。我们可以利用 `return` 的这些特点来改写上例 `IfElse.java` 文件中的 `test()` 方法。代码示例:
351 |
352 | ```java
353 | // control/TestWithReturn.java
354 |
355 | public class TestWithReturn {
356 | static int test(int testval, int target) {
357 | if(testval > target)
358 | return +1;
359 | if(testval < target)
360 | return -1;
361 | return 0; // Match
362 | }
363 |
364 | public static void main(String[] args) {
365 | System.out.println(test(10, 5));
366 | System.out.println(test(5, 10));
367 | System.out.println(test(5, 5));
368 | }
369 | }
370 | ```
371 |
372 | 输出结果:
373 |
374 | ```
375 | 1
376 | -1
377 | 0
378 | ```
379 |
380 | 这里不需要 `else`,因为该方法执行到 `return` 就结束了。
381 |
382 | 如果在方法签名中定义了返回值类型为 **void**,那么在代码执行结束时会有一个隐式的 **return**。 也就是说我们不用在总是在方法中显式地包含 **return** 语句。 **注意**:如果你的方法声明的返回值类型为非 **void** 类型,那么则必须确保每个代码路径都返回一个值。
383 |
384 | ## break 和 continue
385 |
386 | 在任何迭代语句的主体内,都可以使用 **break** 和 **continue** 来控制循环的流程。 其中,**break** 表示跳出当前循环体。而 **continue** 表示停止本次循环,开始下一次循环。
387 |
388 | 下例向大家展示 **break** 和 **continue** 在 **for**、**while** 循环中的使用。代码示例:
389 |
390 | ```java
391 | // control/BreakAndContinue.java
392 | // Break 和 continue 关键字
393 |
394 | import static onjava.Range.*;
395 |
396 | public class BreakAndContinue {
397 | public static void main(String[] args) {
398 | for(int i = 0; i < 100; i++) { // [1]
399 | if(i == 74) break; // 跳出循环
400 | if(i % 9 != 0) continue; // 下一次循环
401 | System.out.print(i + " ");
402 | }
403 | System.out.println();
404 | // 使用 for-in 循环:
405 | for(int i : range(100)) { // [2]
406 | if(i == 74) break; // 跳出循环
407 | if(i % 9 != 0) continue; // 下一次循环
408 | System.out.print(i + " ");
409 | }
410 | System.out.println();
411 | int i = 0;
412 | // "无限循环":
413 | while(true) { // [3]
414 | i++;
415 | int j = i * 27;
416 | if(j == 1269) break; // 跳出循环
417 | if(i % 10 != 0) continue; // 循环顶部
418 | System.out.print(i + " ");
419 | }
420 | }
421 | }
422 | ```
423 |
424 | 输出结果:
425 |
426 | ```
427 | 0 9 18 27 36 45 54 63 72
428 | 0 9 18 27 36 45 54 63 72
429 | 10 20 30 40
430 | ```
431 |
432 | **[1]** 在这个 **for** 循环中,`i` 的值永远不会达到 100,因为一旦 `i` 等于 74,**break** 语句就会中断循环。通常,只有在不知道中断条件何时满足时,才需要 **break**。因为 `i` 不能被 9 整除,**continue** 语句就会使循环从头开始。这使 **i** 递增)。如果能够整除,则将值显示出来。
433 | **[2]** 使用 **for-in** 语法,结果相同。
434 | **[3]** 无限 **while** 循环。循环内的 **break** 语句可中止循环。**注意**,**continue** 语句可将控制权移回循环的顶部,而不会执行 **continue** 之后的任何操作。 因此,只有当 `i` 的值可被 10 整除时才会输出。在输出中,显示值 0,因为 `0%9` 产生 0。还有一种无限循环的形式: `for(;;)`。 在编译器看来,它与 `while(true)` 无异,使用哪种完全取决于你的编程品味。
435 |
436 |
437 | ## 臭名昭著的 goto
438 |
439 | [**goto** 关键字](https://en.wikipedia.org/wiki/Goto) 很早就在程序设计语言中出现。事实上,**goto** 起源于[汇编](https://en.wikipedia.org/wiki/Assembly_language)(assembly language)语言中的程序控制:“若条件 A 成立,则跳到这里;否则跳到那里”。如果你读过由编译器编译后的代码,你会发现在其程序控制中充斥了大量的跳转。较之汇编产生的代码直接运行在硬件 CPU 中,Java 也会产生自己的“汇编代码”(字节码),只不过它是运行在 Java 虚拟机里的(Java Virtual Machine)。
440 |
441 | 一个源码级别跳转的 **goto**,为何招致名誉扫地呢?若程序总是从一处跳转到另一处,还有什么办法能识别代码的控制流程呢?随着 *Edsger Dijkstra*发表著名的 “Goto 有害” 论(_Goto considered harmful_)以后,**goto** 便从此失宠。甚至有人建议将它从关键字中剔除。
442 |
443 | 正如上述提及的经典情况,我们不应走向两个极端。问题不在 **goto**,而在于过度使用 **goto**。在极少数情况下,**goto** 实际上是控制流程的最佳方式。
444 |
445 | 尽管 **goto** 仍是 Java 的一个保留字,但其并未被正式启用。可以说, Java 中并不支持 **goto**。然而,在 **break** 和 **continue** 这两个关键字的身上,我们仍能看出一些 **goto** 的影子。它们并不属于一次跳转,而是中断循环语句的一种方法。之所以把它们纳入 **goto** 问题中一起讨论,是由于它们使用了相同的机制:标签。
446 |
447 | “标签”是后面跟一个冒号的标识符。代码示例:
448 |
449 | ```java
450 | label1:
451 | ```
452 |
453 | 对 Java 来说,唯一用到标签的地方是在循环语句之前。进一步说,它实际需要紧靠在循环语句的前方 —— 在标签和循环之间置入任何语句都是不明智的。而在循环之前设置标签的唯一理由是:我们希望在其中嵌套另一个循环或者一个开关。这是由于 **break** 和 **continue** 关键字通常只中断当前循环,但若搭配标签一起使用,它们就会中断并跳转到标签所在的地方开始执行。代码示例:
454 |
455 | ```java
456 | label1:
457 | outer-iteration {
458 | inner-iteration {
459 | // ...
460 | break; // [1]
461 | // ...
462 | continue; // [2]
463 | // ...
464 | continue label1; // [3]
465 | // ...
466 | break label1; // [4]
467 | }
468 | }
469 | ```
470 |
471 | **[1]** **break** 中断内部循环,并在外部循环结束。
472 | **[2]** **continue** 移回内部循环的起始处。但在条件 3 中,**continue label1** 却同时中断内部循环以及外部循环,并移至 **label1** 处。
473 | **[3]** 随后,它实际是继续循环,但却从外部循环开始。
474 | **[4]** **break label1** 也会中断所有循环,并回到 **label1** 处,但并不重新进入循环。也就是说,它实际是完全中止了两个循环。
475 |
476 | 下面是 **for** 循环的一个例子:
477 |
478 | ```java
479 | // control/LabeledFor.java
480 | // 搭配“标签 break”的 for 循环中使用 break 和 continue
481 |
482 | public class LabeledFor {
483 | public static void main(String[] args) {
484 | int i = 0;
485 | outer: // 此处不允许存在执行语句
486 | for(; true ;) { // 无限循环
487 | inner: // 此处不允许存在执行语句
488 | for(; i < 10; i++) {
489 | System.out.println("i = " + i);
490 | if(i == 2) {
491 | System.out.println("continue");
492 | continue;
493 | }
494 | if(i == 3) {
495 | System.out.println("break");
496 | i++; // 否则 i 永远无法获得自增
497 | // 获得自增
498 | break;
499 | }
500 | if(i == 7) {
501 | System.out.println("continue outer");
502 | i++; // 否则 i 永远无法获得自增
503 | // 获得自增
504 | continue outer;
505 | }
506 | if(i == 8) {
507 | System.out.println("break outer");
508 | break outer;
509 | }
510 | for(int k = 0; k < 5; k++) {
511 | if(k == 3) {
512 | System.out.println("continue inner");
513 | continue inner;
514 | }
515 | }
516 | }
517 | }
518 | // 在此处无法 break 或 continue 标签
519 | }
520 | }
521 | ```
522 |
523 | 输出结果:
524 |
525 | ```
526 | i = 0
527 | continue inner
528 | i = 1
529 | continue inner
530 | i = 2
531 | continue
532 | i = 3
533 | break
534 | i = 4
535 | continue inner
536 | i = 5
537 | continue inner
538 | i = 6
539 | continue inner
540 | i = 7
541 | continue outer
542 | i = 8
543 | break outer
544 | ```
545 |
546 | 注意 **break** 会中断 **for** 循环,而且在抵达 **for** 循环的末尾之前,递增表达式不会执行。由于 **break** 跳过了递增表达式,所以递增会在 `i==3` 的情况下直接执行。在 `i==7` 的情况下,`continue outer` 语句也会到达循环顶部,而且也会跳过递增,所以它也是直接递增的。
547 |
548 | 如果没有 **break outer** 语句,就没有办法在一个内部循环里找到出外部循环的路径。这是由于 **break** 本身只能中断最内层的循环(对于 **continue** 同样如此)。 当然,若想在中断循环的同时退出方法,简单地用一个 **return** 即可。
549 |
550 | 下面这个例子向大家展示了带标签的 **break** 以及 **continue** 语句在 **while** 循环中的用法:
551 |
552 | ```java
553 | // control/LabeledWhile.java
554 | // 带标签的 break 和 conitue 在 while 循环中的使用
555 |
556 | public class LabeledWhile {
557 | public static void main(String[] args) {
558 | int i = 0;
559 | outer:
560 | while(true) {
561 | System.out.println("Outer while loop");
562 | while(true) {
563 | i++;
564 | System.out.println("i = " + i);
565 | if(i == 1) {
566 | System.out.println("continue");
567 | continue;
568 | }
569 | if(i == 3) {
570 | System.out.println("continue outer");
571 | continue outer;
572 | }
573 | if(i == 5) {
574 | System.out.println("break");
575 | break;
576 | }
577 | if(i == 7) {
578 | System.out.println("break outer");
579 | break outer;
580 | }
581 | }
582 | }
583 | }
584 | }
585 | ```
586 |
587 | 输出结果:
588 |
589 | ```
590 | Outer while loop
591 | i = 1
592 | continue
593 | i = 2
594 | i = 3
595 | continue outer
596 | Outer while loop
597 | i = 4
598 | i = 5
599 | break
600 | Outer while loop
601 | i = 6
602 | i = 7
603 | break outer
604 | ```
605 |
606 | 同样的规则亦适用于 **while**:
607 |
608 | 1. 简单的一个 **continue** 会退回最内层循环的开头(顶部),并继续执行。
609 |
610 | 2. 带有标签的 **continue** 会到达标签的位置,并重新进入紧接在那个标签后面的循环。
611 |
612 | 3. **break** 会中断当前循环,并移离当前标签的末尾。
613 |
614 | 4. 带标签的 **break** 会中断当前循环,并移离由那个标签指示的循环的末尾。
615 |
616 | 大家要记住的重点是:在 Java 里需要使用标签的唯一理由就是因为有循环嵌套存在,而且想从多层嵌套中 **break** 或 **continue**。
617 |
618 | **break** 和 **continue** 标签在编码中的使用频率相对较低 (此前的语言中很少使用或没有先例),所以我们很少在代码里看到它们。
619 |
620 | 在 _Dijkstra_ 的 **“Goto 有害”** 论文中,他最反对的就是标签,而非 **goto**。他观察到 BUG 的数量似乎随着程序中标签的数量而增加[^2]。标签和 **goto** 使得程序难以分析。但是,Java 标签不会造成这方面的问题,因为它们的应用场景受到限制,无法用于以临时方式传输控制。由此也引出了一个有趣的情形:对语言能力的限制,反而使它这项特性更加有价值。
621 |
622 | ## switch
623 |
624 | **switch** 有时也被划归为一种选择语句。根据整数表达式的值,**switch** 语句可以从一系列代码中选出一段去执行。它的格式如下:
625 |
626 | ```java
627 | switch(integral-selector) {
628 | case integral-value1 : statement; break;
629 | case integral-value2 : statement; break;
630 | case integral-value3 : statement; break;
631 | case integral-value4 : statement; break;
632 | case integral-value5 : statement; break;
633 | // ...
634 | default: statement;
635 | }
636 | ```
637 |
638 | 其中,**integral-selector** (整数选择因子)是一个能够产生整数值的表达式,**switch** 能够将这个表达式的结果与每个 **integral-value** (整数值)相比较。若发现相符的,就执行对应的语句(简单或复合语句,其中并不需要括号)。若没有发现相符的,就执行 **default** 语句。
639 |
640 | 在上面的定义中,大家会注意到每个 **case** 均以一个 **break** 结尾。这样可使执行流程跳转至 **switch** 主体的末尾。这是构建 **switch** 语句的一种传统方式,但 **break** 是可选的。若省略 **break,** 会继续执行后面的 **case** 语句的代码,直到遇到一个 **break** 为止。通常我们不想出现这种情况,但对有经验的程序员来说,也许能够善加利用。注意最后的 **default** 语句没有 **break**,因为执行流程已到了 **break** 的跳转目的地。当然,如果考虑到编程风格方面的原因,完全可以在 **default** 语句的末尾放置一个 **break**,尽管它并没有任何实际的作用。
641 |
642 | **switch** 语句是一种实现多路选择的干净利落的一种方式(比如从一系列执行路径中挑选一个)。但它要求使用一个选择因子,并且必须是 **int** 或 **char** 那样的整数值。例如,假若将一个字串或者浮点数作为选择因子使用,那么它们在 switch 语句里是不会工作的。对于非整数类型(Java 7 以上版本中的 String 型除外),则必须使用一系列 **if** 语句。 在[下一章的结尾](./06-Housekeeping.md#枚举类型) 中,我们将会了解到**枚举类型**被用来搭配 **switch** 工作,并优雅地解决了这种限制。
643 |
644 | 下面这个例子可随机生成字母,并判断它们是元音还是辅音字母:
645 |
646 | ```java
647 | // control/VowelsAndConsonants.java
648 |
649 | // switch 执行语句的演示
650 | import java.util.*;
651 |
652 | public class VowelsAndConsonants {
653 | public static void main(String[] args) {
654 | Random rand = new Random(47);
655 | for(int i = 0; i < 100; i++) {
656 | int c = rand.nextInt(26) + 'a';
657 | System.out.print((char)c + ", " + c + ": ");
658 | switch(c) {
659 | case 'a':
660 | case 'e':
661 | case 'i':
662 | case 'o':
663 | case 'u': System.out.println("vowel");
664 | break;
665 | case 'y':
666 | case 'w': System.out.println("Sometimes vowel");
667 | break;
668 | default: System.out.println("consonant");
669 | }
670 | }
671 | }
672 | }
673 | ```
674 |
675 | 输出结果:
676 |
677 | ```
678 | y, 121: Sometimes vowel
679 | n, 110: consonant
680 | z, 122: consonant
681 | b, 98: consonant
682 | r, 114: consonant
683 | n, 110: consonant
684 | y, 121: Sometimes vowel
685 | g, 103: consonant
686 | c, 99: consonant
687 | f, 102: consonant
688 | o, 111: vowel
689 | w, 119: Sometimes vowel
690 | z, 122: consonant
691 | ...
692 | ```
693 |
694 | 由于 `Random.nextInt(26)` 会产生 0 到 25 之间的一个值,所以在其上加上一个偏移量 `a`,即可产生小写字母。在 **case** 语句中,使用单引号引起的字符也会产生用于比较的整数值。
695 |
696 | 请注意 **case** 语句能够堆叠在一起,为一段代码形成多重匹配,即只要符合多种条件中的一种,就执行那段特别的代码。这时也应该注意将 **break** 语句置于特定 **case** 的末尾,否则控制流程会继续往下执行,处理后面的 **case**。在下面的语句中:
697 |
698 | ```java
699 | int c = rand.nextInt(26) + 'a';
700 | ```
701 |
702 | 此处 `Random.nextInt()` 将产生 0~25 之间的一个随机 **int** 值,它将被加到 `a` 上。这表示 `a` 将自动被转换为 **int** 以执行加法。为了把 `c` 当作字符打印,必须将其转型为 **char**;否则,将会输出整数。
703 |
704 |
705 | ## switch 字符串
706 |
707 | Java 7 增加了在字符串上 **switch** 的用法。 下例展示了从一组 **String** 中选择可能值的传统方法,以及新式方法:
708 |
709 | ```java
710 | // control/StringSwitch.java
711 |
712 | public class StringSwitch {
713 | public static void main(String[] args) {
714 | String color = "red";
715 | // 老的方式: 使用 if-then 判断
716 | if("red".equals(color)) {
717 | System.out.println("RED");
718 | } else if("green".equals(color)) {
719 | System.out.println("GREEN");
720 | } else if("blue".equals(color)) {
721 | System.out.println("BLUE");
722 | } else if("yellow".equals(color)) {
723 | System.out.println("YELLOW");
724 | } else {
725 | System.out.println("Unknown");
726 | }
727 | // 新的方法: 字符串搭配 switch
728 | switch(color) {
729 | case "red":
730 | System.out.println("RED");
731 | break;
732 | case "green":
733 | System.out.println("GREEN");
734 | break;
735 | case "blue":
736 | System.out.println("BLUE");
737 | break;
738 | case "yellow":
739 | System.out.println("YELLOW");
740 | break;
741 | default:
742 | System.out.println("Unknown");
743 | break;
744 | }
745 | }
746 | }
747 | ```
748 |
749 | 输出结果:
750 |
751 | ```
752 | RED
753 | RED
754 | ```
755 |
756 | 一旦理解了 **switch**,你会明白这其实就是一个逻辑扩展的语法糖。新的编码方式能使得结果更清晰,更易于理解和维护。
757 |
758 | 作为 **switch** 字符串的第二个例子,我们重新访问 `Math.random()`。 它是否产生从 0 到 1 的值,包括还是不包括值 1 呢?在数学术语中,它属于 (0,1)、 [0,1)、(0,1] 、[0,1] 中的哪种呢?(方括号表示“包括”,而括号表示“不包括”)
759 |
760 | 下面是一个可能提供答案的测试程序。 所有命令行参数都作为 **String** 对象传递,因此我们可以 **switch** 参数来决定要做什么。 那么问题来了:如果用户不提供参数 ,索引到 `args` 的数组就会导致程序失败。 解决这个问题,我们需要预先检查数组的长度,若长度为 0,则使用**空字符串** `""` 替代;否则,选择 `args` 数组中的第一个元素:
761 |
762 | ```java
763 | // control/RandomBounds.java
764 |
765 | // Math.random() 会产生 0.0 和 1.0 吗?
766 | // {java RandomBounds lower}
767 | import onjava.*;
768 |
769 | public class RandomBounds {
770 | public static void main(String[] args) {
771 | new TimedAbort(3);
772 | switch(args.length == 0 ? "" : args[0]) {
773 | case "lower":
774 | while(Math.random() != 0.0)
775 | ; // 保持重试
776 | System.out.println("Produced 0.0!");
777 | break;
778 | case "upper":
779 | while(Math.random() != 1.0)
780 | ; // 保持重试
781 | System.out.println("Produced 1.0!");
782 | break;
783 | default:
784 | System.out.println("Usage:");
785 | System.out.println("\tRandomBounds lower");
786 | System.out.println("\tRandomBounds upper");
787 | System.exit(1);
788 | }
789 | }
790 | }
791 | ```
792 |
793 | 要运行该程序,请键入以下任一命令:
794 |
795 | ```java
796 | java RandomBounds lower
797 | // 或者
798 | java RandomBounds upper
799 | ```
800 |
801 | 使用 `onjava` 包中的 **TimedAbort** 类可使程序在三秒后中止。从结果来看,似乎 `Math.random()` 产生的随机值里不包含 0.0 或 1.0。 这就是该测试容易混淆的地方:若要考虑 0 至 1 之间所有不同 **double** 数值的可能性,那么这个测试的耗费的时间可能超出一个人的寿命了。 这里我们直接给出正确的结果:`Math.random()` 的结果集范围包含 0.0 ,不包含 1.0。 在数学术语中,可用 [0,1)来表示。由此可知,我们必须小心分析实验并了解它们的局限性。
802 |
803 | ## 本章小结
804 |
805 | 本章总结了我们对大多数编程语言中出现的基本特性的探索:计算,运算符优先级,类型转换,选择和迭代。 现在让我们准备好,开始步入面向对象和函数式编程的世界吧。 下一章的内容涵盖了 Java 编程中的重要问题:对象的[初始化和清理](./06-Housekeeping.md)。紧接着,还会介绍[封装](./07-Implementation-Hiding.md)(implementation hiding)的核心概念。
806 |
807 |
808 | [^1]: 在早期的语言中,许多决策都是基于让编译器设计者的体验更好。 但在现代语言设计中,许多决策都是为了提高语言使用者的体验,尽管有时会有妥协 —— 这通常会让语言设计者后悔。
809 | [^2]: **注意**,此处观点似乎难以让人信服,很可能只是一个因认知偏差而造成的[因果关系谬误](https://en.wikipedia.org/wiki/Correlation_does_not_imply_causation) 的例子。
810 |
811 |
812 |
--------------------------------------------------------------------------------
/docs/ch7.md:
--------------------------------------------------------------------------------
1 | # 第 7 章 封装
2 |
3 | 访问控制(或者隐藏实现)与"最初的实现不恰当"有关。
4 |
5 | 所有优秀的作者——包括这些编写软件的人——都知道一件好的作品都是经过反复打磨才变得优秀的。如果你把一段代码置于某个位置一段时间,过一会重新来看,你可能发现更好的实现方式。这是重构的原动力之一,重构就是重写可工作的代码,使之更加可读,易懂,因而更易维护。
6 |
7 | 但是,在修改和完善代码的愿望下,也存在巨大的压力。通常,客户端程序员希望你的代码在某些方面保持不变。所以你想修改代码,但他们希望代码保持不变。由此引出了面向对象设计中的一个基本问题:"如何区分变动的事物和不变的事物"。
8 |
9 | 这个问题对于类库而言尤其重要。类库的使用者必须依赖他们所使用的那部分类库,并且知道如果使用了类库的新版本,不需要改写代码。另一方面,类库的开发者必须有修改和改进类库的自由,并保证客户代码不会受这些改动影响。
10 |
11 | 这可以通过约定解决。例如,类库开发者必须同意在修改类库中的一个类时,不会移除已有的方法,因为那样将会破坏客户端程序员的代码。与之相反的情况更加复杂。在有成员属性的情况下,类库开发者如何知道哪些属性被客户端程序员使用?这同样会发生在那些只为实现类库类而创建的方法上,它们也不是设计成可供客户端程序员调用的。如果类库开发者想删除旧的实现,添加新的实现,结果会怎样呢?任何这些成员的改动都可能破环客户端程序员的代码。因此类库开发者会被束缚,不能修改任何事物。
12 |
13 | 为了解决这一问题,Java 提供了访问修饰符供类库开发者指明哪些对于客户端程序员是可用的,哪些是不可用的。访问控制权限的等级,从"最大权限"到"最小权限"依次是:**public**,**protected**,包访问权限(没有关键字)和 **private**。根据上一段的内容,你可能会想,作为一名类库设计者,你会尽可能将一切都设为 **private**,仅向客户端程序员暴露你愿意他们使用的方法。这就是你通常所做的,尽管这与使用其他语言(尤其是 C)编程和访问不受任何限制的人们的直觉相违背。
14 |
15 | 然而,构建类库的概念和对类库组件的访问控制仍然不完善。其中仍然存在问题就是如何将类库组件捆绑到一个内聚到类库单元中。Java 中通过 package 关键字加以控制,类是在相同包下还是不同包下会影响访问修饰符。所以在这章开始,你将会学习如何将类库组件置于同一个包下,之后你就能明白访问修饰符的全部含义。
16 |
17 | ## 包的概念
18 |
19 | 包内包含一组类,它们被组织在一个单独的命名空间下。
20 |
21 | 例如,标准 Java 发布中有一个工具库,它被组织在 **java.util** 命名空间下。**java.util** 中含有一个类,叫做 **ArrayList**。使用 **ArrayList** 的一种方式是用其全名 **java.util.ArrayList**。
22 |
23 | ```java
24 | // hiding/FullQualification.java
25 |
26 | public class FullQualification {
27 | public static void main(String[] args) {
28 | java.util.ArrayList list = new java.util.ArrayList();
29 | }
30 | }
31 | ```
32 |
33 | 这种方式使得程序冗长乏味,因此你可以换一种方式,使用 **import** 关键字。如果需要导入某个类,就需要在 **import** 语句中声明:
34 |
35 | ```java
36 | // hiding/SingleImport.java
37 | import java.util.ArrayList;
38 |
39 | public class SingleImport {
40 | public static void main(String[] args) {
41 | ArrayList list = new ArrayList();
42 | }
43 | }
44 | ```
45 |
46 | 现在你就可以不加限定词,直接使用 **ArrayList** 了。但是对于 **java.util** 包下的其他类,你还是不能用。要导入其中所有的类,只需使用 **\*** ,就像本书中其他示例那样:
47 |
48 | ```java
49 | import java.util.*
50 | ```
51 |
52 | 之所以使用导入,是为了提供一种管理命名空间的机制。所有类名之间都是相互隔离的。类 **A** 中的方法 `f()` 不会与类 **B** 中具有相同签名的方法 `f()` 冲突。但是如果类名冲突呢?假设你创建了一个 **Stack** 类,打算安装在一台已经有别人所写的 **Stack** 类的机器上,该怎么办呢?这种类名的潜在冲突,正是我们需要在 Java 中对命名空间进行完全控制的原因。为了解决冲突,我们为每个类创建一个唯一标识符组合。
53 |
54 | 到目前为止的大部分示例都只存在单个文件,并为本地使用的,所以尚未受到包名的干扰。但是,这些示例其实已经位于包中了,叫做"未命名"包或默认包。这当然是一种选择,为了简单起见,本书其余部分会尽可能采用这种方式。但是,如果你打算为相同机器上的其他 Java 程序创建友好的类库或程序时,就必须仔细考虑以防类名冲突。
55 |
56 | 一个 Java 源代码文件称为一个*编译单元*(有时也称*翻译单元*)。每个编译单元的文件名后缀必须是 **.java**。在编译单元中可以有一个 **public** 类,它的类名必须与文件名相同(包括大小写,但不包括后缀名 **.java**)。每个编译单元中只能有一个 **public** 类,否则编译器不接受。如果这个编译单元中还有其他类,那么在包之外是无法访问到这些类的,因为它们不是 **public** 类,此时它们支持主 **public** 类。
57 |
58 | ### 代码组织
59 |
60 | 当编译一个 **.java** 文件时,**.java** 文件的每个类都会有一个输出文件。每个输出的文件名和 **.java** 文件中每个类的类名相同,只是后缀名是 **.class**。因此,在编译少量的 **.java** 文件后,会得到大量的 **.class** 文件。如果你使用过编译型语言,那么你可能习惯编译后产生一个中间文件(通常称为"obj"文件),然后与使用链接器(创建可执行文件)或类库生成器(创建类库)产生的其他同类文件打包到一起的情况。这不是 Java 工作的方式。在 Java 中,可运行程序是一组 **.class** 文件,它们可以打包压缩成一个 Java 文档文件(JAR,使用 **jar** 文档生成器)。Java 解释器负责查找、加载和解释这些文件。
61 |
62 | 类库是一组类文件。每个源文件通常都含有一个 **public** 类和任意数量的非 **public** 类,因此每个文件都有一个构件。如果把这些组件集中在一起,就需要使用关键字 **package**。
63 |
64 | 如果你使用了 **package** 语句,它必须是文件中除了注释之外的第一行代码。当你如下这样写:
65 |
66 | ```java
67 | package hiding;
68 | ```
69 |
70 | 意味着这个编译单元是一个名为 **hiding** 类库的一部分。换句话说,你正在声明的编译单元中的 **public** 类名称位于名为 **hiding** 的保护伞下。任何人想要使用该名称,必须指明完整的类名或者使用 **import** 关键字导入 **hiding**。(注意,Java 包名按惯例一律小写,即使中间的单词也需要小写,与驼峰命名不同)
71 |
72 | 例如,假设文件名是 **MyClass.java**,这意味着文件中只能有一个 **public** 类,且类名必须是 MyClass(大小写也与文件名相同):
73 |
74 | ```java
75 | // hiding/mypackage/MyClass.java
76 | package hiding.mypackage
77 |
78 | public class MyClass {
79 | // ...
80 | }
81 | ```
82 |
83 | 现在,如果有人想使用 **MyClass** 或 **hiding.mypackage** 中的其他 **public** 类,就必须使用关键字 **import** 来使 **hiding.mypackage** 中的名称可用。还有一种选择是使用完整的名称:
84 |
85 | ```java
86 | // hiding/QualifiedMyClass.java
87 |
88 | public class QualifiedMyClass {
89 | public static void main(String[] args) {
90 | hiding.mypackage.MyClass m = new hiding.mypackage.MyClass();
91 | }
92 | }
93 | ```
94 |
95 | 关键字 **import** 使之更简洁:
96 |
97 | ```java
98 | // hiding/ImportedMyClass.java
99 | import hiding.mypackage.*;
100 |
101 | public class ImportedMyClass {
102 | public static void main(String[] args) {
103 | MyClass m = new MyClass();
104 | }
105 | }
106 | ```
107 |
108 | **package** 和 **import** 这两个关键字将单一的全局命名空间分隔开,从而避免名称冲突。
109 |
110 | ### 创建独一无二的包名
111 |
112 | 你可能注意到,一个包从未真正被打包成单一的文件,它可以由很多 **.class** 文件构成,因而事情就变得有点复杂了。为了避免这种情况,一种合乎逻辑的做法是将特定包下的所有 **.class** 文件都放在一个目录下。也就是说,利用操作系统的文件结构的层次性。这是 Java 解决混乱问题的一种方式;稍后你还会在我们介绍 **jar** 工具时看到另一种方式。
113 |
114 | 将所有的文件放在一个子目录还解决了其他的两个问题:创建独一无二的包名和查找可能隐藏于目录结构某处的类。这是通过将 **.class** 文件所在的路径位置编码成 **package** 名称来实现的。按照惯例,**package** 名称是类的创建者的反顺序的 Internet 域名。如果你遵循惯例,因为 Internet 域名是独一无二的,所以你的 **package** 名称也应该是独一无二的,不会发生名称冲突。如果你没有自己的域名,你就得构造一组不大可能与他人重复的组合(比如你的姓名),来创建独一无二的 package 名称。如果你打算发布 Java 程序代码,那么花些力气去获取一个域名是值得的。
115 |
116 | 此技巧的第二部分是把 **package** 名称分解成你机器上的一个目录,所以当 Java 解释器必须要加载一个 .class 文件时,它能定位到 **.class** 文件所在的位置。首先,它找出环境变量 **CLASSPATH**(通过操作系统设置,有时也能通过 Java 的安装程序或基于 Java 的工具设置)。**CLASSPATH** 包含一个或多个目录,用作查找 .**class** 文件的根目录。从根目录开始,Java 解释器获取包名并将每个句点替换成反斜杠,生成一个基于根目录的路径名(包名 foo.bar.baz 变成 foo\bar\baz 或 foo/bar/baz 或其它,取决于你的操作系统)。然后这个路径与 **CLASSPATH** 的不同项连接,解释器就在这些目录中查找与你所创建的类名称相关的 **.class** 文件(解释器还会查找某些涉及 Java 解释器所在位置的标准目录)。
117 |
118 | 为了理解这点,比如说我的域名 **MindviewInc.com**,将之反转并全部改为小写后就是 **com.mindviewinc**,这将作为我创建的类的独一无二的全局名称。(com、edu、org 等扩展名之前在 Java 包中都是大写,但是 Java 2 之后都统一用小写。)我决定再创建一个名为 **simple** 的类库,从而细分名称:
119 |
120 | ```java
121 | package com.mindviewinc.simple;
122 | ```
123 |
124 | 这个包名可以用作下面两个文件的命名空间保护伞:
125 |
126 | ```java
127 | // com/mindviewinc/simple/Vector.java
128 | // Creating a package
129 | package com.mindviewinc.simple;
130 |
131 | public class Vector {
132 | public Vector() {
133 | System.out.println("com.mindviewinc.simple.Vector");
134 | }
135 | }
136 | ```
137 |
138 | 如前所述,**package** 语句必须是文件的第一行非注释代码。第二个文件看上去差不多:
139 |
140 | ```java
141 | // com/mindviewinc/simple/List.java
142 | // Creating a package
143 | package com.mindviewinc.simple;
144 |
145 | public class List {
146 | System.out.println("com.mindview.simple.List");
147 | }
148 | ```
149 |
150 | 这两个文件都位于我机器上的子目录中,如下:
151 |
152 | ```
153 | C:\DOC\Java\com\mindviewinc\simple
154 | ```
155 |
156 | (注意,本书的每个文件的第一行注释都指明了文件在源代码目录树中的位置——供本书的自动代码提取工具使用。)
157 |
158 | 如果你回头看这个路径,会看到包名 **com.mindviewinc.simple**,但是路径的第一部分呢?CLASSPATH 环境变量会处理它。我机器上的环境变量部分如下:
159 |
160 | ```
161 | CLASSPATH=.;D:\JAVA\LIB;C:\DOC\Java
162 | ```
163 |
164 | CLASSPATH 可以包含多个不同的搜索路径。
165 |
166 | 但是在使用 JAR 文件时,有点不一样。你必须在类路径写清楚 JAR 文件的实际名称,不能仅仅是 JAR 文件所在的目录。因此,对于一个名为 **grape.jar** 的 JAR 文件,类路径应包括:
167 |
168 | ```
169 | CLASSPATH=.;D\JAVA\LIB;C:\flavors\grape.jar
170 | ```
171 |
172 | 一旦设置好类路径,下面的文件就可以放在任意目录:
173 |
174 | ```java
175 | // hiding/LibTest.java
176 | // Uses the library
177 | import com.mindviewinc.simple.*;
178 |
179 | public class LibTest {
180 | public static void main(String[] args) {
181 | Vector v = new Vector();
182 | List l = new List();
183 | }
184 | }
185 | ```
186 |
187 | 输出:
188 |
189 | ```
190 | com.mindviewinc.simple.Vector
191 | com.mindviewinc.simple.List
192 | ```
193 |
194 | 当编译器遇到导入 **simple** 库的 **import** 语句时,它首先会在 CLASSPATH 指定的目录中查找子目录 **com/mindviewinc/simple**,然后从已编译的文件中找出名称相符者(对 **Vector** 而言是 **Vector.class**,对 **List** 而言是 **List.class**)。注意,这两个类和其中要访问的方法都必须是 **public** 修饰的。
195 |
196 | 对于 Java 新手而言,设置 CLASSPATH 是一件麻烦的事(我最初使用时这么觉得),后面版本的 JDK 更加智能。你会发现当你安装好 JDK 时,即使不设置 CLASSPATH,也能够编译和运行基本的 Java 程序。但是,为了编译和运行本书的代码示例(从[https://github.com/BruceEckel/OnJava8-examples](https://github.com/BruceEckel/OnJava8-examples) 取得),你必须将本书程序代码树的基本目录加入到 CLASSPATH 中( gradlew 命令管理自身的 CLASSPATH,所以如果你想直接使用 javac 和 java,不用 Gradle 的话,就需要设置 CLASSPATH)。
197 |
198 | ### 冲突
199 |
200 | 如果通过 **\*** 导入了两个包含相同名字类名的类库,会发生什么?例如,假设程序如下:
201 |
202 | ```java
203 | import com.mindviewinc.simple.*;
204 | import java.util.*;
205 | ```
206 |
207 | 因为 **java.util.\*** 也包含了 **Vector** 类,这就存在潜在的冲突。但是只要你不写导致冲突的代码,就不会有问题——这样很好,否则就得做很多类型检查工作来防止那些根本不会出现的冲突。
208 |
209 | 现在如果要创建一个 Vector 类,就会出现冲突:
210 |
211 | ```java
212 | Vector v = new Vector();
213 | ```
214 |
215 | 这里的 **Vector** 类指的是谁呢?编译器不知道,读者也不知道。所以编译器报错,强制你明确指明。对于标准的 Java 类 **Vector**,你可以这么写:
216 |
217 | ```java
218 | java.util.Vector v = new java.util.Vector();
219 | ```
220 |
221 | 这种写法完全指明了 **Vector** 类的位置(配合 CLASSPATH),那么就没有必要写 **import java.util.\*** 语句,除非使用其他来自 **java.util** 中的类。
222 |
223 | 或者,可以导入单个类以防冲突——只要不在同一个程序中使用有冲突的名字(若使用了有冲突的名字,必须明确指明全名)。
224 |
225 | ### 定制工具库
226 |
227 | 具备了以上知识,现在就可以创建自己的工具库来减少重复的程序代码了。
228 |
229 | 一般来说,我会使用反转后的域名来命名要创建的工具包,比如 **com.mindviewinc.util**,但为了简化,这里我把工具包命名为 **onjava**。
230 |
231 | 比如,下面是"控制流"一章中使用到的 `range()` 方法,采用了 for-in 语法进行简单的遍历:
232 |
233 | ```java
234 | // onjava/Range.java
235 | // Array creation methods that can be used without
236 | // qualifiers, using static imports:
237 | package onjava;
238 |
239 | public class Range {
240 | // Produce a sequence [0,n)
241 | public static int[] range(int n) {
242 | int[] result = new int[n];
243 | for (int i = 0; i < n; i++) {
244 | result[i] = i;
245 | }
246 | return result;
247 | }
248 | // Produce a sequence [start..end)
249 | public static int[] range(int start, int end) {
250 | int sz = end - start;
251 | int[] result = new int[sz];
252 | for (int i = 0; i < sz; i++) {
253 | result[i] = start + i;
254 | }
255 | return result;
256 | }
257 | // Produce sequence [start..end) incrementing by step
258 | public static int[] range(int start, int end, int step) {
259 | int sz = (end - start) / step;
260 | int[] result = new int[sz];
261 | for (int i = 0; i < sz; i++) {
262 | result[i] = start + (i * step);
263 | }
264 | return result;
265 | }
266 | }
267 | ```
268 |
269 | 这个文件的位置一定是在某个以一个 CLASSPATH 位置开始,然后接着是 **onjava** 的目录下。编译完之后,就可以在系统的任何地方使用 **import static** 语句来使用这些方法了。
270 |
271 | 从现在开始,无论何时你创建了有用的新工具,都可以把它加入到自己的类库中。在本书中,你将会看到更多的组件加入到 **onjava** 库。
272 |
273 | ### 使用 import 改变行为
274 |
275 | Java 没有 C 的*条件编译*功能,该功能使你不必更改任何程序代码而能够切换开关产生不同的行为。Java 之所以去掉此功能,可能是因为 C 在绝大多数情况下使用该功能解决跨平台问题:程序代码的不同部分要根据不同的平台来编译。而 Java 自身就是跨平台设计的,这个功能就没有必要了。
276 |
277 | 但是,条件编译还有其他的用途。调试是一个很常见的用途,调试功能在开发过程中是开启的,在发布的产品中是禁用的。可以通过改变导入的 **package** 来实现这一目的,修改的方法是将程序中的代码从调试版改为发布版。这个技术可用于任何种类的条件代码。
278 |
279 | ### 使用包的忠告
280 |
281 | 当创建一个包时,包名就隐含了目录结构。这个包必须位于包名指定的目录中,该目录必须在以 CLASSPATH 开始的目录中可以查询到。 最初使用关键字 **package** 可能会有点不顺,因为除非遵守"包名对应目录路径"的规则,否则会收到很多意外的运行时错误信息如找不到特定的类,即使这个类就位于同一目录中。如果你收到类似信息,尝试把 **package** 语句注释掉,如果程序能运行的话,你就知道问题出现在哪里了。
282 |
283 | 注意,编译过的代码通常位于与源代码的不同目录中。这是很多工程的标准,而且集成开发环境(IDE)通常会自动为我们做这些。必须保证 JVM 通过 CLASSPATH 能找到编译后的代码。
284 |
285 | ## 访问权限修饰符
286 |
287 | Java 访问权限修饰符 **public**,**protected** 和 **private** 位于定义的类名,属性名和方法名之前。每个访问权限修饰符只能控制它所修饰的对象。
288 |
289 | 如果不提供访问修饰符,就意味着"包访问权限"。所以无论如何,万物都有某种形式的访问控制权。接下来的几节中,你将学习各种类型的访问权限。
290 |
291 | ### 包访问权限
292 |
293 | 本章之前的所有示例要么使用 **public** 访问修饰符,要么就没使用修饰符(默认访问)。默认访问权限没有关键字,通常被称为包访问权限(有时也称为 friendly)。这意味着当前包中的所有其他类都可以访问那个成员。对于这个包之外的类,这个成员看上去是 **private** 的。由于一个编译单元(即一个文件)只能隶属于一个包,所以通过包访问权限,位于同一编译单元中的所有类彼此之间都是可访问的。
294 |
295 | 包访问权限可以把相关类聚到一个包下,以便它们能轻易地相互访问。包里的类给它们的包访问权限的成员赋予了相互访问的权限,所以你"拥有”了包内的程序代码。只有你拥有的代码才能访问你拥有的其他代码是有意义的。包访问为把类聚在一个包中提供了理由。在许多语言中,在文件中组织定义的方式是任意的,但是在 Java 中你被强制以一种合理的方式组织它们。另外,你可以把不能访问当前包里的类的其他类排除在外。
296 |
297 | 类控制着哪些代码有权访问自己的成员。其他包中的代码不能一上来就说"嗨,我是 **Bob** 的朋友!",然后想看到 **Bob** 的 **protected**,包访问权限和 **private** 成员。取得对成员的访问权的唯一方式是:
298 |
299 | 1. 使成员成为 public。那么无论是谁,无论在哪,都可以访问它。
300 | 2. 赋予成员默认包访问权限,不用加任何访问修饰符,然后将其他类放在相同的包内。这样,其他类就可以访问该成员。
301 | 3. 在"复用"这一章你将看到,继承的类既可以访问 **public** 成员,也可以访问 **protected** 成员(但不能访问 **private** 成员)。只有当两个类处于同一个包内,它才可以访问包访问权限的成员。但现在不用担心继承和 **protected**。
302 | 4. 提供访问器(accessor)和修改器(mutator)方法(有时也称为"get/set" 方法),从而读取和改变值。
303 |
304 | ### public: 接口访问权限
305 |
306 | 当你使用关键字 **public**,就意味着紧随 public 后声明的成员对于每个人都是可用的,尤其是使用类库的客户端程序员更是如此。假设定义了一个包含下面编译单元的 **dessert** 包:
307 |
308 | ```java
309 | // hiding/dessert/Cookie.java
310 | // Creates a library
311 | package hiding.dessert;
312 |
313 | public class Cookie {
314 | public Cookie() {
315 | System.out.println("Cookie constructor");
316 | }
317 |
318 | void bite() {
319 | System.out.println("bite");
320 | }
321 | }
322 | ```
323 |
324 | 记住,**Cookie.java** 文件产生的类文件必须位于名为 **dessert** 的子目录中,该子目录在 **hiding** (表明本书的"封装"章节)下,它必须在 CLASSPATH 的几个目录之下。不要错误地认为 Java 总是会将当前目录视作查找行为的起点之一。如果你的 CLASSPATH 中没有 `.`,Java 就不会查找单独当前目录。
325 |
326 | 现在,使用 **Cookie** 创建一个程序:
327 |
328 | ```java
329 | // hiding/Dinner.java
330 | // Uses the library
331 | import hiding.dessert.*;
332 |
333 | public class Dinner {
334 | public static void main(String[] args) {
335 | Cookie x = new Cookie();
336 | // -x.bite(); // Can't access
337 | }
338 | }
339 | ```
340 |
341 | 输出:
342 |
343 | ```
344 | Cookie constructor
345 | ```
346 |
347 | 你可以创建一个 **Cookie** 对象,因为它构造器和类都是 **public** 的。(后面会看到更多 **public** 的概念)但是,在 **Dinner.java** 中无法访问到 **Cookie** 对象中的 `bite()` 方法,因为 `bite()` 只提供了包访问权限,因而在 **dessert** 包之外无法访问,编译器禁止你使用它。
348 |
349 | ### 默认包
350 |
351 | 你可能惊讶地发现,以下代码尽管看上去破坏了规则,但是仍然可以编译:
352 |
353 | ```java
354 | // hiding/Cake.java
355 | // Accesses a class in a separate compilation unit
356 | class Cake {
357 | public static void main(String[] args) {
358 | Pie x = new Pie();
359 | x.f();
360 | }
361 | }
362 | ```
363 |
364 | 输出:
365 |
366 | ```
367 | Pie.f()
368 | ```
369 |
370 | 同一目录下的第二个文件:
371 |
372 | ```java
373 | // hiding/Pie.java
374 | // The other class
375 | class Pie {
376 | void f() {
377 | System.out.println("Pie.f()");
378 | }
379 | }
380 | ```
381 |
382 | 最初看上去这两个文件毫不相关,但在 **Cake** 中可以创建一个 **Pie** 对象并调用它的 `f()` 方法。(注意,你的 CLASSPATH 中一定得有 `.`,这样文件才能编译)通常会认为 **Pie** 和 `f()` 具有包访问权限,因此不能被 **Cake** 访问。它们的确具有包访问权限,这是部分正确。**Cake.java** 可以访问它们是因为它们在相同的目录中且没有给自己设定明确的包名。Java 把这样的文件看作是隶属于该目录的默认包中,因此它们为该目录中所有的其他文件都提供了包访问权限。
383 |
384 | ### private: 你无法访问
385 |
386 | 关键字 **private** 意味着除了包含该成员的类,其他任何类都无法访问这个成员。同一包中的其他类无法访问 **private** 成员,因此这等于说是自己隔离自己。另一方面,让许多人合作创建一个包也是有可能的。使用 **private**,你可以自由地修改那个被修饰的成员,无需担心会影响同一包下的其他类。
387 |
388 | 默认的包访问权限通常提供了足够的隐藏措施;记住,使用类的客户端程序员无法访问包访问权限成员。这样做很好,因为默认访问权限是一种我们常用的权限(同时也是一种在忘记添加任何访问权限时自动得到的权限)。因此,通常考虑的是把哪些成员声明成 **public** 供客户端程序员使用。所以,最初不常使用关键字 **private**,因为程序没有它也可以照常工作。然而,使用 **private** 是非常重要的,尤其是在多线程环境中。(在"并发编程"一章中将看到)。
389 |
390 | 以下是一个使用 **private** 的例子:
391 |
392 | ```java
393 | // hiding/IceCream.java
394 | // Demonstrates "private" keyword
395 |
396 | class Sundae {
397 | private Sundae() {}
398 | static Sundae makeASundae() {
399 | return new Sundae();
400 | }
401 | }
402 |
403 | public class IceCream {
404 | public static void main(String[] args) {
405 | //- Sundae x = new Sundae();
406 | Sundae x = Sundae.makeASundae();
407 | }
408 | }
409 | ```
410 |
411 | 以上展示了 **private** 的用武之地:控制如何创建对象,防止别人直接访问某个特定的构造器(或全部构造器)。例子中,你无法通过构造器创建一个 **Sundae** 对象,而必须调用 `makeASundae()` 方法创建对象。
412 |
413 | 任何可以肯定只是该类的"助手"方法,都可以声明为 **private**,以确保不会在包中的其他地方误用它,也防止了你会去改变或删除它。将方法声明为 **private** 确保了你拥有这种选择权。
414 |
415 | 对于类中的 **private** 属性也是一样。除非必须公开底层实现(这种情况很少见),否则就将属性声明为 **private**。然而,不能因为类中某个对象的引用是 **private**,就认为其他对象也无法拥有该对象的 **public** 引用(参见附录:对象传递和返回)。
416 |
417 | ### protected: 继承访问权限
418 |
419 | 要理解 **protected** 的访问权限,我们在内容上需要作一点跳跃。首先,在介绍本书"复用"章节前,你不必真正理解本节的内容。但为了内容的完整性,这里作了简要介绍,举了个使用 **protected** 的例子。
420 |
421 | 关键字 **protected** 处理的是继承的概念,通过继承可以利用一个现有的类——我们称之为基类,然后添加新成员到现有类中而不必碰现有类。我们还可以改变类的现有成员的行为。为了从一个类中继承,需要声明新类 extends 一个现有类,像这样:
422 |
423 | ```java
424 | class Foo extends Bar {}
425 | ```
426 |
427 | 类定义的其他部分看起来是一样的。
428 |
429 | 如果你创建了一个新包,并从另一个包继承类,那么唯一能访问的就是被继承类的 **public** 成员。(如果在同一个包中继承,就可以操作所有的包访问权限的成员。)有时,基类的创建者会希望某个特定成员能被继承类访问,但不能被其他类访问。这时就需要使用 **protected**。**protected** 也提供包访问权限,也就是说,相同包内的其他类可以访问 **protected** 元素。
430 |
431 | 回顾下先前的文件 **Cookie.java**,下面的类不能调用包访问权限的方法 `bite()`:
432 |
433 | ```java
434 | // hiding/ChocolateChip.java
435 | // Can't use package-access member from another package
436 | import hiding.dessert.*;
437 |
438 | public class ChocolateChip extends Cookie {
439 | public ChocolateChip() {
440 | System.out.println("ChocolateChip constructor");
441 | }
442 |
443 | public void chomp() {
444 | //- bite(); // Can't access bite
445 | }
446 |
447 | public static void main(String[] args) {
448 | ChocolateChip x = new ChocolateChip();
449 | x.chomp();
450 | }
451 | }
452 | ```
453 |
454 | 输出:
455 |
456 | ```
457 | Cookie constructor
458 | ChocolateChip constructor
459 | ```
460 |
461 | 如果类 **Cookie** 中存在一个方法 `bite()`,那么它的任何子类中都存在 `bite()` 方法。但是因为 `bite()` 具有包访问权限并且位于另一个包中,所以我们在这个包中无法使用它。你可以把它声明为 **public**,但这样一来每个人都能访问它,这可能也不是你想要的。如果你将 **Cookie** 改成如下这样:
462 |
463 | ```java
464 | // hiding/cookie2/Cookie.java
465 | package hiding.cookie2;
466 |
467 | public class Cookie {
468 | public Cookie() {
469 | System.out.println("Cookie constructor");
470 | }
471 |
472 | protected void bite() {
473 | System.out.println("bite");
474 | }
475 | }
476 | ```
477 |
478 | 这样,`bite()` 对于所有继承 **Cookie** 的类,都是可访问的:
479 |
480 | ```java
481 | // hiding/ChocolateChip2.java
482 | import hiding.cookie2.*;
483 |
484 | public class ChocolateChip2 extends Cookie {
485 | public ChocoalteChip2() {
486 | System.out.println("ChocolateChip2 constructor");
487 | }
488 |
489 | public void chomp() {
490 | bite(); // Protected method
491 | }
492 |
493 | public static void main(String[] args) {
494 | ChocolateChip2 x = new ChocolateChip2();
495 | x.chomp();
496 | }
497 | }
498 | ```
499 |
500 | 输出:
501 |
502 | ```
503 | Cookie constructor
504 | ChocolateChip2 constructor
505 | bite
506 | ```
507 |
508 | 尽管 `bite()` 也具有包访问权限,但它不是 **public** 的。
509 |
510 | ### 包访问权限 Vs Public 构造器
511 |
512 | 当你定义一个具有包访问权限的类时,你可以在类中定义一个 public 构造器,编译器不会报错:
513 |
514 | ```java
515 | // hiding/packageaccess/PublicConstructor.java
516 | package hiding.packageaccess;
517 |
518 | class PublicConstructor {
519 | public PublicConstructor() {}
520 | }
521 | ```
522 |
523 | 有一个 Checkstyle 工具,你可以运行命令 **gradlew hiding:checkstyleMain** 使用它,它会指出这种写法是虚假的,而且从技术上来说是错误的。实际上你不能从包外访问到这个 **public** 构造器:
524 |
525 | ```java
526 | // hiding/CreatePackageAccessObject.java
527 | // {WillNotCompile}
528 | import hiding.packageaccess.*;
529 |
530 | public class CreatePackageAcessObject {
531 | public static void main(String[] args) {
532 | new PublicConstructor();
533 | }
534 | }
535 | ```
536 |
537 | 如果你编译下这个类,会得到编译错误信息:
538 |
539 | ```
540 | CreatePackageAccessObject.java:6:error:
541 | PublicConstructor is not public in hiding.packageaccess;
542 | cannot be accessed from outside package
543 | new PublicConstructor();
544 | ^
545 | 1 error
546 | ```
547 |
548 | 因此,在一个具有包访问权限的类中定义一个 **public** 的构造器并不能真的使这个构造器成为 **public**,在声明的时候就应该标记为编译时错误。
549 |
550 | ## 接口和实现
551 |
552 | 访问控制通常被称为实现的隐藏。将数据和方法包装进类中并把具体实现隐藏被称作是封装。其结果就是一个同时带有特征和行为的数据类型。
553 |
554 | 出于两个重要的原因,访问控制在数据类型内部划定了边界。第一个原因是确立客户端程序员可以使用和不能使用的边界。可以在结构中建立自己的内部机制而不必担心客户端程序员偶尔将内部实现作为他们可以使用的接口的一部分。
555 |
556 | 这直接引出了第二个原因:将接口与实现分离。如果在一组程序中使用结构,而客户端程序员只能向 **public** 接口发送消息的话,那么就可以自由地修改任何不是 **public** 的事物(例如包访问权限,protected,或 private 修饰的事物),却不会破坏客户端代码。
557 |
558 | 为了清晰起见,你可以采用一种创建类的风格:**public** 成员放在类的开头,接着是 **protected** 成员,包访问权限成员,最后是 **private** 成员。这么做的好处是类的使用者可以从头读起,首先会看到对他们而言最重要的部分(public 成员,因为可以从文件外访问它们),直到遇到非 **public** 成员时停止阅读,下面就是内部实现了:
559 |
560 | ```java
561 | // hiding/OrganizedByAccess.java
562 |
563 | public class OrganizedByAccess {
564 | public void pub1() {/* ... */}
565 | public void pub2() {/* ... */}
566 | public void pub3() {/* ... */}
567 | private void priv1() {/* ... */}
568 | private void priv2() {/* ... */}
569 | private void priv3() {/* ... */}
570 | private int i;
571 | // ...
572 | }
573 | ```
574 |
575 | 这么做只能是程序阅读起来稍微容易一些,因为实现和接口还是混合在一起。也就是说,你仍然能看到源代码——实现部分,因为它就在类中。另外,javadoc 提供的注释文档功能降低了程序代码的可读性对客户端程序员的重要性。将接口展现给类的使用者实际上是类浏览器的任务,类浏览器会展示所有可用的类,并告诉你如何使用它们(比如说哪些成员可用)。在 Java 中,JDK 文档起到了类浏览器的作用。
576 |
577 | ## 类访问权限
578 |
579 | 访问权限修饰符也可以用于确定类库中的哪些类对于类库的使用者是可用的。如果希望某个类可以被客户端程序员使用,就把关键字 **public** 作用于整个类的定义。这甚至控制着客户端程序员能否创建类的对象。
580 |
581 | 为了控制一个类的访问权限,修饰符必须出现在关键字 **class** 之前:
582 |
583 | ```java
584 | public class Widget {
585 | ```
586 |
587 | 如果你的类库名是 **hiding**,那么任何客户端程序员都可以通过如下声明访问 **Widget**:
588 |
589 | ```java
590 | import hiding.Widget;
591 | ```
592 |
593 | 或者
594 |
595 | ```java
596 | import hiding.*;
597 | ```
598 |
599 | 这里有一些额外的限制:
600 |
601 | 1. 每个编译单元(即每个文件)中只能有一个 **public** 类。这表示,每个编译单元有一个公共的接口用 **public** 类表示。该接口可以包含许多支持包访问权限的类。一旦一个编译单元中出现一个以上的 **public** 类,编译就会报错。
602 | 2. **public** 类的名称必须与含有该编译单元的文件名相同,包括大小写。所以对于 **Widget** 来说,文件名必须是 **Widget.java**,不能是 **widget.java** 或 **WIDGET.java**。再次强调,如果名字不匹配,编译器会报错。
603 | 3. 虽然不是很常见,但是编译单元内没有 **public** 类也是可能的。这时可以随意命名文件(尽管随意命名会让代码的阅读者和维护者感到困惑)。
604 |
605 | 如果获取了一个在 **hiding** 包中的类,只用来完成 **Widget** 或 **hiding** 包下一些其他 **public** 类所要执行的任务,怎么办呢? 你不想自找麻烦为客户端程序员创建说明文档,并且你认为不久后会完全改变原有方案并将旧版本删除,替换成新版本。为了保留此灵活性,需要确保客户端程序员不依赖隐藏在 **hiding** 中的任何特定细节,那么把 **public** 关键字从类中去掉,给予它包访问权限,就可以了。
606 |
607 | 当你创建了一个包访问权限的类,把类中的属性声明为 **private** 仍然是有意义的——应该尽可能将所有属性都声明为 **private**,但是通常把方法声明成与类(包访问权限)相同的访问权限也是合理的。由于一个包访问权限的类只能被用于包内,除非你被强制将某些方法声明为 **public**,这种情况下,编译器会告诉你。
608 |
609 | 注意,类既不能是 **private** 的(这样除了该类自身,任何类都不能访问它),也不能是 **protected** 的。所以对于类的访问权限只有两种选择:包访问权限或者 **public**。为了防止类被外界访问,可以将所有的构造器声明为 **private**,这样只有你自己能创建对象(在类的 static 成员中):
610 |
611 | ```java
612 | // hiding/Lunch.java
613 | // Demonstrates class access specifiers. Make a class
614 | // effectively private with private constructors:
615 |
616 | class Soup1 {
617 | private Soup1() {}
618 |
619 | public static Soup1 makeSoup() { // [1]
620 | return new Soup1();
621 | }
622 | }
623 |
624 | class Soup2 {
625 | private Soup2() {}
626 |
627 | private static Soup2 ps1 = new Soup2(); // [2]
628 |
629 | public static Soup2 access() {
630 | return ps1;
631 | }
632 |
633 | public void f() {}
634 | }
635 | // Only one public class allowed per file:
636 | public class Lunch {
637 | void testPrivate() {
638 | // Can't do this! Private constructor:
639 | //- Soup1 soup = new Soup1();
640 | }
641 |
642 | void testStatic() {
643 | Soup1 soup = Soup1.makeSoup();
644 | }
645 |
646 | void testSingleton() {
647 | Soup2.access().f();
648 | }
649 | }
650 | ```
651 |
652 | 可以像 [1] 那样通过 **static** 方法创建对象,也可以像 [2] 那样先创建一个静态对象,当用户需要访问它时返回对象的引用即可。
653 |
654 | 到目前为止,大部分的方法要么返回 void,要么返回基本类型,所以 [1] 处的定义乍看之下会有点困惑。方法名(**makeSoup**)前面的 **Soup1** 表明了方法返回的类型。到目前为止,这里经常是 **void**,即不返回任何东西。然而也可以返回对象的引用,就像这里一样。这个方法返回了对 **Soup1** 类对象的引用。
655 |
656 | **Soup1** 和 **Soup2** 展示了如何通过将你所有的构造器声明为 **private** 的方式防止直接创建某个类的对象。记住,如果你不显式地创建构造器,编译器会自动为你创建一个无参构造器(没有参数的构造器)。如果我们编写了无参构造器,那么编译器就不会自动创建构造器了。将构造器声明为 **private**,那么谁也无法创建该类的对象了。但是现在别人该怎么使用这个类呢?上述例子给出了两个选择。在 **Soup1** 中,有一个 **static** 方法,它的作用是创建一个新的 **Soup1** 对象并返回对象的引用。如果想要在返回引用之前在 **Soup1** 上做一些额外操作,或是记录创建了多少个 **Soup1** 对象(可以用来限制数量),这种做法是有用的。
657 |
658 | **Soup2** 用到了所谓的*设计模式*。这种模式叫做*单例模式*,因为它只允许创建类的一个对象。**Soup2** 类的对象是作为 **Soup2** 的 **static** **private** 成员而创建的,所以有且只有一个,你只能通过 **public** 修饰的 `access()` 方法访问到这个对象。
659 |
660 | ## 本章小结
661 |
662 | 无论在什么样的关系中,划定一些供各成员共同遵守的界限是很重要的。当你创建了一个类库,也就与该类库的使用者产生了联系,他们是类库的客户端程序员,需要使用你的类库创建应用或更大的类库。
663 |
664 | 没有规则,客户端程序员就可以对类的所有成员为所欲为,即使你希望他们不要操作部分成员。这种情况下,所有事物都是公开的。
665 |
666 | 本章讨论了类库是如何通过类构建的:首先,介绍了将一组类打包到类库的方式,其次介绍了类如何控制对其成员的访问。
667 |
668 | 据估计,用 C 语言开发项目,当代码量达到 5 万行和 10 万行时就会出现问题,因为 C 语言只有单一的命名空间,名称开始冲突造成额外的管理开销。在 Java 中,关键字 **package**,包命名模式和关键字 **import** 给了你对于名称的完全控制权,因此可以轻易地避免名称冲突的问题。
669 |
670 | 控制成员访问权限有两个原因。第一个原因是使用户不要接触他们不该接触的部分,这部分对于类内部来说是必要的,但是不属于客户端程序员所需接口的一部分。因此将方法和属性声明为 **private** 对于客户端程序员来说是一种服务,可以让他们清楚地看到什么是重要的,什么可以忽略。这可以简化他们对类的理解。
671 |
672 | 第二个也是最重要的原因是为了让类库设计者更改类内部的工作方式,而不用担心会影响到客户端程序员。比如最初以某种方式创建一个类,随后发现如果更改代码结构可以极大地提高运行速度。如果接口与实现被明确地隔离和保护,你可以实现这一目的,而不必强制客户端程序员重新编写代码。访问权限控制确保客户端程序员不会依赖某个类的底层实现的任何部分。
673 |
674 | 当你具备更改底层实现的能力时,不但可以自由地改善设计,还可能会随意地犯错。无论如何细心地计划和设计,都有可能犯错。当了解到犯错是相对安全的时候,你可以更加放心地实验,更快地学会,更快地完成项目。
675 |
676 | 类的 **public** 接口是用户真正看到的,所以在分析和设计阶段决定这部分接口是最重要的部分。尽管如此,你仍然有改变的空间。如果最初没有创建出正确的接口,可以添加更多的方法,只要你不删除那些客户端程序员已经在他们的代码中使用的东西。
677 |
678 | 注意到访问权限控制关注的是类库创建者和外部使用者之间的关系,一种交流方式。很多情况下,事实并非如此。例如,你自己编写了所有的代码,或者在一个小组中工作,所有的东西都放在同一个包下。这些情况下,交流方式则是另外一种,此时严格地遵循访问权限规则也许不是最佳选择,默认(包)访问权限也许就足够好了。
679 |
--------------------------------------------------------------------------------
/docs/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/cover.png
--------------------------------------------------------------------------------
/docs/figures/On-Java-8_En-176.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/figures/On-Java-8_En-176.png
--------------------------------------------------------------------------------
/docs/figures/On-Java-8_En-204.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/figures/On-Java-8_En-204.png
--------------------------------------------------------------------------------
/docs/figures/On-Java-8_En-206.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/figures/On-Java-8_En-206.png
--------------------------------------------------------------------------------
/docs/figures/On-Java-8_En-216.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/figures/On-Java-8_En-216.png
--------------------------------------------------------------------------------
/docs/figures/On-Java-8_En-217.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/figures/On-Java-8_En-217.png
--------------------------------------------------------------------------------
/docs/figures/On-Java-8_En-218.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/figures/On-Java-8_En-218.png
--------------------------------------------------------------------------------
/docs/figures/On-Java-8_En-220.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/figures/On-Java-8_En-220.png
--------------------------------------------------------------------------------
/docs/figures/On-Java-8_En-224.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/figures/On-Java-8_En-224.png
--------------------------------------------------------------------------------
/docs/figures/On-Java-8_En-230.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/figures/On-Java-8_En-230.png
--------------------------------------------------------------------------------
/docs/figures/On-Java-8_En-234.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/figures/On-Java-8_En-234.png
--------------------------------------------------------------------------------
/docs/figures/On-Java-8_En-26.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/figures/On-Java-8_En-26.png
--------------------------------------------------------------------------------
/docs/figures/On-Java-8_En-279.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/figures/On-Java-8_En-279.png
--------------------------------------------------------------------------------
/docs/figures/On-Java-8_En-31.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/figures/On-Java-8_En-31.png
--------------------------------------------------------------------------------
/docs/figures/On-Java-8_En-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/figures/On-Java-8_En-32.png
--------------------------------------------------------------------------------
/docs/figures/On-Java-8_En-33.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/figures/On-Java-8_En-33.png
--------------------------------------------------------------------------------
/docs/figures/On-Java-8_En-34.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/figures/On-Java-8_En-34.png
--------------------------------------------------------------------------------
/docs/figures/On-Java-8_En-36.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/figures/On-Java-8_En-36.png
--------------------------------------------------------------------------------
/docs/figures/On-Java-8_En-37.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/figures/On-Java-8_En-37.png
--------------------------------------------------------------------------------
/docs/figures/On-Java-8_En-39.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/figures/On-Java-8_En-39.png
--------------------------------------------------------------------------------
/docs/figures/On-Java-8_En-40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/figures/On-Java-8_En-40.png
--------------------------------------------------------------------------------
/docs/figures/On-Java-8_En-429.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/figures/On-Java-8_En-429.png
--------------------------------------------------------------------------------
/docs/figures/On-Java-8_En-612.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/figures/On-Java-8_En-612.png
--------------------------------------------------------------------------------
/docs/figures/On-Java-8_En-639.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/figures/On-Java-8_En-639.png
--------------------------------------------------------------------------------
/docs/figures/On-Java-8_En-640.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/figures/On-Java-8_En-640.png
--------------------------------------------------------------------------------
/docs/figures/On-Java-8_En-643.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/figures/On-Java-8_En-643.png
--------------------------------------------------------------------------------
/docs/figures/On-Java-8_En-645.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/figures/On-Java-8_En-645.png
--------------------------------------------------------------------------------
/docs/figures/On-Java-8_En-713.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/figures/On-Java-8_En-713.png
--------------------------------------------------------------------------------
/docs/figures/On-Java-8_En-714.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/figures/On-Java-8_En-714.png
--------------------------------------------------------------------------------
/docs/figures/On-Java-8_En-716.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/figures/On-Java-8_En-716.png
--------------------------------------------------------------------------------
/docs/figures/On-Java-8_En-718.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/figures/On-Java-8_En-718.png
--------------------------------------------------------------------------------
/docs/figures/On-Java-8_En-719.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/figures/On-Java-8_En-719.png
--------------------------------------------------------------------------------
/docs/figures/On-Java-8_En-720.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/figures/On-Java-8_En-720.png
--------------------------------------------------------------------------------
/docs/figures/On-Java-8_En-721.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/figures/On-Java-8_En-721.png
--------------------------------------------------------------------------------
/docs/figures/On-Java-8_En-722.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/figures/On-Java-8_En-722.png
--------------------------------------------------------------------------------
/docs/figures/On-Java-8_En-723.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/figures/On-Java-8_En-723.png
--------------------------------------------------------------------------------
/docs/figures/On-Java-8_En-725.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/figures/On-Java-8_En-725.png
--------------------------------------------------------------------------------
/docs/images/1545758268350.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/images/1545758268350.png
--------------------------------------------------------------------------------
/docs/images/1545763399825.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/images/1545763399825.png
--------------------------------------------------------------------------------
/docs/images/1545764724202.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/images/1545764724202.png
--------------------------------------------------------------------------------
/docs/images/1545764780795.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/images/1545764780795.png
--------------------------------------------------------------------------------
/docs/images/1545764820176.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/images/1545764820176.png
--------------------------------------------------------------------------------
/docs/images/1545839316314.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/images/1545839316314.png
--------------------------------------------------------------------------------
/docs/images/1545841270997.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/images/1545841270997.png
--------------------------------------------------------------------------------
/docs/images/1554546258113.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/images/1554546258113.png
--------------------------------------------------------------------------------
/docs/images/1554546378822.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/images/1554546378822.png
--------------------------------------------------------------------------------
/docs/images/1554546452861.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/images/1554546452861.png
--------------------------------------------------------------------------------
/docs/images/1554546627710.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/images/1554546627710.png
--------------------------------------------------------------------------------
/docs/images/1554546666685.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/images/1554546666685.png
--------------------------------------------------------------------------------
/docs/images/1554546693664.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/images/1554546693664.png
--------------------------------------------------------------------------------
/docs/images/1554546847181.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/images/1554546847181.png
--------------------------------------------------------------------------------
/docs/images/1554546861836.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/images/1554546861836.png
--------------------------------------------------------------------------------
/docs/images/1554546881189.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/images/1554546881189.png
--------------------------------------------------------------------------------
/docs/images/1554546890132.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/images/1554546890132.png
--------------------------------------------------------------------------------
/docs/images/1561774164644.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/images/1561774164644.png
--------------------------------------------------------------------------------
/docs/images/1562204648023.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/images/1562204648023.png
--------------------------------------------------------------------------------
/docs/images/1562252767216.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/images/1562252767216.png
--------------------------------------------------------------------------------
/docs/images/1562406479787.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/images/1562406479787.png
--------------------------------------------------------------------------------
/docs/images/1562409366637.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/images/1562409366637.png
--------------------------------------------------------------------------------
/docs/images/1562409926765.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/images/1562409926765.png
--------------------------------------------------------------------------------
/docs/images/1562653648586.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/images/1562653648586.png
--------------------------------------------------------------------------------
/docs/images/1562737974623.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/images/1562737974623.png
--------------------------------------------------------------------------------
/docs/images/1562999314238.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/images/1562999314238.png
--------------------------------------------------------------------------------
/docs/images/QQGroupQRCode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/images/QQGroupQRCode.png
--------------------------------------------------------------------------------
/docs/images/collection.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/images/collection.png
--------------------------------------------------------------------------------
/docs/images/cover.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/images/cover.jpg
--------------------------------------------------------------------------------
/docs/images/cover_small.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/images/cover_small.jpg
--------------------------------------------------------------------------------
/docs/images/designproxy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/images/designproxy.png
--------------------------------------------------------------------------------
/docs/images/image-20190409114913825-4781754.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/images/image-20190409114913825-4781754.png
--------------------------------------------------------------------------------
/docs/images/level_1_title.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/images/level_1_title.png
--------------------------------------------------------------------------------
/docs/images/level_2_title.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/images/level_2_title.png
--------------------------------------------------------------------------------
/docs/images/map.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/images/map.png
--------------------------------------------------------------------------------
/docs/images/qqgroup.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/images/qqgroup.png
--------------------------------------------------------------------------------
/docs/images/reader.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/images/reader.png
--------------------------------------------------------------------------------
/docs/images/simple-collection-taxonomy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/docs/images/simple-collection-taxonomy.png
--------------------------------------------------------------------------------
/docs/introduction.md:
--------------------------------------------------------------------------------
1 | # 简介
2 |
3 | > “我的语言极限,即是我的世界的极限。” ——路德维希·维特根斯坦(_Wittgenstein_)
4 |
5 | 这句话无论对于自然语言还是编程语言来说都是一样的。你所使用的编程语言会将你的思维模式固化并逐渐远离其他语言,而且往往发生在潜移默化中。Java 作为一门傲娇的语言尤其如此。
6 |
7 | Java 是一门派生语言,早期语言设计者为了不想在项目中使用 C++ 而创造了这种看起来很像 C++,却比 C++ 有了改进的新语言(原始的项目并未成功)。Java 最核心的变化就是加入了“虚拟机”和“垃圾回收机制”,这两个概念在之后的章节会有详细描述。 此外,Java 还在其他方面推动了行业发展。例如,现在绝大多数编程语言都支持文档注释语法和 HTML 文档生成工具。
8 |
9 | Java 最主要的概念之一“对象”来自 SmallTalk 语言。SmallTalk 语言恪守“对象”(在下一章中描述)是编程的最基本单元。于是,万物皆对象。历经时间的检验,人们发现这种信念太过狂热。有些人甚至认为“对象”的概念是完全错误的,应该舍弃。就我个人而言,把一切事物都抽象成对象不仅是一项不必要的负担,同时还会招致许多设计朝着不好的方向发展。尽管如此,“对象”的概念依然有其闪光点。固执地要求所有东西都是一个对象(特别是一直到最底层级别)是一种设计错误;相反,完全逃避“对象”的概念似乎同样太过苛刻。
10 |
11 | Java 语言曾规划设计的许多功能并未按照承诺兑现。本书中,我将尝试解释这些原因,力争让读者知晓这些功能,并明白为什么这些功能最终并不适用。这无关 Java 是一种好语言或者坏语言,一旦你了解了该语言的缺陷和局限性,你就能够:
12 |
13 | 1. 明白有些功能特性为什么会被“废弃”。
14 |
15 | 2. 熟悉语言边界,更好地设计和编码。
16 |
17 | 编程的过程就是复杂性管理的过程:业务问题的复杂性,以及依赖的计算机的复杂性。由于这种复杂性,我们的大多数软件项目都失败了。
18 |
19 | 许多语言设计决策时都考虑到了复杂性,并试图降低语言的复杂性,但在设计过程中遇到了一些更棘手的问题,最终导致语言设计不可避免地“碰壁”,复杂性增加。例如,C++ 必须向后兼容 C(允许 C 程序员轻松迁移),并且效率很高。这些目标非常实用,并且也是 C++ 在编程界取得了成功的原因之一,但同时也引入了额外的复杂性,导致某些用 C++ 编写的项目开发失败。当然,你可以责怪程序员和管理人员手艺不精,但如果有一种编程语言可以帮助你在开发过程中发现错误,那岂不是更好?
20 |
21 | 虽然 VB(Visual BASIC)绑定在 BASIC 上,但 BASIC 实际上并不是一种可扩展的语言。大量扩展的堆积造成 VB 的语法难以维护。Perl 向后兼容 awk、sed、grep 以及其它要替换的 Unix 工具。因此它常常被诟病产生了一堆“只写代码”(_write-only code_,写代码的人自己都看不懂的代码)。另一方面,C ++,VB,Perl 和其他语言(如 SmallTalk)在设计时重点放在了对某些复杂问题的处理上,因而在解决这些特定类型的问题方面非常成功。
22 |
23 | 通信革命使我们相互沟通更加便利。无论是一对一沟通,还是团队里的互相沟通,甚至是地球上不同地区的沟通。据说下一次革命需要的是一种全球性的思维,这种思维源于足量的人以及足量相互连接。我不知道 Java 是否能成为这场革命的工具之一,但至少这种可能性让我觉得:我现在正在做的传道授业的事情是有意义的!
24 |
25 | ## 前提条件
26 |
27 | 阅读本书需要读者对编程有基本的了解:
28 |
29 | - 程序是一系列“陈述(语句、代码)”构成
30 |
31 | - 子程序、方法、宏的概念
32 |
33 | - 控制语句(例如 **if**),循环结构(例如 **while**)
34 |
35 | 可能你已在学校、书籍或网络上了学过这些。只要你觉得对上述的编程基本概念熟悉,你就可以完成本书的学习。
36 |
37 | 你可以通过在 On Java 8 的网站上免费下载 《Think in C》来补充学习 Java 所需要的前置知识。本书介绍了 Java 语言的基本控制机制以及面对对象编程(OOP)的概念。在本书中我引述了一些 C/C++ 语言中的一些特性来帮助读者更好的理解 Java。毕竟 Java 是在它们的基础之上发明的,理解他们之间的区别,有助于读者更好地学习 Java。我会试图简化这些引述,尽量让没有 C/C++ 基础的读者也能很好地理解。
38 |
39 | ## JDK 文档
40 |
41 | 甲骨文公司已经提供了免费的标准 JDK 文档。除非有必要,否则本书中将不再赘述 API 相关的使用细节。使用浏览器来即时搜索最新最全的 JDK 文档好过翻阅本书来查找。只有在需要补充特定的示例时,我才会提供有关的额外描述。
42 |
43 | ## C 编程思想
44 |
45 | _Thinking in C_ 已经可以在 [www.OnJava8.com](https://archive.org/details/ThinkingInC) 免费下载。Java 的基础语法是基于 C 语言的。_Thinking in C_ 中有更适合初学者的编程基础介绍。 我已经委托 Chuck Allison 将这本 C 基础的书籍作为独立产品附赠于本书的 CD 中。希望大家在阅读本书时,都已具备了学习 Java 的良好基础。
46 |
47 | ## 源码下载
48 |
49 | 本书中所有源代码的示例都在版权保护的前提下通过 GitHub 免费提供。你可以将这些代码用于教育。任何人不得在未经正确引用代码来源的情况下随意重新发布此代码示例。在每个代码文件中,你都可以找到以下版权声明文件作为参考:
50 |
51 | **Copyright.txt**
52 |
53 | ©2017 MindView LLC。版权所有。如果上述版权声明,本段和以下内容,特此授予免费使用,复制,修改和分发此计算机源代码(源代码)及其文档的许可,且无需出于下述目的的书面协议所有副本中都有五个编号的段落。
54 |
55 | 1. 允许编译源代码并将编译代码仅以可执行格式包含在个人和商业软件程序中。
56 |
57 | 2. 允许在课堂情况下使用源代码而不修改源代码,包括在演示材料中,前提是 “On Java 8” 一书被引用为原点。
58 |
59 | 3. 可以通过以下方式获得将源代码合并到印刷媒体中的许可:MindView LLC,PO Box 969,Crested Butte,CO 81224 MindViewInc@gmail.com
60 |
61 | 4. 源代码和文档的版权归 MindView LLC 所有。提供的源代码没有任何明示或暗示的担保,包括任何适销性,适用于特定用途或不侵权的默示担保。MindView LLC 不保证任何包含源代码的程序的运行不会中断或没有错误。MindView LLC 不对任何目的的源代码或包含源代码的任何软件的适用性做出任何陈述。包含源代码的任何程序的质量和性能的全部风险来自源代码的用户。用户理解源代码是为研究和教学目的而开发的,建议不要仅仅因任何原因依赖源代码或任何包含源代码的程序。如果源代码或任何产生的软件证明有缺陷,则用户承担所有必要的维修,修理或更正的费用。
62 |
63 | 5. 在任何情况下,MINDVIEW LLC 或其出版商均不对任何一方根据任何法律理论对直接,间接,特殊,偶发或后果性损害承担任何责任,包括利润损失,业务中断,商业信息丢失或任何其他保险公司。由于 MINDVIEW LLC 或其出版商已被告知此类损害的可能性,因此使用本源代码及其文档或因无法使用任何结果程序而导致的个人受伤或者个人受伤。MINDVIEW LLC 特别声明不提供任何担保,包括但不限于对适销性和特定用途适用性的暗示担保。此处提供的源代码和文档基于“原样”基础,没有 MINDVIEW LLC 的任何随附服务,MINDVIEW LLC 没有义务提供维护,支持,更新,增强或修改。
64 |
65 | **请注意**,MindView LLC 仅提供以下唯一网址发布更新书中的代码示例,https://github.com/BruceEckel/OnJava8-examples 。你可在上述条款范围内将示例免费使用于项目和课堂中。
66 |
67 | 如果你在源代码中发现错误,请在下面的网址提交更正:https://github.com/BruceEckel/OnJava8-examples/issues
68 |
69 | ## 编码样式
70 |
71 | 本书中代码标识符(关键字,方法,变量和类名)以粗体,固定宽度代码字体显示。像 “\*class” 这种在代码中高频率出现的关键字可能让你觉得粗体有点乏味。(译者注:由于中英排版差异,中文翻译过程并未完全参照原作者的说明。具体排版格式请参考[此处](https://github.com/ruanyf/document-style-guide))其他显示为正常字体。本书文本格式尽可能遵循 Oracle 常见样式,并保证在大多数 Java 开发环境中被支持。书中我使用了自己喜欢的字体风格。Java 是一种自由的编程语言,你也可以使用 IDE(集成开发环境)工具(如 IntelliJ IDEA,Eclipse 或 NetBeans)将格式更改为适合你的格式。
72 |
73 | 本书代码文件使用自动化工具进行测试,并在最新版本的 Java 编译通过(除了那些特别标记的错误之外)。本书重点介绍并使用 Java 8 进行测试。如果你必须了解更早的语言版本,可以在 [www.OnJava8.com](http://www.OnJava8.com) 免费下载 《Thinking in Java》。
74 |
75 | ## BUG 提交
76 |
77 | 本书经过多重校订,但还是难免有所遗漏被新读者发现。如果你在正文或示例中发现任何错误的内容,请在[此处](https://github.com/BruceEckel/OnJava8-examples/issues)提交错误以及建议更正,作者感激不尽。
78 |
79 | ## 邮箱订阅
80 |
81 | 你可以在 [www.OnJava8.com 上](http://www.OnJava8.com) 订阅邮件。邮件不含广告并尽量提供干货。
82 |
83 | ## Java 图形界面
84 |
85 | Java 在图形用户界面和桌面程序方面的发展可以说是一段悲伤的历史。Java 1.0 中图形用户界面(GUI)库的原始设计目标是让用户能在所有平台提供一个漂亮的界面。但遗憾的是,这个理想没有实现。相反,Java 1.0 AWT(抽象窗口工具包)在所有平台都表现平平,并且有诸多限制。你只能使用四种字体。另外,Java 1.0 AWT 编程模型也很笨拙且非面向对象。我的一个曾在 Java 设计期间工作过的学生道出了缘由:早期的 AWT 设计是在仅仅在一个月内构思、设计和实施的。不得不说这是一个“奇迹”,但同时更是“设计失败”的绝佳教材。
86 |
87 | 在 Java 1.1 版本的 AWT 中 情况有所改善,事件模型带来更加清晰的面向对象方法,并添加了 JavaBeans,致力于面向易于创建可视化编程环境的组件编程模型(已废弃)。
88 |
89 | Java 2(Java 1.2)通过使用 Java 基类(JFC)内容替换来完成从旧版 Java 1.0 AWT 的转换。其中 GUI 部分称为 Swing。这是一组丰富的 JavaBeans,它们创建了一个合理的 GUI。修订版 3(3 之前都不好)比以往更适用于开发图形界面程序。
90 |
91 | Sun 在图形界面的最后一次尝试,称为 JavaFX。当 Oracle 收购 Sun 时,他们将原来雄心勃勃的项目(包括脚本语言)改为库,现在它似乎是 Java 官方唯一还在开发中的 UI 工具包(参见维基百科关于 JavaFX 的文章) - 但即使如此,JavaFX 最终似乎也失败了。
92 |
93 | 现今 Swing 依然是 Java 发行版的一部分(只接受维护,不再有新功能开发)。而 Java 现在是一个开源项目,它应该始终可用。此外,Swing 和 JavaFX 有一些有限的交互性。这些可能是为了帮助开发者过渡到 JavaFX。
94 |
95 | 桌面程序领域似乎从未尝勾起 Java 设计师的野心。Java 没有在图形界面取得该有的一席之地。另外,曾被大肆吹嘘的 JavaBeans 也没有获得任何影响力。(许多不幸的作者花了很多精力在 Swing 上编写书籍,甚至只用 JavaBeans 编写书籍)。Java 图形界面程序大多数情况下仅用于 IDE(集成开发环境)和一些企业内部应用程序。你可以采用 Java 开发图形界面,但这并非 Java 最擅长的领域。如果你必须学习 Swing,可以参考 _Thinking in Java_ 第 4 版(可从 www.OnJava8.com 获得)或者通过其他专门的书籍中学习。。
96 |
--------------------------------------------------------------------------------
/docs/preface.md:
--------------------------------------------------------------------------------
1 | # 前言
2 |
3 | > 本书基于 Java 8 版本来教授当前 Java 编程的最优实践。
4 |
5 | 此前,我的另一本 Java 书籍 _Thinking in Java, 4th Edition_(《Java 编程思想》 第 4 版 Prentice Hall 2006)依然适用于 Java 5 编程。Android 编程就是始于此语言版本。
6 |
7 | 随着 Java 8 的出现,这门语言在许多地方发生了翻天覆地的变化。在新的版本中,代码的运用和实现上与以往不尽相同。这也促使了我时隔多年后再次创作了这本新书。《On Java 8》旨在面向已具有编程基础的开发者们。对于初学者,可以先在 [Code.org](http://Code.org) 或者 [Khan Academy](https://www.khanacademy.org/computing/computer-programming) 等网站上补充必要的前置知识。同时,[OnJava8.com](http://www.OnJava8.com) 上也有免费的 Thinking in C(《C 编程思想》)专题知识。
8 |
9 | 与几年前我们依赖印刷媒体相比,YouTube,博客和 StackOverflow 等网站的出现让寻找答案变得简单。请结合这些学习途径和努力坚持下去。本书可作为编程入门书籍,同时也适用于想要扩展知识的在职程序员。每次在世界各地的演讲中,我都非常感谢 《_Thinking in Java_》 这本书给我带来的所有荣誉。它对于我重塑 [Reinventing Business](http://www.reinventing-business.com) 项目和促进交流是非常宝贵的。最后,写这本书的原因之一 希望这本书可以为我的这个项目众筹。似乎下一步要创建一个所谓的蓝绿色组织(Teal Organization)才合乎逻辑的。
10 |
11 | ## 教学目标
12 |
13 | 每章教授一个或一组相关的概念,并且这些知识不依赖于尚未学习到的章节。如此,学习者可以在当前知识的背景框架下循序渐进地掌握 Java。
14 |
15 | 本书的教学目标:
16 |
17 | 1. 循序渐进地呈现学习内容,以便于你在不依赖后置知识框架的情况下轻松完成现有的学习任务,同时尽量保证前面章节的内容在后面的学习中得到运用。如果确有必要引入我们还没学习到的知识概念,我会做个简短地介绍。
18 |
19 | 2. 尽可能地使用简单和简短的示例,方便读者理解。而不强求引入解决实际问题的例子。因为我发现,相比解决某个实际问题,读者更乐于看到自己真正理解了示例的每个细节。或许我会因为这些“玩具示例”而被一些人所诟病,但我更愿意看到我的读者们因此能保持饶有兴趣地学习。
20 |
21 | 3. 把我知道以及我认为对于你学习语言很重要的东西都告诉你。我认为信息的重要性是分层次结构的。绝大多数情况下,我们没必要弄清问题的所有本质。好比编程语言中的某些特性和实现细节,95% 的程序员都不需要去知道。这些细节除了会加重你的学习成本,还让你更觉得这门语言好复杂。如果你非要考虑这些细节,那么它还会迷惑该代码的阅读者/维护者,所以我主张选择简单的方法解决问题。
22 |
23 | 4. 希望本书能为你打下坚实的基础,方便你将来学习更难的课程和书籍。
24 |
25 | ## 语言设计错误
26 |
27 | 每种语言都有设计错误。当新手程序员涉足语言特性并猜测应用场景和使用方式时,他们体验到极大的不确定性和挫折感。承认错误令人尴尬,但这种糟糕的初学者经历比认识到你错了什么还要糟糕。哎,每一种语言/库的设计错误都会永久地嵌入在 Java 的发行版中。
28 |
29 | 诺贝尔经济学奖得主约瑟夫·斯蒂格利茨(_Joseph Stiglitz_)有一套适用于这里的人生哲学,叫做“承诺升级理论”:继续犯错误的成本由别人承担,而承认错误的成本由自己承担。
30 |
31 | 看过我此前作品的读者们应该清楚,我一般倾向于指出这些错误。Java 拥有一批狂热的粉丝。他们把语言当成是阵营而不是纯粹的编程工具。我写过 Java 书籍,所以他们兀自认为我自然也是这个“阵营”的一份子。当我指出 Java 的这些错误时,会造成两种影响:
32 |
33 | 1. 早先许多错误“阵营”的人成为了牺牲品。最终,时隔多年后,大家都意识到这是个设计上的错误。然而错误已然成为 Java 历史的一部分了。
34 |
35 | 2. 更重要的是,新手程序员并没有经历过“语言为何采用某种方式实现”的争议过程。特别是那些隐约察觉不对却依然说服自己“我必须要这么做”或“我只是没学明白”从而继续错下去的人。更糟糕的是,教授这些编程知识的老师们没能深入地去研究这里是否有设计上的错误,而是继续错误的解读。总之,通过了解语言设计上的错误,能让开发者们更好地理解和意识到错误的本质,从而更快地进步。
36 |
37 | 对编程语言的设计错误理解至关重要,甚至影响程序员的开发效率。部分公司在开发过程中避免使用语言的某些功能特性。这些功能特性表面上看起来高大上,但是弄不好却可能出现意料之外的错误,影响整个开发进程。
38 |
39 | 已知的语言设计错误会给新的一门编程语言的作者提供参考。探索一门语言能做什么是很有趣的一件事,而语言设计错误能提醒你哪些“坑”是不能再趟的。多年以来,我一直感觉 Java 的设计者们有点脱离群众。Java 的有些设计错误错的太明显,我甚至怀疑设计者们到底是为出于服务用户还是其他动机设计了这些功能。Java 语言有许多臭名昭著的设计错误,很可能这也是诱惑所在。Java 似乎并不尊重开发者。为此我很长时间内不想与 Java 有任何瓜葛。很大程度上,这也是我不想碰 Java 的原因吧。
40 |
41 | 如今再审视 Java 8,我发现了许多变化。设计者们对于语言和用户的态度似乎发生了根本性上的改变。忽视用户投诉多年之后,Java 的许多功能和类库都已被搞砸了。
42 |
43 | 新功能的设计与以往有很大不同。掌舵者开始重视程序员的编程经验。新功能的开发都是在努力使语言变得更好,而非仅仅停留在快速堆砌功能而不去深入研究它们的含义。甚至有些新特性的实现方式非常优雅(至少在 Java 约束下尽可能优雅)。
44 |
45 | 我猜测可能是部分设计者的离开让他们意识到了这点。说真的,我没想到会有这些变化!因为这些原因,写这本书的体验要比以往好很多。Java 8 包含了一系列基础和重要的改进。遗憾的是,为了严格地“向后兼容”,我们不大可能看到戏剧性的变化,当然我希望我是错的。尽管如此,我很赞赏那些敢于自我颠覆,并为 Java 设定更好路线的人。第一次,对于自己所写的部分 Java 8 代码我终于可以说“赞!”
46 |
47 | 最后,本书所著时间似乎也还不错,因为 Java 8 引入的新功能已经强烈地影响了今后 Java 的编码方式。截止我在写这本书时,Java 9 似乎更专注于对语言底层的基础结构功能的重要更新,而非本书所关注的新编码方式。话说回来,得益于电子书出版形式的便捷,假如我发现本书有需要更新或添加的内容,我可以第一时间将新版本推送给现有读者。
48 |
49 | ## 测试用例
50 |
51 | 书中代码示例基于 Java 8 和 Gradle 编译构建,并且代码示例都保存在[这个自由访问的 GitHub 的仓库](https://github.com/BruceEckel/OnJava8-Examples) 中。我们需要内置的测试框架,以便于在每次构建系统时自动运行。否则,你将无法保证自己代码的可靠性。为了实现这一点,我创建了一个测试系统来显示和验证大多数示例的输出结果。这些输出结果我会附加在示例结尾的代码块中。有时仅显示必要的那几行或者首尾行。利用这种方式来改善读者的阅读和学习体验,同时也提供了一种验证示例正确性的方法。
52 |
53 | ## 普及性
54 |
55 | Java 的普及性对于其受欢迎程度有重要意义。学习 Java 会让你更容易找到工作。相关的培训材料,课程和其他可用的学习资源也很多。对于企业来说,招聘 Java 程序员相对容易。如果你不喜欢 Java 语言,那么最好不要拿他当作你谋生的工具,因为这种生活体验并不好。作为一家公司,在技术选型前一定不要单单只考虑 Java 程序员好招。每种语言都有其适用的范围,有可能你们的业务更适用于另一种编程语言来达到事半功倍的效果。如果你真的喜欢 Java,那么欢迎你。希望这本书能丰富你的编程经验!
56 |
57 | ## 关于安卓
58 |
59 | 本书基于 Java 8 版本。如果你是 Andriod 程序员,请务必学习 Java 5。在《On Java 8》出版的时候,我的另一本基于 Java 5 的著作 _Thinking in Java 4th Edition_(《Java 编程思想》第四版)已经可以在[www.OnJava8.com](http://www.OnJava8.com)上免费下载了。此外,还有许多其他专用于 Andriod 编程的资源。
60 |
61 | ## 电子版权声明
62 |
63 | 《On Java 8》仅提供电子版,并且仅通过 [www.OnJava8.com](http://www.OnJava8.com) 提供。任何未经 授权的其他来源或流传送机构都是非法的。本作品受版权保护!未经许可,请勿通过以任何方式分享或发布。你可以使用这些示例进行教学,只要不对本书非法重新出版。有关完整详细信息,请参阅示例分发中的 Copyright.txt 文件。对于视觉障碍者,电子版本有可搜索性,字体大小调整或文本到语音等诸多好处。
64 |
65 | 任何购买这本书的读者,还需要一台计算机来运行和写作代码。另外电子版在计算机上和移动设备上的显示效果俱佳,推荐使用平板设备阅读。相比购买传统纸质版的价格,平板电脑价格都足够便宜。在床上阅读电子版比看这样一本厚厚的实体书要方便得多。起初你可能会有些不习惯,但我相信很快你就会发现它带来的优点远胜过不适。我已经走过这个阶段,Google Play 图书的浏览器阅读体验非常好,包括在 Linux 和 iOS 设备上。作为一次尝试,我决定尝试通过 Google 图书进行出版。
66 |
67 | **注意**:在撰写本文时,通过 Google Play 图书网络浏览器应用阅读图书虽然可以忍受,但体验还是有点差强人意,我强烈推荐读者们使用平板电脑来阅读。
68 |
69 | ## 版本说明
70 |
71 | 本书采用 [Pandoc](http://pandoc.org) 风格的 Markdown 编写,使用 Pandoc 生成 ePub v3 格式。
72 |
73 | 正文字体为 Georgia,标题字体为 Verdana。 代码字体使用的 Ubuntu Mono,因为它特别紧凑,单行能容纳更多的代码。 我选择将代码内联(而不是将列表放入图像,参照其他书籍),因为我觉得这个功能很重要:让代码块能适应字体大小得改变而改变(否则,买电子版,还图什么呢?)。
74 |
75 | 书中的提取,编译和测试代码示例的构建过程都是自动化的。所有自动化操作都是通过我在 Python 3 中编写的程序来实现的。
76 |
77 | ## 封面设计
78 |
79 | 《On Java 8》的封面是根据 W.P.A.(Works Progress Administration 1935 年至 1943 年美国大萧条期间的一个巨大项目,它使数百万失业人员重新就业)的马赛克创作的。它还让我想起了《绿野仙踪》(_The Wizard of Oz_)系列丛书中的插图。 我的好朋友、设计师丹 _Daniel Will-Harris_([www.will-harris.com](http://www.will-harris.com))和我都喜欢这个形象。
80 |
81 | ## 感谢的人
82 |
83 | 感谢 _Domain-Driven Design_(《领域驱动设计》 )的作者 _Eric Evans_ 建议书名,以及其他新闻组校对的帮助。
84 |
85 | 感谢 _James Ward_ 为我开始使用 Gradle 工具构建这本书,以及他多年来的帮助和友谊。
86 |
87 | 感谢 _Ben Muschko_ 在整理构建文件方面的工作,还有感谢 _Hans Dockter_ 给 _Ben_ 提供了时间。
88 |
89 | 感谢 _Jeremy Cerise_ 和 _Bill Frasure_ 来到开发商务虚会预订,并随后提供了宝贵的帮助。
90 |
91 | 感谢所有花时间和精力来科罗拉多州克雷斯特德比特(Crested Butte, Colorado)镇参加我的研讨会,开发商务聚会和其他活动的人!你们的贡献可能不容易看到,但却非常重要!
92 |
93 | ## 献礼
94 |
95 | > 谨以此书献给我敬爱的父亲 E. Wayne Eckel。
96 | > 1924 年 4 月 1 日至 2016 年 11 月 23 日
97 |
--------------------------------------------------------------------------------
/docs/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 |
--------------------------------------------------------------------------------
/gitee-deploy.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | # abort on errors
4 | set -e
5 |
6 | # build
7 | yarn docs:build
8 |
9 | # navigate into the build output directory
10 | cd docs/.vuepress/dist
11 |
12 | # if you are deploying to a custom domain
13 | echo 'http://gdut_yy.gitee.io/doc-onjava8/' > CNAME
14 |
15 | git init
16 | git add -A
17 | git commit -m 'deploy'
18 |
19 | # if you are deploying to https://.github.io
20 | git push -f git@gitee.com:gdut_yy/doc-onjava8.git master
21 |
22 | # if you are deploying to https://.github.io/
23 | # git push -f git@github.com:/.git master:gh-pages
24 |
25 | cd -
--------------------------------------------------------------------------------
/images/1545758268350.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/images/1545758268350.png
--------------------------------------------------------------------------------
/images/1545763399825.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/images/1545763399825.png
--------------------------------------------------------------------------------
/images/1545764724202.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/images/1545764724202.png
--------------------------------------------------------------------------------
/images/1545764780795.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/images/1545764780795.png
--------------------------------------------------------------------------------
/images/1545764820176.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/images/1545764820176.png
--------------------------------------------------------------------------------
/images/1545839316314.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/images/1545839316314.png
--------------------------------------------------------------------------------
/images/1545841270997.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/images/1545841270997.png
--------------------------------------------------------------------------------
/images/1554546258113.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/images/1554546258113.png
--------------------------------------------------------------------------------
/images/1554546378822.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/images/1554546378822.png
--------------------------------------------------------------------------------
/images/1554546452861.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/images/1554546452861.png
--------------------------------------------------------------------------------
/images/1554546627710.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/images/1554546627710.png
--------------------------------------------------------------------------------
/images/1554546666685.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/images/1554546666685.png
--------------------------------------------------------------------------------
/images/1554546693664.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/images/1554546693664.png
--------------------------------------------------------------------------------
/images/1554546847181.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/images/1554546847181.png
--------------------------------------------------------------------------------
/images/1554546861836.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/images/1554546861836.png
--------------------------------------------------------------------------------
/images/1554546881189.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/images/1554546881189.png
--------------------------------------------------------------------------------
/images/1554546890132.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/images/1554546890132.png
--------------------------------------------------------------------------------
/images/1561774164644.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/images/1561774164644.png
--------------------------------------------------------------------------------
/images/1562204648023.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/images/1562204648023.png
--------------------------------------------------------------------------------
/images/1562252767216.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/images/1562252767216.png
--------------------------------------------------------------------------------
/images/1562406479787.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/images/1562406479787.png
--------------------------------------------------------------------------------
/images/1562409366637.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/images/1562409366637.png
--------------------------------------------------------------------------------
/images/1562409926765.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/images/1562409926765.png
--------------------------------------------------------------------------------
/images/1562653648586.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/images/1562653648586.png
--------------------------------------------------------------------------------
/images/1562737974623.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/images/1562737974623.png
--------------------------------------------------------------------------------
/images/1562999314238.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/images/1562999314238.png
--------------------------------------------------------------------------------
/images/QQGroupQRCode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/images/QQGroupQRCode.png
--------------------------------------------------------------------------------
/images/collection.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/images/collection.png
--------------------------------------------------------------------------------
/images/cover.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/images/cover.jpg
--------------------------------------------------------------------------------
/images/cover_small.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/images/cover_small.jpg
--------------------------------------------------------------------------------
/images/designproxy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/images/designproxy.png
--------------------------------------------------------------------------------
/images/image-20190409114913825-4781754.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/images/image-20190409114913825-4781754.png
--------------------------------------------------------------------------------
/images/level_1_title.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/images/level_1_title.png
--------------------------------------------------------------------------------
/images/level_2_title.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/images/level_2_title.png
--------------------------------------------------------------------------------
/images/map.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/images/map.png
--------------------------------------------------------------------------------
/images/qqgroup.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/images/qqgroup.png
--------------------------------------------------------------------------------
/images/reader.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/images/reader.png
--------------------------------------------------------------------------------
/images/simple-collection-taxonomy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Syasuker/OnJava8-zh/a7b675715d9b16be59c1b71a38282d4927c509b7/images/simple-collection-taxonomy.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "docs:dev": "vuepress dev docs",
4 | "docs:build": "vuepress build docs"
5 | },
6 | "dependencies": {
7 | "axios": "^0.19.0"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------