mirror of
https://github.com/LingCoder/OnJava8.git
synced 2026-09-03 05:12:56 +08:00
更新docsify阅读方式
This commit is contained in:
112
docs/book/00-Introduction.md
Normal file
112
docs/book/00-Introduction.md
Normal file
@@ -0,0 +1,112 @@
|
||||
|
||||
# 简介
|
||||
|
||||
> “我的语言极限,即是我的世界的极限。” ——路德维希·维特根斯坦(*Wittgenstein*)
|
||||
|
||||
这句话无论对于自然语言还是编程语言来说都是一样的。你所使用的编程语言会将你的思维模式固化并逐渐远离其他语言,而且往往发生在不知不觉中。Java 作为一门傲娇的语言尤其如此。
|
||||
|
||||
Java是一门派生语言,早期语言设计者为了不想在项目中使用 C++ 而创造了这种看起来很像 C++,却比 C++ 有了改进的新语言(他们原始的项目到最后也没有实现)。Java最核心的变化就是加入了“虚拟机”和“垃圾回收机制”,这两个概念在本书之后的章节会有详细描述。 此外,Java 还在其他方面推动行业了发展,例如,现在绝大多数编程语言都包含文档注释语法和 HTML 文档生成的工具。
|
||||
|
||||
Java 最主要的概念之一“对象”来自 SmallTalk 语言。SmallTalk 语言坚定不移的认为“对象”(在下一章中描述)是编程的最基本单元,于是,万物皆对象。时间已经检验了这种信念,并发现它太过狂热,有些人甚至认为“对象”是完全错误的概念,应该被丢弃。就我个人而言,把一切都当成一个对象不仅是一项不必要的负担,而且还会招致许多设计朝着不好的方向发展。尽管如此,“对象”的概念依然有其闪光点。固执地要求所有东西都是一个对象(特别是一直到最底层级别)是一个设计错误,相反,完全逃避“对象”的概念似乎同样太过苛刻。
|
||||
|
||||
很多 Java 语言规划设计的功能并没有像承诺的那样完成。在本书中,我会尝试解释这些原因。力争让读者知道这些功能,并知道为什么这些功能并不适用。这无关 Java 是一种好语言或者坏语言,一旦你了解了该语言的缺陷和局限性,你就能够:
|
||||
|
||||
1. 遇到“被砍掉”的功能特性时不会疑惑。
|
||||
|
||||
2. 熟悉语言边界,以便更好地设计和编码。
|
||||
|
||||
编程的过程就是复杂性管理的过程:业务问题的复杂性,以及依赖的计算机的复杂性。由于这种复杂性,我们的大多数编程项目都失败了。
|
||||
|
||||
许多语言设计决策时都考虑到了复杂性,试图降低语言的复杂性,但在设计过程中遇到了一些更棘手的问题,最终导致语言设计不可避免地“碰壁”,复杂性增加。例如,C++ 必须向后兼容 C(允许 C 程序员轻松迁移),并且效率很高。这些目标非常实用,并且也是 C++ 在编程界取得了成功的原因之一,但同时也引入了额外的复杂性,导致某些用C++编写的项目开发失败。当然,你可以责怪程序员和管理人员手艺不精,但如果有一种编程语言可以帮助你在开发过程中发现错误,那岂不是更好?
|
||||
|
||||
虽然 Visual BASIC(VB)绑定在 BASIC 上,但 BASIC 实际上并不是一种可扩展的语言,在 VB 上堆积的所有扩展造成大量无法维护的语法。Perl 向后兼容 awk,sed,grep 以及其它要替换的 Unix 工具,因此它经常被指责生产了一堆“只写代码”(写代码的人自己都看不懂的代码)。另一方面,C ++,VB,Perl 和其他语言(如 SmallTalk)在设计时关注点放在了一些复杂问题的处理上,因此在解决某些特定类型问题方面非常成功。
|
||||
|
||||
通信革命使我们所有人更容易地相互沟通:无论是一对一沟通,还是团队里的互相沟通,甚至是整个地球不同地区。据说下一次革命是一种新全球思维的形成,这种全球思维源于足量的人以及足量相互连接。Java 可能会也可能不会成为这场革命的工具之一,但至少这种可能性让我觉得我现在正在做的传道授业是一件有意义的事情。
|
||||
|
||||
|
||||
|
||||
|
||||
## 前提条件
|
||||
|
||||
阅读本书需要读者对编程有基本的了解:
|
||||
|
||||
- 程序是一系列“陈述(语句、代码)”构成
|
||||
|
||||
- 子程序、方法、宏的概念
|
||||
|
||||
- 控制语句(例如 “if”),循环结构(例如“while”)
|
||||
|
||||
|
||||
你可能已经在学校、书籍或网络上了学过这些。只要你对上述的编程基本概念觉得熟悉,你就可以完成本书的学习。
|
||||
|
||||
你可以通过在 On Java 8的网站上免费下载 Think in C 来补充学习 Java 所需要的前置知识。 本书介绍了 Java 语言的基本控制机制以及面对对象编程(OOP)的概念。在本书中我引述了一些 C/C ++ 语言中的一些特性来帮助读者更好的理解 Java,毕竟 Java 是在它们的基础之上发明的,理解他们之间的区别,有助于读者更好的学习 Java。 我试图去简化这些引述,以便让没有C/C++基础的人更好的理解。
|
||||
|
||||
|
||||
## JDK文档
|
||||
|
||||
甲骨文公司已经提供了标准免费的 JDK 文档。除非必要,否则在本书中将不再赘述有关 API 使用的细节。使用浏览器来即时搜索最新最全的 JDK 文档要好过翻阅本书来查找。只有在需要补充特定的示例时,我才会提供有关的额外描述。
|
||||
|
||||
|
||||
## C编程思想
|
||||
|
||||
*Thinking in C* 已经可以在 www.OnJava8.com 免费下载。Java 的基础语法是基于 C 语言的。*Thinking in C* 中有更适合初学者的编程基础介绍。 我已经委托 Chuck Allison 将这本 C 基础的书籍作为独立产品附赠于本书的 CD 中。希望大家在阅读本书时,都已具备了学习 JAVA 的良好基础。
|
||||
|
||||
|
||||
## 源码下载
|
||||
|
||||
本书中所有源代码的示例都在版权保护的前提下通过 GITHUB 免费提供。你可以将这些代码用于教育。任何人不得在未经正确引用代码来源的情况下随意重新发布此代码示例。在每个代码文件中,你都可以找到以下版权声明文件作为参考:
|
||||
|
||||
**Copyright.txt**
|
||||
|
||||
©2017 MindView LLC。版权所有。如果上述版权声明,本段和以下内容,特此授予免费使用,复制,修改和分发此计算机源代码(源代码)及其文档的许可,且无需出于下述目的的书面协议所有副本中都有五个编号的段落。
|
||||
|
||||
1. 允许编译源代码并将编译代码仅以可执行格式包含在个人和商业软件程序中。
|
||||
|
||||
2. 允许在课堂情况下使用源代码而不修改源代码,包括在演示材料中,前提是“On Java 8”一书被引用为原点。
|
||||
|
||||
3. 可以通过以下方式获得将源代码合并到印刷媒体中的许可:MindView LLC,PO Box 969,Crested Butte,CO 81224 MindViewInc@gmail.com
|
||||
|
||||
4. 源代码和文档的版权归 MindView LLC 所有。提供的源代码没有任何明示或暗示的担保,包括任何适销性,适用于特定用途或不侵权的默示担保。MindView LLC 不保证任何包含源代码的程序的运行不会中断或没有错误。MindView LLC 不对任何目的的源代码或包含源代码的任何软件的适用性做出任何陈述。包含源代码的任何程序的质量和性能的全部风险来自源代码的用户。用户理解源代码是为研究和教学目的而开发的,建议不要仅仅因任何原因依赖源代码或任何包含源代码的程序。如果源代码或任何产生的软件证明有缺陷,则用户承担所有必要的维修,修理或更正的费用。
|
||||
|
||||
5. 在任何情况下,MINDVIEW LLC 或其出版商均不对任何一方根据任何法律理论对直接,间接,特殊,偶发或后果性损害承担任何责任,包括利润损失,业务中断,商业信息丢失或任何其他保险公司。由于 MINDVIEW LLC 或其出版商已被告知此类损害的可能性,因此使用本源代码及其文档或因无法使用任何结果程序而导致的个人受伤或者个人受伤。MINDVIEW LLC 特别声明不提供任何担保,包括但不限于对适销性和特定用途适用性的暗示担保。此处提供的源代码和文档基于“原样”基础,没有MINDVIEW LLC的任何随附服务,MINDVIEW LLC 没有义务提供维护,支持,更新,增强或修改。
|
||||
|
||||
|
||||
**请注意**,MindView LLC 仅提供以下唯一网址发布更新书中的代码示例,https://github.com/BruceEckel/OnJava8-examples 。你可在上述条款范围内将示例免费使用于项目和课堂中。
|
||||
|
||||
如果你在源代码中发现错误,请在下面的网址提交更正:https://github.com/BruceEckel/OnJava8-examples/issues
|
||||
|
||||
|
||||
## 编码样式
|
||||
|
||||
本书中代码标识符(关键字,方法,变量和类名)以粗体,固定宽度代码字体显示。像“**class**”这种在代码中高频率出现的关键字可能让你觉得粗体有点乏味。其他显示为正常字体。本书文本格式尽可能遵循 Oracle 常见样式,并保证在大多数 Java 开发环境中被支持。书中我使用了自己喜欢的字体风格。Java 是一种自由的编程语言,你也可以使用 IDE(集成开发环境)工具(如 IntelliJ IDEA,Eclipse 或 NetBeans)将格式更改为适合你的格式。
|
||||
|
||||
本书代码文件使用自动化工具进行测试,并在最新版本的 Java 编译通过(除了那些特别标记的错误之外)。本书重点介绍并使用 Java 8 进行测试。如果你必须了解更早的语言版本,可以在 [www.OnJava8.com](http://www.OnJava8.com) 免费下载 *Thinking in Java*。
|
||||
|
||||
|
||||
## BUG提交
|
||||
|
||||
本书经过多重校订,但还是难免有所遗漏被新读者发现。如果你在正文或示例中发现任何错误的内容,请在[此处](https://github.com/BruceEckel/OnJava8-examples/issues)提交错误以及建议更正,作者感激不尽。
|
||||
|
||||
|
||||
## 邮箱订阅
|
||||
|
||||
你可以在 [www.OnJava8.com上](http://www.OnJava8.com) 订阅邮件。邮件不含广告并尽量提供干货。
|
||||
|
||||
|
||||
## Java图形界面
|
||||
|
||||
Java 在图形用户界面和桌面程序方面的发展可以说是一段悲伤的历史。Java 1.0 中图形用户界面(GUI)库的原始设计目标是让用户能在所有平台提供一个漂亮的界面。但遗憾的是这个理想没有实现。相反,Java 1.0 抽象窗口工具包(AWT)在所有平台都表现平平,并且有诸多限制;你只能使用四种字体。另外, Java 1.0 AWT 编程模型也很笨拙且不面向对象。我的一个曾在 Java 设计期间工作过的学生道出了缘由:早期的 AWT 设计是在仅仅在一个月内构思,设计和实施的。不得不说这是一个“奇迹”,但同时更是“设计失败”的绝佳教材。
|
||||
|
||||
在 Java 1.1 版本的AWT中 情况有所改善,事件模型采用了更加清晰的面向对象方法,并添加了JavaBeans,致力于面向易于创建可视化编程环境的组件编程模型(已废弃)。
|
||||
|
||||
Java 2(Java 1.2)通过基本上用 Java 基类(JFC)替换所有内容来完成从旧 Java 1.0 AWT的转换,其中 GUI 部分称为 “Swing”。这是一组丰富的JavaBeans,它们创建了一个合理的GUI。修订版 3(3之前都不好)比以往更适用于开发图形界面程序。
|
||||
|
||||
Sun 在图形界面的最后一次尝试,称为 JavaFX。当 Oracle 收购 Sun 时,他们将原来雄心勃勃的项目(包括脚本语言)改为库,现在它似乎是 Java 官方唯一还在开发中的UI工具包(参见维基百科关于JavaFX 的文章) - 但即使如此。JavaFX 似乎最终后也失败了。
|
||||
|
||||
现今 Swing 依然是 Java 发行版的一部分(只接受维护,不再有新功能开发),而 Java 现在是一个开源项目,它应该始终可用。此外,Swing 和 JavaFX 有一些有限的交互性。这些可能是为了帮助开发者过渡到 JavaFX。
|
||||
|
||||
桌面程序领域似乎从未尝勾起 Java 设计师的野心。Java 没有在图形界面取得该有的一席之地。另外,曾被大肆吹嘘的 JavaBeans 也没有获得任何影响力。(许多不幸的作者花了很多精力在 Swing 上编写书籍,甚至只用 JavaBeans 编写书籍)。Java 图形界面程序大多数情况下仅用于 集成开发环境(IDE)和一些企业内部应用程序。你是可以使用 Java 开发图形界面,但这不是 Java 最擅长的领域。如果你必须学习 Swing,它可以在 *Thinking in Java* 第4版(可从 www.OnJava8.com 获得)和其他专门的书籍中学习。。
|
||||
|
||||
|
||||
<!-- 分页 -->
|
||||
<div style="page-break-after: always;"></div>
|
||||
63
docs/book/00-On-Java-8.md
Normal file
63
docs/book/00-On-Java-8.md
Normal file
@@ -0,0 +1,63 @@
|
||||
<div align="center">
|
||||
<img src="https://raw.githubusercontent.com/LingCoder/OnJava8/master/images/cover.jpg" alt="cover" width="100%"/>
|
||||
</div>
|
||||
|
||||
<div style="page-break-after: always;"></div>
|
||||
|
||||
<center style="165px 0px 0px">
|
||||
<p style="font-size: 6em;font-weight: bold;text-align: center"> On Java 8 </p>
|
||||
</center>
|
||||
|
||||
<center style="165px 0px 0px">
|
||||
<p style="font-size: 2em;font-weight: bold;text-align: center">Bruce Eckel</p>
|
||||
|
||||
</center>
|
||||
|
||||
<center> MindView LLC </center>
|
||||
|
||||
|
||||
<center>2017</center>
|
||||
|
||||
|
||||
<center>©MindView LLC 版权所有</center>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div style="page-break-after: always;"></div>
|
||||
|
||||
|
||||
|
||||
|
||||
# On Java 8
|
||||
|
||||
|
||||
|
||||
**版权©2017**
|
||||
|
||||
|
||||
**作者 Bruce Eckel, President, MindView LLC.**
|
||||
|
||||
|
||||
**版本号:7**
|
||||
|
||||
|
||||
**ISBN 978-0-9818725-2-0**
|
||||
|
||||
|
||||
**原书可在该网站购买 [www.OnJava8.com](http://www.OnJava8.com)**
|
||||
|
||||
|
||||
|
||||
|
||||
本书出版自美国,版权所有,翻版必究。未经授权不得非法存储在检索系统中,或以电子,机械,影印,录制任何形式传输等。制造商和销售商使用商标用来区分其产品标识。如果这些名称出现在这本书中,并且出版商知道商标要求,则这些名称已经用大写字母或所有大写字母打印。
|
||||
|
||||
Java 是甲骨文公司(Oracle. Inc.)的商标。Windows 95,Windows NT,Windows 2000,Windows XP,Windows 7,Windows 8和 Windows 10是微软公司(Microsoft Corporation)的商标。
|
||||
此处提及的所有其他产品名称和公司名称均为其各自所有者的财产。作者和出版商在编写本书时已经仔细校对过,但不作任何明示或暗示的保证,对错误或遗漏不承担任何责任。对于因使用此处包含的信息或程序而产生的偶然或间接损失,我们不承担任何责任。
|
||||
|
||||
这本书是以平板电脑和计算机为载体的电子书,非传统纸质版书籍。
|
||||
故所有布局和格式设计旨在优化您在各种电子书阅读平台和系统上的观看体验。
|
||||
封面由 Daniel Will-Harris 设计,[www.Will-Harris.com](http://www.Will-Harris.com)。
|
||||
|
||||
<div style="page-break-after: always;"></div>
|
||||
126
docs/book/00-Preface.md
Normal file
126
docs/book/00-Preface.md
Normal file
@@ -0,0 +1,126 @@
|
||||
|
||||
|
||||
# 前言
|
||||
|
||||
> 本书基于 Java 8版本来教授目前最惯用的Java编码形式。
|
||||
|
||||
在此之前,我的另一本 Java 书籍 Thinking in Java 第4版(《Java编程思想》 Prentice Hall 2006)对于 Java 5的编程依然有指导意义。Java 5是用于 Android 编程的语言版本。
|
||||
|
||||
随着 Java 8的出现,这门语言在许多地方发生了翻天覆地的变化。新的 Java 代码在使用和实现上与以往不尽相同。这也是为什么时隔两年后我创作了这本新书。《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编程思想》)专题知识。
|
||||
|
||||
与几年前我们依赖印刷媒体相比,像 YouTube,博客和 StackOverflow 这样的网站让寻找答案变得非常容易。请将这些与坚持不懈的努力相结合。你可以将本书作为你的编程入门书籍。同样她也适用于想要扩展知识的在职程序员。每次在世界各地的演讲中,我都非常感谢 Thinking in Java 这本书给我带来的所有荣誉。事实证明,这些荣誉对我现在的 [Reinventing Business](http://www.reinventing-business.com) 项目中和加强外界与公司的联系是非常宝贵的。最后,写这本书的原因之一是支持我 [Reinventing Business](http://www.reinventing-business.com) 重塑,似乎下一个合乎逻辑的步骤是实际创建一个所谓的蓝绿色组织(Teal Organization)。我希望这本书可以成为该项目的一种众筹。
|
||||
|
||||
|
||||
## 教学目标
|
||||
|
||||
每章教授一个或一组相关的概念,并且这些知识不依赖于尚未学习到的章节。这样以来,学习者可以在当前知识的背景框架下循序渐进地掌握 JAVA 。
|
||||
|
||||
本书的教学目标:
|
||||
|
||||
|
||||
1. 循序渐进地呈现学习内容,以便于你在不依赖后置知识框架的情况下轻松完成现有的学习任务,同时尽量保证前面章节的内容在后面的学习中得到运用。如果确有必要引入我们还没学习到的知识概念,我会做个简短地介绍。
|
||||
|
||||
2. 尽可能地使用简单和简短的示例,方便读者理解。而不强求引入解决实际问题的例子。因为我发现,相比解决某个实际问题,读者更乐于看到自己真正理解了示例的每个细节。或许我会因为这些“玩具示例”而被一些人所诟病,但我更愿意看到我的读者们因此能保持饶有兴趣地学习。
|
||||
|
||||
3. 把我知道的以及我认为对于你学习语言很重要的东西都告诉你。我认为信息的重要性是分层次结构的。绝大多数情况下,我们没必要弄清问题的所有本质。好比编程语言中的某些特性和实现细节,95%的程序员都不需要取知道。这些细节除了会加重你的学习成本,还让你更觉得这门语言好复杂。如果你非要考虑这些细节,那么它还会迷惑该代码的阅读者/维护者,所以我主张选择简单的方法解决问题。
|
||||
|
||||
4. 希望本书能为你打下坚实的基础,方便你将来学习更难的课程和书籍。
|
||||
|
||||
|
||||
|
||||
## 语言设计错误
|
||||
|
||||
每种语言都有设计错误。当新程序员必须涉足语言特性,并猜测应用场景和使用方式时,他们体验到极大的不确定性和挫折感。承认错误令人尴尬,但这种糟糕的初学者经历比认识到你错了什么还要糟糕。哎,每一种设语言/库的设计错误都会永久地嵌入在 Java 的发行版中。
|
||||
|
||||
诺贝尔经济学奖得主约瑟夫·斯蒂格利茨(Joseph Stiglitz)有一套适用于这里的人生哲学,叫做“承诺升级理论”:继续犯错误的成本由别人承担,而承认错误的成本由自己承担。
|
||||
|
||||
如果你有读过我过去的作品,那么你应该知道,我一般倾向于指出这些语言设计错误。Java 已经发展出了一批狂热的追随者,他们对待这门语言更像是阵营而不是编程工具。因为我已经写过有关 Java 的书,所以他们理所当然的认为我也是这个“阵营”的一份子。于是,当我指出Java的错误时,就会造成两种影响:
|
||||
|
||||
1. 最初,会有许多错误“阵营”的人成为了牺牲品。最终,时隔多年,大家都意识到这是个设计上的错误,然后错误就这样成为了 Java 历史的一部分。
|
||||
|
||||
2. 更重要的是,新程序员并没有经历过“他们”为什么要采用这种实现方式的斗争过程。特别是那些隐隐约约感觉不对却依然说服自己“我必须要这么做”或者“我只是没学明白”从而继续错下去的人。更糟糕的是,教授这些编程知识的老师们没能深入的去研究这里是否有设计上的错误,而是继续错误的解读。通过了解语言设计上的错误,能让开发者们更好的理解和意识到错误的本质,从而更快地进步。
|
||||
|
||||
理解编程语言的设计错误至关重要,甚至影响程序员的开发效率。一些公司在开发过程中会避免使用语言的某些功能特性。表面上看这些“功能特性”很高大上,但是弄不好却可能出现意料之外的错误,影响整个开发进程。
|
||||
|
||||
已知的语言设计错误也会给新的一门编程语言的发明提供参考意义。探索一门语言能做什么是很有趣的一件事,而语言设计错误能提醒你哪些“坑”是不能再趟的。多年以来,我一直感觉 Java 的设计者们有点脱离群众的。Java 有些设计错误不免错的太明显,我都怀疑语言设计师们到底是为了用户服务还是出于其他的动机设计了这些功能。Java 语言有许多臭名昭著的设计错误,很可能这也是诱惑所在。我感觉这是对程序员的不尊重。为此我很长时间不想于 Java 有任何瓜葛,很大程度上,这也是我为什么不想碰 Java的原因吧。
|
||||
|
||||
现今当我再来看 Java 8时,我发现与之前有许多不同的地方。Java 语言的设计师们似乎对于语言和用户的态度发生了根本性上的改变。在多年忽视用户投诉之后,许多功能和库已经被语言搞砸了。
|
||||
|
||||
新功能的设计与以往有很大不同。掌舵者开始重视程序员的编程经验。新功能最终都在努力使语言变得更好,而不仅仅是停留在快速添加想法而不深入研究它们的含义。有一些新功能实现上非常优雅(至少在 Java 约束下尽可能优雅)。
|
||||
|
||||
我猜测可能是一些人离开设计组让他们意识到了这点。我没想到会有这些变化!因为这些原因吧,写这本书的体验要比以往的经历要好得多。Java 8包含了一系列基础和重要的改进。哎,不过 Java有严格的“向后兼容”承诺。所以可能我们不大可能看到戏剧性的变化,当然我希望我是错的。尽管如此,我很赞赏那些敢于自我颠覆,并为 Java设定更好路线的人。第一次,对于自己所写的部分 Java 8代码我终于可以说“我喜欢这个!”
|
||||
|
||||
最后,本书所著时间似乎也很不错,因为 Java 8引入的新功能已经强烈的影响了今后Java的编码方式。截止我在写这本书时,Java 9似乎更专注于对语言底层的基础结构功能的重要更新,但是这些并不会影响本书所关注的编码类型。话说回来,得益于电子书出版形式的便捷,如果我发现本书有需要更新或添加的内容,我可以很快将新版本推送给现有读者。
|
||||
|
||||
|
||||
|
||||
## 测试用例
|
||||
|
||||
本书中的代码示例基于 Java 8和 Gradle 编译构建,并且这些代码示例都保存在[这个自由访问的GitHub的仓库](https://github.com/BruceEckel/OnJava8-Examples) 中。我们需要内置的测试框架在每次构建系统时运行,否则你将无法保证自己代码的可靠性。为了在本书中实现这一点,我创建了一个测试系统来显示和验证大多数示例的输出结果。这个输出结果我会附加在示例结尾的代码块中。有时仅显示必要的那几行或者首尾行。利用这种方式来改善读者的阅读和学习体验,同时也提供了一种验证示例正确性的方法。
|
||||
|
||||
|
||||
|
||||
## 普及性
|
||||
|
||||
Java 的普及性对于其受欢迎程度有重要意义。学习 Java 会让你更容易找到工作。相关的培训材料,课程和其他可用的学习资源也很多。对于企业来说,招聘 Java 程序员也相对容易。如果你不喜欢 Java 语言,那么最好不要拿他当作你谋生的工具,因为这种生活体验并不好。作为一家公司,在技术选型前一定不要单单只考虑 Java 程序员好招。每种语言都有其适用的范围,有可能你们的业务更适用于另一种编程语言来达到事半功倍的效果。如果你真的喜欢Java,那么欢迎你。希望这本书能丰富你的编程经验!
|
||||
|
||||
|
||||
|
||||
## 关于安卓
|
||||
|
||||
这本书基于 Java 8版本。如果你是 Andriod 程序员,请务必学习 Java 5。在《On Java 8》出版的时候,我的另一本基于 Java 5的著作 Thinking in Java 4th Edition(《Java编程思想》第四版)已经可以在[www.OnJava8.com](http://www.OnJava8.com)上免费下载了。此外,还有许多其他专用于 Andriod 编程的资源。
|
||||
|
||||
|
||||
|
||||
|
||||
## 电子版权声明
|
||||
|
||||
《On Java 8》仅提供电子版,并且仅通过 [www.OnJava8.com](http://www.OnJava8.com) 提供。任何未经 <mindviewinc@gmail.com> 授权的其他来源或流传送机构都是非法的。本作品受版权保护!未经许可,请勿通过以任何方式分享或发布。您可以使用这些示例进行教学,只要不对本书非法重新出版。有关完整详细信息,请参阅示例分发中的 Copyright.txt 文件。对于视觉障碍者,电子版本有可搜索性,字体大小调整或文本到语音等诸多好处。
|
||||
|
||||
任何购买这本书的读者,还需要一台计算机来运行和写作代码。另外电子版在计算机上和移动设备上的显示效果俱佳,推荐使用平板设备阅读。相比购买传统纸质版的价格,平板电脑价格都足够便宜。在床上阅读电子版比看这样一本厚厚的实体书要方便得多。起初你可能会有些不习惯,但我相信很快你就会发现它带来的优点远胜过不适。我已经走过这个阶段,Google Play 图书的浏览器阅读体验非常好,包括在 Linux 和 iOS 设备上。作为一次尝试,我决定尝试通过 Google 图书进行出版。
|
||||
|
||||
**注意**:在撰写本文时,通过 Google Play 图书网络浏览器应用阅读图书虽然可以忍受,但体验还是有点差强人意,我强烈推荐读者们使用平板电脑来阅读。
|
||||
|
||||
|
||||
|
||||
|
||||
## 版本说明
|
||||
|
||||
本书采用 Pandoc 风格的 Markdown 编写,使用 Pandoc 生成 ePub v3格式。
|
||||
|
||||
正文字体为 Georgia,标题字体为 Verdana。 代码字体使用的 Ubuntu Mono,因为它特别紧凑,单行能容纳更多的代码。 我选择将代码内联(而不是将列表放入图像,因为我看过一些书籍),因为对我来说让读者能够在调整正文字体大小时,代码块也可自适应调整的功能非常重要(否则,买电子版,还图什么呢?)。
|
||||
|
||||
本书的提取,编译和测试代码示例的构建过程都是自动化的。所有自动化操作都是通过我在 Python 3中编写的程序来实现的。
|
||||
|
||||
|
||||
|
||||
|
||||
## 封面设计
|
||||
|
||||
《On Java 8》的封面通过 W.P.A.(Works Progress Administration 1935年至1943年美国大萧条期间的一个巨大项目,它使数百万失业人员重新就业)的马赛克创作的(WPA,这是)。它还让我想起了绿野仙踪(The Wizard of Oz)系列丛书中的插图。 我的好朋友、设计师丹尼尔威尔哈里斯([www.will-harris.com](http://www.will-harris.com))和我都喜欢这个形象。
|
||||
|
||||
|
||||
|
||||
|
||||
## 感谢的人
|
||||
|
||||
感谢 Domain-Driven Design(《领域驱动设计》 )的作者 Eric Evans 建议书名,以及其他新闻组校对帮助。
|
||||
|
||||
感谢 James Ward 为我开始使用 Gradle 工具构建这本书,以及他多年来的帮助和友谊。
|
||||
|
||||
感谢 Ben Muschko 构建文件的抛光工作,还有感谢 Hans Dockter 给 Ben 这个时间。
|
||||
|
||||
感谢 Jeremy Cerise 和 Bill Frasure 来到开发商务虚会预订,并随后提供了宝贵的帮助。
|
||||
|
||||
感谢所有花时间和精力来Crested Butte, Colorado(科罗拉多州克雷斯特德比特)镇参加我的研讨会,开发商务聚会和其他活动的人!你们的贡献可能没被轻易看到,但它非常重要!
|
||||
|
||||
|
||||
|
||||
|
||||
## 献礼
|
||||
|
||||
> 谨以此书献给我敬爱的父亲 E. Wayne Eckel。
|
||||
> 1924年4月1日至2016年11月23日
|
||||
|
||||
<!-- 分页 -->
|
||||
<div style="page-break-after: always;"></div>
|
||||
304
docs/book/01-What-is-an-Object.md
Normal file
304
docs/book/01-What-is-an-Object.md
Normal file
@@ -0,0 +1,304 @@
|
||||
|
||||
[TOC]
|
||||
|
||||
|
||||
# 第一章 对象的概念
|
||||
|
||||
|
||||
> “我们没有意识到惯用语言的结构有多大的力量。可以毫不夸张地说,它通过语义反应机制奴役我们,语言表现出来并在无意识中给我们留下深刻印象的结构会自动投射到我们周围的世界。” -- Alfred Korzybski (1930)
|
||||
|
||||
计算机革命的起源来自机器。编程语言就像是那台机器,它不仅是我们思维的放大工具和另一种表达媒介,更像我们思想的一部分。语言的灵感来自其他形式的表达,如写作,绘画,雕塑,动画和电影制作。编程语言就是创建应用程序的思想结构。
|
||||
|
||||
面向对象编程(*Object-oriented programming OOP*)是一种编程思维方式和编码架构。本章讲述 OOP 的基本概述。如果读者对不太理解,可先行跳过本章。等你对编程具有一定理解后,请务必再回过头来看。因为这样你才能理解面对对象编程的重要性以及如何使用它来设计你的程序。
|
||||
|
||||
## 抽象
|
||||
|
||||
所有编程语言都提供一种“抽象”的方法。从某种程度上来说,解决问题的复杂性与抽象的种类和质量直接相关。这里的“种类”意思是:你准备对什么进行抽象?汇编语言是对机器底层的一种少量抽象。后来的许多“命令式”语言(如 FORTRAN,BASIC 和 C)是对汇编语言的一种抽象。与汇编语言相比,这些语言已有了长足的进步,但它们的抽象原理依然要求我们着重考虑计算机的结构,而非问题本身的结构。
|
||||
|
||||
在机器模型(“解决方案空间”)与实际解决的问题模型(“问题空间”)之间,程序员必须建立起一种关联。这个过程要求人们付出较大的精力,而且由于它脱离了编程语言本身的范围,造成程序代码很难编写,而且要花较大的代价进行维护。由此造成的副作用便是一门完善的“编程方法”学科。
|
||||
|
||||
为机器建模的另一个方法是为要解决的问题制作模型。对一些早期语言来说,如 LISP 和 APL,它们的做法是“从不同的角度观察世界”——“所有问题都归纳为列表”或“所有问题都归纳为算法”。PROLOG 则将所有
|
||||
问题都归纳为决策链。对于这些语言,我们认为它们一部分是“基于约束”的编程,另一部分则是专为
|
||||
处理图形符号设计的(后者被证明限制性太强)。每种方法都有自己特殊的用途,适合解决某一类的问题。只要超出了它们力所能及的范围,就会显得非常笨拙。
|
||||
|
||||
面向对象的程序设计在此基础上跨出了一大步,程序员可利用一些工具表达“问题空间”内的元素。由于这种
|
||||
表达非常具有普遍性,所以不必受限于特定类型的问题。我们将问题空间中的元素以及它们在解决方案空间的表示物称作“对象”(**Object**)。当然,还有一些在问题空间没有对应的对象体。通过添加新的对象类型,程序可进行灵活的调整,以便与特定的问题配合。所以在阅读方案的描述代码时,会读到对问题进行表达的话语。与我们以前见过的相比,这无疑是一种更加灵活、更加强大的语言抽象方法。总之,OOP 允许我们根据问题来描述问题,而不是根据方案。然而,仍有一个联系途径回到计算机。每个对象都类似一台小计算机;它们有自己的状态,而且可要求它们进行特定的操作。与现实世界的“对象”或者“物体”相比,编程“对象”与它们也存在共通的地方:它们都有自己的特征和行为。
|
||||
|
||||
Smalltalk 作为第一种成功的面向对象程序设计语言和 Java 的基础语言,*Alan Kay* 总结了其五大基本特征。通过这些特征,我们可理解“纯粹”的面向对象程序设计方法是什么样的:
|
||||
|
||||
> 1. **万物皆对象**。你可以将对象想象成一种特殊的变量。它可以存储数据,可以在你对其“发出请求”时执行本身的操作。理论上讲,你可以从要解决的问题身上抽象出概念性的组件,然后在程序中将其表达为一个对象。
|
||||
> 2. **程序是一组对象,通过信息传递来告知彼此该做什么**。要请求一个对象,你需要向该对象发送信息。
|
||||
> 3. **每个对象都有自己的存储空间,可容纳其他对象**。或者说,通过封装现有对象,可制作出新型对象。所以,尽管对象的概念非常简单,但在程序中却可达到任意高的复杂程度。
|
||||
> 4. **每个对象都有一种类型**。根据语法,每个对象都是某个“类”的一个“实例”。其中,“类”(Class)是“类型”(Type)的同义词。一个类最重要的特征就是“能将什么信息发给它?”。
|
||||
> 5. **同一类所有对象都能接收相同的信息**。这实际是别有含义的一种说法,大家不久便能理解。由于类型为“圆”(Circle)的一个对象也属于类型为“形状”(Shape)的一个对象,所以一个圆完全能接收形状信息。这意味着可让程序代码统一指挥“形状”,令其自动控制所有符合“形状”描述的对象,其中自然包括“圆”。这一特性称为对象的“可替换性”,是OOP最重要的概念之一。
|
||||
|
||||
Grady Booch 提供了对对象更简洁的描述:一个对象具有自己的状态,行为和身份。这意味着对象有自己的内部数据(由状态提供)、方法 (由特性提供),并彼此区分(每个对象在内存中都有唯一的地址)。
|
||||
|
||||
|
||||
## 接口
|
||||
|
||||
亚里士多德(*Aristotle*)或许是认真研究“类型”概念的第一人,他曾谈及“鱼类和鸟类”的问题。是世界首例面向对象语言 Simula-67 中,第一次用到了这样的一个概念:
|
||||
所有对象——尽管各有特色——都属于某一系列对象的一部分,这些对象具有通用的特征和行为。在Simula-67 中,首次用到了 `class` 这个关键字,它为程序引入了一个全新的类型(`class` 和 type 通常可互换使用,有些人进行了进一步的区分,他们强调“类型”决定了接口,而“类”是那个接口的一种特殊实现方式)。
|
||||
|
||||
Simula 是一个很好的例子。正如这个名字所暗示的,它的作用是“模拟”(*Simulate*)象“银行出纳员”这
|
||||
样的经典问题。在这个例子里,我们有一系列出纳员、客户、帐号以及交易等。每类成员(元素)都具有一
|
||||
些通用的特征:每个帐号都有一定的余额;每名出纳都能接收客户的存款;等等。与此同时,每个成员都有
|
||||
自己的状态;每个帐号都有不同的余额;每名出纳都有一个名字。所以在计算机程序中,能用独一无二的实
|
||||
体分别表示出纳员、客户、帐号以及交易。这个实体便是“对象”,而且每个对象都隶属一个特定的“类”,那个类具有自己的通用特征与行为。
|
||||
|
||||
因此,在面向对象的程序设计中,尽管我们真正要做的是新建各种各样的数据“类型”(*Type*),但几乎所
|
||||
有面向对象的程序设计语言都采用了 `class` 关键字。当您看到“type”这个字的时候,请同时想到 `class`;反之亦然。
|
||||
|
||||
创建好一个类后,可根据情况生成许多对象。随后,可将那些对象作为要解决问题中存在的元素进行处理。事实上,当我们进行面向对象的程序设计时,面临的最大一项挑战性就是:如何在“问题空间”(问题实际存
|
||||
在的地方)的元素与“方案空间”(对实际问题进行建模的地方,如计算机)的元素之间建立理想的“一对
|
||||
一”的映射关系。
|
||||
|
||||
那么如何利用对象完成真正有用的工作呢?必须有一种办法能向对象发出请求,令其解决一些实际的问题,比如完成一次交易、在屏幕上画一些东西或者打开一个开关等等。每个对象仅能接受特定的请求。我们向对象发出的请求是通过它的“接口”(Interface)定义的,对象的“类型”或“类”则规定了它的接口形式。“类
|
||||
型”与“接口”的对应关系是面向对象程序设计的基础。
|
||||
|
||||
下面让我们以电灯泡为例:
|
||||
|
||||

|
||||
|
||||
```java
|
||||
Light lt = new Light();
|
||||
lt.on();
|
||||
```
|
||||
|
||||
在这个例子中,类型/类的名称是 **Light**,可向 **Light** 对象发出的请求包括包括打开(on)、关闭(*off*)、变得更明亮(*brighten*)或者变得更暗淡(*dim*)。通过简单地声明一个名字(lt),我们为 **Light** 对象创建了一个“句柄”。然后用new关键字新建类型为 **Light** 的一个对象。再用等号将其赋给句柄。
|
||||
|
||||
为了向对象发送一条信息,我们列出句柄名(*lt*),再用一个句点符号(`.`)把它同信息名称(*on*)连接起来。从中可以看出,使用一些预先定义好的类时,我们在程序里采用的代码是非常简单和直观的。
|
||||
|
||||
上图遵循 **UML**(*Unified Modeling Language*,统一建模语言)的格式。每个类由一个框表示,框的顶部有类型名称,框中间部分要描述的任何数据成员,以及方法(属于此对象的方法,它们接收任何发送到该对象的信息)在框的底部。通常,只有类的名称和公共方法在 **UML** 设计图中显示,因此中间部分未显示,如本例所示。如果您只对类名感兴趣,则也不需要显示方法信息。
|
||||
|
||||
|
||||
## 服务提供
|
||||
|
||||
在开发或理解程序设计时,我们可以将对象看成是“服务提供者”。你的程序本身将为用户提供服务,并且它能通过调用其他对象提供的服务来实现这一点。我们的最终目标是开发或调用工具库中已有的一些对象,提供理想的服务来解决问题。
|
||||
|
||||
那么问题来了:我们该选择哪个对像来解决问题呢? 例如,你正在开发一个记事本程序。你可能会想在屏幕输入一个默认的记事本对象,一个用于检测不同类型打印机并执行打印的对象。这些对象中的某些已经有了。那对于还没有的对象,我们该设计成啥样呢?这些对象需要提供哪些服务,以及还需要调用其他哪些对象?
|
||||
|
||||
我们可以将这些问题一一分解,抽象成一组服务。软件设计的基本原则是高内聚:每个组件的内部作用明确,功能紧密相关。然而经常有人将太多功能塞进一个对象中。例如:在支票打印模块中,你需要设计一个可以同时读取文本格式又能正确识别不同打印机型号的对象。正确的做法是提供三个或更多对象:一个对象检查所有排版布局的目录;一个或一组可以识别不同打印机型号的对象展示通用的打印界面;第三个对象组合上述两个服务来完成任务。这样,每个对象都提供了一组紧密的服务。在良好的面向对象设计中,每个对象功能单一且高效。这样的程序设计可以提高我们代码的复用性,同时也方便别人阅读和理解我们的代码。只有让人知道你提供什么服务,别人才能更好地将其应用到其他模块或程序中。
|
||||
|
||||
|
||||
## 封装
|
||||
|
||||
我们可以把编程的侧重领域划分为研发和应用。应用程序员调用研发程序员构建的基础工具类来做快速开发。研发程序员开发一个工具类,该工具类仅向应用程序员公开必要的内容,并隐藏内部实现的细节。这样可以有效地避免该工具类被错误的使用和更改,从而减少程序出错的可能。彼此职责划分清晰,相互协作。当应用程序员调用研发程序员开发的工具类时,双方建立了关系。应用程序员通过使用现成的工具类组装应用程序或者构建更大的工具库。如果工具类的创建者将类的内部所有信息都公开给调用者,那么有些使用规则就不容易被遵守。因为前者无法保证后者是否按照正确的规则来使用或是改变了该工具类。只有设定访问控制,才能从根本上阻止。
|
||||
|
||||
因此,使用访问控制的原因有以下2点:
|
||||
|
||||
1. 让应用程序员不要触摸他们不应该触摸的部分。(请注意,这也是一个哲学决策。部分编程语言认为如果程序员有需要,则应该让他们访问细节部分。);
|
||||
|
||||
2. 使类库的创建者(研发程序员)在不影响后者使用的情况下完善更新工具库。例如,我们开发了一个功能简单的工具类,后来发现可以通过优化代码来提高执行速度。假如工具类的接口和实现部分明确分开并受到保护,那我们就可以轻松的完成改造。
|
||||
|
||||
Java 有三个显式关键字来设置类中的访问权限:`public`(公开),`private`(私有)和`protected`(受保护)。这些修饰符可以明确谁能访问后面的方法、变量或类。
|
||||
|
||||
1. `public` (公开) 表示任何人都可以访问和使用该元素;
|
||||
|
||||
2. `private` (私有) 除了类本身,外界无法直接访问该元素和内部方法。`private` 是你和调用者之间的屏障。任何试图访问私有成员的人都会收到编译时错误;
|
||||
|
||||
3. `protected` (受保护) 类似于 `private`,区别是继承类(下一节就会引入继承的概念)可以访问 `protected` 的成员,但不能访问 `private` 成员;
|
||||
|
||||
4. `default` (默认) 如果你不使用前面的三者,默认就是 `default` 访问权限。`default` 被称之为包访问,因为该权限下的资源可以同一包(库组件)中其他类的成员访问。
|
||||
|
||||
|
||||
## 复用
|
||||
|
||||
一个类经创建和测试后,理应是可复用的。然而很多时候,由于程序员没有足够的编程经验和远见,我们的代码复用性并不强。
|
||||
|
||||
代码和设计方案的复用性是面向对象的程序设计的优点之一。我们可以通过重复使用某个 `class` 来达到这种复用性。同时,我们也可以将这个 `class` 作为另一个 `class` 的成员变量来使用。新的对象可以是由任意数量、类型的其他对象构成。这里涉及到“组合”和“聚合”的概念:
|
||||
|
||||
* **组合**(*Composition*) 经常用来表示“拥有”关系(*Has-a Relationship*)。例如,“汽车拥有了主机”。
|
||||
|
||||
* **聚合**(*Aggregation*) 动态的 **组合**。
|
||||
|
||||

|
||||
|
||||
上图中实心棱形指向“ **Car** ”表示 **组合** 的关系;如果是 **聚合** 关系,可以使用空心棱形。
|
||||
|
||||
(**译者注**:组合和聚合都属于关联关系的一种,只是额外具有整体-部分的意义。至于是聚合还是组合,需要根据实际的业务需求来判断。可能相同超类和子类,在不同的业务场景,关联关系会发生变化。只看代码是无法区分聚合和组合的,具体是哪一种关系,只能从语义级别来区分。聚合关系中,整件不会拥有部件的生命周期,所以整件删除时,部件不会被删除。再者,多个整件可以共享同一个部件。组合关系中,整件拥有部件的生命周期,所以整件删除时,部件一定会跟着删除。而且,多个整件不可以同时间共享同一个部件。这个区别可以用来区分某个关联关系到底是组合还是聚合。两个类生命周期不同步,则是聚合关系,生命周期同步就是组合关系。)
|
||||
|
||||
使用“组合”关系会为我们的程序带来极大的灵活性。通常新构建的 `class` “成员对象”会使用 `private` 访问权限,这样应用程序员则无法对其直接访问。我们就可以在不干扰客户代码的前提下,从容地修改那些成员。也可以在“运行期”更改成员,这进一步增大了灵活性。下面一节要讲到的“继承”并不具备这种灵活性,因为编译器必须对通过继承创建的类加以限制。
|
||||
|
||||
在面向对象编程中经常重点强调“继承”。在新程序员的印象里,或许早已先入为主地认为“继承应当随处可见”。沿着这种思路产生的程序设计通常拙劣又复杂。相反,在创建新类时首先要考虑“组合”,因为它更简单灵活,并且设计逻辑清晰。等我们有一些编程经验后,一旦需要用到继承,就会明显意识到这一点。
|
||||
|
||||
## 继承
|
||||
|
||||
“继承”给面向对象编程带来极大的便利。它在概念上允许我们将各式各样数据和功能封装到一起。这样便可恰当表达“问题空间”的概念,而不是强制使用底层机器的习惯用法。
|
||||
|
||||
通过使用 `class` 关键字,这些概念在编程语言中表示为基本单元。但若能利用现成的数据类型,对其进行“克隆”,再根据情况进行添加和修改,情况就显得理想多了。“继承”正是针对这个目标而设计的。但继承并不完全等价于克隆。在继承过程中,若原始类(正式名称叫作基础类、超类或父类)发生了变化,修改过的“克隆”类(正式名称叫作继承类或者子类)也会反映出这种变化。
|
||||
|
||||
如果我们可以采用现有的类,克隆它,然后对克隆进行添加和修改,那就更好了。这实际上是通过继承得到的,只是如果原始类(称为基类或超类或父类)发生了更改,修改的“克隆”(称为派生类或继承类或子类或子类)也反映了这些更改。
|
||||
|
||||

|
||||
|
||||
这个图中的箭头从派生类指向基类。正如您将看到的,通常有多个派生类。类型不仅仅描述一组对象的约束,它还涉及其他类型。两种类型可以具有共同的特征和行为,但是一种类型可能包含比另一种类型更多的特征,并且还可以处理更多的信息(或者以不同的方式处理它们)。继承通过基本类型和派生类型的概念来表达这种相似性。基类型包含派生自它的类型之间共享的所有特征和行为。创建基本类型以表示思想的核心。从基类型中,可以派生出其他类型来表示实现该核心的不同方式。
|
||||
|
||||

|
||||
|
||||
例如,垃圾回收机对垃圾进行分类。基本类型是“垃圾”。每块垃圾都有重量、值等,并且可以被切碎、熔化或分解。由此,可以衍生出更具体的垃圾类型,这些垃圾具有附加特征(瓶子有颜色,钢罐有磁性)或行为(可以压碎铝罐)。此外,一些行为可以不同(纸张的价值取决于它的类型和条件)。使用继承,您将构建一个类型层次结构,该层次结构根据类型表达您试图解决的问题。第二个例子是常见的“形状”例子,可能用于计算机辅助设计系统或游戏模拟。基本类型是“形状”,每个形状都有大小、颜色、位置等等。每个形状可以绘制、擦除、移动、着色等。由此,可以导出(继承)特定类型的形状——圆形、正方形、三角形等等——每个形状可以具有附加的特征和行为。
|
||||
|
||||

|
||||
|
||||
例如,某些形状可以翻转。有些行为可能不同,比如计算形状的面积时。类型层次结构体现了形状之间的相似性和差异。以与问题相同的术语转换解决方案是有用的,因为您不需要中间模型来从问题的描述获得解决方案的描述。对于对象,类型层次结构是模型的一个重要方面,因此您可以直接从真实世界中的系统描述转到代码中的系统描述。的确,有时候,那些被训练去寻找复杂解决方案的人在面向对象设计的简单性方面有困难。从现有类型继承创建新类型。这种新类型不仅包含现有类型的所有成员(尽管私有成员被隐藏起来并且不可访问),更重要的是它复制了基类的接口。也就是说,基类对象接受的所有信息也被派生类对象接受。根据类接受的信息,我们知道类的类型,因此派生类与基类是相同的类型。
|
||||
|
||||

|
||||
|
||||
在前面的例子中,“圆是形状”。这种通过继承的类型等价是理解面向对象编程含义的基本网关之一。因为基类和派生类都具有相同的基本接口,所以必须有一些实现来支持该接口。也就是说,当对象接收到特定信息时,必须有可执行代码。如果继承一个类并且不做其他任何事情,则来自基类接口的方法直接进入派生类。这意味着派生类的对象不仅具有相同的类型,而且具有相同的行为,这并不特别有趣。有两种方法可以区分新派生类与原始基类。第一种方法很简单:向派生类添加全新的方法。这些新方法不是基类接口的一部分。这意味着基类没有按照您想要的那样多,所以您添加了更多的方法。继承的这种简单而原始的用途有时是解决问题的完美解决方案。然而,事先还是要仔细调查自己的基础类是否真的需要这些额外的方法。
|
||||
|
||||
|
||||
## 多态
|
||||
|
||||
我们在处理类的层次结构时,通常是定义对象的基类而不是对象本身。通过这种方式,我们可以编写出不局限于特定类型的代码。在上个“形状”的图例中,“方法”(method)操纵的是通用“形状”,而不关心它们是“圆”、“正方形”、“三角形”还是某种尚未定义的形状。所有的形状都可以被绘制、擦除和移动,因此“方法”向其中的任何代表“形状”的对象发送信息都不必担心对象如何处理信息。
|
||||
|
||||
这样代码不受添加的新类型影响,并且添加新类型是扩展面向对象程序以处理新情况的常用方法。 举个例子来说吧:你可以通过这个通用的“形状”基类来得到一个新的“五角星”形状得子类。通过派生新的子类来扩展设计的这种能力是封装变化的基本方法之一。
|
||||
|
||||
这样的设计减少了程序的维护难度。我们把派生的对象类型统一看成是它本身的基础类型(“圆”是一种“形状”,“自行车”是“车”,“鸬鹚”也是“鸟”等等)。编译器(compiler)在编译时期无法精确的知道什么“形状”被擦除,哪一种“车”在行驶,或者是某种“鸟”在飞行。当程序接收这种信息之前程序员并不知道哪段代码会被执行。“擦除”的方法可以平等地应用到每一种可能的“形状”上。
|
||||
|
||||
如果不需要考虑执行了哪部分代码,那我们就能添加一个新的不同执行方式的子类而不需要更改调用它的方法。那么编译器在不确定该执行哪部分代码时是怎么做的呢?举个例子,下图的 **BirdController** 对象和通用 **Bird** 对象中,**BirdController** 不知道 **Bird** 的确切类型却还能一起工作。从 **BirdController** 的角度来看,这是很方便的,因为它不需要特殊的代码来确定 **Bird** 工作的确切类型或行为。那么,在调用 **move()** 方法时是如何保证发生正确的行为(鹅走路、苍蝇或游泳、企鹅走路或游泳)的呢?
|
||||
|
||||

|
||||
|
||||
答案是继承的主要转折点:在传统意义上,编译器不能进行函数调用。由非 OOP 编译器生成的函数调用生成所谓的早期绑定,这个术语您可能从未听说过,因为您从未以其他方式考虑过。这意味着编译器生成对特定函数名的调用,该调用解析为要执行的代码的绝对地址。
|
||||
|
||||
通过继承,程序直到运行时才能确定代码的地址,因此当信息被发送到对象时,还需要其他一些方案。为了解决这个问题,面向对象语言使用后期绑定的概念。当向对象发送信息时,调用的代码直到运行时才确定。编译器确保方法存在,并对参数和返回值执行类型检查,但是它不知道要执行的确切代码。
|
||||
|
||||
为了执行后期绑定,Java 使用一个特殊的代码位来代替绝对调用。这段代码使用对象中存储的信息来计算方法主体的地址(此过程在多态性章节中有详细介绍)。因此,每个对象的行为根据特定代码位的内容而不同。当您向对象发送信息时,该对象实际上确定如何处理该信息。在某些语言中,必须显式地授予方法后期绑定属性的灵活性。例如,C++使用虚拟关键字。在这些语言中,默认情况下方法没有动态绑定。在 Java 中,动态绑定是默认行为,不需要额外的关键字来生成多态性。
|
||||
|
||||
为了演示多态性,我们编写了一段代码,它忽略了类型的特定细节,只与基类对话。该代码与特定于类型的信息分离,因此更易于编写和更容易理解。而且,如果通过继承添加了一个新类型(例如,一个六边形),那么代码对于新类型的 Shape 就像对现有类型一样有效。因此,该程序是可扩展的。
|
||||
|
||||
代码示例:
|
||||
|
||||
```java
|
||||
void doSomething(Shape shape) {
|
||||
shape.erase();
|
||||
// ...
|
||||
shape.draw();
|
||||
}
|
||||
```
|
||||
|
||||
此方法与任何 Shape 都相关,因此它独立于所绘制和擦除的对象的特定类型。此时程序的其他部分使用**doSomething()** 方法:
|
||||
|
||||
```java
|
||||
Circle circle = new Circle();
|
||||
Triangle triangle = new Triangle();
|
||||
Line line = new Line();
|
||||
doSomething(circle);
|
||||
doSomething(triangle);
|
||||
doSomething(line);
|
||||
|
||||
```
|
||||
|
||||
可以看到无论传入的“形状”是什么,程序都正确的执行了。
|
||||
|
||||

|
||||
|
||||
这实际是一个非常有用的编程技巧。分析下面这行代码:
|
||||
|
||||
```java
|
||||
doSomething(circle);
|
||||
```
|
||||
这里将 **Circle**(圆)句柄传递给一个本来期待 **Shape**(形状)句柄的方法。由于圆也是一种几何形状,所
|
||||
以 **doSomething(circle)** 能正确地执行。也就是说,**doSomething()** 能接受任意 **Shape** 的信息。这是完全安全和合乎逻辑的事情。
|
||||
|
||||
这种把子类当成其基类来处理的过程叫做“向上转型”(**upcasting**)。在面向对象的编程里,经常利用这种方法来给程序解耦。再看下面的 **doSomething()** 代码示例:
|
||||
|
||||
```java
|
||||
shape.erase();
|
||||
// ...
|
||||
shape.draw();
|
||||
|
||||
```
|
||||
|
||||
我们可以看到程序并未这样表达:“如果你是一个 Circle ,就这样做;如果你是一个 Square,就那样做;等等”。若那样编写代码,就需检查 Shape 所有可能的类型,如圆、矩形等等。这显然是非常麻烦的,而且每次添加了一种新的 Shape 类型后,都要相应地进行修改。在这里,我们只需说:“你是一种几何形状,我知道你能将自己删掉,即 erase();请自己采取具体行动,并控制所有的细节吧。”
|
||||
|
||||
尽管我们没作出任何特殊指示,程序的操作也是完全正确和恰当的。我们知道,为 Circle 调用draw()时执行的代码与为一个 Square 或 Line 调用 draw() 时执行的代码是不同的。但在将 draw() 信息发给一个匿名 Shape 时,根据 Shape 句柄当时连接的实际类型,会相应地采取正确的操作。这非常神奇,因为当 Java 编译器为 doSomething() 编译代码时,它并不知道自己要操作的准确类型是什么。
|
||||
|
||||
尽管我们确实可以保证最终会为 Shape 调用 erase()、 draw(),但并不能确定特定的 Circle,Square 或者 Line 调用什么。最后,程序执行的操作却依然是正确的,这是怎么做到的呢?
|
||||
|
||||
将信息发给对象时,如果程序不知道接受的具体类型是什么,但最终执行是正确的,这就是对象的“多态性”(**Polymorphism**)。面向对象的程序设计语言是通过“动态绑定”的方式来实现对象的多态性的。编译器和运行期系统会负责对所有细节的控制;我们只需知道要做什么,以及如何利用多态性来更好的设计程序。
|
||||
|
||||
## 单继承
|
||||
|
||||
自从 C++ 引入以来,OOP 问题变得尤为突出。是否所有的类都应该默认从一个基类继承呢?这个答案在 Java 中是肯定的。(实际上,除 C++ 以外的其他虚拟机语言也是这样。)在 Java 中这个最终的基类的名字就是 `Object`。
|
||||
|
||||
Java 的单继承结构有很多好处。由于所有对象都有继承自一个公共接口,因此它们最终都属于同一个基本类型。相反的,对于 C++ 所使用的多继承的方案则是不保证所有的对象都属于同一个的基类。这种方案的限制更少一点。从向后兼容的角度看,多继承的方案更符合 C 的模型。
|
||||
|
||||
对于完全面向对象编程,我们必须要构建自己的层次结构,以提供与其他 OOP 语言同样的便利。我们经常会使用到新的类库和不兼容的接口。为了整合它们而花费大气力(有可能还要用上多继承)以获得 C++ 样的“灵活性”值得吗?如果从零开始,Java 这样的替代方案会是更好的选择。
|
||||
|
||||
另外,单继承的结构使得垃圾收集器的实现更为容易。这也是 Java 在 C++ 基础上的根本改进之一。
|
||||
|
||||
由于运行期的类型信息会存在于所有对象中,所以我们永远不会遇到判断不了对象类型的情况。这对于系统级操作尤其重要,例如[异常处理](#异常处理)。同时,这也让我们的编程具有更大的灵活性。
|
||||
|
||||
|
||||
## 集合
|
||||
|
||||
通常,我们并不知道解决某个具体问题需要的对象数量,持续时间,以及对象的存储方式。那么我们如何知悉程序在创建时需要分配的内存空间呢?
|
||||
|
||||
在面向对象的设计中,问题的解决方案有些千篇一律:创建一个新类型的对象来引用、容纳其他的对象。当然,我们也可以使用多数编程语言都支持的“数组”(**Array**)。在 Java 中“集合”(**Collection**)的使用率更高。(也可称之为“容器”,但“集合”这个称呼更通用。)
|
||||
|
||||
“集合”这种类型的对象可以存储任意类型、数量的其他对象。它能根据需要自动扩容,我们不用关心过程是如何实现的。
|
||||
|
||||
还好,一般优秀的 OOP 语言都会将“集合”作为其基础包。在 C++ 中,“集合”是其标准库的一部分。通常被称为 STL(标准模板库,*the Standard Template Library*)。SmallTalk 有一套非常完整的集合库。同样,Java 的标准库中也提供许多现成的集合类。在一些库中,一两个集合泛型就能满足我们所有的需求了。在 Java 中不同的需求对应不同种类的集合
|
||||
|
||||
在一些库中,一个或两个泛型集合被认为是对所有需求都足够好的,而在其他(Java)中,不同类型的集合对应不同的需求:常见的有 List,常用于保存序列;Map,也称为关联数组,常用于将对象与其他对象关联);Set,只能保存非重复的值;其他还包括如队列(*Queue*)、树(*Tree*)、堆(*Stack*)等等。从设计的角度来看,我们真正想要的是一个能够解决某个问题的集合。如果一种集合就满足所有需求,那么我们就不需要剩下的了。之所以选择集合有以下两个原因:
|
||||
|
||||
1. 集合可以提供不同类型的接口和外部行为。堆栈、队列的应用场景和集合、列表不同,为我们解决问题提供了灵活的方案。
|
||||
|
||||
2. 不同的集合种类对应着不同的用途。例如,List 的两种基本类型:ArrayList 和 LinkedList。虽然两者具有相同接口和外部行为,但是在某些操作中它们的效率差别很大。在 ArrayList 中随机查找元素是很高效的,而 LinkedList 随机查找效率低下。反之,在 LinkedList 中插入元素的效率要比在 ArrayList 中高。由于底层数据结构的不同,每种集合类型在执行相同的操作时会表现出效率上的差异。
|
||||
|
||||
通过对 List 接口的抽象,我们可以很容易的将 LinkedList 改为 ArrayList。在 Java 5泛型出来之前,集合中保存的是通用类型 `Object`。Java 单继承的结构意味着所有元素都基于 `Object` 类,所以在集合中可以保存任何类型的数据。这也使得集合易于重用。要使用这样的集合时,我们先要往集合添加元素。由于 Java 5版本前的集合只保存 `Object`,当我们往集合中添加元素时,元素便向上转型成了 `Object`,从而丢失自己原有的类型特性。这时我们再从对象中取出该元素时,元素的类型变成了 `Object`。那么我们该怎么将其转回原先具体的类型的?这里,我们使用了强制类型转换将其转为更具体的类型。这个过程称之为对象的“向下转型”。通过“向上转型”,我们知道“圆形”也是一种“形状”,这个过程是安全的。可是我们不能从“Object”看出其就是“圆圈”或“形状”,所以除非我们能确定元素的具体类型信息,否则“向下转型”就是不安全的。也不能说这样的错误就时完全危险的,因为一旦我们转化了错误的类型,程序就会运行出错,抛出“运行时异常”(*RuntimeException*)。(后面的章节会提到) 无论如何,我们要寻找一种在取出集合元素时确定其具体类型的方法。另外,每次取出元素都要做额外的“向下转型”对程序和程序员都是一种开销。以某种方式创建集合,以确认保存元素的具体类型,减少集合元素“向下转型”中的开销和可能出现的错误难道不好吗?这种解决方案就是:参数化类型机制(*Parameterized Type Mechanism*)。
|
||||
|
||||
参数化类型机制可以使得编译器能够自动识别某个 `class` 的具体类型并正确地执行. 举个例子,对集合的参数化类型机制可以让其仅接受“形状”这种类型的元素,并以“形状”类型取出元素。Java 5版本支持了参数化类型机制,称之为“泛型”(*Generic*)。泛型是 Java 5的主要特性之一。举个例子,你可以按以下方式向 ArrayList 种添加 Shape(形状):
|
||||
|
||||
```java
|
||||
ArrayList<Shape> shapes = new ArrayList<>();
|
||||
```
|
||||
|
||||
泛型的应用,让 Java 的许多标准库和组件都发生了改变。在本书的代码示例中,你也会经常看到泛型的身影。
|
||||
|
||||
|
||||
## 生命周期
|
||||
|
||||
我们在使用对象时要注意的一个关键问题就是对象的创建和销毁方式。每个对象的生存都需要资源,尤其是内存。为了资源的重复利用,当对象不再被使用时我们应该及时释放资源,清理内存。
|
||||
|
||||
在简单的编程场景下,对象的清理并不是问题。我们创建对象,按需使用,最后销毁它。然而,情况往往要比这更复杂:
|
||||
|
||||
假设,我们正在为机场设计一个空中交通管制的系统(该例也适用于仓库货柜管理、影带出租或者宠物寄养仓库系统)。第一步比较简单:创建一个用来保存飞机的集合,每当有飞机进入交通管制区域时,我们就创建一个“飞机”对象并将其加入到集合中,等到飞机离开时将其从这个集合中清除。与此同时,我们还需要一个记录飞机信息的系统。这些数据的重要性靠后,可以放在系统空闲的时候再后台处理。比如,我们要记录所有飞机中的小型飞机的的信息(比如飞行计划)。此时,我们又创建了第二个集合来记录所有小型飞机。 每当创建一个“飞机”对象的时候,将其放入第一个集合;若它属于小型飞机,也必须同时将其放入第二个集合里。
|
||||
|
||||
现在问题开始棘手了:我们怎么知道何时该清理这些对象呢?当某一个系统处理完成,而其他系统可能还没有处理完成。这样的问题在其他的场景下也可能发生。在 C++ 程序设计中,当使用完一个对象后,必须明确将其删除,这就让问题变复杂了。
|
||||
|
||||
这个对象的数据在哪?它的生命周期是怎么被控制的? 在 C++ 设计中采用的观点是效率第一,因此它将选择权交给了程序员。为了获得最大的运行时速度,程序员可以在编写程序时,通过将对象放在堆栈(*Stack*,有时称为自动变量或作用域变量)或静态存储区域(*static storage area*)中来确定内存占用和生存时间。这些区域的对象会被优先分配内存和释放。这种控制在某些情况下非常有用。
|
||||
|
||||
然而相对的,我们也牺牲了程序的灵活性。因为在编写代码时,我们必须要弄清楚对象的数量、生存时间还有类型。如果我们要用它来解决一个相当普遍的问题时(如计算机辅助设计、仓库管理或空中交通管制等),限制就太大了。
|
||||
|
||||
第二种方法是在堆内存(*Heap*)中动态地创建对象。在这种方式下,直到程序运行我们才能确定需要创建的对象数量、生存时间和类型。什么时候需要,什么时候在堆内存中创建。 因为内存的占用是动态管理的,所以在运行时,在堆内存上开辟空间所需的时间可能比在栈内存上要长(但也不一定)。在栈内存开辟空间通常是一条将栈指针向下移动,另一条将其向后移动的汇编指令。开辟堆内存空间的时间取决于内存机制的设计。
|
||||
|
||||
动态方法使一般的逻辑假设对象趋于复杂,因此额外的内存查找和释放的开销对对象的创建影响不大。(原文:*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.*)此外,更好的灵活性对于问题的解决至关重要。
|
||||
|
||||
Java 使用动态内存分配。每次创建对象时,使用 `new` 关键字构建该对象的动态实例。这又带来另一个问题:对象的生存周期。较之堆内存,在栈内存中创建对象,编译器能够确定该对象的生命周期并自动销毁;然而如果你在堆内存创建对象的话,编译器是不知道它的生命周期的。在 C++ 中你必须以编程方式确定何时销毁对象,否则可能导致内存泄漏。Java 的内存管理是建立在垃圾收集器上的,它能自动发现对象不再被使用并释放内存。垃圾收集器的存在带来了极大的便利,它减少了我们之前必须要跟踪的问题和编写相关代码的数量。因此,垃圾收集器提供了更高级别的保险,以防止潜在的内存泄漏问题,这使得许多 C++ 项目没落。
|
||||
|
||||
Java 的垃圾收集器被设计用来解决内存释放的问题(虽然这不包括对象清理的其他方面)。垃圾收集器知道对象什么时候不再被使用并且自动释放内存。结合单继承和仅可在堆中创建对象的机制,Java 的编码过程较之 C++ 要简单得多。我们所要做的决定和要克服的障碍也会少很多!
|
||||
|
||||
|
||||
## 异常处理
|
||||
|
||||
自编程语言被发明以来,程序的错误处理一直都是个难题,因为很难设计出一个好的错误处理方案。许多编程语言都忽略了这个问题,把这个问题丢给了程序类库的设计者。他们提出了在许多情况下都可以工作但很容易被规避的半途而废的措施,通常只需忽略错误。多数错误处理方案的主要问题是:它们依赖程序员之间的约定俗成而不是语言层面的限制。换句话说,如果程序员赶时间或没想起来,这些方案就很容易被忘记。
|
||||
|
||||
异常处理机制将程序错误直接交给编程语言甚至是操作系统。“异常”(*Exception*)是一个从出错点“抛出”(*thrown*)后能被特定类型的异常处理程序捕获(*catch*)的一个对象。它不会干扰程序的正常运行,仅当程序出错的时候才被执行。这让我们的编码更简单:不用再反复检查错误了。另外,如果 **throw** 的异常类型和 **catch** 的不符,则不会触发该条件下的异常处理程序。异常的发生是不会被忽略的,它终究会在某一时刻被处理。
|
||||
|
||||
最后,“异常机制”提供了一种可靠地从意外情况中恢复的方法,使得我们可以编写出更健壮的程序。有时你只要处理好抛出的异常情况并恢复程序的运行即可,无需退出。
|
||||
|
||||
Java 的异常处理机制在编程语言中脱颖而出。在 Java 中,异常处理从一开始就被连接起来,因此你不得不使用它。这是 Java 语言唯一接受的错误报告方法。如果没有编写适当的异常处理代码,你将会收到一条编译时错误消息。这种保证错误一致性的方法有时会让程序的错误处理变得更容易。值得注意的是,异常处理并不是面向对象的特性。尽管在面向对象的语言中异常通常由对象表示,但是在面向对象语言之前也存在异常处理。
|
||||
|
||||
|
||||
## 本章小结
|
||||
|
||||
面向过程程序包含数据定义和函数调用。要找到程序的意图,你必须要在脑中建立一个模型,弄清函数调用和更底层的概念。这些程序往往容易混淆,因为表达式的术语更多地面向计算机而不是我们要解决的问题。这就是我们在设计程序时需要中间表示的原因。OOP 在面向过程编程的基础上增加了许多新的概念,所以有人会认为使用 Java 来编程会比同等的面向过程编程要更复杂。在这里,我想给大家一个惊喜:通常按照 Java 规范编写的程序会比面向过程程序更容易被理解。
|
||||
|
||||
你看到的是对象的概念,这些概念是站在“问题空间”的(而不是站在计算机角度的“解决方案空间”),以及发送给对象以指示该空间中的活动的信息。面向对象编程的一个优点是:设计良好的 Java 程序代码更容易被人阅读理解。由于 Java 类库的复用性,通常程序要写的代码也会少得多。
|
||||
|
||||
OOP 和 Java 不一定适合每个人。评估自己的需求以及与现有方案作比较是很重要的。请充分考虑后再决定是不是选择 Java。如果在可预见的未来,Java 并不能很好的满足你的特定需求,那么你应该去寻找其他替代方案(特别是,我推荐看 Python)。如果你依然选择 Java 作为你的开发语言,我希望你至少应该清楚你选择的是什么,以及为什么选择这个方向。
|
||||
<!-- 分页 -->
|
||||
<div style="page-break-after: always;"></div>
|
||||
|
||||
191
docs/book/02-Installing-Java-and-the-Book-Examples.md
Normal file
191
docs/book/02-Installing-Java-and-the-Book-Examples.md
Normal file
@@ -0,0 +1,191 @@
|
||||
[TOC]
|
||||
|
||||
# 第二章 安装Java和本书用例
|
||||
|
||||
现在,我们来为这次阅读之旅做些准备吧!
|
||||
|
||||
在开始学习 Java 之前,你必须要先安装好 Java 和本书的源代码示例。因为考虑到可能有“专门的初学者”从本书开始学习编程,所以我会仔细解释计算机命令行 Shell 的这个过程。 如果你已经有此方面的经验了,可以跳过这段安装说明。如果你对此处描述的任何术语或过程仍不清楚,还可以通过 Google 搜索找到答案。具体的问题或困难请试着在 StackOverflow 上提问。或者去 YouTube 看有没有相关的安装说明。
|
||||
|
||||
|
||||
## 编辑器
|
||||
|
||||
首先你需要安装一个编辑器来创建和修改本书用例里的 Java 代码。有可能你还需要一个编辑器来更改系统配置文件。
|
||||
|
||||
相比一些重量级的 IDE(Integrated Development Environments,开发集成环境)软件,如Eclipse、NetBeans和IntelliJ IDEA (译者注:做项目强烈推荐IDEA),编辑器是一种基础的运行程序的文本编辑器。如果你已经有了一个 IDE 用着还顺手,那就可以直接用了。为了方便后面的学习和统一下教学环境,我推荐大家使用 Atom 这个编辑器。大家可以在 [atom.io](http://atom.io) 网站下载。
|
||||
|
||||
Atom 是一个免费开源、易于安装且跨平台(支持 Window、Mac和Linux)的文本编辑器。内置支持 Java 文件。相比 IDE 的厚重,她比较轻量级,是学习本书的理想工具。Atom 包含了许多方便的编辑功能,相信你一定会爱上她!更多关于 Atom 使用的细节问题可以到她们的网站上。
|
||||
|
||||
还有很多其他的编辑器。有一种亚文化的群体,他们热衷于争论哪个更好用!如果你找到一个你更喜欢的编辑器,换一种使用也没什么难度。重要的是,你要找一个用着舒服的。
|
||||
|
||||
|
||||
## Shell
|
||||
|
||||
如果你之前没有接触过编程,那么有可能对 Shell(命令行窗口) 不太熟悉。shell 的历史可以追溯到早期的计算时代,当时在计算机上的操作是都通过输入命令进行的,计算机通过回显响应。所有的操作都是基于文本的。
|
||||
|
||||
尽管和现在的图形用户界面相比,Shell 操作方式很原始。但是同时 shell 也为我们提供了许多有用的功特性。在学习本书的过程中,我们会经常使用到 Shell,包括现在这部分的安装,还有运行 Java 程序。
|
||||
|
||||
Mac:单击聚光灯(屏幕右上角的放大镜图标),然后键入“terminal”。单击看起来像小电视屏幕的应用程序(您也可以单击“return”)。这就启动了你的用户下的 shell窗口。
|
||||
|
||||
windows:首先,通过目录打开 windows 资源管理器:
|
||||
Windows 7: 单击屏幕左下角的“开始”图标,输入“explorer”后按回车键。
|
||||
Windows 8: 按 Windows+Q, 输入 “explorer” 后按回车键。
|
||||
Windows 10: 按 Windows+E 打开资源管理器,导航到所需目录,单击窗口左上角的“文件“选项卡,选择“打开 Window PowerShell”启动 Shell。
|
||||
Linux: 在 home 目录打开 Shell。
|
||||
Debian: 按 Alt+F2, 在弹出的对话框中输入“gnome-terminal”
|
||||
Ubuntu: 在屏幕中鼠标右击,选择 “打开终端”, 或者按住 Ctrl+Alt+T
|
||||
Redhat: 在屏幕中鼠标右击,选择 “打开终端”
|
||||
Fedora: 按 Alt+F2,在弹出的对话框中输入“gnome-terminal”
|
||||
|
||||
**目录**
|
||||
目录是 Shell 的基础元素之一。目录用来保存文件和其他目录。目录就好比树的分支。如果书籍是您系统上的一个目录,并且它有两个其他目录作为分支,例如数学和艺术,那么我们就可以说你有一个书籍目录里,它包含数学和艺术两个子目录。注意:Windows 使用“\”而不是“/”来分隔路径。
|
||||
|
||||
**Shell基本操作**
|
||||
我在这展示的 Shell 操作和系统中大体相同。出于本书的原因,下面列举一些在 Shell 中的基本操作:
|
||||
|
||||
```shell
|
||||
更改目录: cd <路径>
|
||||
cd .. 移动到上级目录
|
||||
pushd <路径> 记住来源的同时移动到其他目录,popd 返回上一个目录
|
||||
|
||||
目录列举: ls 列举出当前目录下所有的文件和子目录名(不包含隐藏文件),
|
||||
可以选择使用通配符 * 来缩小搜索的范围。
|
||||
示例(1): 列举所有以“.java”结尾的文件,输入ls *.java
|
||||
示例(2): 列举所有以“F”开头,“.java”结尾的文件,输入ls F*.java
|
||||
|
||||
增加目录:
|
||||
Mac/Linux 系统:mkdir
|
||||
示例:mkdir books
|
||||
Windows 系统:md
|
||||
示例:md books
|
||||
|
||||
移除文件:
|
||||
Mac/Linux 系统:rm
|
||||
示例:rm somefile.java
|
||||
Windows 系统:del
|
||||
示例:del somefile.java
|
||||
|
||||
移除目录:
|
||||
Mac/Linux 系统:rm -r
|
||||
示例:rm -r somefile.java
|
||||
Windows 系统:deltree
|
||||
示例:deltree somefile.java
|
||||
|
||||
重复命令: !! 重复上条命令
|
||||
示例:!n 重复倒数第n条命令
|
||||
|
||||
命令历史:
|
||||
Mac/Linux 系统:history
|
||||
Windows 系统:按 F7 键
|
||||
|
||||
文件解压:
|
||||
Linux/Mac 都有命令行解压程序,您可以通过互联网为Windows安装命令行解压程序。
|
||||
图形界面下(Windows 资源管理器、Mac Finder ,Linux Nautilus 或其他等效软件)右键单击该文件,
|
||||
在 Mac 上选择“open”,在 Linux 上选择“extract here”,或在 Windows 上选择“extract all…”。
|
||||
要了解关于shell 的更多信息,请在维基百科中搜索 Windows shell,Mac/Linux用户可搜索 bash shell。
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Java安装
|
||||
|
||||
为了安装了和运行代码示例,首先你必须安装 JDK 1.8。本书中采用的是 JDK 1.8版本。
|
||||
|
||||
|
||||
**Windows**
|
||||
|
||||
1. 以下为 Chocolatey 的[安装说明](https://chocolatey.org/)。
|
||||
2. 在命令行提示符下输入下面的命令,等待片刻,结束后 Java 安装完成并自动完成环境变量设置。
|
||||
|
||||
```shell
|
||||
choco install jdk8`.
|
||||
```
|
||||
|
||||
**Macintosh**
|
||||
|
||||
Mac 系统自带的 Java 版本太老,为了确保本书的代码示例能被正确执行,你必须先更新它到 Java 8。我们需要管理员权限来运行下面的步骤:
|
||||
1. 以下为 HomeBrew 的[安装说明](https://brew.sh/)。安装完成后执行命令 `brew update` 更新到最新版本
|
||||
2. 在命令行下执行下面的命令来安装 Java。
|
||||
|
||||
```shell
|
||||
brew cask install java
|
||||
```
|
||||
|
||||
当以上安装都完成后,如果你有需要,可以使用游客账户来运行本书中的代码示例。
|
||||
|
||||
**Linux**
|
||||
|
||||
* **Ubuntu/Debian**:
|
||||
|
||||
```shell
|
||||
sudo apt-get update
|
||||
sudo apt-get install default-jdk
|
||||
```
|
||||
* **Fedora/Redhat**:
|
||||
|
||||
```shell
|
||||
su-c "yum install java-1.8.0-openjdk"
|
||||
```
|
||||
|
||||
|
||||
## 校验安装
|
||||
|
||||
打开新的命令行输入:
|
||||
|
||||
```shell
|
||||
java -version
|
||||
```
|
||||
|
||||
正常情况下 你应该看到以下类似信息(版本号信息可能不一样):
|
||||
|
||||
```shell
|
||||
java version "1.8.0_112"
|
||||
Java(TM) SE Runtime Environment (build 1.8.0_112-b15)
|
||||
Java HotSpot(TM) 64-Bit Server VM (build 25.112-b15, mixed mode)
|
||||
```
|
||||
如果提示命令找不到或者无法被识别,请根据安装说明重试;如果还不行,尝试到 [StackOverflow](https://stackoverflow.com/search?q=installing+java) 寻找答案。
|
||||
|
||||
|
||||
## 安装和运行代码示例
|
||||
|
||||
当 Java 安装完毕,下一步就是安装本书的代码示例了。安装步骤所有平台一致:
|
||||
|
||||
1. 在 [GitHub 仓库](https://github.com/BruceEckel/OnJava8-Examples/archive/master.zip)中下载本书代码示例
|
||||
2. 解压到你所选目录里。
|
||||
3. 使用 Windows 资源管理器, Mac Finder, or Linux 的 Nautilus 或其他等效工具浏览,在该目录下打开 Shell 命令行。
|
||||
4. 如果你在正确的目录中,你应该看到该目录中名为 gradlew 和 gradlew.bat 的文件,以及许多其他文件和目录。目录与书中的章节相对应。
|
||||
5. 在命令行中输入下面的命令运行:
|
||||
|
||||
```shell
|
||||
Windows 系统:
|
||||
gradlew run
|
||||
|
||||
Mac/Linux 系统:
|
||||
./gradlew run
|
||||
```
|
||||
|
||||
第一次安装时 Gradle 需要安装自身和其他的相关的包,请稍等片刻。安装完成后,后续的安装将会快很多。
|
||||
|
||||
**注意**: 第一次运行 gradlew 命令时必须连接互联网。
|
||||
|
||||
|
||||
**Gradle基础任务**
|
||||
|
||||
本书构建的大量 Gradle 任务都可以自动运行。Gradle 设置使用约定大于配置的方式,简单设置即可具备高可用性。本书中“一起去骑行”的某些任务不适用于此或无法执行成功。以下是你通常会使用上的分级任务列表:
|
||||
|
||||
```java
|
||||
编译本书中的所有 java 文件,除了部分错误示范的
|
||||
gradlew compileJava
|
||||
|
||||
编译并执行 java 文件(某些文件是库组件)
|
||||
gradlew run
|
||||
|
||||
执行所有的单元测试(在本书学习中校验自己的代码是由正确)
|
||||
gradlew test
|
||||
|
||||
编译且运行一个特别的示例程序
|
||||
gradlew <本书章节>:<示例名称>
|
||||
示例:gradlew objects:HelloDate
|
||||
```
|
||||
<!-- 分页 -->
|
||||
<div style="page-break-after: always;"></div>
|
||||
|
||||
636
docs/book/03-Objects-Everywhere.md
Normal file
636
docs/book/03-Objects-Everywhere.md
Normal file
@@ -0,0 +1,636 @@
|
||||
[TOC]
|
||||
|
||||
# 第三章 万物皆对象
|
||||
|
||||
>如果我们说不同的语言,我们会感觉到一个不同的世界!— Ludwig Wittgenstein (1889-1951)
|
||||
|
||||
尽管 Java 基于 C++ ,但 Java 是一种更纯粹的面向对象程序设计语言。Java 和 C++ 都是混合语言。在 Java 中,语言设计者认为混合并不像 C++ 那样重要。混合语言允许多种编程风格;这也是 C++ 支持与 C 语言的向后兼容性原因。因为 C++ 是 C 语言的超集,所以它也包含了许多 C 语言的不良特性,这可能使得 C++ 在某些方面过于复杂。
|
||||
|
||||
Java 语言预设你已经编写过面对对象的程序。在此之前,你必须将自己的思维置于面对对象的世界。在本章中你将了解 Java 语言的基本组成,学习 Java (几乎)万物皆对象的思想。
|
||||
|
||||
<!-- You Manipulate Objects with References -->
|
||||
## 对象操纵
|
||||
|
||||
名字的意义在于,当我们听到“玫瑰”这个词时,就会想到一种闻起来很甜蜜的的花。(引用自 莎士比亚,《罗密欧与朱丽叶》)。
|
||||
|
||||
所有的编程语言都会操纵内存中的元素。有时程序员必须要有意识地直接或间接地操纵它们。示例:在 C/C++ 语言中是通过指针来完成操作的。
|
||||
|
||||
Java 使用万物皆对象的思想和特有的语法方式来简化问题。虽然万物皆可为对象,但你操纵的标识符实际上是只对象的“引用” [^1]。 示例:我们可以将这种“引用”想象成电视(对象)和遥控器(引用)之间的关系。只要拥有对象的“引用”,就可以操纵该“对象”。我们无需直接接触电视,只要掌握遥控器就可以在房中自由地控制电视(对象)的频道和音量。此外,没有电视机,遥控器也可以单独存在。引申来说,仅仅因为你有一个“引用”并不意味着你必然有一个关联的“对象”。
|
||||
|
||||
下面来创建一个 String 的引用,用于保存单词语句。代码示例:
|
||||
|
||||
```java
|
||||
String s;
|
||||
```
|
||||
|
||||
这里我们仅仅只是创建了一个 String 对象的引用,而非对象。直接拿来使用会出现错误:因为此时你并没有给变量 s 赋值--附加任何引用的对象。通常更安全的做法是:在声明变量引用的同时初始化对象信息。代码示例:
|
||||
|
||||
```java
|
||||
String s = "asdf";
|
||||
```
|
||||
|
||||
Java 语法允许我们使用带双引号的文本内容来初始化字符串。同样,其他类型的对象也有相应的初始化方式。
|
||||
|
||||
<!-- You Must Create All the Objects -->
|
||||
## 对象创建
|
||||
|
||||
“引用”用来连接“对象”。在 Java 中,通常我们使用`new`这个操作符来创建一个新的对象。`new`关键字代表:创建一个新的对象实例。所以,前面的代码实例我们也可以这样来表示:
|
||||
|
||||
```java
|
||||
String s = new String("asdf");
|
||||
```
|
||||
以上的代码示例展示了字符串对象的创建过程,以及如何初始化生成字符串。Java 本身自带了许多现成的数据类型,在此基础之上我们还可以创建自己的数据类型。类型的创建是 Java 的基本操作。在本书后面的学习中将会接触到。
|
||||
|
||||
<!-- Where Storage Lives -->
|
||||
### 数据存储
|
||||
|
||||
那么, 程序在运行时是如何存储的呢?尤其是内存。下面我们就来形象地描述下, Java 中数据存储的5个不同的地方:
|
||||
|
||||
1. **寄存器** (*Registers*) 最快的保存区域,位于CPU内部 [^2]。然而,寄存器的数量十分有限,所以寄存器是根据需要由编译器分配。我们对其没有直接的控制权,也无法在自己的程序里找到寄存器存在的踪迹(另一方面,C/C++ 允许开发者向编译器建议寄存器的分配)。
|
||||
|
||||
2. **栈内存**(*Stack*) 存在于常规内存(RAM)区域中,可通过栈指针获得处理器的直接支持。栈指针下移创建新内存,上移释放该内存,顺序后进先出,速度仅次于寄存器。创建程序时,Java 编译器必须准确地知道栈内保存的所有数据的“长度”以及生命周期。栈内存的这种约束限制了程序的灵活性。因此,虽然在栈内存上存在一些 Java 数据,特别是对象引用,但 Java 对象本身却是保存在堆内存的。
|
||||
|
||||
3. **堆内存**(*Heap*) 这是一种常规用途的内存池(也在 RAM区域),所有 Java 对象都存在于其中。与栈内存不同,编译器不需要知道对象必须在堆内存上停留多长时间。因此,用堆内存保存数据更具灵活性。创建一个对象时,只需用 `new` 命令实例化代码即可。执行这些代码时,数据会在堆内存里自动进行保存。这种灵活性是有代价的:分配和清理堆内存要比栈内存需要更多的时间(如果你甚至可以用 Java 在栈内存上创建对象,就像在C++ 中那样)。随着时间的推移,Java 的堆内存分配机制现已非常快,因此这不是一个值得关心的问题了。
|
||||
|
||||
4. **常量存储** (*Constant storage*) 常量值通常直接放在程序代码中,因为它们永远不会改变。如需严格保护,可考虑将它们置于只读存储器(ROM)中 [^3]。
|
||||
|
||||
5. **非 RAM 存储** (*Non-RAM storage*) 数据完全存在于程序之外,在程序未运行以及脱离程序控制后依然存在。两个主要的例子:(1)序列化对象:对象被转换为字节流,通常被发送到另一台机器;(2)持久化对象:对象被放置在磁盘上,即使程序终止,数据依然存在。这些存储的方式都是将对象转存于另一个介质中,并在需要时恢复到常规内存中。Java 为轻量级持久性提供支持。诸如 JDBC 和 Hibernate 之类的库为使用数据库存储和检索对象信息提供了更复杂的支持。
|
||||
|
||||
|
||||
<!-- Special Case: Primitive Types -->
|
||||
### 基本类型的存储
|
||||
|
||||
有一组类型在 Java 中使用频率很高,这就是 Java 的基本类型。对于此类数据的存储我们需要特别对待。之所以说这么说,是因为它们的创建并不是通过 `new` 关键字来产生。通常 `new` 出来的对象都是保存在 **Heap** 内存中的, 用它来创建小、简单的基本类型的数据是不划算的。所以对于这些基本类型的创建方法, Java 使用了和 C/C++ 一样的策略。也就是说,不是使用 `new` 创建变量,而是使用一个“自动”变量。 这个变量容纳了具体的值,并置于栈内存中,能够更高效地存取。
|
||||
|
||||
Java 预设了每种基本类型的初始内存占用大小。 这些大小标准不会随着机器环境的变化而变化。这种不变性也是Java 的跨平台的一个原因。
|
||||
|
||||
| 基本类型 | 大小 | 最小值 | 最大值 | 包装类型 |
|
||||
| :------: | :------: | :------: | :------: | :------: |
|
||||
| boolean | — | — | — | Boolean |
|
||||
| char | 16 bits | Unicode 0 | Unicode $2^{16}-1$ | Character |
|
||||
| byte | 8 bits | $-128$ | $+127$ | Byte |
|
||||
| short | 16 bits | $-2^{15}$ | $+2^{15}-1$ | Short |
|
||||
| int | 32 bits | $-2^{31}$ | $-2^{31}-1$ | Integer |
|
||||
| long | 64 bits | $-2^{63}$ | $-2^{63}-1$ | Long |
|
||||
| float | 32 bits | IEEE754 | IEEE754 | Float |
|
||||
| double | 64 bits |IEEE754 | IEEE754 | Double |
|
||||
| void | — | — | — | Void |
|
||||
|
||||
所有的数值类型都是有正/负符号的。布尔(boolean)类型的大小没有明确的规定,通常定义为采用文字 “true” 和 “false”。基本类型有自己对应的包装类型,如果你希望在堆内存里表示基本类型的数据,就需要用到它们的包装类。代码示例:
|
||||
|
||||
```java
|
||||
char c = 'x';
|
||||
Character ch = new Character(c);
|
||||
```
|
||||
或者你也可以使用下面的形式 基本类型自动转换成包装类型(自动装箱):
|
||||
|
||||
```java
|
||||
Character ch = new Character('x');
|
||||
```
|
||||
相对的,包装类型转化为基本类型(自动拆箱):
|
||||
|
||||
```java
|
||||
char c = ch;
|
||||
```
|
||||
|
||||
个中原因将在以后的章节里解释。
|
||||
|
||||
<!-- High-Precision Numbers -->
|
||||
### 高精数值的存储
|
||||
|
||||
在 Java 中有两种类型的数据可用于高精度的计算。它们是 `BigInteger` 和 `BigDecimal`。尽管它们大致可以划归为“包装类型”,但是它们并没有相应的基本类型形式。
|
||||
|
||||
这两个类都有自己特殊的“方法”,对应于我们针对基本类型数值执行的操作。也就是说,能对 int 或 float 做的运算,在 BigInteger 和 BigDecimal 这里也同样可以做一样可以,只不过必须要通过调用它们的方法来实现而非运算符。此外,由于涉及到的计算量更多,所以运算速度会慢一些。诚然,我们牺牲了速度,但换来了精度。
|
||||
|
||||
BigInteger 支持任意精度的整数。可用于精确表示任意大小的整数值,同时在运算过程中不会丢失精度。
|
||||
BigDecimal 支持任意精度的定点数字。例如,可用它进行精确的币值计算。至于具体使用什么方法,更多详情,请参考 JDK 官方文档。
|
||||
|
||||
|
||||
<!-- Arrays in Java -->
|
||||
### 数组的存储
|
||||
|
||||
|
||||
许多编程语言都支持数组类型。在 C 和 C++ 中使用数组是危险的,因为那些数组只是内存块。如果程序访问了其内存块之外的数组或在初始化之前使用该段内存(常见编程错误),则结果是不可预测的。
|
||||
|
||||
Java 的设计主要目标之一是安全性,因此许多困扰 C 和 C++ 程序员的问题不会在 Java 中再现。在 Java 中,数组使用前需要被初始化,并且不能访问数组长度以外数据。这种长度检查的代价是每个阵列都有少量的内存开销以及在运行时验证索引的额外时间,但是这种安全性的前提对于提高的生产率是值得的。(并且 Java 经常可以优化这些操作)。
|
||||
|
||||
当我们创建对象数组时,实际上是创建了一个数据的引用,并且每个引用的初始值都为 **null** 。在使用该数组之前,我们必须为每个引用分配一个对象 。如果我们尝试使用为**null**的引用,则会在运行时报告该问题。因此,在 Java 中就防止了数组操作的典型错误。
|
||||
|
||||
我们还可创建基本类型的数组。编译器通过将该数组的内存归零来保证初始化。本书稍后将详细介绍数组,特别是在数组章节中。
|
||||
|
||||
|
||||
|
||||
<!-- Comments -->
|
||||
## 代码注释
|
||||
|
||||
|
||||
Java 中有两种类型的注释。第一种是传统的 C 风格的注释,以 `/*` 开头,可以跨越多行,到 `*/ ` 结束。**注意**,许多程序员在多行注释的每一行开头添加 `*`,所以你经常会看到:
|
||||
|
||||
```java
|
||||
/* This is a comment
|
||||
* that continues
|
||||
* across lines
|
||||
*/
|
||||
```
|
||||
|
||||
但请记住, `/*` 和 `*/` 之间的内容都是被忽略的。所以你将其改为下面的风格也是没有区别的。
|
||||
|
||||
```JAVA
|
||||
/* This is a comment that
|
||||
continues across lines */
|
||||
```
|
||||
|
||||
第二种注释形式来自 C++ 。它是单行注释,以 `//` 开头并一直持续到行结束。这种注释方便且常用,因为它很直观和简单。所以你经常看到:
|
||||
|
||||
```JAVA
|
||||
// 这是单行注释
|
||||
```
|
||||
|
||||
<!-- You Never Need to Destroy an Object -->
|
||||
## 对象清理
|
||||
|
||||
在一些编程语言中,管理存储的生命周期需要大量的工作。一个变量需要存续多久?如果我们想销毁它,应该什么时候去做呢?存储生命周期的混乱会导致许多错误,本小结将会向你介绍 Java 是如何通过释放存储来简化这个问题的。
|
||||
|
||||
<!-- Scoping -->
|
||||
### 作用域
|
||||
|
||||
大多数程序语言都有作用域的概念。这将确定在该范围内定义的名称的可见性和生存周期。在 C、C++ 和 Java 中,作用域是由大括号 `{}` 的位置决定的。下面是 Java 代码作用域的一个示例:
|
||||
|
||||
```JAVA
|
||||
{
|
||||
int x = 12;
|
||||
// 仅 x 变量可用
|
||||
{
|
||||
int q = 96;
|
||||
// x 和 q 变量皆可用
|
||||
}
|
||||
// 仅 x 变量可用
|
||||
// 变量 q 不在作用域内
|
||||
}
|
||||
```
|
||||
|
||||
Java 的变量只有在其作用域内才可用。缩进使得 Java 代码更易于阅读。由于 Java 是一种自由形式的语言,额外的空格、制表符和回车并不会影响程序的生成结果。在 Java 中,你不能执行以下操作,即使这在 C 和 C++ 中是合法的:
|
||||
|
||||
```JAVA
|
||||
{
|
||||
int x = 12;
|
||||
{
|
||||
int x = 96; // Illegal
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在上例中, Java 便编译器会在提示变量 x 已经被定义过了。因此,在 C 和 C++ 中可以于更大作用域中“隐藏”变量的能力在 Java 中是不被允许的。 因为 Java 的设计者认为这样的定义会混淆编程。
|
||||
|
||||
<!-- Scope of Objects -->
|
||||
### 对象作用域
|
||||
|
||||
Java 对象与基本类型具有不同的生命周期。当我们使用 `new` 关键字来创建 Java 对象时,它的生命周期将会超出作用域的末尾。因此,下面这段代码示例
|
||||
|
||||
```JAVA
|
||||
{
|
||||
String s = new String("a string");
|
||||
}
|
||||
// 作用域结束
|
||||
```
|
||||
上例中,变量 s 的范围在标注的地方结束了。但是,引用的字符串对象依然还在占用内存。在这段代码中,变量 s 的唯一引用超出了作用域,因此它无法在作用域外被访问。在后面的章节中,你我们还会学习怎么在编程中传递和复制对象的引用。
|
||||
|
||||
只要程序需要, `new` 出来的对象就不会被销毁。 相比在 C++ 编码中操作内存可能会出现的诸多问题,这些困扰在 Java 中都不复存在了。在 C++ 中你不仅要确保对象的内存在你操作的范围内存在,还必须在完成后销毁对象。
|
||||
|
||||
那么问题来了:我们在 Java 中并没有去主动清理这些对象,那么它是如何避免 C++ 中出现的内存泄漏和程序终止的问题呢?答案是:Java 的垃圾收集器会检查所有 `new` 出来的对象并判断哪些不再可达,继而释放那些被占用的内存。至此,我们不必再自己操作回收内存了。取而代之,只需简单创建对象即可。当其不再被需要时,能自行被垃圾收集器释放。垃圾回收机制有效防止了因程序员忘记释放内存而造成的“内存泄漏”问题。
|
||||
|
||||
<!-- Creating New Data Types: class -->
|
||||
## 类的创建
|
||||
|
||||
### 类型
|
||||
|
||||
如果一切都是对象,那么我们用什么来表示对象类的具体展现和行为呢?顾名思义,你可能很自然地想到 `type` 关键字。但是,事实上大多数面向对象的语言都使用 `class` 关键字类来描述一种新的对象。 通常在 `class` 关键字的后面的紧跟类的的名称。如下代码示例:
|
||||
|
||||
```java
|
||||
class ATypeName {
|
||||
// 这里是类的内部
|
||||
}
|
||||
```
|
||||
|
||||
在上例中,我们介绍了如何创建一个新的类型,尽管这个类里只有一行注释。但是我们一样可以通过 `new` 关键字来创建一个对象。如下:
|
||||
|
||||
```JAVA
|
||||
ATypeName a = new ATypeName();
|
||||
```
|
||||
|
||||
到现在为止,我们还不能用这个对象来做什么事,比如发送一些有趣的信息啊。除非我们在这个类里定义一些方法。
|
||||
|
||||
<!-- Fields -->
|
||||
### 属性
|
||||
|
||||
当我们创建好了一个类之后,我们可以往类里存放两种类型的元素。方法(**method**)和属性(**field**)。类的属性可以是基本类型。如果类的属性是对象的话,那么必须要初始化该引用将其关联到一个实际的对象上(通过之前介绍的创建对象的方法)。每个对象都会为其属性保留独立的存储空间。通常,属性不再对象之间做共享。下面是一个包含部分属性的类的代码示例:
|
||||
|
||||
```JAVA
|
||||
class DataOnly {
|
||||
int i;
|
||||
double d;
|
||||
boolean b;
|
||||
}
|
||||
```
|
||||
|
||||
除非持有数据,不然这个类不能做任何事。在此之前,我们可以通过下面的代码示例来创建它的对象:
|
||||
|
||||
```JAVA
|
||||
DataOnly data = new DataOnly();
|
||||
```
|
||||
|
||||
我们必须通过这个对象的引用来指定属性值。格式:对象名称.方法名称或成员名称。代码示例:
|
||||
|
||||
```JAVA
|
||||
data.i = 47;
|
||||
data.d = 1.1;
|
||||
data.b = false;
|
||||
```
|
||||
|
||||
如果你想修改对象内部包含的另一个对象的数据,可以通过这样的格式修改。代码示例:
|
||||
|
||||
```JAVA
|
||||
myPlane.leftTank.capacity = 100;
|
||||
```
|
||||
|
||||
你可以用这种方式嵌套许多对象(尽管这样的设计会带来混淆)。
|
||||
|
||||
|
||||
<!-- Default Values for Primitive Members -->
|
||||
### 基本类型默认值
|
||||
|
||||
如果类的成员变量(属性)是基本类型,那么在类初始化时,这些类型将会被赋予一个初始值。
|
||||
|
||||
| 基本类型 | 初始值 |
|
||||
| :-----: |:-----: |
|
||||
| boolean | false |
|
||||
| char | \u0000 |
|
||||
| byte | 0 |
|
||||
| short |0 |
|
||||
| int | 0 |
|
||||
| long | 0L |
|
||||
| float | 0.0f |
|
||||
| double | 0.0d |
|
||||
|
||||
这些默认值仅在 Java 初始化类的时候才会被赋予。这种方式确保了基本类型的属性始终能被初始化(在C++ 中不会),从而减少了 bug 的来源。但是,这些初始值对于程序来说并不一定是合法或者正确的。 所以,为了安全,我们最好始终显式地初始化变量。
|
||||
|
||||
这种默认值的赋予并不适用于局部变量 —— 那些不属于类的属性的变量。 因此,若在方法中定义的基本类型数据,如下:
|
||||
|
||||
```JAVA
|
||||
int x;
|
||||
```
|
||||
|
||||
这里的变量 x 不会自动初始化为0,因而在使用变量 x 之前,程序员有责任主动地为其赋值(和 C 、C++ 一致)。如果我们忘记了这一步,在 JAVA 中将会提示我们“编译时错误,该变量尚未被初始化”。 这一点做的比 C++ 要更好,在后者中,编译器只是提示警告,而在 JAVA 中则直接报错。
|
||||
|
||||
|
||||
<!-- Methods, Arguments,and Return Values -->
|
||||
### 方法使用
|
||||
|
||||
在许多语言(如 C 和 C++)中,术语函数(**function**)用于描述命名子程序。在 Java 中,我们使用术语方法(**method**)来表示“做某事的方式”。
|
||||
|
||||
在 JAVA 中,方法决定着对象能接收哪些信息。方法的基础部分包含名称、参数、返回类型、方法体。格式如:
|
||||
|
||||
```java
|
||||
[返回类型][方法名](/*参数列表*/){
|
||||
// 方法体
|
||||
}
|
||||
```
|
||||
|
||||
#### 返回类型
|
||||
|
||||
方法的返回类型表明了当你调用它时会返回的结果类型。参数列表则显示了可被传递到方法内部的参数类型及名称。方法的名称和参数列表被统称为**方法签名**(**signature of the method**)。签名作为方法的唯一性标识。
|
||||
|
||||
Java 中的方法只能作为类的一部分创建。它只能被对象所调用 [^4],并且该对象必须有权限来执行调用。若对象调用错误的方法,则程序将在编译时报错。
|
||||
|
||||
我们可以通过在对象名的后面跟上 `.` 符号+方法名及其参数来调用一个方法。代码示例:
|
||||
|
||||
```JAVA
|
||||
[对象引用].[方法名](参数1, 参数2, 参数3);
|
||||
```
|
||||
|
||||
若方法不带参数,例如一个对象 a 的方法 f 不带参数并返回 int 型结果,我们可以如下表示。代码示例:
|
||||
|
||||
```JAVA
|
||||
int x = a.f();
|
||||
```
|
||||
|
||||
上例中方法 f 的返回值必须兼容接收的变量 x 。这种调用方法的行为有时被称为向对象传递信息。面向对象编程可以被总结为:向对象传递信息。
|
||||
|
||||
<!-- The Argument List -->
|
||||
#### 参数列表
|
||||
|
||||
方法参数列表指定传递给方法的信息。正如你可能猜到的,这些信息 —— 就像 Java 中的其他所有信息 —— 采用对象的形式。参数列表必须指定对象类型和每个对象的名称。同样,我们并没有直接处理对象,而是在传递对象引用 [^5] 。但是引用的类型必须是正确的。如果方法需要 String 参数,则必须传入 String,否则编译器将报错。
|
||||
|
||||
```JAVA
|
||||
int storage(String s) {
|
||||
|
||||
return s.length() * 2;
|
||||
}
|
||||
```
|
||||
|
||||
此方法计算并返回某个字符串的长度。参数 s 的类型为 String 。将 字符串变量 s 传递给 storage() 后,我们可以将其视为任何其他对象一样 —— 我们可以想起传递信息。在这里,我们调用 length() 方法,它是一个 String 方法,返回字符串长度。字符串中每个字符的大小为16位或两个字节。您还可以看到 **return** 关键字,它执行两项操作。首先,它意味着“方法执行结束”。其次,如果方法有返回值,那么该值就位于 **return** 语句之后。这里,返回值是通过计算
|
||||
|
||||
```JAVA
|
||||
s.length() * 2
|
||||
```
|
||||
产生的。在方法中,我们可以返回任意的数据。如果我们不想方法返回什么数据,则可以通过给方法标识 `void` 来表明这是一个无需返回值的方法。 代码示例:
|
||||
|
||||
```JAVA
|
||||
boolean flag() {
|
||||
return true;
|
||||
}
|
||||
|
||||
double naturalLogBase() {
|
||||
return 2.718;
|
||||
}
|
||||
|
||||
void nothing() {
|
||||
return;
|
||||
}
|
||||
|
||||
void nothing2() {
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
当返回类型为 **void** 时, **return** 关键字仅用于退出方法,因此在方法结束处的 **return** 可被省略。我们可以随时从方法中返回。若方法返回类型为非 `void`,则编译器会强制返回相应类型的值。
|
||||
|
||||
上面的描述可能会让你感觉程序只不过是一堆包含各种方法的对象,将对象作为方法参数来传递信息给其他的对象。从表面上来看的确如此。但在下一章的运算符中我们将会学习如何在方法中做出决策来完成更底层、详细的工作。对于本章,知道如何传递信息就够了。
|
||||
|
||||
|
||||
<!-- Writing a Java Program -->
|
||||
## 程序编写
|
||||
|
||||
在看到第一个 Java 程序之前,我们还必须了解其他几个问题。
|
||||
|
||||
#### 命名可见性
|
||||
|
||||
命名控制在任何一门编程语言中都是一个问题。如果程序员在两个模块中使用相同的命名,那么如何区分这两个名称,并防止两个名称发生“冲突”呢?在 C 语言编程中这是很具有挑战性的,因为程序通常是一个无法管理的名称海洋。C++ 将函数嵌套在类中,它们不能与嵌套在其他类中的函数名冲突。然而,C++ 继续允许全局数据和全局函数,因此仍有可能发生冲突。为了解决这个问题,C++ 使用附加的关键字引入命名空间。
|
||||
|
||||
Java 采取了一种新的方法避免了以上这些问题:为一个库生成一个明确的名称,Java 创建者希望我们反向使用自己的网络域名,因为域名通常是唯一的。自从我的域名为 MindviewInc.com 开始,我就将我的 foibles 工具库命名为 com.mindviewinc.utility.foibles。根据你的域名的反向信息,`.`代表着一个子目录。
|
||||
|
||||
在 Java 1.0和 Java 1.1中,域扩展 com,edu,org,net 等按惯例大写,因此类库中会出现这样类似的名称:Com.mindviewinc.utility.foibles。然而,在 Java 2 的开发过程中,他们发现这会导致问题,所以现在整个包名都是小写的。此机制意味着所有文件都自动存在于自己的命名空间中,文件中的每个类都具有唯一标识符。这样,该语言可以防止名称冲突。
|
||||
|
||||
使用反向 URL 是一种新的命名空间方法,在此之前尚未有其他语言这么做过。Java 中有许多这些“创造性”地解决问题的方法。正如你想象,如果我们未经测试就添加一个功能并用于生产,那么在将来发现该功能的问题再想纠正,通常为时已晚。(有些问题错误得足以从语言中删除。)
|
||||
|
||||
使用反向 URL 将命名空间与文件路径相关联不会导致BUG,但它却给源代码管理带来麻烦。例如在 com.mindviewinc.utility.foiles 这样的目录结构中,我们创建了com、mindviewinc 空目录。它们存在的唯一目的就是用来表示这个反向的 URL。
|
||||
|
||||
这种方式似乎为我们在编写 Java 程序中的某个问题打开了大门。空目录填充了深层次结构,它们不仅用于表示反向 URL,还用于捕获其他信息。这些长路径基本上用于存储有关目录中的内容的数据。如果你希望以最初设计的方式使用目录,这种方法可以从“令人沮丧”到“令人抓狂”,对于产生的 Java 代码,你基本上不得不使用专门为此设计的 IDE 来管理代码。例如 NetBeans,Eclipse 或 IntelliJ IDEA。实际上,这些 IDE 都为我们管理和创建深度空目录层次结构。
|
||||
|
||||
对于这本书中的例子,我不想让深层次的层次结构给你的学习带来额外的麻烦,这实际上需要你在开始之前学习熟悉一种重量级的 IDE。所以,我们的每个章节的示例都位于一个浅的子目录中,以章节标题为名。这导致我偶尔会与遵循深度层次方法的工具发生冲突。
|
||||
|
||||
<!-- Using Other Components -->
|
||||
#### 使用其他组件
|
||||
|
||||
|
||||
无论何时在程序中使用预定义的类,编译器都必须找到该类。在一般情况下,该类已存在于被调用的源代码文件中。此时我们使用该类 —— 即使该类未在文件中稍后定义(Java 消除了所谓的“前向引用”问题)。而那些存在于其他文件中的类怎么样?你可能认为编译器应该足够智能去找到它,但这样是有问题的。想象一下,假如你要使用某个类,但目录中存在多个同名的类(可能用途不同)。或者更糟糕的是,假设你正在编写程序,并且在构建它时,你将向库中添加一个与现有类名称冲突的新类。
|
||||
|
||||
要解决此问题,你必须通过使用 `import` 关键字来告诉 Java 编译器具体要使用的类。import 表示编译器引入一个包,它是一个类库。(在其他语言中,库可以包含函数和数据以及类,但请记住,Java 中的所有活动都在类中进行。)大多数时候,我们都在使用 JAVA 标准库中的组件。有了这些,你不用担心长的反向域名。你只用说,例如:
|
||||
|
||||
```JAVA
|
||||
import java.util.ArrayList;
|
||||
```
|
||||
|
||||
上例可以告诉编译器使用位于标准库 util 下的 ArrayList 类。在 util 包含许多类,我们可以使用通配符 `*` 来导入其中部分类,无需显式导入。代码示例:
|
||||
|
||||
```JAVA
|
||||
import java.util.*;
|
||||
```
|
||||
|
||||
本书中的示例很小,为简单起见,我们通常会使用 `.*` 形式略过导入。然而,许多教程书籍都会要求程序员单独导入每个类。
|
||||
|
||||
|
||||
<!-- The static Keyword -->
|
||||
#### static关键字
|
||||
|
||||
类是对象的外观及行为方式的描述。通常只有在使用 `new` 关键字之后程序才能被分配存储空间以及使用其方法。这种方式在两种情况下是不足的。
|
||||
|
||||
1. 有时你只需要为特定字段分配一个共享存储空间,无论该类创建了多少个对象,或者即使没有创建任何对象;
|
||||
|
||||
2. 创建一个与此类本身任何对象无关的方法。也就是说,即使没有创建对象,也能调用该方法。
|
||||
|
||||
**static** 关键字(从 C++ 采用)就符合我们的要求。当我们说某些东西是静态的时,它意味着该字段或方法不依赖于任何特定的对象实例 。 即使我们从未创建过该类的对象,也可以调用其静态方法或访问静态字段。相反,对于普通的非静态字段和方法,我们必须要先创建一个对象并使用该对象来访问该字段或方法,因为非静态字段和方法必须与替对象关联 [^6] 。
|
||||
|
||||
一些面向对象的语言使用类数据(**class data**)和类方法(**class method**)这样的术语来表述静态。静态的数据意味着该数据和方法仅存在于类中,而非类的任何实例对象中。有时 Java 文献也使用这些术语。我们可以通过在类的属性或方法前添加 `static` 修饰来表示这是一个静态属性或静态方法。 代码示例:
|
||||
|
||||
```JAVA
|
||||
class StaticTest {
|
||||
static int i = 47;
|
||||
}
|
||||
```
|
||||
|
||||
现在,即使你创建了两个 StaticTest 对象,但是静态变量 i 仍只占一份存储空间。两个对象都会共享相同的变量 i。 代码示例:
|
||||
|
||||
```JAVA
|
||||
StaticTest st1 = new StaticTest();
|
||||
StaticTest st2 = new StaticTest();
|
||||
```
|
||||
|
||||
st1.i 和 st2.i 的值都是47,因为它们属于同一段内存。引用静态变量有两种方法。在前面的示例中,我们可以通过一个对象来命名它;例如,st2.i。同时,你也可以通过它的类名直接调用它(这是非静态成员不能执行的操作):
|
||||
|
||||
```JAVA
|
||||
StaticTest.i ++;
|
||||
```
|
||||
|
||||
`++` 运算符将会使变量结果 + 1。此时 st1.i 和 st2.i 的值就变成了48了。
|
||||
|
||||
使用类名直接引用静态变量的首选方法,因为它强调了变量的静态属性。类似的逻辑也适用于静态方法。我们可以通过对象引用静态方法,就像使用任何方法一样,也可以使用特殊的附加语法 classname.method()来直接调用静态属性或方法 [^7]。 代码示例:
|
||||
|
||||
```JAVA
|
||||
class Incrementable {
|
||||
static void increment() {
|
||||
StaticTest.i++;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
上例中 Incrementable 类调用静态方法 increment()。后者再使用 `++` 运算符递增静态变量 int i。我们依然可以先实例化对象再调用该方法。 代码示例:
|
||||
|
||||
```JAVA
|
||||
Incrementable sf = new Incrementable();
|
||||
sf.increment();
|
||||
```
|
||||
|
||||
当然了,首选的方法是直接通过类来调用它。代码示例:
|
||||
|
||||
```JAVA
|
||||
Incrementable.increment();
|
||||
```
|
||||
|
||||
相比非静态的对象,`static` 属性改变了创建数据的方式。同样,当 `static` 关键字修饰方法时,它允许我们无需创建对象就可以直接通过类的引用来调用该方法。正如我们所知,`static` 关键字的这些特性对于应用程序入口点的 main() 方法尤为重要。
|
||||
应用于字段的 `static` 肯定会更改创建数据的方式 —— `static` 针对每个类和非 `static` 针对每个对象。当应用于方法时,`static` 允许您在不创建对象的情况下调用该方法。正如您将看到的,在定义作为运行应用程序入口点的main()方法时,这是非常重要的。
|
||||
|
||||
|
||||
<!-- Your First Java Program -->
|
||||
## 小试牛刀
|
||||
|
||||
最后,我们来开始编写第一个完整的程序。我们使用 Java 标准库来展示一个字符串和日期。
|
||||
|
||||
```JAVA
|
||||
|
||||
// objects/HelloDate.java
|
||||
import java.util.*;
|
||||
|
||||
public class HelloDate {
|
||||
public static void main(String[] args) {
|
||||
System.out.println("Hello, it's: ");
|
||||
System.out.println(new Date());
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在这本书中,代码块的第一行,我将使用注释行,其中包含文件的路径信息(使用本章的目录名对象),后跟文件名。我的工具可以根据这些信息自动提取和测试书籍的代码,你也可以通过参考第一行注释信息轻松地在 Github 库中找到相应的代码示例。
|
||||
|
||||
如果你想在代码中使用到一些额外的库,那么你需要在程序文件的开始处使用 **import** 关键字来导入它们。之所以说是额外的,因为有一些库已经默认自动包含到每个文件里了。例如:**java.lang** 包。
|
||||
|
||||
现在打开你的浏览器在 [Oracle](https://www.oracle.com/) 上查看文档。如果你还没有在 [Oracle](https://www.oracle.com/) 网站上下载 JDK 文档,那就趁现在 [^8] 。查看包列表,你会看到 Java 附带的所有不同的类库。
|
||||
|
||||
选择 **java.lang**。这里显示的是该库中所有类的列表。由于 **java.lang** 隐式包含在每个 Java代码文件中,因此这些类是自动可用的。**java.lang** 中没有列出 **Date** 类,所以我们必须将其导入库才能使用它。如果你不清楚某个类名或者想查看所有的类,可以在 Java 文档中选择“Tree”。
|
||||
|
||||
现在,我们可以找到 Java 附带的每个类。使用浏览器的“查找”功能查找 “Date”。搜索结果中将会列出 java.util.Date,显而易见,它在 util 库中,所以我们必须导入 java.util.* 才能使用 Date。
|
||||
|
||||
如果你在文档中选择 java.lang,然后选择 System,你会看到 System 类中有几个字段,如果你选择 `out`,你会发现它是一个静态的 PrintStream 对象。 所以,即使我们没有使用 new 创建, `out` 对象就已经存在并可以使用。 `out` 对象可以执行的操作取决于 PrintStream 。 其在文档中的描述中显示为超链接,如果单击该链接,我们将可以看到 PrintStream 所对应的方法列表。(更多详情,将在本书后面介绍。) 现在我们重点说的是 println() 这个方法。 它的作用是 “将信息输出到控制台,并以换行符结束。” 既然如此,我们可以这样编码来输出信息到控制台。 代码示例:
|
||||
|
||||
```JAVA
|
||||
|
||||
System.out.println("A String of things");
|
||||
```
|
||||
|
||||
每个 java 源文件中允许有多个类。与此同时,源文件的名称必须要和其中一个类名相同,否则编译器将会报错。每个独立的程序应该包含一个 main 方法作为程序运行的入口。其方法签名和返回类型如下。代码示例:
|
||||
|
||||
```JAVA
|
||||
public static void main(String[] args) {
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
关键字 `public` 表示方法可以被外界访问到。( 更多详情将在 **隐藏实现** 章节讲到)
|
||||
main() 方法的参数是一个 字符串(String) 数组。 参数 `args` 并没有在当前的程序中使用到,但是 Java 虚拟机强制要求必须要有。 这是因为它们被用于保存命令行中的参数。
|
||||
|
||||
下面我们来看一段有趣的代码:
|
||||
|
||||
```JAVA
|
||||
System.out.println(new Date());
|
||||
```
|
||||
|
||||
上面的示例中,我们创建了一个日期(Date)型对象并将其转化为字符串类型并输出到控制台中。 一旦这一行语句执行完毕,我们就不再需要该日期对象了。这时, JAVA 的垃圾回收器就可以将其占用的内存回收,我们无需去主动清除它们。
|
||||
|
||||
查看 JDK 文档,我们可以看到, **System** 类下还有很多其他有用的方法。( Java 的牛逼之处还在于,它拥有一个庞大的标准库资源。) 代码示例:
|
||||
|
||||
```JAVA
|
||||
|
||||
// objects/ShowProperties.java
|
||||
public class ShowProperties {
|
||||
public static void main(String[] args) {
|
||||
System.getProperties().list(System.out);
|
||||
System.out.println(System.getProperty("user.name"));
|
||||
System.out.println(System.getProperty("java.library.path"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
输出结果(前20行):
|
||||
|
||||
```text
|
||||
java.runtime.name=Java(TM) SE Runtime Environment
|
||||
sun.boot.library.path=C:\Program
|
||||
Files\Java\jdk1.8.0_112\jr...
|
||||
java.vm.version=25.112-b15
|
||||
java.vm.vendor=Oracle Corporation
|
||||
java.vendor.url=http://java.oracle.com/
|
||||
path.separator=;
|
||||
java.vm.name=Java HotSpot(TM) 64-Bit Server VM
|
||||
file.encoding.pkg=sun.io
|
||||
user.script=
|
||||
user.country=US
|
||||
sun.java.launcher=SUN_STANDARD
|
||||
sun.os.patch.level=
|
||||
java.vm.specification.name=Java Virtual Machine
|
||||
Specification
|
||||
user.dir=C:\Users\Bruce\Documents\GitHub\on-ja...
|
||||
java.runtime.version=1.8.0_112-b15
|
||||
java.awt.graphicsenv=sun.awt.Win32GraphicsEnvironment
|
||||
java.endorsed.dirs=C:\Program
|
||||
Files\Java\jdk1.8.0_112\jr...
|
||||
os.arch=amd64
|
||||
java.io.tmpdir=C:\Users\Bruce\AppData\Local\Temp\
|
||||
```
|
||||
|
||||
上例主方法中的第一行,会输出所有的系统属性,也就是环境信息。 **list()** 方法将结果发送给它的参数 **System.out** 在本书的后面,我们还会接触到将结果输出到其他地方,如文件中。另外,我们还可以请求特定的属性。该例中我们使用到了 **user.name** 和 **java.library.path**。 末尾的 “/* Output:”标记表示此文件生成的输出的开头。本书中产生输出的大多数示例将会以包含此注释形式的输出,因此我们可以看到输出并知道它是正确的。带有这个标签允许在使用编译器检查并执行后将输出自动更新到本书的文本中。
|
||||
|
||||
<!-- Compiling and Running -->
|
||||
### 编译和运行
|
||||
|
||||
要编译和运行本书中的代码示例,首先必须具有 Java 编程环境。 第二章的示例中描述了安装过程。如果你遵循这些说明,那么你将会在不受 Oracle 的限制的条件下用到 Java 开发人员工具包(JDK)。如果你使用其他开发系统,请查看该系统的文档以确定如何编译和运行程序。 第二章还介绍了如何安装本书的示例。
|
||||
|
||||
移动到 objects 子目录下并键入:
|
||||
|
||||
``` bash
|
||||
javac HelloDate.java
|
||||
```
|
||||
|
||||
此命令不应生成任何响应。如果我们收到任何类型的错误消息,则表示未正确安装 JDK,并且必须检查这些问题。
|
||||
|
||||
此外,仅仅是执行的话,则可以键入:
|
||||
|
||||
```JAVA
|
||||
java HelloDate
|
||||
```
|
||||
我们将会获得信息反馈,日期输出。这是我们编译和运行本书中每个程序(包含main())的过程 [^9]。但是,本书的源代码在根目录中也有一个名为 **build.gradle** 的文件,其中包含用于自动构建,测试和运行本书文件的 **Gradle** 配置。当您第一次运行 `gradlew` 命令时,**Gradle** 将自动安装(前提已安装Java)。
|
||||
|
||||
<!-- Coding Style -->
|
||||
## 编码风格
|
||||
|
||||
|
||||
Java 的代码约定规范(*Code Conventions for the Java Programming Language*)[^10] 要求类名的首字母大写。 如果类名是由多个单词构成的,则每个单词的首字母都应大写(不采用下划线来分隔)例如:
|
||||
|
||||
```JAVA
|
||||
class AllTheColorsOfTheRainbow {
|
||||
// ```
|
||||
```
|
||||
|
||||
|
||||
这种风格也被称之为“驼峰式”。对于几乎所有其他方法,字段(成员变量)和对象引用名称 - 除了标识符的首字母是小写之外其他规范与类的命名一致。代码示例:
|
||||
|
||||
```JAVA
|
||||
class AllTheColorsOfTheRainbow {
|
||||
int anIntegerRepresentingColors;
|
||||
void changeTheHueOfTheColor(int newHue) {
|
||||
// ...
|
||||
}
|
||||
// ...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在 Oracle 的库中,开放式花括号的位置同样遵循和本书中相同的规范。
|
||||
|
||||
|
||||
## 本章小结
|
||||
|
||||
|
||||
本章向您展示了简单的 Java 程序编写以及该语言相关的基本概念。到目前为止,我们的示例都只是些简单的顺序执行。在接下来的两章里,我们将会接触到 Java 的一些基本操作符,以及如何去控制程序执行的流程。
|
||||
|
||||
|
||||
[^1]: 这里可能有争议。有人说这是一个指针,但这假定了一个潜在的实现。此外,Java 引用的语法更类似于 C++ 引用而非指针。在 *Thinking in Java* 的第 1 版中,我发明了一个新术语叫“句柄”(*handle*),因为 C++ 引用和Java 引用有一些重要的区别。作为一个从 C++ 的过来人,我不想混淆 Java 可能的最大受众 —— C++ 程序员。在*Thinking in Java* 的第 2 版中,我认为“引用”(*reference*)是更常用的术语,从 C++ 转过来的人除了引用的术语之外,还有很多东西需要处理,所以他们不妨双脚都跳进去。但是,也有些人甚至不同意“引用”。在某书中我读到一个观点:Java 支持引用传递的说法是完全错误的,因为 Java 对象标识符(根据该作者)实际上是“对象引用”(*object references*),并且一切都是值传递。所以你不是通过引用传递,而是“通过值传递对象引用。人们可以质疑我的这种解释的准确性,但我认为我的方法简化了对概念的理解而又没对语言造成伤害(嗯,语言专家可能会说我骗你,但我会说我只是对此进行了适当的抽象。)
|
||||
|
||||
[^2]: 大多数微处理器芯片都有额外的高速缓冲存储器,但这是按照传统存储器而不是寄存器。
|
||||
|
||||
[^3]: 一个例子是字符串常量池。所有文字字符串和字符串值常量表达式都会自动放入特殊的静态存储中。
|
||||
|
||||
[^4]: 静态方法,我们很快就能接触到,它可以在没有对象的情况下直接被类调用。
|
||||
|
||||
[^5]: 通常除了前面提到的“特殊”数据类型 boolean,char,byte,short,int,long,float 和 double。通常来说,传递对象就意味者传递对象的引用。
|
||||
|
||||
[^6]: 静态方法在使用之前不需要创建对象,因此它们不能直接调用非静态的成员或方法(因为非静态成员和方法必须要先实例化为对象才可以被使用)。
|
||||
|
||||
[^7]: 在某些情况下,它还为编译器提供了更好的优化可能。
|
||||
|
||||
[^8]: 请注意,此文档未包含在 JDK 中;你必须单独下载才能获得它。
|
||||
|
||||
[^9]: 对于本书中编译和运行命令行的每个程序,你可能还需要设置 CLASSPATH 。
|
||||
|
||||
[^10]: 为了保持本书的代码排版紧凑,我并没完全遵守规范,但我尽量会做到符合 Java 标准。
|
||||
<!-- 分页 -->
|
||||
<div style="page-break-after: always;"></div>
|
||||
681
docs/book/04-Operators.md
Normal file
681
docs/book/04-Operators.md
Normal file
@@ -0,0 +1,681 @@
|
||||
[TOC]
|
||||
|
||||
<!-- Operators -->
|
||||
# 第四章 运算符
|
||||
|
||||
|
||||
>运算符操纵数据。
|
||||
|
||||
Java 是从 C++ 的基础上做了一些改进和简化发展而成的。对于 C/C++ 程序员来说,Java 的运算符并不陌生。如果你已了解 C 或 C++,大可以跳过本章和下一章,直接阅读 Java 与 C/C++ 不同的地方。
|
||||
|
||||
如果理解这两章的内容对你来说还有点困难,那么我推荐你先了解下 *Thinking in C* 再继续后面的学习。 这本书现在可以在 [www.OnJava8.com](http://www.OnJava8.com]) 上免费下载。它的内容包含音频讲座、幻灯片、练习和解决方案,专门用于帮助你快速掌握学习 Java 所需的基础知识。
|
||||
|
||||
|
||||
<!-- Using-Java-Operators -->
|
||||
## 使用说明
|
||||
|
||||
|
||||
运算符接受一个或多个参数并生成新值。这个参数与普通方法调用的形式是不同的,但效果是相同的。加法 `+`,减法 `-`,乘法 `*`,除法 `/` 以及赋值 `=` 在任何编程语言中的工作方式都是类似的。所有运算符都能根据自己的运算对象生成一个值。除此以外,一个运算符可改变运算对象的值,这叫作“副作用”(**Side Effect**)。运算符最常见的用途就是修改自己的运算对象,从而产生副作用。但要注意生成的值亦可由没有副作用的运算符生成。
|
||||
|
||||
几乎所有运算符都只能操作基本类型(*Primitives*)。唯一的例外是 `=`、`==` 和 `!=`,它们能操作所有对象(这也是令人混淆的一个地方)。除此以外,**String** 类支持 `+` 和 `+=`。
|
||||
|
||||
|
||||
<!-- Precedence -->
|
||||
## 优先级
|
||||
|
||||
|
||||
运算符的优先级决定了存在多个运算符时一个表达式各部分的计算顺序。Java 对计算顺序作出了特别的规定。其中,最简单的规则就是乘法和除法在加法和减法之前完成。程序员经常都会忘记其他优先级规则,所以应该用括号明确规定计算顺序。代码示例:
|
||||
|
||||
```JAVA
|
||||
// operators/Precedence.java
|
||||
public class Precedence {
|
||||
|
||||
public static void main(String[] args) {
|
||||
int x = 1, y = 2, z = 3;
|
||||
int a = x + y - 2/2 + z; // [1]
|
||||
int b = x + (y - 2)/(2 + z); // [2]
|
||||
System.out.println("a = " + a);
|
||||
System.out.println("b = " + b);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
输出结果:
|
||||
|
||||
```
|
||||
a = 5
|
||||
b = 1
|
||||
```
|
||||
|
||||
这些语句看起来大致相同,但从输出中我们可以看出它们具有非常不同的含义,具体取决于括号的使用。
|
||||
|
||||
我们注意到,在 `System.out.println()` 语句中使用了 `+` 运算符。 但是在这里 `+` 代表的意思是字符串连接符。编译器会将 `+` 连接的非字符串尝试转换为字符串。上例中的输出结果说明了 a 和 b 都已经被转化成了字符串。
|
||||
|
||||
|
||||
<!-- Assignment -->
|
||||
## 赋值
|
||||
|
||||
|
||||
运算符的赋值是由符号 `=` 完成的。它代表着获取 `=` 右边的值并赋给左边的变量。右边可以是任何常量、变量或者是可产生一个返回值的表达式。但左边必须是一个明确的、已命名的变量。也就是说,必须要有一个物理的空间来存放右边的值。举个例子来说,可将一个常数赋给一个变量( A = 4 ),但不可将任何东西赋给一个常数(比如不能 4 = A)。
|
||||
|
||||
基本类型的赋值都是直接的,而不像对象,赋予的只是其内存的引用。举个例子,a = b ,如果 b 是基本类型,那么 赋值操作会将 b 的值复制一根给变量 a, 此后若 a 的值发生改变是不会影响到 b 的。作为一名程序员,着应该成为我们的常识。
|
||||
|
||||
如果是为对象赋值,那么结果就不一样了。对一个对象进行操作时,我们实际上操作的是它的引用。所以我们将右边的对象赋予给左边时,赋予的只是该对象的引用。此时,两者指向的堆中的对象还是同一个。代码示例:
|
||||
|
||||
```JAVA
|
||||
// operators/Assignment.java
|
||||
// Assignment with objects is a bit tricky
|
||||
class Tank {
|
||||
int level;
|
||||
}
|
||||
|
||||
public class Assignment {
|
||||
|
||||
public static void main(String[] args) {
|
||||
Tank t1 = new Tank();
|
||||
Tank t2 = new Tank();
|
||||
t1.level = 9;
|
||||
t2.level = 47;
|
||||
System.out.println("1: t1.level: " + t1.level +
|
||||
", t2.level: " + t2.level);
|
||||
t1 = t2;
|
||||
System.out.println("2: t1.level: " + t1.level +
|
||||
", t2.level: " + t2.level);
|
||||
t1.level = 27;
|
||||
System.out.println("3: t1.level: " + t1.level +
|
||||
", t2.level: " + t2.level);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
输出结果:
|
||||
|
||||
```
|
||||
1: t1.level: 9, t2.level: 47
|
||||
2: t1.level: 47, t2.level: 47
|
||||
3: t1.level: 27, t2.level: 27
|
||||
```
|
||||
|
||||
这是个简单的 `Tank` 类,通过 main 方法 `new` 出了 2 个实例对象。 两个对象的 `level` 属性分别被赋予了不同的值。 然后,t2 的值被赋予给 t1。在许多编程语言里,预期的结果是 t1 和 t2 的值会一直相对独立。但是,在 Java 中,由于赋予的只是对象的引用,改变 t1 也就改变了 t2。 这是因为 t1 和 t2 此时指向的是堆中同一个对象。(t1 原始对象的引用在 t2 赋值给其时被丢失,它就将会在垃圾回收时被清理)。
|
||||
|
||||
这种现象通常称为别名(*aliasing*),这是 Java 处理对象的一种基本方式。但是假若你不想这里出现这样的混淆的话,你可以这么做。代码示例:
|
||||
|
||||
```JAVA
|
||||
t1.level = t2.level;
|
||||
```
|
||||
|
||||
较之前的做法,这样做保留了两个单独的对象,而不是丢弃一个并将 t1 和 t2 绑定到同一个对象。但是这样的操作有点违背 JAVA 的设计原则。对象的赋值是个需要重视的环节,否则你可能收获意外的“惊喜”。
|
||||
|
||||
<!-- Aliasing During Method Calls -->
|
||||
### 方法调用中的别名现象
|
||||
|
||||
当我们把对象传递给方法时,会发生别名现象。
|
||||
|
||||
```JAVA
|
||||
// operators/PassObject.java
|
||||
// 正在传递的对象可能不是你之前使用的
|
||||
class Letter {
|
||||
char c;
|
||||
}
|
||||
|
||||
public class PassObject {
|
||||
static void f(Letter y) {
|
||||
y.c = 'z';
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
Letter x = new Letter();
|
||||
x.c = 'a';
|
||||
System.out.println("1: x.c: " + x.c);
|
||||
f(x);
|
||||
System.out.println("2: x.c: " + x.c);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
输出结果:
|
||||
|
||||
```
|
||||
1: x.c: a
|
||||
2: x.c: z
|
||||
```
|
||||
|
||||
在许多编程语言中,方法 **f()** 似乎在内部复制其参数 **Letter y**。但是一旦传递了一个引用,那么实际上 `y.c ='z';` 是在方法 **f()** 之外改变对象。别名现象以及其解决方案是个复杂的问题,在附录中有包含:[对象传递和返回](./Appendix-Passing-and-Returning-Objects.md)。意识到这一点,我们可以警惕类似的陷阱。
|
||||
|
||||
|
||||
<!-- Mathematical Operators -->
|
||||
## 算术运算符
|
||||
|
||||
Java 的基本算术运算符与其他大多编程语言是相同的。其中包括加号 `+`、减号 `-`、除号 `/`、乘号 `*` 以及模数`%`(从整数除法中获得余数)。整数除法会直接砍掉小数,而不是进位。
|
||||
|
||||
Java 也用一种简写形式同时进行运算和赋值操作,由运算符后跟等号表示,并且与语言中的所有运算符一致(只要有意义)。为了将 4 的值赋予给变量 x 同时将结果赋予给 x , 可用 x += 4 来表示。下面带来代码更多代码示例:
|
||||
|
||||
|
||||
```JAVA
|
||||
// operators/MathOps.java
|
||||
// The mathematical operators
|
||||
import java.util.*;
|
||||
|
||||
public class MathOps {
|
||||
public static void main(String[] args) {
|
||||
// Create a seeded random number generator:
|
||||
Random rand = new Random(47);
|
||||
int i, j, k;
|
||||
// Choose value from 1 to 100:
|
||||
j = rand.nextInt(100) + 1;
|
||||
System.out.println("j : " + j);
|
||||
k = rand.nextInt(100) + 1;
|
||||
System.out.println("k : " + k);
|
||||
i = j + k;
|
||||
System.out.println("j + k : " + i);
|
||||
i = j - k;
|
||||
System.out.println("j - k : " + i);
|
||||
i = k / j;
|
||||
System.out.println("k / j : " + i);
|
||||
i = k * j;
|
||||
System.out.println("k * j : " + i);
|
||||
i = k % j;
|
||||
System.out.println("k % j : " + i);
|
||||
j %= k;
|
||||
System.out.println("j %= k : " + j);
|
||||
// 浮点运算测试
|
||||
float u, v, w; // Applies to doubles, too
|
||||
v = rand.nextFloat();
|
||||
System.out.println("v : " + v);
|
||||
w = rand.nextFloat();
|
||||
System.out.println("w : " + w);
|
||||
u = v + w;
|
||||
System.out.println("v + w : " + u);
|
||||
u = v - w;
|
||||
System.out.println("v - w : " + u);
|
||||
u = v * w;
|
||||
System.out.println("v * w : " + u);
|
||||
u = v / w;
|
||||
System.out.println("v / w : " + u);
|
||||
// 下面的操作同样适用于 char,
|
||||
// byte, short, int, long, and double:
|
||||
u += v;
|
||||
System.out.println("u += v : " + u);
|
||||
u -= v;
|
||||
System.out.println("u -= v : " + u);
|
||||
u *= v;
|
||||
System.out.println("u *= v : " + u);
|
||||
u /= v;
|
||||
System.out.println("u /= v : " + u);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
输出结果:
|
||||
|
||||
```
|
||||
j : 59
|
||||
k : 56
|
||||
j + k : 115
|
||||
j - k : 3
|
||||
k / j : 0
|
||||
k * j : 3304
|
||||
k % j : 56
|
||||
j %= k : 3
|
||||
v : 0.5309454
|
||||
w : 0.0534122
|
||||
v + w : 0.5843576
|
||||
v - w : 0.47753322
|
||||
v * w : 0.028358962
|
||||
v / w : 9.940527
|
||||
u += v : 10.471473
|
||||
u -= v : 9.940527
|
||||
u *= v : 5.2778773
|
||||
u /= v : 9.940527
|
||||
```
|
||||
|
||||
为了生成随机数字,程序首先创建一个 **Random** 对象。不带参数的 **Random** 对象会利用当前的时间用作随机数生成器的“种子”(*seed*),从而为程序的每次执行生成不同的输出。在本书的示例中,重要的是每个示例末尾的输出尽可能一致,以便可以使用外部工具进行验证。所以我们通过在创建 **Random** 对象时提供种子(随机数生成器的初始化值,其始终为特定种子值产生相同的序列),让程序每次执行都生成相同的随机数,如此以来输出结果就是可验证的 [^1]。 若需要生成随机值,可删除代码示例中的种子参数。该对象通过调用方法 **nextInt()** 和 **nextFloat()**(还可以调用 **nextLong()** 或 **nextDouble()**),使用 **Random** 对象生成许多不同类型的随机数。**nextInt()** 的参数设置生成的数字的上限,下限为零,为了避免零除的可能性,结果偏移1。
|
||||
|
||||
|
||||
<!-- Unary Minus and Plus Operators -->
|
||||
### 一元加减运算符
|
||||
|
||||
一元加 `+` 减 `-` 运算符的操作和二元是相同的。编译器可自动识别使用何种方式解析运算:
|
||||
|
||||
```JAVA
|
||||
x = -a;
|
||||
```
|
||||
|
||||
上例的代码表意清晰,编译器可正确识别。下面再看一个示例:
|
||||
|
||||
```JAVA
|
||||
x = a * -b;
|
||||
```
|
||||
|
||||
虽然编译器可以正确的识别,但是程序员可能会迷惑。为了避免混淆,推荐下面的写法:
|
||||
|
||||
```JAVA
|
||||
x = a * (-b);
|
||||
```
|
||||
|
||||
一元减号可以得到数据的负值。一元加号的作用相反,不过它唯一能影响的就是把较小的数值类型自动转换为了 **int** 类型。
|
||||
|
||||
|
||||
<!-- Auto-Increment-and-Decrement -->
|
||||
## 递增和递减
|
||||
|
||||
|
||||
和 C 语言类似,Java 提供了许多快捷运算方式。快捷运算可使代码可读性,可写性都更强。其中包括递增 `++` 和递减 `--`,意为“增加或减少一个单位”。举个例子来说,假设 a 是一个 **int** 类型的值,则表达式 `++a` 就等价于 `a = a + 1`。 递增和递减运算符不仅可以修改变量,还可以生成变量的值。
|
||||
|
||||
每种类型的运算符,都有两个版本可供选用;通常将其称为“前缀版”和“后缀版”。“前递增”表示 `++` 运算符位于变量或表达式的前面;而“后递增”表示 `++` 运算符位于变量或表达式的后面。类似地,“前递减”意味着 `--` 运算符位于变量或表达式的前面;而“后递减”意味着 `--` 运算符位于变量或表达式的后面。对于前递增和前递减(如 `++a` 或 `--a`),会先执行递增/减运算,再返回值。而对于后递增和后递减(如 `a++` 或 `a--`),会先返回值,再执行递增/减运算。代码示例:
|
||||
|
||||
```JAVA
|
||||
// operators/AutoInc.java
|
||||
// 演示 ++ 和 -- 运算符
|
||||
public class AutoInc {
|
||||
public static void main(String[] args) {
|
||||
int i = 1;
|
||||
System.out.println("i: " + i);
|
||||
System.out.println("++i: " + ++i); // 前递增
|
||||
System.out.println("i++: " + i++); // 后递增
|
||||
System.out.println("i: " + i);
|
||||
System.out.println("--i: " + --i); // 前递减
|
||||
System.out.println("i--: " + i--); // 后递减
|
||||
System.out.println("i: " + i);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
输出结果:
|
||||
|
||||
```
|
||||
i: 1
|
||||
++i: 2
|
||||
i++: 2
|
||||
i: 3
|
||||
--i: 2
|
||||
i--: 2
|
||||
i: 1
|
||||
```
|
||||
|
||||
对于前缀形式,我们将在执行递增/减操作后获取值;使用后缀形式,我们将在执行递增/减操作之前获取值。它们是唯一具有“副作用”的运算符(除那些涉及赋值的以外) —— 它们既改变操作数又改变值。
|
||||
|
||||
C++ 名称来自于递增运算符,同时也代表着“比 C 更进一步”。在早期的 Java 演讲中,*Bill Joy*( Java 创建者之一)说“**Java = C ++ --**”(C++ 减减)。这意味着 Java 是在 C++ 的基础上减少了许多不必要的东西,因此语言更简单。随着进一步地学习,我们会发现 Java 的确有许多地方相对 C++ 来说更简便,但是在其他方面,难度并不会比 C++ 小多少。
|
||||
|
||||
|
||||
<!-- Relational-Operators -->
|
||||
## 关系运算符
|
||||
|
||||
|
||||
关系运算符会通过产生一个布尔(**boolean**)结果来表示被操作的数值之间的关系。如果关系为真,则结果为 **true**,如果关系非真,则结果为 **false**。关系运算符包括小于 `<`,大于 `>`,小于或等于 `<=`,大于或等于 `>=`,等价 `==` 和不等价 `!=`。`==` 和 `!=` 可与所有基本类型搭配使用。但与其他类型的比较就不太适合了,因为布尔值只能表示 **true** 或 **false**,所以比较它们之间的“大于”或“小于”没有意义。
|
||||
|
||||
<!-- Testing Object Equivalence -->
|
||||
### 测试对象等价
|
||||
|
||||
关系运算符 `==` 和 `!=` 同样适用于所有对象之间的比较运算,但产生的结果却经常混淆 Java 的初学者。下面是代码示例:
|
||||
|
||||
```JAVA
|
||||
// operators/Equivalence.java
|
||||
public class Equivalence {
|
||||
public static void main(String[] args) {
|
||||
Integer n1 = 47;
|
||||
Integer n2 = 47;
|
||||
System.out.println(n1 == n2);
|
||||
System.out.println(n1 != n2);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
输出结果:
|
||||
|
||||
```
|
||||
true
|
||||
false
|
||||
```
|
||||
|
||||
上例的结果看起来是我们所期望的。但其实事情并非那么简单。下面我们来创建自己的类:
|
||||
|
||||
```JAVA
|
||||
// operators/EqualsMethod2.java
|
||||
// 默认的 equals() 方法没有比较内容
|
||||
class Value {
|
||||
int i;
|
||||
}
|
||||
|
||||
public class EqualsMethod2 {
|
||||
public static void main(String[] args) {
|
||||
Value v1 = new Value();
|
||||
Value v2 = new Value();
|
||||
v1.i = v2.i = 100;
|
||||
System.out.println(v1.equals(v2));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
输出结果:
|
||||
|
||||
```
|
||||
false
|
||||
```
|
||||
|
||||
上例的结果再次令人困惑:结果是错误的。原因: **equals()** 的默认行为是比较对象的引用而非具体内容。因此,除非你在新类中重写 **equals()** 方法,否则我们将获取不到想要的结果。不幸的是,在学习 [复用](./08-Reuse.md)(**Reuse**) 章节后我们才能接触到“覆盖”(**override**),并且直到 [附录:集合主题](./Appendix-Collection-Topics.md),才能知道定义 **equals()** 方法的正确方式,但是现在明白 **equals()** 行为方式也可能为你节省一些时间。
|
||||
|
||||
大多数 Java 库类通过覆写 **equals()** 方法比较对象的内容而不是其引用。
|
||||
|
||||
<!-- Logical-Operators -->
|
||||
## 逻辑运算符
|
||||
|
||||
每个逻辑运算符 `&&` (**AND**)、`||`(**OR**)和 `!`(**非**)根据参数的逻辑关系生成布尔值 `true` 或 `false`。下面的代码示例使用了关系运算符和逻辑运算符:
|
||||
|
||||
```JAVA
|
||||
// operators/Bool.java
|
||||
// 关系运算符和逻辑运算符
|
||||
import java.util.*;
|
||||
public class Bool {
|
||||
public static void main(String[] args) {
|
||||
Random rand = new Random(47);
|
||||
int i = rand.nextInt(100);
|
||||
int j = rand.nextInt(100);
|
||||
System.out.println("i = " + i);
|
||||
System.out.println("j = " + j);
|
||||
System.out.println("i > j is " + (i > j));
|
||||
System.out.println("i < j is " + (i < j));
|
||||
System.out.println("i >= j is " + (i >= j));
|
||||
System.out.println("i <= j is " + (i <= j));
|
||||
System.out.println("i == j is " + (i == j));
|
||||
System.out.println("i != j is " + (i != j));
|
||||
// 将 int 作为布尔处理不是合法的 Java 写法
|
||||
//- System.out.println("i && j is " + (i && j));
|
||||
//- System.out.println("i || j is " + (i || j));
|
||||
//- System.out.println("!i is " + !i);
|
||||
System.out.println("(i < 10) && (j < 10) is "
|
||||
+ ((i < 10) && (j < 10)) );
|
||||
System.out.println("(i < 10) || (j < 10) is "
|
||||
+ ((i < 10) || (j < 10)) );
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
输出结果:
|
||||
|
||||
```
|
||||
i = 58
|
||||
j = 55
|
||||
i > j is true
|
||||
i < j is false
|
||||
i >= j is true
|
||||
i <= j is false
|
||||
i == j is false
|
||||
i != j is true
|
||||
(i < 10) && (j < 10) is false
|
||||
(i < 10) || (j < 10) is false
|
||||
```
|
||||
|
||||
在 JAVA 逻辑运算中,我们不能像 C/C++ 那样使用非布尔值, 而仅能使用 **AND**、**OR**、**NOT**。
|
||||
下面是一次错误的尝试: ~~a || -~~。 但是,后续表达式使用关系比较生成布尔值,然后对结果使用逻辑运算结果。请注意,如果在预期为 **String** 类型的位置使用 **boolean**类型的值,则结果会自动转为适当的文本格式。
|
||||
|
||||
我们可以将前一个程序中 **int** 的定义替换为除 **boolean** 之外的任何其他基本数据类型。
|
||||
但请注意,**float** 类型的数值比较非常严格。只要最小位部分的数字不同则两个数值之间的比较仍然是“非等”的;只要数字最小位是大于 0 的,那么它就不等于 0。
|
||||
|
||||
|
||||
<!-- Short-Circuiting -->
|
||||
### 短路
|
||||
|
||||
逻辑运算符支持一种称为“短路”(*short-circuiting*)的现象。整个表达式会在运算到可以明确结果时就停止并返回结果,这意味着该逻辑表达式的后半部分不会被执行到。代码示例:
|
||||
|
||||
```JAVA
|
||||
// operators / ShortCircuit.java
|
||||
// 逻辑运算符的短路行为
|
||||
public class ShortCircuit {
|
||||
|
||||
static boolean test1(int val) {
|
||||
System.out.println("test1(" + val + ")");
|
||||
System.out.println("result: " + (val < 1));
|
||||
return val < 1;
|
||||
}
|
||||
|
||||
static boolean test2(int val) {
|
||||
System.out.println("test2(" + val + ")");
|
||||
System.out.println("result: " + (val < 2));
|
||||
return val < 2;
|
||||
}
|
||||
|
||||
static boolean test3(int val) {
|
||||
System.out.println("test3(" + val + ")");
|
||||
System.out.println("result: " + (val < 3));
|
||||
return val < 3;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
boolean b = test1(0) && test2(2) && test3(2);
|
||||
System.out.println("expression is " + b);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
输出结果:
|
||||
|
||||
```
|
||||
test1(0)
|
||||
result: true
|
||||
test2(2)
|
||||
result: false
|
||||
expression is false
|
||||
```
|
||||
|
||||
每个测试都对参数执行比较并返回 `true` 或 `false`。同时控制台也会在方法执行时打印他们的执行状态。 下面的表达式:
|
||||
|
||||
```JAVA
|
||||
test1(0)&& test2(2)&& test3(2)
|
||||
```
|
||||
|
||||
可能你的预期是程序会执行 3 个 **test** 方法并返回。我们来分析一下:第一个方法的结果返回 `true`,因此表达式会继续走下去。紧接着,第二个方法的返回结果是 `false`。这就代表这整个表达式的结果肯定为 `false`,所以就没有必要再判断剩下的表达式部分了。
|
||||
|
||||
所以,运用“短路”可以节省部分不必要的运算,从而提高程序潜在的性能。
|
||||
|
||||
|
||||
<!-- Literals -->
|
||||
## 字面值常量
|
||||
|
||||
|
||||
通常,当我们向程序中插入一个字面值常量(**Literal**)时,编译器会确切地识别它的类型。当类型不明确时,必须辅以字面值常量关联来帮助编译器识别。代码示例:
|
||||
|
||||
```JAVA
|
||||
// operators/Literals.java
|
||||
public class Literals {
|
||||
public static void main(String[] args) {
|
||||
int i1 = 0x2f; // 16进制 (小写)
|
||||
System.out.println(
|
||||
"i1: " + Integer.toBinaryString(i1));
|
||||
int i2 = 0X2F; // 16进制 (大写)
|
||||
System.out.println(
|
||||
"i2: " + Integer.toBinaryString(i2));
|
||||
int i3 = 0177; // 8进制 (前导0)
|
||||
System.out.println(
|
||||
"i3: " + Integer.toBinaryString(i3));
|
||||
char c = 0xffff; // 最大 char 型16进制值
|
||||
System.out.println(
|
||||
"c: " + Integer.toBinaryString(c));
|
||||
byte b = 0x7f; // 最大 byte 型16进制值 10101111;
|
||||
System.out.println(
|
||||
"b: " + Integer.toBinaryString(b));
|
||||
short s = 0x7fff; // 最大 short 型16进制值
|
||||
System.out.println(
|
||||
"s: " + Integer.toBinaryString(s));
|
||||
long n1 = 200L; // long 型后缀
|
||||
long n2 = 200l; // long 型后缀 (容易与数值1混淆)
|
||||
long n3 = 200;
|
||||
|
||||
// Java 7 二进制字面值常量:
|
||||
byte blb = (byte)0b00110101;
|
||||
System.out.println(
|
||||
"blb: " + Integer.toBinaryString(blb));
|
||||
short bls = (short)0B0010111110101111;
|
||||
System.out.println(
|
||||
"bls: " + Integer.toBinaryString(bls));
|
||||
int bli = 0b00101111101011111010111110101111;
|
||||
System.out.println(
|
||||
"bli: " + Integer.toBinaryString(bli));
|
||||
long bll = 0b00101111101011111010111110101111;
|
||||
System.out.println(
|
||||
"bll: " + Long.toBinaryString(bll));
|
||||
float f1 = 1;
|
||||
float f2 = 1F; // float 型后缀
|
||||
float f3 = 1f; // float 型后缀
|
||||
double d1 = 1d; // double 型后缀
|
||||
double d2 = 1D; // double 型后缀
|
||||
// (long 型的字面值同样适用于十六进制和8进制 )
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
输出结果:
|
||||
|
||||
```
|
||||
i1: 101111
|
||||
i2: 101111
|
||||
i3: 1111111
|
||||
c: 1111111111111111
|
||||
b: 1111111
|
||||
s: 111111111111111
|
||||
blb: 110101
|
||||
bls: 10111110101111
|
||||
bli: 101111101011111010111110101111
|
||||
bll: 101111101011111010111110101111
|
||||
```
|
||||
|
||||
在文本值的后面添加字面值常量可以让编译器识别该文本值的类型。对于 **Long** 型数值,结尾使用大写 `L` 或小写 `l` 皆可(不推荐使用 `l`,因为容易与阿拉伯数值 1 混淆)。大写 `F` 或小写 `f` 表示 **float** 浮点数。大写 `D` 或小写 `d` 表示 **double** 双精度。
|
||||
|
||||
|
||||
十六进制(以 16 为基数),适用于所有整型数据类型,由前导 `0x` 或 `0x` 表示,后跟 0-9 或 a-f (大写或小写)。如果我们在初始化某个类型的数值时,赋值超出其范围,那么编译器会报错(不管值的数字形式如何)。在上例的代码中,**char**、**byte** 和 **short** 的值已经是最大了。如果超过这些值,编译器将自动转型为 **int**,并且我们需要声明强制转换(强制转换将在本章后面定义)。这告诉我们已越过该类型的范围界限。
|
||||
|
||||
八进制(以 8 为基数)由 0~7 之间的数字和前导零 `0` 表示。
|
||||
|
||||
Java 7 引入了二进制的字面值常量,由前导 `0B` 或 `0B` 表示,它可以初始化所有的整数类型。
|
||||
|
||||
使用整型数值类型时,显示其二进制形式会很有用。在 Long 型和 Integer 型中这很容易实现,调用其静态的 **toBinaryString()** 方法即可。 但是请注意,若将较小的类型传递给 **Integer.tobinarystring()** 时,类型将自动转换为 **int**。
|
||||
|
||||
<!-- Underscores in Literals -->
|
||||
### 下划线
|
||||
|
||||
|
||||
Java 7 中有一个深思熟虑的补充:我们可以在数字文字中包含下划线 `_`,以使结果更清晰。这对于大数值的分组数字特别有用。代码示例:
|
||||
|
||||
|
||||
```JAVA
|
||||
// operators/Underscores.java
|
||||
public class Underscores {
|
||||
public static void main(String[] args) {
|
||||
double d = 341_435_936.445_667;
|
||||
System.out.println(d);
|
||||
int bin = 0b0010_1111_1010_1111_1010_1111_1010_1111;
|
||||
System.out.println(Integer.toBinaryString(bin));
|
||||
System.out.printf("%x%n", bin); // [1]
|
||||
long hex = 0x7f_e9_b7_aa;
|
||||
System.out.printf("%x%n", hex);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
输出结果:
|
||||
|
||||
```
|
||||
3.41435936445667E8
|
||||
101111101011111010111110101111
|
||||
2fafafaf
|
||||
7fe9b7aa
|
||||
```
|
||||
|
||||
|
||||
下面是合理使用的规则:
|
||||
|
||||
1. 仅限单 `_`,不能多条相连。
|
||||
2. 数值开头和结尾不允许出现 `_`。
|
||||
3. `F`、`D` 和 `L`的前后禁止出现 `_`。
|
||||
4. 二进制前导 `b` 和 十六进制 `x` 前后禁止出现 `_`。
|
||||
|
||||
|
||||
[1] 注意 `%n`的使用。熟悉 C 风格的程序员可能习惯于看到 `\n` 来表示换行符。问题在于它给你的是一个“Unix风格”的换行符。此外,如果我们使用的是 Windows,则必须指定 `\r\n`。这种差异的包袱应该由编程语言来解决。这就是Java用 `%n` 实现的,忽略平台间差异生成适当的换行符。但当你使用 `System.out.printf()` 或 `System.out.format()` 时。对于 `System.out.println()`,我们仍然必须使用 `\n`;如果你使用 `%n`,`println()` 只会输出 `%n` 而不是换行符。
|
||||
|
||||
|
||||
<!-- Exponential Notation -->
|
||||
### 指数计数法
|
||||
|
||||
指数总是采用一种我认为很不直观的记号方法:
|
||||
|
||||
```JAVA
|
||||
// operators/Exponents.java
|
||||
// "e" 表示 10 的几次幂
|
||||
public class Exponents {
|
||||
public static void main(String[] args) {
|
||||
// 大写 E 和小写 e 的效果相同:
|
||||
float expFloat = 1.39e-43f;
|
||||
expFloat = 1.39E-43f;
|
||||
System.out.println(expFloat);
|
||||
double expDouble = 47e47d; // 'd' 是可选的
|
||||
double expDouble2 = 47e47; // 自动转换为 double
|
||||
System.out.println(expDouble);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
输出结果:
|
||||
|
||||
```
|
||||
1.39E-43
|
||||
4.7E48
|
||||
```
|
||||
|
||||
在科学与工程学领域,“e”代表自然对数的基数,约等于 2.718 (Java 一种更精确的 **double** 值采用 **Math.E** 的形式)。它在类似 “1.39×e 的 -47 次方”这样的指数表达式中使用,意味着“1.39×2.718 的-47次方”。然而,自 FORTRAN 语言发明后,人们自然而然地觉得e 代表 “10几次幂”。这种做法显得颇为古怪,因为 FORTRAN 最初面向的是科学与工程设计领域。
|
||||
|
||||
理所当然,它的设计者应对这样的混淆概念持谨慎态度 [^2]。但不管怎样,这种特别的表达方法在 C,C++ 以及现在的 Java 中顽固地保留下来了。所以倘若习惯 e 作为自然对数的基数使用,那么在 Java 中看到类似“1.39e-47f”这样的表达式时,请转换你的思维,从程序设计的角度思考它;它真正的含义是“1.39×10 的-47次方”。
|
||||
|
||||
注意如果编译器能够正确地识别类型,就不必使用尾随字面值常量。对于下述语句:
|
||||
|
||||
```JAVA
|
||||
long n3 = 200;
|
||||
```
|
||||
|
||||
它并不存在含糊不清的地方,所以 200 后面的 L 大可省去。然而,对于下述语句:
|
||||
|
||||
```
|
||||
float f4 = 1e-47f; //10 的幂数
|
||||
```
|
||||
|
||||
编译器通常会将指数作为 **double** 类型来处理,所以假若没有这个尾随的 `f`,编辑器就会报错,提示我们应该将 **double** 型转换成 **float** 型。
|
||||
|
||||
|
||||
<!-- Bitwise-Operators -->
|
||||
## 按位运算符
|
||||
|
||||
|
||||
<!-- Shift Operators -->
|
||||
## 移位运算符
|
||||
|
||||
|
||||
<!-- Ternary-if-else-Operator -->
|
||||
## 三元运算符
|
||||
|
||||
|
||||
<!-- String-Operator-+-and-+= -->
|
||||
## 字符串运算符
|
||||
|
||||
|
||||
<!-- Common-Pitfalls-When-Using-Operators -->
|
||||
## 常见陷阱
|
||||
|
||||
|
||||
<!-- Casting-Operators -->
|
||||
## 类型转换
|
||||
|
||||
|
||||
<!-- Java-Has-No-sizeof -->
|
||||
## Java没有sizeof
|
||||
|
||||
|
||||
<!-- A-Compendium-of-Operators -->
|
||||
## 运算符总结
|
||||
|
||||
|
||||
<!-- Summary -->
|
||||
## 本章小结
|
||||
|
||||
|
||||
<!-- 分页 -->
|
||||
<div style="page-break-after: always;"></div>
|
||||
789
docs/book/05-Control-Flow.md
Normal file
789
docs/book/05-Control-Flow.md
Normal file
@@ -0,0 +1,789 @@
|
||||
[TOC]
|
||||
|
||||
# 第五章 控制流
|
||||
|
||||
> 程序操纵它的世界并做出选择。 在Java中,您可以使用执行控制语句进行选择。
|
||||
|
||||
Java 使用 C 的所有执行控制语句,因此如果您使用 C 或 C++ 进行编程,那么您所看到的大部分内容都是熟悉的。 大多数过程编程语言都有某种控制语句,并且语言之间经常存在重叠。在 Java 中,关键字包括 **if-else,while,do-while,for,return,break** 和名为 **switch** 的选择语句。 但是,Java 并不支持备受诟病的 **goto**(它仍然是解决某些类型问题的最有效方法)。 你仍然可以进行类似goto的跳转,但它比其他语言中的 **goto** 更受限制。
|
||||
|
||||
[TOC]
|
||||
|
||||
## true和flase
|
||||
|
||||
条件表达式的示例是 **a == b**。这里有一个条件表达式示例,它使用条件操作符 **==** 来查看 **a** 的值是否等于 **b** 的值。 表达式返回 **true** 或 **false**。 如果显示条件表达式的结果,则会生成表示布尔值的字符串 **true** 和 **false**。
|
||||
|
||||
```java
|
||||
// control/TrueFalse.java
|
||||
public class TrueFalse {
|
||||
public static void main(String[] args) {
|
||||
System.out.println(1 == 1);
|
||||
System.out.println(1 == 2);
|
||||
}
|
||||
}
|
||||
/* Output:
|
||||
true false */
|
||||
```
|
||||
|
||||
您在上一章中看到的任何关系运算符都可以生成条件语句。 请注意,Java 不允许您使用数字作为布尔值,即使它在 C 和 C++ 中允许(在这些语言中,“真”为非零,而“假”是零)。如果想在布尔测试中使用非布尔值,例如**if (a)**,必须首先使用一个条件表达式将其转换为布尔值,例如 **if (a!= 0)**。
|
||||
|
||||
## if-else
|
||||
|
||||
**if-else** 语句是控制程序流程的最基本的形式。 其中的 **else** 是可选的,因此您可以使用两种形式的 **if**:
|
||||
|
||||
```java
|
||||
if(Boolean-expression)
|
||||
statement
|
||||
```
|
||||
|
||||
或
|
||||
|
||||
```java
|
||||
if(Boolean-expression)
|
||||
statement
|
||||
else
|
||||
statement
|
||||
```
|
||||
|
||||
布尔表达式必须生成布尔结果,statement 指用分号结尾的简单语句,或复合语句——封闭在花括号内的一组简单语句。 每当使用 “statement” 一词时,它总是暗示语句可以是简单的或复合的。
|
||||
|
||||
作为 **if-else** 的一个例子,下面这个 **test()** 方法可以告诉您,您猜的数大于、小于还是等于目标数:
|
||||
|
||||
```java
|
||||
// control/IfElse.java
|
||||
public class IfElse {
|
||||
static int result = 0;
|
||||
static void test(int testval, int target) {
|
||||
if(testval > target)
|
||||
result = +1;
|
||||
else if(testval < target) // [1]
|
||||
result = -1;
|
||||
else
|
||||
result = 0; // Match
|
||||
}
|
||||
public static void main(String[] args) {
|
||||
test(10, 5);
|
||||
System.out.println(result);
|
||||
test(5, 10);
|
||||
System.out.println(result);
|
||||
test(5, 5);
|
||||
System.out.println(result);
|
||||
}
|
||||
}
|
||||
/* Output:
|
||||
1
|
||||
-1
|
||||
0
|
||||
*/
|
||||
```
|
||||
|
||||
**[1]** “else if” 不是新关键字,只是一个 else 后跟一个新的 if 语句。
|
||||
|
||||
尽管 Java(如之前的 C 和 C++)是一种“自由格式”语言,但通常会缩进控件流语句的主体,以便读者可以轻松确定它的开始和结束位置。
|
||||
|
||||
<!--Iteration Statements-->
|
||||
|
||||
## 迭代语句
|
||||
|
||||
循环由 **while**,**do-while** 和 **for** 控制,有时称为迭代语句。 语句会重复执行,直到起控制作用的 **布尔表达式** (Boolean expression)计算为“假”。
|
||||
|
||||
|
||||
### while
|
||||
|
||||
while循环的形式是:
|
||||
|
||||
```java
|
||||
while(Boolean-expression)
|
||||
statement
|
||||
```
|
||||
|
||||
在循环开始时,会计算一次布尔表达式的值,而在语句的每次进一步迭代之前再次计算一次。
|
||||
|
||||
下面这个简单的例子将可产生随机数,直到满足特定条件:
|
||||
|
||||
```java
|
||||
// control/WhileTest.java
|
||||
|
||||
public class WhileTest {
|
||||
static boolean condition() {
|
||||
boolean result = Math.random() < 0.99;
|
||||
System.out.print(result + ", ");
|
||||
return result;
|
||||
}
|
||||
public static void main(String[] args) {
|
||||
while(condition())
|
||||
System.out.println("Inside 'while'");
|
||||
System.out.println("Exited 'while'");
|
||||
}
|
||||
}
|
||||
/* Output: (First and Last 5 Lines)
|
||||
true, Inside 'while'
|
||||
true, Inside 'while'
|
||||
true, Inside 'while'
|
||||
true, Inside 'while'
|
||||
true, Inside 'while'
|
||||
...________...________...________...________...
|
||||
true, Inside 'while'
|
||||
true, Inside 'while'
|
||||
true, Inside 'while'
|
||||
true, Inside 'while'
|
||||
false, Exited 'while'
|
||||
*/
|
||||
```
|
||||
|
||||
**condition()** 方法用到了 **Math** 库里的 **static** (静态)方法 **random()**,该方法的作用是产生 0 和 1 之间 (包括 0,但不包括 1) 的一个**double**值。**result** 的值是通过比较操作符<而得到它的,这个操作符产生 **boolean** 类型的结果。在打印 **boolean** 类型的值时,将自动地得到适合的字符串 **true** 或 **false**。while 的条件表达式意思是说:“只要 **condition()**返回 **true**,就重复执行循环体中的语句”。
|
||||
|
||||
### do-while
|
||||
|
||||
**do-while** 的格式如下:
|
||||
|
||||
```java
|
||||
do
|
||||
statement
|
||||
while(Boolean-expression);
|
||||
```
|
||||
|
||||
**while** 和 **do-while** 之间的唯一区别是 **do-while** 的语句总是至少执行一次,即使表达式第一次计算为 false 也是如此。 在 **while** 循环结构中,如果条件第一次就为 **false**,那么其中的语句根本不会执行。在实际应用中,**while** 比 **do-while** 更常用一点。
|
||||
|
||||
### for
|
||||
|
||||
**for** 循环可能是最常用的迭代形式。 该循环在第一次迭代之前执行初始化。 随后,它会执行条件测试,并在每次迭代结束时,进行某种形式的“步进”。**for** 循环的形式是:
|
||||
|
||||
```java
|
||||
for(initialization; Boolean-expression; step)
|
||||
statement
|
||||
```
|
||||
|
||||
初始化 (initialization) 表达式、布尔表达式 (Boolean-expression) ,或者步进 (step) 运算,都可以为空。在每次迭代之前都会测试表达式,并且一旦计算结果为 false,就会在 for 语句后面的行继续执行。 在每个循环结束时,执行一次步进。
|
||||
|
||||
**for**循环通常用于“计数”任务:
|
||||
|
||||
```java
|
||||
// control/ListCharacters.java
|
||||
|
||||
public class ListCharacters {
|
||||
public static void main(String[] args) {
|
||||
for(char c = 0; c < 128; c++)
|
||||
if(Character.isLowerCase(c))
|
||||
System.out.println("value: " + (int)c +
|
||||
" character: " + c);
|
||||
}
|
||||
}
|
||||
/* Output: (First 10 Lines)
|
||||
value: 97 character: a
|
||||
value: 98 character: b
|
||||
value: 99 character: c
|
||||
value: 100 character: d
|
||||
value: 101 character: e
|
||||
value: 102 character: f
|
||||
value: 103 character: g
|
||||
value: 104 character: h
|
||||
value: 105 character: i
|
||||
value: 106 character: j
|
||||
...
|
||||
*/
|
||||
```
|
||||
|
||||
请注意,变量 **c** 在程序用到它的地方被定义的,也就是在for循环的控制表达式内,而不是在 **main()** 的开头。**c** 的范围是由 **for** 控制的语句。
|
||||
|
||||
像C这样的传统过程语言要求在块的开头定义所有变量。 当编译器创建块时,它可以为这些变量分配空间。 在Java和C ++中,您可以在整个块中传播变量声明,并在需要时定义它们。 这允许更自然的编码风格并使代码更容易理解。[1]
|
||||
|
||||
这个程序也使用了 **java.lang.Character** 包装器类,这个类不但能把 **char** 基本类型的值包装进对象,还提供了一些别的有用的方法。这里用到了 **static isLowerCase()** 方法来检查问题中的字符是否为小写字母。
|
||||
|
||||
<!--The Comma Operator-->
|
||||
|
||||
#### 逗号操作符
|
||||
|
||||
逗号运算符(不是逗号分隔符,逗号作为分隔符用于分隔定义和方法参数)在 Java 中只有一个用法:在**for** 循环的控制表达式中。 在控制表达式的初始化和步进控制中,您可以使用逗号分隔多个语句,并按顺序计算这些语句。使用逗号运算符,您可以在 **for** 语句中定义多个变量,但它们必须属于同一类型:
|
||||
|
||||
```java
|
||||
// control/CommaOperator.java
|
||||
|
||||
public class CommaOperator {
|
||||
public static void main(String[] args) {
|
||||
for(int i = 1, j = i + 10; i < 5; i++, j = i * 2) {
|
||||
System.out.println("i = " + i + " j = " + j);
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Output:
|
||||
i = 1 j = 11
|
||||
i = 2 j = 4
|
||||
i = 3 j = 6
|
||||
i = 4 j = 8
|
||||
*/
|
||||
```
|
||||
|
||||
**for** 语句中的 **int** 定义覆盖了 **i** 和 **j**,在初始化部分实际上可以拥有任意数量的具有相同类型的变量定义。在控制表达式中定义变量的能力仅限于 **for** 循环。 您不能将此方法与任何其他选择或迭代语句一起使用。
|
||||
|
||||
可以看到,无论在初始化还是在步进部分,语句都是顺序执行的。
|
||||
|
||||
## for-in语法
|
||||
|
||||
Java 5引入了更简洁的 **for** 语法,用于数组和集合(您将在“数组和集合”章节中了解更多有关这些内容的信息)。 这有时被称为 **增强版for循环**,并且您将看到的大部分文档称为 *for-each* 语法,但Java 8添加了大量使用的 **forEach()**。 这会混淆术语,因此我称之为*for-in* (例如,在Python中,你实际上是 **for x in sequence**,所以有合理的先例)。 请记住,您可能会在其他地方看到它的不同叫法。
|
||||
|
||||
**for-in** 会自动为您生成每个项,因此你不必创建 **int** 变量去对由访问项构成的序列进行计数。 例如,假设您有一个 **float** 数组,并且您想要选取该数组中的每个元素:
|
||||
|
||||
```java
|
||||
// control/ForInFloat.java
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class ForInFloat {
|
||||
public static void main(String[] args) {
|
||||
Random rand = new Random(47);
|
||||
float[] f = new float[10];
|
||||
for(int i = 0; i < 10; i++)
|
||||
f[i] = rand.nextFloat();
|
||||
for(float x : f)
|
||||
System.out.println(x);
|
||||
}
|
||||
}
|
||||
/* Output:
|
||||
0.72711575
|
||||
0.39982635
|
||||
0.5309454
|
||||
0.0534122
|
||||
0.16020656
|
||||
0.57799757
|
||||
0.18847865
|
||||
0.4170137
|
||||
0.51660204
|
||||
0.73734957
|
||||
*/
|
||||
```
|
||||
|
||||
这个数组是用旧式的for循环组装的,因为在组装时必须按索引访问它。在下面这行中可以看到foreach方法:
|
||||
|
||||
```java
|
||||
for(float x : f) {
|
||||
```
|
||||
|
||||
这条语句定义了一个 **float** 类型的变量 **x**,继而将每一个 **f** 的元素赋值给 **x**。
|
||||
|
||||
任何返回一个数组的方法都可以使用 **for-in**。例如 **String** 类有一个方法 **toCharArray()**,它返回一个 **char** 数组,因此可以很容易地下像下面这样迭代在字符串里面的所有字符:
|
||||
|
||||
```java
|
||||
// control/ForInString.java
|
||||
|
||||
public class ForInString {
|
||||
public static void main(String[] args) {
|
||||
for(char c : "An African Swallow".toCharArray())
|
||||
System.out.print(c + " ");
|
||||
}
|
||||
}
|
||||
/* Output:
|
||||
A n A f r i c a n S w a l l o w
|
||||
*/
|
||||
```
|
||||
|
||||
就像在集合的章节所看到的,for-in还可以用于任何 **iterable** 对象。
|
||||
|
||||
许多 **for** 语句都会在一个整型值序列中步进,就像下面这样:
|
||||
|
||||
```java
|
||||
for(int i = 0; i < 100; i++)
|
||||
```
|
||||
|
||||
对于这样的语句,for-in语法将不起作用,除非先创建一个 **int** 数组。为了简化这些任务,我会在**onjava.Range** 包中创建一个名为 **range()** 的方法,它将自动生成恰当的数组。
|
||||
|
||||
隐藏实施过程这一章节( Implementation Hiding )介绍了静态导入。 但是,您无需了解这些详细信息即可开始使用此库。 你可以在 **import** 语句中看到 **static import** 语法:
|
||||
|
||||
```java
|
||||
// control/ForInInt.java
|
||||
|
||||
import static onjava.Range.*;
|
||||
|
||||
public class ForInInt {
|
||||
public static void main(String[] args) {
|
||||
for(int i : range(10)) // 0..9
|
||||
System.out.print(i + " ");
|
||||
System.out.println();
|
||||
for(int i : range(5, 10)) // 5..9
|
||||
System.out.print(i + " ");
|
||||
System.out.println();
|
||||
for(int i : range(5, 20, 3)) // 5..20 step 3
|
||||
System.out.print(i + " ");
|
||||
System.out.println();
|
||||
for(int i : range(20, 5, -3)) // Count down
|
||||
System.out.print(i + " ");
|
||||
System.out.println();
|
||||
}
|
||||
}
|
||||
/* Output:
|
||||
0 1 2 3 4 5 6 7 8 9
|
||||
5 6 7 8 9
|
||||
5 8 11 14 17
|
||||
20 17 14 11 8
|
||||
*/
|
||||
```
|
||||
|
||||
**range()** 方法已经被重载,重载表示相同的方法名可以具有不同的参数列表(你将很快学习重载)。**range()** 的第一个种重载形式是从0开始产生值,直到范围的上限,但不包括该上限。第二种重载形式是从第一个值开始产生值,直至比第二值小1的值为止。第三种形式有一个步进值,因此它每次的增量为该值。第四种 **range()** 表明还可以递减。**range()**是所谓生成器的一个非常简单的版本,有关生成器的内容将在本书稍后进行介绍。
|
||||
|
||||
**range()** 允许在更多地方使用 for-in 语法,因此可以说提高可读性。
|
||||
|
||||
请注意,**System.out.print()** 不会输出换行符,因此您可以分段输出一行。
|
||||
|
||||
*for-in* 语法不仅可以节省编写代码的时间。 更重要的是,它更容易阅读并说明你要做什么(获取数组的每个元素)而不是详细说明你是如何做到的(“我正在创建这个索引,所以我可以使用它来选择每个数组元素。“)。 本书只要有可能就会使用 *for-in* 语法。
|
||||
|
||||
## return
|
||||
|
||||
在Java中有几个关键字代表无条件分支,这意味着分支在没有任何测试的情况下发生。 这些包括**return**,**break**,**continue** 和跳转到带标签语句的方法,类似于其他语言中的 **goto**。
|
||||
|
||||
**return**关键字有两方面的用途:一方面指定一个方法返回什么值(假设它没有 **void** 返回值),另一方面它会导致当前的方法退出,并返回那个值。可据此改写上面的 **IfElse.java** 里的 **test()** 方法,使其利用这些特点:
|
||||
|
||||
```java
|
||||
// control/TestWithReturn.java
|
||||
|
||||
public class TestWithReturn {
|
||||
static int test(int testval, int target) {
|
||||
if(testval > target)
|
||||
return +1;
|
||||
if(testval < target)
|
||||
return -1;
|
||||
return 0; // Match
|
||||
}
|
||||
public static void main(String[] args) {
|
||||
System.out.println(test(10, 5));
|
||||
System.out.println(test(5, 10));
|
||||
System.out.println(test(5, 5));
|
||||
}
|
||||
}
|
||||
/* Output:
|
||||
1
|
||||
-1
|
||||
0
|
||||
*/
|
||||
```
|
||||
|
||||
不需要 **else**,因为该方法在执行返回后不会继续执行。
|
||||
|
||||
如果在返回 **void** 的方法中没有 **return** 语句,则在该方法结束时会有一个隐式的 **return**,因此一个方法中并不总是需要包含 **return** 语句。 但是,如果您的方法声明它将返回除 **void** 之外的任何返回类型,则必须确保每个代码路径都返回一个值。
|
||||
|
||||
## break和continue
|
||||
|
||||
在任何迭代语句的主体部分,都可以使用 **break** 和 **continue** 来控制循环的流程。 其中,**break** 退出循环而不执行循环中的其余语句。 而 **continue** 停止执行当前的迭代,然后退回循环起始处,以开始下一次迭代。
|
||||
|
||||
下面这个程序向大家展示了 **break** 和 **continue** 在 **for** 和 **while** 循环中的例子:
|
||||
|
||||
```java
|
||||
// control/BreakAndContinue.java
|
||||
// Break and continue keywords
|
||||
|
||||
import static onjava.Range.*;
|
||||
|
||||
public class BreakAndContinue {
|
||||
public static void main(String[] args) {
|
||||
for(int i = 0; i < 100; i++) { // [1]
|
||||
if(i == 74) break; // Out of for loop
|
||||
if(i % 9 != 0) continue; // Next iteration
|
||||
System.out.print(i + " ");
|
||||
}
|
||||
System.out.println();
|
||||
// Using for-in:
|
||||
for(int i : range(100)) { // [2]
|
||||
if(i == 74) break; // Out of for loop
|
||||
if(i % 9 != 0) continue; // Next iteration
|
||||
System.out.print(i + " ");
|
||||
}
|
||||
System.out.println();
|
||||
int i = 0;
|
||||
// An "infinite loop":
|
||||
while(true) { // [3]
|
||||
i++;
|
||||
int j = i * 27;
|
||||
if(j == 1269) break; // Out of loop
|
||||
if(i % 10 != 0) continue; // Top of loop
|
||||
System.out.print(i + " ");
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Output:
|
||||
0 9 18 27 36 45 54 63 72
|
||||
0 9 18 27 36 45 54 63 72
|
||||
10 20 30 40
|
||||
*/
|
||||
```
|
||||
|
||||
**[1]** 在这个 **for** 循环中,**i** 的值永远不会达到 100;因为一旦 **i** 达到 74,**break** 语句就会中断循环。通常,只有在不知道中断条件何时满足时,才需要这样使用 **break**。因为**i** 不能被 9 整除,**continue** 语句就会使执行过程返回到循环的最开头(这使 **i** 递增)。如果能够整除,则将值显示出来。
|
||||
|
||||
**[2]** 使用 for-in 语法将产生相同的结果。
|
||||
|
||||
**[3]** 最后,可以看到一个“无穷 **while** 循环”的情况。然而,循环内部有一个 **break** 语句,可中止循环。请注意,**continue** 语句将控制权移回循环的顶部,而不会执行 **continue** 之后的任何操作。 因此,只有当 i 的值可被 10 整除时才会显示。在输出中,显示值 0,因为 0%9 产生0。
|
||||
|
||||
无限循环的另一种形式是 **for(;;)**。 编译器以同样的方式处理 **while(true)** 和 **for(;;)**,因此使用哪种取决于编程品味。
|
||||
|
||||
<!--The Infamous “Goto”-->
|
||||
|
||||
## 臭名昭著的goto
|
||||
|
||||
goto 关键字很早就在程序设计语言中出现。事实上,goto 是汇编语言的程序控制结构的始祖:“若条件A,则跳到这里;否则跳到那里”。若阅读由几乎所有编译器生成的汇编代码,就会发现程序控制里包含了许多跳转。然而,goto是在源码的级别跳转的,所以招致了不好的声誉。若程序总是从一个地方跳到另一个地方,还有什么办法能识别代码的流程呢?随着 Edsger Dijkstra 著名的 “Goto 有害” 论的问世,goto 便从此失宠。
|
||||
|
||||
事实上,真正的问题并不在于使用 goto,而在于 goto 的滥用。而且在一些少见的情况下,goto 是组织控制流程的最佳手段。
|
||||
|
||||
尽管 goto 仍是 Java 的一个保留字,但并未在语言中得到正式使用;Java 没有 goto。然而,在 break和 continue 这两个关键字的身上,我们仍然能看出一些goto的影子。它并不属于一次跳转,而是中断循环语句的一种方法。之所以把它们纳入 goto 问题中一起讨论,是由于它们使用了相同的机制:标签。
|
||||
|
||||
“标签”是后面跟一个冒号的标识符,就像下面这样:
|
||||
|
||||
```java
|
||||
label1:
|
||||
```
|
||||
|
||||
对 Java 来说,唯一用到标签的地方是在循环语句之前。进一步说,它实际需要紧靠在循环语句的前方——在标签和循环之间置入任何语句都是不明智的。而在循环之前设置标签的唯一理由是:我们希望在其中嵌套另一个循环或者一个开关。这是由于 break 和 continue 关键字通常只中断当前循环,但若随同标签使用,它们就会中断到存在标签的地方。如下所示:
|
||||
|
||||
```java
|
||||
label1:
|
||||
outer-iteration {
|
||||
inner-iteration {
|
||||
// ...
|
||||
break; // [1]
|
||||
// ...
|
||||
continue; // [2]
|
||||
// ...
|
||||
continue label1; // [3]
|
||||
// ...
|
||||
break label1; // [4]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**[1]** **break** 中断内部循环,并在外部循环结束。
|
||||
|
||||
**[2]** **continue** 移回内部循环的起始处。但在条件3中,**continue label1** 却同时中断内部循环以及外部循环,并移至 **label1** 处。
|
||||
|
||||
**[3]** 随后,它实际是继续循环,但却从外部循环开始。
|
||||
|
||||
**[4]** **break label1** 也会中断所有循环,并回到 **label1** 处,但并不重新进入循环。也就是说,它实际是完全中止了两个循环。
|
||||
|
||||
下面是 for 循环的一个例子:
|
||||
|
||||
```java
|
||||
// control/LabeledFor.java
|
||||
// For loops with "labeled break"/"labeled continue."
|
||||
|
||||
public class LabeledFor {
|
||||
public static void main(String[] args) {
|
||||
int i = 0;
|
||||
outer: // Can't have statements here
|
||||
for(; true ;) { // infinite loop
|
||||
inner: // Can't have statements here
|
||||
for(; i < 10; i++) {
|
||||
System.out.println("i = " + i);
|
||||
if(i == 2) {
|
||||
System.out.println("continue");
|
||||
continue;
|
||||
}
|
||||
if(i == 3) {
|
||||
System.out.println("break");
|
||||
i++; // Otherwise i never
|
||||
// gets incremented.
|
||||
break;
|
||||
}
|
||||
if(i == 7) {
|
||||
System.out.println("continue outer");
|
||||
i++; // Otherwise i never
|
||||
// gets incremented.
|
||||
continue outer;
|
||||
}
|
||||
if(i == 8) {
|
||||
System.out.println("break outer");
|
||||
break outer;
|
||||
}
|
||||
for(int k = 0; k < 5; k++) {
|
||||
if(k == 3) {
|
||||
System.out.println("continue inner");
|
||||
continue inner;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Can't break or continue to labels here
|
||||
}
|
||||
}
|
||||
/* Output:
|
||||
i = 0
|
||||
continue inner
|
||||
i = 1
|
||||
continue inner
|
||||
i = 2
|
||||
continue
|
||||
i = 3
|
||||
break
|
||||
i = 4
|
||||
continue inner
|
||||
i = 5
|
||||
continue inner
|
||||
i = 6
|
||||
continue inner
|
||||
i = 7
|
||||
continue outer
|
||||
i = 8
|
||||
break outer
|
||||
*/
|
||||
```
|
||||
|
||||
注意 **break** 会中断 **for** 循环,而且在抵达 **for** 循环的末尾之前,递增表达式不会执行。由于 **break** 跳过了递增表达式,所以递增会在 **i==3** 的情况下直接执行。在 **i==7** 的情况下,**continue outer** 语句也会到达循环顶部,而且也会跳过递增,所以它也是直接递增的。
|
||||
|
||||
如果没有 **break outer** 语句,就没有办法在一个内部循环里找到出外部循环的路径。这是由于 **break** 本身只能中断最内层的循环(对于 **continue** 同样如此)。 当然,若想在中断循环的同时退出方法,简单地用一个 **return** 即可。
|
||||
|
||||
下面这个例子向大家展示了带标签的 **break** 以及 **continue** 语句在 **while** 循环中的用法:
|
||||
|
||||
```java
|
||||
// control/LabeledWhile.java
|
||||
// "While" with "labeled break" and "labeled continue."
|
||||
|
||||
public class LabeledWhile {
|
||||
public static void main(String[] args) {
|
||||
int i = 0;
|
||||
outer:
|
||||
while(true) {
|
||||
System.out.println("Outer while loop");
|
||||
while(true) {
|
||||
i++;
|
||||
System.out.println("i = " + i);
|
||||
if(i == 1) {
|
||||
System.out.println("continue");
|
||||
continue;
|
||||
}
|
||||
if(i == 3) {
|
||||
System.out.println("continue outer");
|
||||
continue outer;
|
||||
}
|
||||
if(i == 5) {
|
||||
System.out.println("break");
|
||||
break;
|
||||
}
|
||||
if(i == 7) {
|
||||
System.out.println("break outer");
|
||||
break outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Output:
|
||||
Outer while loop
|
||||
i = 1
|
||||
continue
|
||||
i = 2
|
||||
i = 3
|
||||
continue outer
|
||||
Outer while loop
|
||||
i = 4
|
||||
i = 5
|
||||
break
|
||||
Outer while loop
|
||||
i = 6
|
||||
i = 7
|
||||
break outer
|
||||
*/
|
||||
```
|
||||
|
||||
同样的规则亦适用于 **while**:
|
||||
|
||||
(1) 简单的一个 **continue** 会退回最内层循环的开头(顶部),并继续执行。
|
||||
|
||||
(2) 带有标签的 **continue** 会到达标签的位置,并重新进入紧接在那个标签后面的循环。
|
||||
|
||||
(3) **break** 会中断当前循环,并移离当前标签的末尾。
|
||||
|
||||
(4) 带标签的 **break** 会中断当前循环,并移离由那个标签指示的循环的末尾。
|
||||
|
||||
大家要记住的重点是:在 Java 里需要使用标签的唯一理由就是因为有循环嵌套存在,而且想从多层嵌套中 **break** 或 **continue**。
|
||||
|
||||
**break** 和 **continue** 标签已经成为相对少用的推测特征(在前面的语言中很少或没有先例),所以你很少在代码里看到它们。
|
||||
|
||||
在 **Dijkstra** 的 **“goto 有害”** 论中,他最反对的就是标签,而非 **goto**。随着标签在一个程序里数量的增多,他发现产生错误的机会也越来越多。标签和 **goto** 使我们难于对程序作静态分析。但是,Java 标签不会造成这方面的问题,因为它们的应用场合已经收到了限制,没有特别的方式用于概念程序的控制。由此也引出了一个有趣的问题:通过限制语句的能力,反而能使一项语言特性更加有用。
|
||||
|
||||
## switch
|
||||
|
||||
**switch** 有时也被划归为一种选择语句。根据整数表达式的值,**switch** 语句可以从一系列代码中选出一段去执行。它的格式如下:
|
||||
|
||||
```java
|
||||
switch(integral-selector) {
|
||||
case integral-value1 : statement; break;
|
||||
case integral-value2 : statement; break;
|
||||
case integral-value3 : statement; break;
|
||||
case integral-value4 : statement; break;
|
||||
case integral-value5 : statement; break;
|
||||
// ...
|
||||
default: statement;
|
||||
}
|
||||
```
|
||||
|
||||
其中,integral-selector (整数选择因子)是一个能够产生整数值的表达式,**switch** 能够将这个表达式的结果与每个 integral-value (整数值)相比较。若发现相符的,就执行对应的语句(简单或复合语句,其中并不需要括号)。若没有发现相符的,就执行 default 语句。
|
||||
|
||||
在上面的定义中,大家会注意到每个 **case** 均以一个 **break** 结尾。这样可使执行流程跳转至 switch 主体的末尾。这是构建 **switch** 语句的一种传统方式,但 **break** 是可选的。若省略 break, 会继续执行后面的 **case** 语句的代码,直到遇到一个 **break** 为止。尽管通常不想出现这种情况,但对有经验的程序员来说,也许能够善加利用。注意最后的 **default** 语句没有 **break**,因为执行流程已到了break的跳转目的地。当然,如果考虑到编程风格方面的原因,完全可以在**default** 语句的末尾放置一个 **break**,尽管它并没有任何实际的用处。
|
||||
|
||||
switch 语句是实现多路选择的一种干净利落的一种方式(比如从一系列执行路径中挑选一个)。但它要求使用一个选择因子,并且必须是 int 或 char 那样的整数值。例如,假若将一个字串或者浮点数作为选择因子使用,那么它们在 switch 语句里是不会工作的。对于非整数类型,则必须使用一系列 if 语句。
|
||||
|
||||
下面这个例子可随机生成字母,并判断它们是元音还是辅音字母:
|
||||
|
||||
```java
|
||||
// control/VowelsAndConsonants.java
|
||||
|
||||
// Demonstrates the switch statement
|
||||
import java.util.*;
|
||||
|
||||
public class VowelsAndConsonants {
|
||||
public static void main(String[] args) {
|
||||
Random rand = new Random(47);
|
||||
for(int i = 0; i < 100; i++) {
|
||||
int c = rand.nextInt(26) + 'a';
|
||||
System.out.print((char)c + ", " + c + ": ");
|
||||
switch(c) {
|
||||
case 'a':
|
||||
case 'e':
|
||||
case 'i':
|
||||
case 'o':
|
||||
case 'u': System.out.println("vowel");
|
||||
break;
|
||||
case 'y':
|
||||
case 'w': System.out.println("Sometimes vowel");
|
||||
break;
|
||||
default: System.out.println("consonant");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Output: (First 13 Lines)
|
||||
y, 121: Sometimes vowel
|
||||
n, 110: consonant
|
||||
z, 122: consonant
|
||||
b, 98: consonant
|
||||
r, 114: consonant
|
||||
n, 110: consonant
|
||||
y, 121: Sometimes vowel
|
||||
g, 103: consonant
|
||||
c, 99: consonant
|
||||
f, 102: consonant
|
||||
o, 111: vowel
|
||||
w, 119: Sometimes vowel
|
||||
z, 122: consonant
|
||||
...
|
||||
*/
|
||||
```
|
||||
|
||||
由于 **Random.nextInt(26)** 会产生 0 到 26 之间的一个值,所以在其上加上一个偏移量 "a",即可产生小写字母。在 **case** 语句中,使用单引号引起的字符也会产生用于比较的整数值。
|
||||
|
||||
请注意 **case** 语句能够堆叠在一起,为一段代码形成多重匹配,即只要符合多种条件中的一种,就执行那段特别的代码。这时也应该注意将 **break** 语句置于特定 **case** 的末尾,否则控制流程会简单地下移,处理后面的 **case**。
|
||||
|
||||
在下面的语句中:
|
||||
|
||||
```java
|
||||
int c = rand.nextInt(26) + 'a';
|
||||
```
|
||||
|
||||
此处 **Random.nextInt()** 将产生 0~25 之间的一个随机 **int** 值,它将被加到 **a** 上。这表示 **a** 将自动被转换为 **int** 以执行假发。为了把 **c** 当作字符打印,必须将其转型为 **char**;否则,将产生整数输出。
|
||||
|
||||
|
||||
<!-- Switching on Strings -->
|
||||
## switch字符串
|
||||
|
||||
Java 7 增加了在字符串上 **switch** 的用法。 此示例展示了您从一组 String 可能性中选择的旧方法,以及使用 switch 的新方法:
|
||||
|
||||
```java
|
||||
// control/StringSwitch.java
|
||||
|
||||
public class StringSwitch {
|
||||
public static void main(String[] args) {
|
||||
String color = "red";
|
||||
// Old way: using if-then
|
||||
if("red".equals(color)) {
|
||||
System.out.println("RED");
|
||||
} else if("green".equals(color)) {
|
||||
System.out.println("GREEN");
|
||||
} else if("blue".equals(color)) {
|
||||
System.out.println("BLUE");
|
||||
} else if("yellow".equals(color)) {
|
||||
System.out.println("YELLOW");
|
||||
} else {
|
||||
System.out.println("Unknown");
|
||||
}
|
||||
// New way: Strings in switch
|
||||
switch(color) {
|
||||
case "red":
|
||||
System.out.println("RED");
|
||||
break;
|
||||
case "green":
|
||||
System.out.println("GREEN");
|
||||
break;
|
||||
case "blue":
|
||||
System.out.println("BLUE");
|
||||
break;
|
||||
case "yellow":
|
||||
System.out.println("YELLOW");
|
||||
break;
|
||||
default:
|
||||
System.out.println("Unknown");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Output:
|
||||
RED
|
||||
RED
|
||||
*/
|
||||
```
|
||||
|
||||
一旦理解了 switch,这种语法就是一个逻辑扩展。 结果更清晰,更易于理解和维护。
|
||||
|
||||
作为 **switch** 字符串的第二个例子,我们重新访问 Math.random()。 它是否产生从 0 到 1 的值,包括还是不包括值 “1”? 在数学术语中,是(0,1),还是 [0,1],还是(0,1)还是 [0,1]? (方括号表示“包括”,而括号表示“不包括”。)
|
||||
|
||||
下面一个可能提供答案的测试程序。 所有命令行参数都作为 **String** 对象传递,因此我们可以 **switch** 参数来决定要做什么。 有一个问题:用户可能不提供任何参数,因此索引到 args 数组会导致程序失败。 要解决这个问题,我们检查数组的长度,如果它为零,我们使用一个空字符串,否则我们选择 **args** 数组中的第一个元素:
|
||||
|
||||
```java
|
||||
// control/RandomBounds.java
|
||||
|
||||
// Does Math.random() produce 0.0 and 1.0?
|
||||
// {java RandomBounds lower}
|
||||
import onjava.*;
|
||||
|
||||
public class RandomBounds {
|
||||
public static void main(String[] args) {
|
||||
new TimedAbort(3);
|
||||
switch(args.length == 0 ? "" : args[0]) {
|
||||
case "lower":
|
||||
while(Math.random() != 0.0)
|
||||
; // Keep trying
|
||||
System.out.println("Produced 0.0!");
|
||||
break;
|
||||
case "upper":
|
||||
while(Math.random() != 1.0)
|
||||
; // Keep trying
|
||||
System.out.println("Produced 1.0!");
|
||||
break;
|
||||
default:
|
||||
System.out.println("Usage:");
|
||||
System.out.println("\tRandomBounds lower");
|
||||
System.out.println("\tRandomBounds upper");
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
要运行该程序,请键入以下任一命令:
|
||||
|
||||
```java
|
||||
java RandomBounds lower
|
||||
or
|
||||
java RandomBounds upper
|
||||
```
|
||||
|
||||
使用 onjava 包中的 TimedAbort 类,程序在三秒后中止,因此看起来 Math.random() 从不生成 0.0 或 1.0。 但这就是这样一个实验可以欺骗的地方。 如果考虑0到1之间所有不同 **double** 类型的分数(double fractions)的数量,实验中达到任何一个值的可能性可能超过一台计算机甚至一个实验者的寿命。 结果是 0.0 包含在 **Math.random()** 的输出中,而 1.0 则不包括在内。 在数学术语中,它是 [0,1)。 您必须小心分析您的实验并了解它们的局限性。结果是 0.0 包含在 **Math.random()** 的输出中,而 1.0 则不包括在内。 在数学术语中,它是 [0,1)。 您必须小心分析您的实验并了解它们的局限性。
|
||||
|
||||
|
||||
|
||||
## 本章小结
|
||||
|
||||
本章总结了我们对大多数编程语言中出现的基本特征的探索:计算,运算符优先级,类型转换,选择和迭代。 现在,您已准备好开始采取措施,使您更接近面向对象和函数式编程的世界。 下一章将介绍初始化和清理对象的重要问题,接下来的章节将介绍隐藏实现细节(implementation hiding)的这一核心概念。
|
||||
|
||||
<!--下面是脚注-->
|
||||
|
||||
1. 在早期的语言中,大量的决策都是基于使编译器编写者的生活更轻松。 你会发现,在现代语言中,大多数设计决策都会让语言用户的生活更轻松,尽管有时会有妥协 - 这通常会让语言设计师感到后悔。
|
||||
|
||||
2. 请注意,这似乎是一个难以支持的主张,并且很可能是称为相关因果关系谬误的认知偏差的一个例子。
|
||||
|
||||
<!-- 分页 -->
|
||||
|
||||
<div style="page-break-after: always;"></div>
|
||||
48
docs/book/06-Housekeeping.md
Normal file
48
docs/book/06-Housekeeping.md
Normal file
@@ -0,0 +1,48 @@
|
||||
[TOC]
|
||||
|
||||
<!-- Housekeeping -->
|
||||
# 第六章 初始化和清理
|
||||
|
||||
|
||||
<!-- Guaranteed Initialization with the Constructor -->
|
||||
## 利用构造器保证初始化
|
||||
|
||||
|
||||
<!-- Method Overloading -->
|
||||
## 方法重载
|
||||
|
||||
|
||||
<!-- No-arg Constructors -->
|
||||
## 无参构造器
|
||||
|
||||
|
||||
<!-- The this Keyword -->
|
||||
## this关键字
|
||||
|
||||
|
||||
<!-- Cleanup: Finalization and Garbage Collection -->
|
||||
## 垃圾回收器
|
||||
|
||||
|
||||
<!-- Member Initialization -->
|
||||
## 成员初始化
|
||||
|
||||
|
||||
<!-- Constructor Initialization -->
|
||||
## 构造器初始化
|
||||
|
||||
|
||||
<!-- Array Initialization -->
|
||||
## 数组初始化
|
||||
|
||||
|
||||
<!-- Enumerated Types -->
|
||||
## 枚举类型
|
||||
|
||||
|
||||
<!-- Summary -->
|
||||
## 本章小结
|
||||
|
||||
|
||||
<!-- 分页 -->
|
||||
<div style="page-break-after: always;"></div>
|
||||
31
docs/book/07-Implementation-Hiding.md
Normal file
31
docs/book/07-Implementation-Hiding.md
Normal file
@@ -0,0 +1,31 @@
|
||||
[TOC]
|
||||
|
||||
<!-- Implementation Hiding -->
|
||||
# 第七章 封装
|
||||
|
||||
|
||||
<!-- package: the Library Unit -->
|
||||
## 包的概念
|
||||
|
||||
|
||||
<!-- Java Access Specifiers -->
|
||||
## 访问权限修饰符
|
||||
|
||||
|
||||
<!-- Interface and Implementation -->
|
||||
## 接口和实现
|
||||
|
||||
|
||||
<!-- Class Access -->
|
||||
## 类访问权限
|
||||
|
||||
|
||||
<!-- Summary -->
|
||||
## 本章小结
|
||||
|
||||
|
||||
|
||||
<!-- 分页 -->
|
||||
|
||||
<div style="page-break-after: always;"></div>
|
||||
|
||||
61
docs/book/08-Reuse.md
Normal file
61
docs/book/08-Reuse.md
Normal file
@@ -0,0 +1,61 @@
|
||||
[TOC]
|
||||
|
||||
<!-- Reuse -->
|
||||
# 第八章 复用
|
||||
|
||||
|
||||
> 代码复用是面向对象编程(OOP)最具魅力的原因之一。
|
||||
|
||||
对于像 C 语言等面向过程语言来说,“复用”通常指的就是“复制代码”。任何语言都可通过简单复制来达到代码复用的目的,但是这样做的效果并不好。Java 围绕“类”(**Class**)来解决问题。我们可以直接使用别人构建或调试过的代码,而非创建新类、重新开始。
|
||||
|
||||
如何在不污染源代码的前提下使用现存代码是需要技巧的。在本章里,你将学习到两种方式来达到这个目的:
|
||||
|
||||
1. 第一种方式直接了当。在新类中创建现有类的对象。这种方式叫做“组合”(**Composition**),通过这种方式复用代码的功能,而非其形式。
|
||||
|
||||
2. 第二种方式更为微妙。通过现有类创建新类。照字面理解:采用现有类形式,又无需在编码时改动其代码,这种方式就叫做“继承”(**Inheritance**)。**继承**是面向对象编程(**OOP**)的重要基础之一。更多功能相关将在[多态](./09-Polymorphism.md)(**Polymorphism**)章节中介绍。
|
||||
|
||||
组合与继承的语法、行为上有许多相似的地方(这其实是有道理的,毕竟都是基于现有类型构建新的类型)。在本章中,你会学到这两种代码复用的方法。
|
||||
|
||||
<!-- Composition Syntax -->
|
||||
## 组合语法
|
||||
|
||||
|
||||
<!-- Inheritance Syntax -->
|
||||
## 继承语法
|
||||
|
||||
|
||||
<!-- Delegation -->
|
||||
## 委托
|
||||
|
||||
|
||||
<!-- Combining Composition and Inheritance -->
|
||||
## 结合组合与继承
|
||||
|
||||
|
||||
<!-- Choosing Composition vs. Inheritance -->
|
||||
## 组合与继承的选择
|
||||
|
||||
|
||||
<!-- protected -->
|
||||
## protected
|
||||
|
||||
|
||||
<!-- Upcasting -->
|
||||
## 向上转型
|
||||
|
||||
|
||||
<!-- The final Keyword -->
|
||||
## final关键字
|
||||
|
||||
|
||||
<!-- Initialization and Class Loading -->
|
||||
## 类初始化和加载
|
||||
|
||||
|
||||
<!-- Summary -->
|
||||
## 本章小结
|
||||
|
||||
|
||||
<!-- 分页 -->
|
||||
<div style="page-break-after: always;"></div>
|
||||
|
||||
32
docs/book/09-Polymorphism.md
Normal file
32
docs/book/09-Polymorphism.md
Normal file
@@ -0,0 +1,32 @@
|
||||
[TOC]
|
||||
|
||||
<!-- Polymorphism -->
|
||||
# 第九章 多态
|
||||
|
||||
|
||||
<!-- Upcasting Revisited -->
|
||||
## 向上转型回溯
|
||||
|
||||
|
||||
<!-- The Twist -->
|
||||
## 深入理解
|
||||
|
||||
|
||||
<!-- Constructors and Polymorphism -->
|
||||
## 构造器和多态
|
||||
|
||||
|
||||
<!-- Covariant Return Types -->
|
||||
## 返回类型协变
|
||||
|
||||
|
||||
<!-- Designing with Inheritance -->
|
||||
## 使用继承设计
|
||||
|
||||
|
||||
<!-- Summary -->
|
||||
## 本章小结
|
||||
|
||||
<!-- 分页 -->
|
||||
|
||||
<div style="page-break-after: always;"></div>
|
||||
52
docs/book/10-Interfaces.md
Normal file
52
docs/book/10-Interfaces.md
Normal file
@@ -0,0 +1,52 @@
|
||||
[TOC]
|
||||
|
||||
<!-- Interfaces -->
|
||||
# 第十章 接口
|
||||
|
||||
|
||||
<!-- Abstract Classes and Methods -->
|
||||
## 抽象类和方法
|
||||
|
||||
|
||||
<!-- Interfaces -->
|
||||
## 接口创建
|
||||
|
||||
|
||||
<!-- Abstract Classes vs. Interfaces -->
|
||||
## 抽象类和接口
|
||||
|
||||
|
||||
<!-- Complete Decoupling -->
|
||||
## 完全解耦
|
||||
|
||||
|
||||
<!-- Combining Multiple Interfaces -->
|
||||
## 多接口结合
|
||||
|
||||
|
||||
<!-- Extending an Interface with Inheritance -->
|
||||
## 使用继承扩展接口
|
||||
|
||||
|
||||
<!-- Adapting to an Interface -->
|
||||
## 接口适配
|
||||
|
||||
|
||||
<!-- Fields in Interfaces -->
|
||||
## 接口字段
|
||||
|
||||
|
||||
<!-- Nesting Interfaces -->
|
||||
## 接口嵌套
|
||||
|
||||
|
||||
<!-- Interfaces and Factories -->
|
||||
## 接口和工厂方法模式
|
||||
|
||||
|
||||
<!-- Summary -->
|
||||
## 本章小结
|
||||
|
||||
<!-- 分页 -->
|
||||
|
||||
<div style="page-break-after: always;"></div>
|
||||
60
docs/book/11-Inner-Classes.md
Normal file
60
docs/book/11-Inner-Classes.md
Normal file
@@ -0,0 +1,60 @@
|
||||
[TOC]
|
||||
|
||||
<!-- Inner Classes -->
|
||||
# 第十一章 内部类
|
||||
|
||||
|
||||
<!-- Creating Inner Classes -->
|
||||
## 创建内部类
|
||||
|
||||
|
||||
<!-- The Link to the Outer Class -->
|
||||
## 链接外部类
|
||||
|
||||
|
||||
<!-- Using .this and .new -->
|
||||
## 内部类this和new的使用
|
||||
|
||||
|
||||
<!-- Inner Classes and Upcasting -->
|
||||
## 内部类向上转型
|
||||
|
||||
|
||||
<!-- Inner Classes in Methods and Scopes -->
|
||||
## 内部类方法和作用域
|
||||
|
||||
|
||||
<!-- Anonymous Inner Classes -->
|
||||
## 匿名内部类
|
||||
|
||||
|
||||
<!-- Nested Classes -->
|
||||
## 嵌套类
|
||||
|
||||
|
||||
<!-- Why Inner Classes? -->
|
||||
## 内部类应用场景
|
||||
|
||||
|
||||
<!-- Inheriting from Inner Classes -->
|
||||
## 继承内部类
|
||||
|
||||
|
||||
<!-- Can Inner Classes Be Overridden? -->
|
||||
## 重写内部类
|
||||
|
||||
|
||||
<!-- Local Inner Classes -->
|
||||
## 内部类局部变量
|
||||
|
||||
|
||||
<!-- Inner-Class Identifiers -->
|
||||
## 内部类标识符
|
||||
|
||||
|
||||
<!-- Summary -->
|
||||
## 本章小结
|
||||
|
||||
<!-- 分页 -->
|
||||
|
||||
<div style="page-break-after: always;"></div>
|
||||
64
docs/book/12-Collections.md
Normal file
64
docs/book/12-Collections.md
Normal file
@@ -0,0 +1,64 @@
|
||||
[TOC]
|
||||
|
||||
<!-- Collections -->
|
||||
# 第十二章 集合
|
||||
|
||||
|
||||
<!-- Generics and Type-Safe Collections -->
|
||||
## 泛型和类型安全的集合
|
||||
|
||||
|
||||
<!-- Basic Concepts -->
|
||||
## 基本概念
|
||||
|
||||
|
||||
<!-- Adding Groups of Elements -->
|
||||
## 添加元素组
|
||||
|
||||
|
||||
<!-- Printing Collections -->
|
||||
## 集合的打印
|
||||
|
||||
|
||||
<!-- List -->
|
||||
## 列表List
|
||||
|
||||
|
||||
<!-- Iterators -->
|
||||
## 迭代器Iterators
|
||||
|
||||
|
||||
<!-- LinkedList -->
|
||||
## 链表LinkedList
|
||||
|
||||
|
||||
<!-- Stack -->
|
||||
## 堆栈Stack
|
||||
|
||||
|
||||
<!-- Set -->
|
||||
## 集合Set
|
||||
|
||||
|
||||
<!-- Map -->
|
||||
## 映射Map
|
||||
|
||||
|
||||
<!-- Queue -->
|
||||
## 队列Queue
|
||||
|
||||
|
||||
<!-- Collection vs. Iterator -->
|
||||
## 集合与迭代器
|
||||
|
||||
|
||||
<!-- for-in and Iterators -->
|
||||
## for-in和迭代器
|
||||
|
||||
|
||||
<!-- Summary -->
|
||||
## 本章小结
|
||||
|
||||
<!-- 分页 -->
|
||||
|
||||
<div style="page-break-after: always;"></div>
|
||||
1452
docs/book/13-Functional-Programming.md
Normal file
1452
docs/book/13-Functional-Programming.md
Normal file
File diff suppressed because it is too large
Load Diff
32
docs/book/14-Streams.md
Normal file
32
docs/book/14-Streams.md
Normal file
@@ -0,0 +1,32 @@
|
||||
[TOC]
|
||||
|
||||
<!-- Streams -->
|
||||
# 第十四章 流式编程
|
||||
|
||||
|
||||
<!-- Java 8 Stream Support -->
|
||||
## 流支持
|
||||
|
||||
|
||||
<!-- Stream Creation -->
|
||||
## 流创建
|
||||
|
||||
|
||||
<!-- Intermediate Operations -->
|
||||
## 中级流操作
|
||||
|
||||
|
||||
<!-- Optional -->
|
||||
## Optional类
|
||||
|
||||
|
||||
<!-- Terminal Operations -->
|
||||
## 终端操作
|
||||
|
||||
|
||||
<!-- Summary -->
|
||||
## 本章小结
|
||||
|
||||
|
||||
<!-- 分页 -->
|
||||
<div style="page-break-after: always;"></div>
|
||||
74
docs/book/15-Exceptions.md
Normal file
74
docs/book/15-Exceptions.md
Normal file
@@ -0,0 +1,74 @@
|
||||
[TOC]
|
||||
|
||||
<!-- Exceptions -->
|
||||
# 第十五章 异常
|
||||
|
||||
|
||||
<!-- Concepts -->
|
||||
## 异常概念
|
||||
|
||||
|
||||
<!-- Basic Exceptions -->
|
||||
## 基本异常
|
||||
|
||||
|
||||
<!-- Catching an Exception -->
|
||||
## 异常捕获
|
||||
|
||||
|
||||
<!-- Creating Your Own Exceptions -->
|
||||
## 自定义异常
|
||||
|
||||
|
||||
<!-- The Exception Specification -->
|
||||
## 异常规范
|
||||
|
||||
|
||||
<!-- Catching Any Exception -->
|
||||
## 任意异常捕获
|
||||
|
||||
|
||||
<!-- Standard Java Exceptions -->
|
||||
## Java标准异常
|
||||
|
||||
|
||||
<!-- Performing Cleanup with finally -->
|
||||
## finally关键字
|
||||
|
||||
|
||||
<!-- Exception Restrictions -->
|
||||
## 异常限制
|
||||
|
||||
|
||||
<!-- Constructors -->
|
||||
## 异常构造
|
||||
|
||||
|
||||
<!-- Try-With-Resources -->
|
||||
## Try-With-Resources用法
|
||||
|
||||
|
||||
<!-- Exception Matching -->
|
||||
## 异常匹配
|
||||
|
||||
|
||||
<!-- Alternative Approaches -->
|
||||
## 异常准则
|
||||
|
||||
|
||||
<!-- Exception Guidelines -->
|
||||
## 异常指南
|
||||
|
||||
|
||||
<!-- Summary -->
|
||||
## 本章小结
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- 分页 -->
|
||||
|
||||
<div style="page-break-after: always;"></div>
|
||||
64
docs/book/16-Validating-Your-Code.md
Normal file
64
docs/book/16-Validating-Your-Code.md
Normal file
@@ -0,0 +1,64 @@
|
||||
[TOC]
|
||||
|
||||
<!-- Validating Your Code -->
|
||||
# 第十六章 代码校验
|
||||
|
||||
|
||||
<!-- Testing -->
|
||||
## 测试
|
||||
|
||||
|
||||
<!-- Preconditions -->
|
||||
## 前提条件
|
||||
|
||||
|
||||
<!-- Test-Driven Development -->
|
||||
## 测试驱动开发
|
||||
|
||||
|
||||
<!-- Logging -->
|
||||
## 日志
|
||||
|
||||
|
||||
<!-- Debugging -->
|
||||
## 调试
|
||||
|
||||
|
||||
<!-- Benchmarking -->
|
||||
## 基准测试
|
||||
|
||||
|
||||
<!-- Profiling and Optimizing -->
|
||||
## 分析和优化
|
||||
|
||||
|
||||
<!-- Style Checking -->
|
||||
## 风格检测
|
||||
|
||||
|
||||
<!-- Static Error Analysis -->
|
||||
## 静态错误分析
|
||||
|
||||
|
||||
<!-- Code Reviews -->
|
||||
## 代码重审
|
||||
|
||||
|
||||
<!-- Pair Programming -->
|
||||
## 结对编程
|
||||
|
||||
|
||||
<!-- Refactoring -->
|
||||
## 重构
|
||||
|
||||
|
||||
<!-- Continuous Integration -->
|
||||
## 持续集成
|
||||
|
||||
|
||||
<!-- Summary -->
|
||||
## 本章小结
|
||||
|
||||
<!-- 分页 -->
|
||||
|
||||
<div style="page-break-after: always;"></div>
|
||||
37
docs/book/17-Files.md
Normal file
37
docs/book/17-Files.md
Normal file
@@ -0,0 +1,37 @@
|
||||
[TOC]
|
||||
|
||||
<!-- File -->
|
||||
# 第十七章 文件
|
||||
|
||||
|
||||
<!-- File and Directory Paths -->
|
||||
## 文件和目录路径
|
||||
|
||||
|
||||
<!-- Directories -->
|
||||
## 目录
|
||||
|
||||
|
||||
<!-- File Systems -->
|
||||
## 文件系统
|
||||
|
||||
|
||||
<!-- Watching a Path -->
|
||||
## 路径监听
|
||||
|
||||
|
||||
<!-- Finding Files -->
|
||||
## 文件查找
|
||||
|
||||
|
||||
<!-- Reading & Writing Files -->
|
||||
## 文件读写
|
||||
|
||||
|
||||
<!-- Summary -->
|
||||
## 本章小结
|
||||
|
||||
|
||||
|
||||
<!-- 分页 -->
|
||||
<div style="page-break-after: always;"></div>
|
||||
50
docs/book/18-Strings.md
Normal file
50
docs/book/18-Strings.md
Normal file
@@ -0,0 +1,50 @@
|
||||
[TOC]
|
||||
|
||||
<!-- -->
|
||||
# 第十八章 字符串
|
||||
|
||||
|
||||
<!-- Immutable Strings -->
|
||||
## 字符串的不可变
|
||||
|
||||
|
||||
<!-- Overloading + vs. StringBuilder -->
|
||||
## 重载和StringBuilder
|
||||
|
||||
|
||||
<!-- Unintended Recursion -->
|
||||
## 意外递归
|
||||
|
||||
|
||||
<!-- Operations on Strings -->
|
||||
## 字符串操作
|
||||
|
||||
|
||||
<!-- Formatting Output -->
|
||||
## 格式化输出
|
||||
|
||||
|
||||
<!-- Regular Expressions -->
|
||||
## 常规表达式
|
||||
|
||||
|
||||
<!-- Scanning Input -->
|
||||
## 扫描输入
|
||||
|
||||
|
||||
<!-- StringTokenizer -->
|
||||
## StringTokenizer类
|
||||
|
||||
|
||||
<!-- Summary -->
|
||||
## 本章小结
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- 分页 -->
|
||||
|
||||
<div style="page-break-after: always;"></div>
|
||||
49
docs/book/19-Type-Information.md
Normal file
49
docs/book/19-Type-Information.md
Normal file
@@ -0,0 +1,49 @@
|
||||
[TOC]
|
||||
|
||||
<!-- Type Information -->
|
||||
# 第十九章 类型信息
|
||||
|
||||
|
||||
<!-- The Need for RTTI -->
|
||||
## 运行时类型信息
|
||||
|
||||
|
||||
<!-- The Class Object -->
|
||||
## 类的对象
|
||||
|
||||
|
||||
<!-- Checking Before a Cast -->
|
||||
## 类型转换检测
|
||||
|
||||
|
||||
<!-- Registered Factories -->
|
||||
## 注册工厂
|
||||
|
||||
|
||||
<!-- Instanceof vs. Class Equivalence -->
|
||||
## 类的等价比较
|
||||
|
||||
|
||||
<!-- Reflection: Runtime Class Information -->
|
||||
## 反射运行时类信息
|
||||
|
||||
|
||||
<!-- Dynamic Proxies -->
|
||||
## 动态代理
|
||||
|
||||
|
||||
<!-- Using Optional -->
|
||||
## Optional类
|
||||
|
||||
|
||||
<!-- Interfaces and Type -->
|
||||
## 接口和类型
|
||||
|
||||
|
||||
<!-- Summary -->
|
||||
## 本章小结
|
||||
|
||||
|
||||
<!-- 分页 -->
|
||||
|
||||
<div style="page-break-after: always;"></div>
|
||||
86
docs/book/20-Generics.md
Normal file
86
docs/book/20-Generics.md
Normal file
@@ -0,0 +1,86 @@
|
||||
[TOC]
|
||||
|
||||
<!-- Generics -->
|
||||
# 第二十章 泛型
|
||||
|
||||
|
||||
<!-- Simple Generics -->
|
||||
## 简单泛型
|
||||
|
||||
|
||||
<!-- Generic Interfaces -->
|
||||
## 泛型接口
|
||||
|
||||
|
||||
<!-- Generic Methods -->
|
||||
## 泛型方法
|
||||
|
||||
|
||||
<!-- Building Complex Models -->
|
||||
## 复杂模型构建
|
||||
|
||||
|
||||
<!-- The Mystery of Erasure -->
|
||||
## 泛型擦除
|
||||
|
||||
|
||||
<!-- Compensating for Erasure -->
|
||||
## 补偿擦除
|
||||
|
||||
|
||||
<!-- Bounds -->
|
||||
## 边界
|
||||
|
||||
|
||||
<!-- Wildcards -->
|
||||
## 通配符
|
||||
|
||||
|
||||
<!-- Issues -->
|
||||
## 问题
|
||||
|
||||
|
||||
<!-- Self-Bounded Types -->
|
||||
## 自我约束类型
|
||||
|
||||
|
||||
<!-- Dynamic Type Safety -->
|
||||
## 动态类型安全
|
||||
|
||||
|
||||
<!-- Exceptions -->
|
||||
## 泛型异常
|
||||
|
||||
|
||||
<!-- Mixins -->
|
||||
## 混入
|
||||
|
||||
|
||||
<!-- Latent Typing -->
|
||||
## 潜在类型
|
||||
|
||||
|
||||
<!-- Compensating for the Lack of (Direct) Latent -->
|
||||
## 补偿不足
|
||||
|
||||
|
||||
<!-- Assisted Latent Typing in Java 8 -->
|
||||
## 辅助潜在类型
|
||||
|
||||
|
||||
<!-- Summary: Is Casting Really So Bad? -->
|
||||
## 泛型的优劣
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- 分页 -->
|
||||
|
||||
<div style="page-break-after: always;"></div>
|
||||
90
docs/book/21-Arrays.md
Normal file
90
docs/book/21-Arrays.md
Normal file
@@ -0,0 +1,90 @@
|
||||
[TOC]
|
||||
|
||||
<!-- Arrays -->
|
||||
# 第二十一章 数组
|
||||
|
||||
|
||||
<!-- Why Arrays are Special -->
|
||||
## 数组特性
|
||||
|
||||
|
||||
<!-- Arrays are First-Class Objects -->
|
||||
## 一等对象
|
||||
|
||||
|
||||
<!-- Returning an Array -->
|
||||
## 返回数组
|
||||
|
||||
|
||||
<!-- Multidimensional Arrays -->
|
||||
## 多维数组
|
||||
|
||||
|
||||
<!-- Arrays and Generics -->
|
||||
## 泛型数组
|
||||
|
||||
|
||||
<!-- Arrays.fill() -->
|
||||
## Arrays的fill方法
|
||||
|
||||
|
||||
<!-- Arrays.setAll() -->
|
||||
## Arrays的setAll方法
|
||||
|
||||
|
||||
<!-- Incremental Generators -->
|
||||
## 增量生成
|
||||
|
||||
|
||||
<!-- Random Generators -->
|
||||
## 随机生成
|
||||
|
||||
|
||||
<!-- Generics and Primitive Arrays -->
|
||||
## 泛型和基本数组
|
||||
|
||||
|
||||
<!-- Modifying Existing Array Elements -->
|
||||
## 数组元素修改
|
||||
|
||||
|
||||
<!-- An Aside On Parallelism -->
|
||||
## 数组并行
|
||||
|
||||
|
||||
<!-- Arrays Utilities -->
|
||||
## Arrays工具类
|
||||
|
||||
|
||||
<!-- Copying an Array -->
|
||||
## 数组拷贝
|
||||
|
||||
|
||||
<!-- Comparing Arrays -->
|
||||
## 数组比较
|
||||
|
||||
|
||||
<!-- Streams and Arrays -->
|
||||
## 流和数组
|
||||
|
||||
|
||||
<!-- Sorting Arrays -->
|
||||
## 数组排序
|
||||
|
||||
|
||||
<!-- Searching with Arrays.binarySearch() -->
|
||||
## binarySearch二分查找
|
||||
|
||||
|
||||
<!-- Accumulating with parallelPrefix() -->
|
||||
## parallelPrefix并行前缀
|
||||
|
||||
|
||||
<!-- Summary -->
|
||||
## 本章小结
|
||||
|
||||
|
||||
|
||||
<!-- 分页 -->
|
||||
|
||||
<div style="page-break-after: always;"></div>
|
||||
58
docs/book/22-Enumerations.md
Normal file
58
docs/book/22-Enumerations.md
Normal file
@@ -0,0 +1,58 @@
|
||||
[TOC]
|
||||
|
||||
<!-- Enumerations -->
|
||||
# 第二十二章 枚举
|
||||
|
||||
|
||||
<!-- Basic enum Features -->
|
||||
## 基本功能
|
||||
|
||||
|
||||
<!-- Adding Methods to an enum -->
|
||||
## 方法添加
|
||||
|
||||
|
||||
<!-- enums in switch Statements -->
|
||||
## switch语句
|
||||
|
||||
|
||||
<!-- The Mystery of values() -->
|
||||
## values方法
|
||||
|
||||
|
||||
<!-- Implements, not Inherits -->
|
||||
## 实现而非继承
|
||||
|
||||
|
||||
<!-- Random Selection -->
|
||||
## 随机选择
|
||||
|
||||
|
||||
<!-- Using Interfaces for Organization -->
|
||||
## 使用接口组织
|
||||
|
||||
|
||||
<!-- Using EnumSet Instead of Flags -->
|
||||
## 使用EnumSet替代Flags
|
||||
|
||||
|
||||
<!-- Using EnumMap -->
|
||||
## 使用EnumMap
|
||||
|
||||
|
||||
<!-- Constant-Specific Methods -->
|
||||
## 常量特定方法
|
||||
|
||||
|
||||
<!-- Multiple Dispatching -->
|
||||
## 多次调度
|
||||
|
||||
|
||||
<!-- Summary -->
|
||||
## 本章小结
|
||||
|
||||
|
||||
|
||||
<!-- 分页 -->
|
||||
|
||||
<div style="page-break-after: always;"></div>
|
||||
30
docs/book/23-Annotations.md
Normal file
30
docs/book/23-Annotations.md
Normal file
@@ -0,0 +1,30 @@
|
||||
[TOC]
|
||||
|
||||
<!-- Annotations -->
|
||||
# 第二十三章 注解
|
||||
|
||||
|
||||
<!-- Basic Syntax -->
|
||||
## 基本语法
|
||||
|
||||
|
||||
<!-- Writing Annotation Processors -->
|
||||
## 编写注解处理器
|
||||
|
||||
|
||||
<!-- Using javac to Process Annotations -->
|
||||
## 使用javac处理注解
|
||||
|
||||
|
||||
<!-- Annotation-Based Unit Testing -->
|
||||
## 基于注解的单元测试
|
||||
|
||||
|
||||
<!-- Summary -->
|
||||
## 本章小结
|
||||
|
||||
|
||||
|
||||
<!-- 分页 -->
|
||||
|
||||
<div style="page-break-after: always;"></div>
|
||||
66
docs/book/24-Concurrent-Programming.md
Normal file
66
docs/book/24-Concurrent-Programming.md
Normal file
@@ -0,0 +1,66 @@
|
||||
[TOC]
|
||||
|
||||
<!-- Concurrent Programming -->
|
||||
# 第二十四章 并发编程
|
||||
|
||||
|
||||
<!-- The Terminology Problem -->
|
||||
## 术语问题
|
||||
|
||||
|
||||
<!-- Concurrency Superpowers -->
|
||||
## 并发的超能力
|
||||
|
||||
|
||||
<!-- Concurrency is for Speed -->
|
||||
## 针对速度
|
||||
|
||||
|
||||
<!-- The Four Maxims of Java Concurrency -->
|
||||
## 四句格言
|
||||
|
||||
|
||||
<!-- The Brutal Truth -->
|
||||
## 残酷的真相
|
||||
|
||||
|
||||
<!-- The Rest of the Chapter -->
|
||||
## 本章其余部分
|
||||
|
||||
|
||||
<!-- Parallel Streams -->
|
||||
## 并行流
|
||||
|
||||
|
||||
<!-- Creating and Running Tasks -->
|
||||
## 创建和运行任务
|
||||
|
||||
|
||||
<!-- Terminating Long-Running Tasks -->
|
||||
## 终止耗时任务
|
||||
|
||||
|
||||
<!-- CompletableFutures -->
|
||||
## CompletableFuture类
|
||||
|
||||
|
||||
<!-- Deadlock -->
|
||||
## 死锁
|
||||
|
||||
|
||||
<!-- Constructors are not Thread-Safe -->
|
||||
## 构造函数非线程安全
|
||||
|
||||
|
||||
<!-- Effort, Complexity,Cost -->
|
||||
## 复杂性和代价
|
||||
|
||||
|
||||
<!-- Summary -->
|
||||
## 本章小结
|
||||
|
||||
|
||||
|
||||
<!-- 分页 -->
|
||||
|
||||
<div style="page-break-after: always;"></div>
|
||||
72
docs/book/25-Patterns.md
Normal file
72
docs/book/25-Patterns.md
Normal file
@@ -0,0 +1,72 @@
|
||||
[TOC]
|
||||
|
||||
<!-- Patterns -->
|
||||
# 第二十五章 设计模式
|
||||
|
||||
|
||||
<!-- The Pattern Concept -->
|
||||
## 概念
|
||||
|
||||
|
||||
<!-- Building Application Frameworks -->
|
||||
## 构建型
|
||||
|
||||
|
||||
<!-- Fronting for an Implementation -->
|
||||
## 面向实施
|
||||
|
||||
|
||||
<!-- Factories: Encapsulating Object Creation -->
|
||||
## 工厂模式
|
||||
|
||||
|
||||
<!-- Function Objects -->
|
||||
## 函数对象
|
||||
|
||||
|
||||
<!-- Changing the Interface -->
|
||||
## 接口改变
|
||||
|
||||
|
||||
<!-- Interpreter: Run-Time Flexibility -->
|
||||
## 解释器
|
||||
|
||||
|
||||
<!-- Callbacks -->
|
||||
## 回调
|
||||
|
||||
|
||||
<!-- Multiple Dispatching -->
|
||||
## 多次调度
|
||||
|
||||
|
||||
<!-- Pattern Refactoring -->
|
||||
## 模式重构
|
||||
|
||||
|
||||
<!-- Abstracting Usage -->
|
||||
## 抽象用法
|
||||
|
||||
|
||||
<!-- Multiple Dispatching -->
|
||||
## 多次派遣
|
||||
|
||||
|
||||
<!-- The Visitor Pattern -->
|
||||
## 访问者模式
|
||||
|
||||
|
||||
<!-- RTTI Considered Harmful? -->
|
||||
## RTTI的优劣
|
||||
|
||||
|
||||
<!-- Summary -->
|
||||
## 本章小结
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- 分页 -->
|
||||
|
||||
<div style="page-break-after: always;"></div>
|
||||
32
docs/book/Appendix-Becoming-a-Programmer.md
Normal file
32
docs/book/Appendix-Becoming-a-Programmer.md
Normal file
@@ -0,0 +1,32 @@
|
||||
[TOC]
|
||||
|
||||
<!-- Appendix: Becoming a Programmer -->
|
||||
# 附录:成为一名程序员
|
||||
|
||||
|
||||
<!-- How I Got Started in Programming -->
|
||||
## 如何开始
|
||||
|
||||
|
||||
<!-- A Career in Computing -->
|
||||
## 码农生涯
|
||||
|
||||
|
||||
<!-- The Mythical 5% -->
|
||||
## 百分之五的神话
|
||||
|
||||
|
||||
<!-- Writing Software Is Like … Writing -->
|
||||
## 重在动手
|
||||
|
||||
|
||||
<!-- Programming as Typing -->
|
||||
## 像打字般编程
|
||||
|
||||
|
||||
<!-- Do What You Love -->
|
||||
## 做你喜欢的事
|
||||
|
||||
<!-- 分页 -->
|
||||
|
||||
<div style="page-break-after: always;"></div>
|
||||
@@ -0,0 +1,29 @@
|
||||
[TOC]
|
||||
|
||||
<!-- Appendix: Benefits and Costs of Static Type Checking -->
|
||||
# 附录:静态语言类型检查
|
||||
|
||||
|
||||
<!-- Foreword -->
|
||||
## 前言
|
||||
|
||||
|
||||
<!-- Static Type Checking vs. Testing -->
|
||||
## 静态类型检查和测试
|
||||
|
||||
|
||||
<!-- How to Argue about Typing -->
|
||||
## 如何提升打字
|
||||
|
||||
|
||||
<!-- The Cost of Productivity -->
|
||||
## 生产力的成本
|
||||
|
||||
|
||||
<!-- Static vs. Dynamic -->
|
||||
## 静态和动态
|
||||
|
||||
|
||||
<!-- 分页 -->
|
||||
|
||||
<div style="page-break-after: always;"></div>
|
||||
78
docs/book/Appendix-Collection-Topics.md
Normal file
78
docs/book/Appendix-Collection-Topics.md
Normal file
@@ -0,0 +1,78 @@
|
||||
[TOC]
|
||||
|
||||
<!-- Appendix: Collection Topics -->
|
||||
# 附录:集合主题
|
||||
|
||||
|
||||
<!-- Sample Data -->
|
||||
## 示例数据
|
||||
|
||||
|
||||
<!-- List Behavior -->
|
||||
## List表现
|
||||
|
||||
|
||||
<!-- Set Behavior -->
|
||||
## Set表现
|
||||
|
||||
|
||||
<!-- Using Functional Operations with any Map -->
|
||||
## 在Map中使用函数式操作
|
||||
|
||||
|
||||
<!-- Selecting Parts of a Map -->
|
||||
## 选择Map的部分
|
||||
|
||||
|
||||
<!-- Filling Collections -->
|
||||
## 集合的fill方法
|
||||
|
||||
|
||||
<!-- Custom Collection and Map using Flyweight -->
|
||||
## 使用Flyweight自定义集合和Map
|
||||
|
||||
|
||||
<!-- Collection Functionality -->
|
||||
## 集合功能
|
||||
|
||||
|
||||
<!-- Optional Operations -->
|
||||
## 可选操作
|
||||
|
||||
|
||||
<!-- Sets and Storage Order -->
|
||||
## Set和存储顺序
|
||||
|
||||
|
||||
<!-- Queues -->
|
||||
## 队列
|
||||
|
||||
|
||||
<!-- Understanding Maps -->
|
||||
## 理解Map
|
||||
|
||||
|
||||
<!-- Utilities -->
|
||||
## 集合工具类
|
||||
|
||||
|
||||
<!-- Holding References -->
|
||||
## 持有引用
|
||||
|
||||
|
||||
<!-- Java 1.0/1.1 Collections -->
|
||||
## 避免旧式类库
|
||||
|
||||
|
||||
<!-- Summary -->
|
||||
## 本章小结
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- 分页 -->
|
||||
|
||||
<div style="page-break-after: always;"></div>
|
||||
20
docs/book/Appendix-Data-Compression.md
Normal file
20
docs/book/Appendix-Data-Compression.md
Normal file
@@ -0,0 +1,20 @@
|
||||
[TOC]
|
||||
|
||||
<!-- Appendix: Data Compression -->
|
||||
# 附录:数据压缩
|
||||
|
||||
|
||||
<!-- Simple Compression with GZIP -->
|
||||
## 使用Gzip简单压缩
|
||||
|
||||
|
||||
<!-- Multifile Storage with Zip -->
|
||||
## 使用zip多文件存储
|
||||
|
||||
|
||||
<!-- Java Archives (Jars) -->
|
||||
## Java的jar
|
||||
|
||||
<!-- 分页 -->
|
||||
|
||||
<div style="page-break-after: always;"></div>
|
||||
37
docs/book/Appendix-IO-Streams.md
Normal file
37
docs/book/Appendix-IO-Streams.md
Normal file
@@ -0,0 +1,37 @@
|
||||
[TOC]
|
||||
|
||||
<!-- Appendix: I/O Streams -->
|
||||
# 附录:流式IO
|
||||
|
||||
|
||||
<!-- Types of InputStream -->
|
||||
## 输入流类型
|
||||
|
||||
|
||||
<!-- Types of OutputStream -->
|
||||
## 输出流类型
|
||||
|
||||
|
||||
<!-- Adding Attributes and Useful Interfaces -->
|
||||
## 添加属性和有用的接口
|
||||
|
||||
|
||||
<!-- Readers & Writers -->
|
||||
## Reader和Writer
|
||||
|
||||
|
||||
<!-- Off By Itself: RandomAccessFile -->
|
||||
## RandomAccessFile类
|
||||
|
||||
|
||||
<!-- Typical Uses of I/O Streams -->
|
||||
## IO流典型用途
|
||||
|
||||
|
||||
<!-- Summary -->
|
||||
## 本章小结
|
||||
|
||||
|
||||
<!-- 分页 -->
|
||||
|
||||
<div style="page-break-after: always;"></div>
|
||||
8
docs/book/Appendix-Javadoc.md
Normal file
8
docs/book/Appendix-Javadoc.md
Normal file
@@ -0,0 +1,8 @@
|
||||
[TOC]
|
||||
|
||||
<!-- Appendix: Javadoc -->
|
||||
# 附录:文档注释
|
||||
|
||||
<!-- 分页 -->
|
||||
|
||||
<div style="page-break-after: always;"></div>
|
||||
42
docs/book/Appendix-Low-Level-Concurrency.md
Normal file
42
docs/book/Appendix-Low-Level-Concurrency.md
Normal file
@@ -0,0 +1,42 @@
|
||||
[TOC]
|
||||
|
||||
<!-- Appendix: Low-Level Concurrency -->
|
||||
# 附录:并发底层原理
|
||||
|
||||
|
||||
<!-- What is a Thread? -->
|
||||
## 线程
|
||||
|
||||
|
||||
<!-- Catching Exceptions -->
|
||||
## 异常捕获
|
||||
|
||||
|
||||
<!-- Sharing Resources -->
|
||||
## 资源共享
|
||||
|
||||
|
||||
<!-- The volatile Keyword -->
|
||||
## volatile关键字
|
||||
|
||||
|
||||
<!-- Atomicity -->
|
||||
## 原子性
|
||||
|
||||
|
||||
<!-- Critical Sections -->
|
||||
## 关键部分
|
||||
|
||||
|
||||
<!-- Library Components -->
|
||||
## 库组件
|
||||
|
||||
|
||||
<!-- Summary -->
|
||||
## 本章小结
|
||||
|
||||
|
||||
|
||||
<!-- 分页 -->
|
||||
<div style="page-break-after: always;"></div>
|
||||
|
||||
998
docs/book/Appendix-New-IO.md
Normal file
998
docs/book/Appendix-New-IO.md
Normal file
@@ -0,0 +1,998 @@
|
||||
[TOC]
|
||||
|
||||
<!-- Appendix: New I/O -->
|
||||
# 附录:新IO
|
||||
|
||||
|
||||
Java “新” I/O 库,是在Java 1.4引入到 **Java .nio.* package** 中。它有一个目标 : 速度。
|
||||
|
||||
实际上,“旧的”I/O包是使用 **nio** 重新实现了,因此速度也得到提升,因此即使不显式地使用nio 编写代码,也可以提速。这种速度的提高既发生在文件 I/O 中(这里讨论),也发生在网络I/O 中(例如用于Internet编程)。
|
||||
|
||||
速度来自于使用更接近操作系统执行 I/O 方式的结构 : 通道和缓冲区。把它想象成一个煤矿;通道是包含煤层的矿井(数据),缓冲区是您发送到矿井的小车。车里装满了煤,你从车上拿煤。也就是说,你不能直接和通道互动 ; 您与缓冲区交互并将缓冲区发送到通道中。通道要么从缓冲区中提取数据,要么将数据放入缓冲区。
|
||||
|
||||
本附录将深入探讨 nio 包。像I/O streams 这样的高级库使用 **nio**,但是大多数时候您不需要在这个级别使用 I/O 。在Java 7和Java 8,您(理想情况下)除了特殊情况外,甚至不需要使用I/O流。理想情况下,您将经常使用的所有内容都包含在文件一章中。只有在处理性能问题 (例如,可能需要内存映射文件)或创建自己的 I/O 库时,才需要理解 **nio**。
|
||||
|
||||
<!-- ByteBufferS -->
|
||||
## ByteBuffer
|
||||
|
||||
|
||||
直接与通道通信的缓冲区只有一个类型,就是 bytebuffer 。也就是说,一个保存原始字节的缓冲区。如果您查看 **java.nio.ByteBuffer** 的JDK文档,你会发现它很基本 : 您可以通过告诉它要分配多少存储空间来创建一个方法,并且可以使用一些方法来放置和获取数据,这些方法可以是原始字节形式的数据,也可以是原始数据类型的数据。但是没有办法放置或获取对象,甚至字符串。它是相当低层次的,因为这使得大多数操作系统的映射更加有效。
|
||||
|
||||
“旧的” I/O 中的三个类被修改,可以生成一个 **FileChannel** 分别是: **FileInputStream**、**FileOutputStream**,以及用于读写的 **RandomAccessFile**。
|
||||
|
||||
注意,这些是字节操作流,符合 **nio** 的底层特性。 字符模式类的 **Reader** 和 **Writer** 不生成通道,但通道类具有从通道生成 **Reader** 和 **Writer** 的实用方法。
|
||||
|
||||
在这里,我们使用所有三种类型的流来生成可写、可读/可写和可读的通道:
|
||||
|
||||
```java
|
||||
// (c)2017 MindView LLC: see Copyright.txt
|
||||
// We make no guarantees that this code is fit for any purpose.
|
||||
// Visit http://OnJava8.com for more book information.
|
||||
// Getting channels from streams
|
||||
import java.nio.*;
|
||||
import java.nio.channels.*;
|
||||
import java.io.*;
|
||||
|
||||
public class GetChannel {
|
||||
private static String name = "data.txt";
|
||||
private static final int BSIZE = 1024;
|
||||
public static void main(String[] args) {
|
||||
// Write a file:
|
||||
try(
|
||||
FileChannel fc = new FileOutputStream(name)
|
||||
.getChannel()
|
||||
) {
|
||||
fc.write(ByteBuffer
|
||||
.wrap("Some text ".getBytes()));
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
// Add to the end of the file:
|
||||
try(
|
||||
FileChannel fc = new RandomAccessFile(
|
||||
name, "rw").getChannel()
|
||||
) {
|
||||
fc.position(fc.size()); // Move to the end
|
||||
fc.write(ByteBuffer
|
||||
.wrap("Some more".getBytes()));
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
// Read the file:
|
||||
try(
|
||||
FileChannel fc = new FileInputStream(name)
|
||||
.getChannel()
|
||||
) {
|
||||
ByteBuffer buff = ByteBuffer.allocate(BSIZE);
|
||||
fc.read(buff);
|
||||
buff.flip();
|
||||
while(buff.hasRemaining())
|
||||
System.out.write(buff.get());
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
System.out.flush();
|
||||
}
|
||||
}
|
||||
/* Output:
|
||||
Some text Some more
|
||||
*/
|
||||
```
|
||||
|
||||
对于这里显示的任何流类,**getChannel( )** 将生成一个 **FileChannel** 。通道是相当基本的:给它一个**ByteBuffer** 用于读写,并为独占访问锁定文件的区域(稍后将对此进行描述)。
|
||||
|
||||
将字节放入 **ByteBuffer** 的一种方法是直接使用 “put” 方法,将一个或多个字节填入 ByteBuffer,当然也可以是其它原始类型。但是,正如这里所看到的,您还可以使用 **wrap()** 方法在 **ByteBuffer**中“包装”现有字节数组。执行此操作时,不会复制底层数组,而是将其用作生成的 **ByteBuffer** 的存储。这样生产的 **ByteBuffer** 是由数组“支持”的。
|
||||
|
||||
data.txt 文件使用RandomAccessFile重新打开。注意,您可以在文件中移动 **FileChanne** l; 在这里,它被移动到末尾,以便添加额外的写操作。
|
||||
|
||||
对于只读访问,必须使用 **static allocate()** 方法显式地分配 **ByteBuffer**。**nio** 的目标是快速移动大量数据,因此 **ByteBuffer** 的大小应该很重要——实际上,这里使用的1K可能比您通常使用的要小很多(您必须对工作应用程序进行试验,以找到最佳大小)。
|
||||
|
||||
还可以通过使用 **allocateDirect()** 而不是 **allocation()** 来生成一个“直接”缓冲区来获得更快的速度,该缓冲区可以与操作系统进行更高的耦合。然而,这种分配的开销更大,而且实际实现因操作系统的不同而有所不同,因此,您必须再次试验您的工作应用程序,以发现直接缓冲区是否会为您带来速度上的优势。
|
||||
|
||||
一旦您调用 **read()** 来告诉 **FileChannel** 将字节存储到 **ByteBuffer** 中,您必须调用缓冲区上的 **flip()**来告诉它准备好提取字节(是的,这看起来有点粗糙,但是请记住,这是非常低级的,而且是为了达到最高速度)。如果我们要为进一步的 **read()** 操作使用缓冲区,我们还将调用 **clear()** 为每个 **read()** 准备缓冲区。这个简单的文件复制程序演示:
|
||||
|
||||
```java
|
||||
// newio/ChannelCopy.java
|
||||
|
||||
// Copying a file using channels and buffers
|
||||
// {java ChannelCopy ChannelCopy.java test.txt}
|
||||
import java.nio.*;
|
||||
import java.nio.channels.*;
|
||||
import java.io.*;
|
||||
|
||||
public class ChannelCopy {
|
||||
private static final int BSIZE = 1024;
|
||||
public static void main(String[] args) {
|
||||
if(args.length != 2) {
|
||||
System.out.println(
|
||||
"arguments: sourcefile destfile");
|
||||
System.exit(1);
|
||||
}
|
||||
try(
|
||||
FileChannel in = new FileInputStream(
|
||||
args[0]).getChannel();
|
||||
FileChannel out = new FileOutputStream(
|
||||
args[1]).getChannel()
|
||||
) {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(BSIZE);
|
||||
while(in.read(buffer) != -1) {
|
||||
buffer.flip(); // Prepare for writing
|
||||
out.write(buffer);
|
||||
buffer.clear(); // Prepare for reading
|
||||
}
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
打开一个 **FileChannel** 用于读取,另一个 **FileChannel** 用于写入。分配了一个 **ByteBuffer**,当**FileChannel.read() **返回 **-1** 时(毫无疑问,这是Unix和C语言中的一个剩余物 (holdover) ),意味着您已经完成了输入。每次 **read()** 将数据放入缓冲区之后,**flip()** 都会准备好缓冲区,以便 **write()** 提取它的信息。写()之后,信息仍然在缓冲区中,**clear()** 重置所有内部指针,以便在另一次 **read()** 期间接受数据。
|
||||
|
||||
但是,前面的程序并不是处理这种操作的理想方法。特殊方法 transferTo() 和 transferFrom( )允许你直接连接一个通道到另一个:
|
||||
|
||||
```java
|
||||
// newio/TransferTo.java
|
||||
|
||||
// Using transferTo() between channels
|
||||
// {java TransferTo TransferTo.java TransferTo.txt}
|
||||
import java.nio.channels.*;
|
||||
import java.io.*;
|
||||
|
||||
public class TransferTo {
|
||||
public static void main(String[] args) {
|
||||
if(args.length != 2) {
|
||||
System.out.println(
|
||||
"arguments: sourcefile destfile");
|
||||
System.exit(1);
|
||||
}
|
||||
try(
|
||||
FileChannel in = new FileInputStream(
|
||||
args[0]).getChannel();
|
||||
FileChannel out = new FileOutputStream(
|
||||
args[1]).getChannel()
|
||||
) {
|
||||
in.transferTo(0, in.size(), out);
|
||||
// Or:
|
||||
// out.transferFrom(in, 0, in.size());
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
你不会经常这样做,但知道这一点很好。
|
||||
|
||||
|
||||
<!-- Converting Data -->
|
||||
## 转换数据
|
||||
|
||||
|
||||
在 **GetChannel** 中打印文件中的信息。在 java 中,我们每次提取一个字节的数据,并将每个字节转换为 char。这看起来很简单——如果您查看**java.nio.CharBuffer** 类,您将看到它有一个toString()方法,该方法说,“返回一个包含这个缓冲区中的字符的字符串。”
|
||||
|
||||
既然 **ByteBuffer** 可以用 **asCharBuffer()** 方法看作 **CharBuffer**,为什么不使用它呢? 从下面输出语句的第一行可以看出,这并不正确:
|
||||
|
||||
```java
|
||||
// newio/BufferToText.java
|
||||
// (c)2017 MindView LLC: see Copyright.txt
|
||||
// We make no guarantees that this code is fit for any purpose.
|
||||
// Visit http://OnJava8.com for more book information.
|
||||
// Converting text to and from ByteBuffers
|
||||
import java.nio.*;
|
||||
import java.nio.channels.*;
|
||||
import java.nio.charset.*;
|
||||
import java.io.*;
|
||||
|
||||
public class BufferToText {
|
||||
private static final int BSIZE = 1024;
|
||||
public static void main(String[] args) {
|
||||
try(
|
||||
FileChannel fc = new FileOutputStream(
|
||||
"data2.txt").getChannel()
|
||||
) {
|
||||
fc.write(ByteBuffer.wrap("Some text".getBytes()));
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
ByteBuffer buff = ByteBuffer.allocate(BSIZE);
|
||||
try(
|
||||
FileChannel fc = new FileInputStream(
|
||||
"data2.txt").getChannel()
|
||||
) {
|
||||
fc.read(buff);
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
buff.flip();
|
||||
// Doesn't work:
|
||||
System.out.println(buff.asCharBuffer());
|
||||
// Decode using this system's default Charset:
|
||||
buff.rewind();
|
||||
String encoding =
|
||||
System.getProperty("file.encoding");
|
||||
System.out.println("Decoded using " +
|
||||
encoding + ": "
|
||||
+ Charset.forName(encoding).decode(buff));
|
||||
// Encode with something that prints:
|
||||
try(
|
||||
FileChannel fc = new FileOutputStream(
|
||||
"data2.txt").getChannel()
|
||||
) {
|
||||
fc.write(ByteBuffer.wrap(
|
||||
"Some text".getBytes("UTF-16BE")));
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
// Now try reading again:
|
||||
buff.clear();
|
||||
try(
|
||||
FileChannel fc = new FileInputStream(
|
||||
"data2.txt").getChannel()
|
||||
) {
|
||||
fc.read(buff);
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
buff.flip();
|
||||
System.out.println(buff.asCharBuffer());
|
||||
// Use a CharBuffer to write through:
|
||||
buff = ByteBuffer.allocate(24);
|
||||
buff.asCharBuffer().put("Some text");
|
||||
try(
|
||||
FileChannel fc = new FileOutputStream(
|
||||
"data2.txt").getChannel()
|
||||
) {
|
||||
fc.write(buff);
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
// Read and display:
|
||||
buff.clear();
|
||||
try(
|
||||
FileChannel fc = new FileInputStream(
|
||||
"data2.txt").getChannel()
|
||||
) {
|
||||
fc.read(buff);
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
buff.flip();
|
||||
System.out.println(buff.asCharBuffer());
|
||||
}
|
||||
}
|
||||
/* Output:
|
||||
????
|
||||
Decoded using windows-1252: Some text
|
||||
Some text
|
||||
Some textNULNULNUL
|
||||
*/
|
||||
```
|
||||
|
||||
缓冲区包含普通字节,为了将这些字节转换为字符,我们必须在输入时对它们进行编码(这样它们输出时就有意义了),或者在输出时对它们进行解码。
|
||||
|
||||
这可以使用 **java.nio.charset** 来完成。**Charset** 类,它提供工具来编码成许多不同类型的字符集:
|
||||
|
||||
```java
|
||||
// newio/AvailableCharSets.java
|
||||
// (c)2017 MindView LLC: see Copyright.txt
|
||||
// We make no guarantees that this code is fit for any purpose.
|
||||
// Visit http://OnJava8.com for more book information.
|
||||
// Displays Charsets and aliases
|
||||
import java.nio.charset.*;
|
||||
import java.util.*;
|
||||
|
||||
public class AvailableCharSets {
|
||||
public static void main(String[] args) {
|
||||
SortedMap<String,Charset> charSets =
|
||||
Charset.availableCharsets();
|
||||
for(String csName : charSets.keySet()) {
|
||||
System.out.print(csName);
|
||||
Iterator aliases = charSets.get(csName)
|
||||
.aliases().iterator();
|
||||
if(aliases.hasNext())
|
||||
System.out.print(": ");
|
||||
while(aliases.hasNext()) {
|
||||
System.out.print(aliases.next());
|
||||
if(aliases.hasNext())
|
||||
System.out.print(", ");
|
||||
}
|
||||
System.out.println();
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Output: (First 7 Lines)
|
||||
Big5: csBig5
|
||||
Big5-HKSCS: big5-hkscs, big5hk, Big5_HKSCS, big5hkscs
|
||||
CESU-8: CESU8, csCESU-8
|
||||
EUC-JP: csEUCPkdFmtjapanese, x-euc-jp, eucjis,
|
||||
Extended_UNIX_Code_Packed_Format_for_Japanese, euc_jp,
|
||||
eucjp, x-eucjp
|
||||
EUC-KR: ksc5601-1987, csEUCKR, ksc5601_1987, ksc5601,
|
||||
5601,
|
||||
euc_kr, ksc_5601, ks_c_5601-1987, euckr
|
||||
GB18030: gb18030-2000
|
||||
GB2312: gb2312, euc-cn, x-EUC-CN, euccn, EUC_CN,
|
||||
gb2312-80,
|
||||
gb2312-1980
|
||||
...
|
||||
*/
|
||||
```
|
||||
|
||||
回到 **BufferToText** 。java 中,如果您 **rewind()** 缓冲区(回到数据的开头),然后使用该平台的默认字符集 **decode()** 数据,生成的 **CharBuffer** 将在控制台上正常显示。要发现默认字符集,使用**System.getProperty(“file.encoding”)**,它生成命名字符集的字符串。
|
||||
|
||||
另一种方法是使用字符集 **encode()**,该字符集在读取文件时生成可打印的内容,如您在**BufferToText.java** 的第三部分中所看到的。这里,**UTF-16BE** 用于将文本写入文件,当读取文本时,您所要做的就是将其转换为 **CharBuffer**,并生成预期的文本。最后,您将看到如果通过**CharBuffer** 写入 **ByteBuffer** 会发生什么(稍后您将对此有更多的了解)。注意,为 **ByteBuffer** 分配了24个字节。
|
||||
|
||||
由于每个字符需要两个字节,这对于12个字符已经足够了,但是“some text”只有9个字节。其余的零字节仍然出现在由其toString()生成的CharBuffer的表示中,如输出所示。
|
||||
|
||||
|
||||
<!-- Fetching Primitives -->
|
||||
## 获取原始类型
|
||||
|
||||
|
||||
虽然 **ByteBuffer** 只包含字节,但它包含了一些方法,用于从它所包含的字节中生成每种不同类型的基元值。这个例子展示了使用以下方法插入和提取各种值:
|
||||
|
||||
```java
|
||||
// newio/GetData.java
|
||||
// (c)2017 MindView LLC: see Copyright.txt
|
||||
// We make no guarantees that this code is fit for any purpose.
|
||||
// Visit http://OnJava8.com for more book information.
|
||||
// Getting different representations from a ByteBuffer
|
||||
import java.nio.*;
|
||||
|
||||
public class GetData {
|
||||
private static final int BSIZE = 1024;
|
||||
public static void main(String[] args) {
|
||||
ByteBuffer bb = ByteBuffer.allocate(BSIZE);
|
||||
// Allocation automatically zeroes the ByteBuffer:
|
||||
int i = 0;
|
||||
while(i++ < bb.limit())
|
||||
if(bb.get() != 0)
|
||||
System.out.println("nonzero");
|
||||
System.out.println("i = " + i);
|
||||
bb.rewind();
|
||||
// Store and read a char array:
|
||||
bb.asCharBuffer().put("Howdy!");
|
||||
char c;
|
||||
while((c = bb.getChar()) != 0)
|
||||
System.out.print(c + " ");
|
||||
System.out.println();
|
||||
bb.rewind();
|
||||
// Store and read a short:
|
||||
bb.asShortBuffer().put((short)471142);
|
||||
System.out.println(bb.getShort());
|
||||
bb.rewind();
|
||||
// Store and read an int:
|
||||
bb.asIntBuffer().put(99471142);
|
||||
System.out.println(bb.getInt());
|
||||
bb.rewind();
|
||||
// Store and read a long:
|
||||
bb.asLongBuffer().put(99471142);
|
||||
System.out.println(bb.getLong());
|
||||
bb.rewind();
|
||||
// Store and read a float:
|
||||
bb.asFloatBuffer().put(99471142);
|
||||
System.out.println(bb.getFloat());
|
||||
bb.rewind();
|
||||
// Store and read a double:
|
||||
bb.asDoubleBuffer().put(99471142);
|
||||
System.out.println(bb.getDouble());
|
||||
bb.rewind();
|
||||
}
|
||||
}
|
||||
/* Output:
|
||||
i = 1025
|
||||
H o w d y !
|
||||
12390
|
||||
99471142
|
||||
99471142
|
||||
9.9471144E7
|
||||
9.9471142E7
|
||||
*/
|
||||
```
|
||||
|
||||
在分配 **ByteBuffer** 之后,将检查它的值,以确定缓冲区分配是否会自动将内容归零——它确实会这样做。检查所有1,024个值(直到 position的值为 缓冲区的 **limit()** ),所有值都为零。
|
||||
|
||||
将原始值插入ByteBuffer的最简单方法是使用 **asCharBuffer()** 、**asShortBuffer()** 等获取该缓冲区的适当“视图”,然后使用该视图的 **put()** 方法。
|
||||
|
||||
|
||||
这将对每个基本数据类型执行。其中唯一有点奇怪的是 **ShortBuffer** 的 **put()**,它需要强制转换 (强制转换并更改结果值)。所有其他视图缓冲区都不需要在它们的 **put()** 方法中强制转换。
|
||||
|
||||
<!-- View Buffers -->
|
||||
## 视图缓冲区
|
||||
|
||||
|
||||
“视图缓冲区”通过特定原始类型的窗口来查看底层 **ByteBuffer**。**ByteBuffer** 仍然是“支持”视图的实际存储,因此对视图所做的任何更改都反映在对 **ByteBuffer** 中的数据的修改中。
|
||||
|
||||
如前面的示例所示,这方便地将基本类型插入 **ByteBuffer**。视图缓冲区还可以从 **ByteBuffer** 读取原始值,一次读取一个(ByteBuffer允许),或者批量读取(数组)。下面是一个通过 **IntBuffer**在**ByteBuffer** 中操作 int 的例子:
|
||||
|
||||
```java
|
||||
// newio/IntBufferDemo.java
|
||||
// (c)2017 MindView LLC: see Copyright.txt
|
||||
// We make no guarantees that this code is fit for any purpose.
|
||||
// Visit http://OnJava8.com for more book information.
|
||||
// Manipulating ints in a ByteBuffer with an IntBuffer
|
||||
import java.nio.*;
|
||||
|
||||
public class IntBufferDemo {
|
||||
private static final int BSIZE = 1024;
|
||||
public static void main(String[] args) {
|
||||
ByteBuffer bb = ByteBuffer.allocate(BSIZE);
|
||||
IntBuffer ib = bb.asIntBuffer();
|
||||
// Store an array of int:
|
||||
ib.put(new int[]{ 11, 42, 47, 99, 143, 811, 1016 });
|
||||
// Absolute location read and write:
|
||||
System.out.println(ib.get(3));
|
||||
ib.put(3, 1811);
|
||||
// Setting a new limit before rewinding the buffer.
|
||||
ib.flip();
|
||||
while(ib.hasRemaining()) {
|
||||
int i = ib.get();
|
||||
System.out.println(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Output:
|
||||
99
|
||||
11
|
||||
42
|
||||
47
|
||||
1811
|
||||
143
|
||||
811
|
||||
1016
|
||||
*/
|
||||
```
|
||||
|
||||
重载的 **put()** 方法首先用于存储 **int** 数组。下面的 **get()** 和 **put()** 方法调用直接访问底层 **ByteBuffer** 中的 **int** 位置。请注意,通过直接操作 **ByteBuffer** ,这些绝对位置访问也可以用于基本类型。
|
||||
|
||||
一旦底层 **ByteBuffer** 通过视图缓冲区填充了 **int** 或其他基本类型,那么就可以直接将该 **ByteBuffer **写入通道。您可以轻松地从通道读取数据,并使用视图缓冲区将所有内容转换为特定类型的原语。下面是一个例子,通过在同一个 **ByteBuffer** 上生成不同的视图缓冲区,将相同的字节序列解释为 **short**、**int**、**float**、**long **和 **double**:
|
||||
|
||||
```java
|
||||
// newio/ViewBuffers.java
|
||||
// (c)2017 MindView LLC: see Copyright.txt
|
||||
// We make no guarantees that this code is fit for any purpose.
|
||||
// Visit http://OnJava8.com for more book information.
|
||||
import java.nio.*;
|
||||
|
||||
public class ViewBuffers {
|
||||
public static void main(String[] args) {
|
||||
ByteBuffer bb = ByteBuffer.wrap(
|
||||
new byte[]{ 0, 0, 0, 0, 0, 0, 0, 'a' });
|
||||
bb.rewind();
|
||||
System.out.print("Byte Buffer ");
|
||||
while(bb.hasRemaining())
|
||||
System.out.print(
|
||||
bb.position()+ " -> " + bb.get() + ", ");
|
||||
System.out.println();
|
||||
CharBuffer cb =
|
||||
((ByteBuffer)bb.rewind()).asCharBuffer();
|
||||
System.out.print("Char Buffer ");
|
||||
while(cb.hasRemaining())
|
||||
System.out.print(
|
||||
cb.position() + " -> " + cb.get() + ", ");
|
||||
System.out.println();
|
||||
FloatBuffer fb =
|
||||
((ByteBuffer)bb.rewind()).asFloatBuffer();
|
||||
System.out.print("Float Buffer ");
|
||||
while(fb.hasRemaining())
|
||||
System.out.print(
|
||||
fb.position()+ " -> " + fb.get() + ", ");
|
||||
System.out.println();
|
||||
IntBuffer ib =
|
||||
((ByteBuffer)bb.rewind()).asIntBuffer();
|
||||
System.out.print("Int Buffer ");
|
||||
while(ib.hasRemaining())
|
||||
System.out.print(
|
||||
ib.position()+ " -> " + ib.get() + ", ");
|
||||
System.out.println();
|
||||
LongBuffer lb =
|
||||
((ByteBuffer)bb.rewind()).asLongBuffer();
|
||||
System.out.print("Long Buffer ");
|
||||
while(lb.hasRemaining())
|
||||
System.out.print(
|
||||
lb.position()+ " -> " + lb.get() + ", ");
|
||||
System.out.println();
|
||||
ShortBuffer sb =
|
||||
((ByteBuffer)bb.rewind()).asShortBuffer();
|
||||
System.out.print("Short Buffer ");
|
||||
while(sb.hasRemaining())
|
||||
System.out.print(
|
||||
sb.position()+ " -> " + sb.get() + ", ");
|
||||
System.out.println();
|
||||
DoubleBuffer db =
|
||||
((ByteBuffer)bb.rewind()).asDoubleBuffer();
|
||||
System.out.print("Double Buffer ");
|
||||
while(db.hasRemaining())
|
||||
System.out.print(
|
||||
db.position()+ " -> " + db.get() + ", ");
|
||||
}
|
||||
}
|
||||
/* Output:
|
||||
Byte Buffer 0 -> 0, 1 -> 0, 2 -> 0, 3 -> 0, 4 -> 0, 5
|
||||
-> 0, 6 -> 0, 7 -> 97,
|
||||
Char Buffer 0 -> NUL, 1 -> NUL, 2 -> NUL, 3 -> a,
|
||||
Float Buffer 0 -> 0.0, 1 -> 1.36E-43,
|
||||
Int Buffer 0 -> 0, 1 -> 97,
|
||||
Long Buffer 0 -> 97,
|
||||
Short Buffer 0 -> 0, 1 -> 0, 2 -> 0, 3 -> 97,
|
||||
Double Buffer 0 -> 4.8E-322,
|
||||
*/
|
||||
```
|
||||
|
||||
**ByteBuffer** 通过“包装”一个8字节数组生成,然后通过所有不同基本类型的视图缓冲区显示该数组。下图显示了从不同类型的缓冲区读取数据时,数据显示的差异:
|
||||
|
||||

|
||||
|
||||
<!-- Endians -->
|
||||
|
||||
### 端
|
||||
|
||||
不同的机器可以使用不同的字节顺序方法来存储数据。“Big endian”将最重要的字节(the most significant byte)放在最低内存地址中,而“little endian”将最重要的字节放在最高内存地址中。
|
||||
|
||||
当存储一个大于一个字节的量时,例如 **int**、**float** 等,您可能需要考虑字节排序。字节缓冲区以大端字节形式存储数据,通过网络发送的数据总是使用大端字节顺序。您可以使用 **order()** 和**ByteOrder** 参数来更改 **ByteBuffer** 端,可选参数只有2个:**ByteOrder.BIG_ENDIAN** 或 **ByteOrder.LITTLE_ENDIAN**。
|
||||
|
||||
考虑一个包含以下两个字节的 **ByteBuffer** :将数据作为一个**short** (**ByteBuffer. asshortbuffer()**) 读取,生成数字 97 (00000000 01100001)。更改为 little endian 将生成数字 24832 (01100001 00000000)。
|
||||
|
||||
这显示了字节顺序的变化取决于 endian 设置:
|
||||
|
||||
```java
|
||||
// newio/Endians.java
|
||||
// (c)2017 MindView LLC: see Copyright.txt
|
||||
// We make no guarantees that this code is fit for any purpose.
|
||||
// Visit http://OnJava8.com for more book information.
|
||||
// Endian differences and data storage
|
||||
import java.nio.*;
|
||||
import java.util.*;
|
||||
|
||||
public class Endians {
|
||||
public static void main(String[] args) {
|
||||
ByteBuffer bb = ByteBuffer.wrap(new byte[12]);
|
||||
bb.asCharBuffer().put("abcdef");
|
||||
System.out.println(Arrays.toString(bb.array()));
|
||||
bb.rewind();
|
||||
bb.order(ByteOrder.BIG_ENDIAN);
|
||||
bb.asCharBuffer().put("abcdef");
|
||||
System.out.println(Arrays.toString(bb.array()));
|
||||
bb.rewind();
|
||||
bb.order(ByteOrder.LITTLE_ENDIAN);
|
||||
bb.asCharBuffer().put("abcdef");
|
||||
System.out.println(Arrays.toString(bb.array()));
|
||||
}
|
||||
}
|
||||
/* Output:
|
||||
[0, 97, 0, 98, 0, 99, 0, 100, 0, 101, 0, 102]
|
||||
[0, 97, 0, 98, 0, 99, 0, 100, 0, 101, 0, 102]
|
||||
[97, 0, 98, 0, 99, 0, 100, 0, 101, 0, 102, 0]
|
||||
*/
|
||||
|
||||
```
|
||||
|
||||
**ByteBuffer** 分配空间将 **charArray** 中的所有字节作为外部缓冲区保存,因此可以调用 **array()** 方法来显示底层字节。**array()** 方法是“可选的”,您只能在数组支持的缓冲区上调用它 ,否则您将得到一个 **UnsupportedOperationException**。
|
||||
|
||||
**charArray** 通过 **CharBuffer** 视图插入到 **ByteBuffer **中。当显示底层字节时,默认顺序与随后的大端序相同,而小端序则交换字节。
|
||||
|
||||
<!-- Data Manipulation with Buffers -->
|
||||
## 使用缓冲区进行数据操作
|
||||
|
||||
|
||||
下图说明了 nio 类之间的关系,展示了如何移动和转换数据。例如,要将字节数组写入文件,使用ByteBuffer.wrap() 方法包装字节数组,使用 **getChannel()** 在 **FileOutputStream** 上打开通道,然后从 **ByteBuffer** 将数据写入 **FileChannel**。
|
||||
|
||||

|
||||
|
||||
**ByteBuffer** 是将数据移入和移出通道的唯一方法,您只能创建一个独立的原始类型的缓冲区,或者使用“as”方法从ByteBuffer获得一个新缓冲区。也就是说,不能将基元类型的缓冲区转换为ByteBuffer。但您能够通过视图缓冲区将原始数据移动到 **ByteBuffer** 中或移出 **ByteBuffer**。
|
||||
|
||||
|
||||
|
||||
### 缓冲区的细节
|
||||
|
||||
缓冲区由数据和四个索引组成,以有效地访问和操作该数据: 标记、位置、限制 和 容量(mark, position, limit and capacity)。有一些方法可以设置和重置这些索引并查询它们的值。
|
||||
|
||||
**capacity()** 返回缓冲区的 capacity。
|
||||
|
||||
**clear()** 清除缓冲区,将 position 设置为零并 设 limit 为 capacity。您可以调用此方法来覆盖现有缓冲区。
|
||||
|
||||
**flip()** 将 limit 设置为位置,并将 position 设置为零。此方法用于准备缓冲区,以便在数据写入缓冲区后进行读取。
|
||||
|
||||
**limit()** 返回 limit 的值。
|
||||
|
||||
**limit(int lim)** 重设 limit
|
||||
|
||||
**mark()** 设置 mark 为当前的 position
|
||||
|
||||
**position()** 返回 position
|
||||
|
||||
**position(int pos) ** 设置 position
|
||||
|
||||
**remaining()** 返回 limit - position 。
|
||||
|
||||
**hasRemaining()** 如果在 position 与 limit 中间有元素,返回 **true**
|
||||
|
||||
从缓冲区插入和提取数据的方法更新这些索引来反映所做的更改。这个例子使用了一个非常简单的算法(交换相邻的字符)来打乱和整理字符在CharBuffer:
|
||||
|
||||
```java
|
||||
// newio/UsingBuffers.java
|
||||
// (c)2017 MindView LLC: see Copyright.txt
|
||||
// We make no guarantees that this code is fit for any purpose.
|
||||
// Visit http://OnJava8.com for more book information.
|
||||
import java.nio.*;
|
||||
|
||||
public class UsingBuffers {
|
||||
private static
|
||||
void symmetricScramble(CharBuffer buffer) {
|
||||
while(buffer.hasRemaining()) {
|
||||
buffer.mark();
|
||||
char c1 = buffer.get();
|
||||
char c2 = buffer.get();
|
||||
buffer.reset();
|
||||
buffer.put(c2).put(c1);
|
||||
}
|
||||
}
|
||||
public static void main(String[] args) {
|
||||
char[] data = "UsingBuffers".toCharArray();
|
||||
ByteBuffer bb =
|
||||
ByteBuffer.allocate(data.length * 2);
|
||||
CharBuffer cb = bb.asCharBuffer();
|
||||
cb.put(data);
|
||||
System.out.println(cb.rewind());
|
||||
symmetricScramble(cb);
|
||||
System.out.println(cb.rewind());
|
||||
symmetricScramble(cb);
|
||||
System.out.println(cb.rewind());
|
||||
}
|
||||
}
|
||||
/* Output:
|
||||
UsingBuffers
|
||||
sUniBgfuefsr
|
||||
UsingBuffers
|
||||
*/
|
||||
```
|
||||
|
||||
虽然可以通过使用 **char** 数组调用 **wrap()** 直接生成 **CharBuffer**,但是底层的 **ByteBuffer** 将被分配,而 **CharBuffer** 将作为 **ByteBuffer** 上的视图生成。这强调了目标始终是操作 **ByteBuffer**,因为它与通道交互。
|
||||
|
||||
下面是程序在 **symmetricgrab()** 方法入口时缓冲区的样子:
|
||||
|
||||

|
||||
|
||||
position 指向缓冲区中的第一个元素,capacity 和 limie 紧接在最后一个元素之后。在**symmetricgrab()** 中,while循环迭代到 position 等于 limit。当在缓冲区上调用相对位置的 get() 或 put() 函数时,缓冲区的位置会发生变化。您还可以调用绝对位置的 get() 和 put() 方法,它们包含索引参数 : get()或put()发生的位置。这些方法不修改缓冲区 position 的值。
|
||||
|
||||
当控件进入while循环时,使用 **mark()** 调用设置 mark 的值。缓冲区的状态为:
|
||||
|
||||
两个相对 get() 调用将前两个字符的值保存在变量c1和c2中。在这两个调用之后,缓冲区看起来是这样的 : 为了执行交换,我们在位置0处编写c2,在位置1处编写c1。我们可以使用绝对put方法来实现这一点,或者用 reset() 方法,将 position 的值设置为 mark 。
|
||||
|
||||
两个put()方法分别编写c2和c1 : 在循环的下一次迭代中,将mark设置为position的当前值 : 该过程将继续,直到遍历整个缓冲区为止。在while循环的末尾,position位于缓冲区的末尾。如果显示缓冲区,则只显示位置和限制之间的字符。因此,要显示缓冲区的全部内容,必须使用 rewind() 将位置设置为缓冲区的开始位置。这是 rewind() 调用后 buffer 的状态(mark的值变成undefined):
|
||||
|
||||

|
||||
|
||||
再次调用 **symmetricgrab()** 函数时,**CharBuffer** 将经历相同的过程并恢复到原始状态。
|
||||
|
||||
<!-- Memory-Mapped Files -->
|
||||
## 内存映射文件
|
||||
|
||||
|
||||
内存映射文件允许您创建和修改太大而无法放入内存的文件。使用内存映射文件,您可以假装整个文件都在内存中,并将其视为一个非常大的数组来访问它。这种方法大大简化了您编写的修改文件的代码:
|
||||
|
||||
```java
|
||||
// newio/LargeMappedFiles.java
|
||||
// (c)2017 MindView LLC: see Copyright.txt
|
||||
// We make no guarantees that this code is fit for any purpose.
|
||||
// Visit http://OnJava8.com for more book information.
|
||||
// Creating a very large file using mapping
|
||||
import java.nio.*;
|
||||
import java.nio.channels.*;
|
||||
import java.io.*;
|
||||
|
||||
public class LargeMappedFiles {
|
||||
static int length = 0x8000000; // 128 MB
|
||||
public static void
|
||||
main(String[] args) throws Exception {
|
||||
try(
|
||||
RandomAccessFile tdat =
|
||||
new RandomAccessFile("test.dat", "rw")
|
||||
) {
|
||||
MappedByteBuffer out = tdat.getChannel().map(
|
||||
FileChannel.MapMode.READ_WRITE, 0, length);
|
||||
for(int i = 0; i < length; i++)
|
||||
out.put((byte)'x');
|
||||
System.out.println("Finished writing");
|
||||
for(int i = length/2; i < length/2 + 6; i++)
|
||||
System.out.print((char)out.get(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Output:
|
||||
Finished writing
|
||||
xxxxxx
|
||||
*/
|
||||
```
|
||||
|
||||
为了读写,我们从 **RandomAccessFile** 开始,获取该文件的通道,然后调用 **map()** 来生成**MappedByteBuffer** ,这是一种特殊的直接缓冲区。您必须指定要在文件中映射的区域的起始点和长度—这意味着您可以选择映射大文件的较小区域。**MappedByteBuffer** 继承了它的**ByteBuffer**,所以它有所有的 **ByteBuffer **方法。这里只展示了 **put()** 和 **get()** 的最简单用法,但是您也可以使用 **asCharBuffer()** 等方法。
|
||||
|
||||
使用前面的程序创建的文件有128mb长,可能比您的操作系统一次允许的内存要大。该文件似乎可以同时访问,因为它只有一部分被带进内存,而其他部分被交换出去。这样,一个非常大的文件(最多2gb)可以很容易地修改。注意,底层操作系统的文件映射工具用于最大化性能。
|
||||
|
||||
### 性能
|
||||
|
||||
虽然“旧”流I/O的性能通过使用 **nio** 实现得到了改进,但是映射文件访问往往要快得多。这个程序做一个简单的性能比较:
|
||||
|
||||
```java
|
||||
// newio/MappedIO.java
|
||||
// (c)2017 MindView LLC: see Copyright.txt
|
||||
// We make no guarantees that this code is fit for any purpose.
|
||||
// Visit http://OnJava8.com for more book information.
|
||||
import java.util.*;
|
||||
import java.nio.*;
|
||||
import java.nio.channels.*;
|
||||
import java.io.*;
|
||||
|
||||
public class MappedIO {
|
||||
private static int numOfInts = 4_000_000;
|
||||
private static int numOfUbuffInts = 100_000;
|
||||
private abstract static class Tester {
|
||||
private String name;
|
||||
Tester(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
public void runTest() {
|
||||
System.out.print(name + ": ");
|
||||
long start = System.nanoTime();
|
||||
test();
|
||||
double duration = System.nanoTime() - start;
|
||||
System.out.format("%.3f%n", duration/1.0e9);
|
||||
}
|
||||
public abstract void test();
|
||||
}
|
||||
private static Tester[] tests = {
|
||||
new Tester("Stream Write") {
|
||||
@Override
|
||||
public void test() {
|
||||
try(
|
||||
DataOutputStream dos =
|
||||
new DataOutputStream(
|
||||
new BufferedOutputStream(
|
||||
new FileOutputStream(
|
||||
new File("temp.tmp"))))
|
||||
) {
|
||||
for(int i = 0; i < numOfInts; i++)
|
||||
dos.writeInt(i);
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
new Tester("Mapped Write") {
|
||||
@Override
|
||||
public void test() {
|
||||
try(
|
||||
FileChannel fc =
|
||||
new RandomAccessFile("temp.tmp", "rw")
|
||||
.getChannel()
|
||||
) {
|
||||
IntBuffer ib =
|
||||
fc.map(FileChannel.MapMode.READ_WRITE,
|
||||
0, fc.size()).asIntBuffer();
|
||||
for(int i = 0; i < numOfInts; i++)
|
||||
ib.put(i);
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
new Tester("Stream Read") {
|
||||
@Override
|
||||
public void test() {
|
||||
try(
|
||||
DataInputStream dis =
|
||||
new DataInputStream(
|
||||
new BufferedInputStream(
|
||||
new FileInputStream("temp.tmp")))
|
||||
) {
|
||||
for(int i = 0; i < numOfInts; i++)
|
||||
dis.readInt();
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
new Tester("Mapped Read") {
|
||||
@Override
|
||||
public void test() {
|
||||
try(
|
||||
FileChannel fc = new FileInputStream(
|
||||
new File("temp.tmp")).getChannel()
|
||||
) {
|
||||
IntBuffer ib =
|
||||
fc.map(FileChannel.MapMode.READ_ONLY,
|
||||
0, fc.size()).asIntBuffer();
|
||||
while(ib.hasRemaining())
|
||||
ib.get();
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
new Tester("Stream Read/Write") {
|
||||
@Override
|
||||
public void test() {
|
||||
try(
|
||||
RandomAccessFile raf =
|
||||
new RandomAccessFile(
|
||||
new File("temp.tmp"), "rw")
|
||||
) {
|
||||
raf.writeInt(1);
|
||||
for(int i = 0; i < numOfUbuffInts; i++) {
|
||||
raf.seek(raf.length() - 4);
|
||||
raf.writeInt(raf.readInt());
|
||||
}
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
new Tester("Mapped Read/Write") {
|
||||
@Override
|
||||
public void test() {
|
||||
try(
|
||||
FileChannel fc = new RandomAccessFile(
|
||||
new File("temp.tmp"), "rw").getChannel()
|
||||
) {
|
||||
IntBuffer ib =
|
||||
fc.map(FileChannel.MapMode.READ_WRITE,
|
||||
0, fc.size()).asIntBuffer();
|
||||
ib.put(0);
|
||||
for(int i = 1; i < numOfUbuffInts; i++)
|
||||
ib.put(ib.get(i - 1));
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
public static void main(String[] args) {
|
||||
Arrays.stream(tests).forEach(Tester::runTest);
|
||||
}
|
||||
}
|
||||
/* Output:
|
||||
Stream Write: 0.615
|
||||
Mapped Write: 0.050
|
||||
Stream Read: 0.577
|
||||
Mapped Read: 0.015
|
||||
Stream Read/Write: 4.069
|
||||
Mapped Read/Write: 0.013
|
||||
*/
|
||||
```
|
||||
|
||||
**Tester** 是一个模板方法模式,它为匿名内部子类中定义的 **test()** 的各种实现创建一个测试框架。每个子类都执行一种测试,因此 test() 方法还为您提供了执行各种I/O活动的原型。
|
||||
|
||||
虽然映射的写似乎使用 **FileOutputStream**,但是文件映射中的所有输出必须使用 **RandomAccessFile**,就像前面代码中的读/写一样。
|
||||
|
||||
请注意,**test()** 方法包括初始化各种I/O对象的时间,因此,尽管映射文件的设置可能很昂贵,但是与流I/O相比,总体收益非常可观。
|
||||
|
||||
<!-- File Locking -->
|
||||
## 文件锁定
|
||||
|
||||
|
||||
文件锁定同步访问,因此文件可以是共享资源。但是,争用同一个文件的两个线程可能位于不同的jvm 中,可能一个是 Java 线程,另一个是操作系统中的某个本机线程。文件锁定对其他操作系统进程是可见的,因为Java文件锁定直接映射到本机操作系统锁定工具。
|
||||
|
||||
```java
|
||||
// newio/FileLocking.java
|
||||
// (c)2017 MindView LLC: see Copyright.txt
|
||||
// We make no guarantees that this code is fit for any purpose.
|
||||
// Visit http://OnJava8.com for more book information.
|
||||
import java.nio.channels.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.io.*;
|
||||
|
||||
public class FileLocking {
|
||||
public static void main(String[] args) {
|
||||
try(
|
||||
FileOutputStream fos =
|
||||
new FileOutputStream("file.txt");
|
||||
FileLock fl = fos.getChannel().tryLock()
|
||||
) {
|
||||
if(fl != null) {
|
||||
System.out.println("Locked File");
|
||||
TimeUnit.MILLISECONDS.sleep(100);
|
||||
fl.release();
|
||||
System.out.println("Released Lock");
|
||||
}
|
||||
} catch(IOException | InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Output:
|
||||
Locked File
|
||||
Released Lock
|
||||
*/
|
||||
```
|
||||
|
||||
通过调用 **FileChannel** 上的 **tryLock()** 或 **lock()**,可以获得整个文件的 **FileLock**。( **SocketChannel、DatagramChannel **和 **ServerSocketChannel **不需要锁定,因为它们本质上是单进程实体 ; 通常不会在两个进程之间共享一个网络套接字) **tryLock()** 是非阻塞的。它试图抓住锁,但如果它不能抓住了(当其他进程已经持有相同的锁,并且它不是共享的),它只是从方法调用返回。
|
||||
|
||||
**lock()** 会阻塞,直到获得锁,或者调用 **lock()** 的线程中断,或者调用 **lock()** 方法的通道关闭。使 用**FileLock.release()** 释放锁。还可以使用 **tryLock(long position, long size, boolean shared)** 或**lock(long position, long size, boolean shared)** 锁定文件的一部分,锁住该区域(size-position)。第三个参数指定是否共享此锁。
|
||||
|
||||
虽然零参数锁定方法适应文件大小的变化,但是如果文件大小发生变化,具有固定大小的锁不会发生变化。如果从一个位置到另一个位置获得一个锁,并且文件的增长超过了 position + size ,那么超出位置+大小的部分没有被锁定。零参数锁定方法锁定整个文件,即使它在增长。
|
||||
|
||||
底层操作系统必须提供对独占锁或共享锁的支持。如果操作系统不支持共享锁,并且请求共享锁,则使用独占锁。可以使用 **FileLock.isShared()** 查询锁的类型 (共享或独占)。
|
||||
|
||||
<!-- Locking Portions of a Mapped File -->
|
||||
|
||||
### 锁定映射文件的某些部分
|
||||
|
||||
文件映射通常用于非常大的文件。您可能需要锁定此类文件的某些部分,以便其他进程可以修改未锁定的部分。例如,数据库必须同时对许多用户可用。这里你可以看到两个线程,每个线程都锁定文件的不同部分:
|
||||
|
||||
```java
|
||||
// newio/LockingMappedFiles.java
|
||||
// (c)2017 MindView LLC: see Copyright.txt
|
||||
// We make no guarantees that this code is fit for any purpose.
|
||||
// Visit http://OnJava8.com for more book information.
|
||||
// Locking portions of a mapped file
|
||||
import java.nio.*;
|
||||
import java.nio.channels.*;
|
||||
import java.io.*;
|
||||
|
||||
public class LockingMappedFiles {
|
||||
static final int LENGTH = 0x8FFFFFF; // 128 MB
|
||||
static FileChannel fc;
|
||||
public static void
|
||||
main(String[] args) throws Exception {
|
||||
fc = new RandomAccessFile("test.dat", "rw")
|
||||
.getChannel();
|
||||
MappedByteBuffer out = fc.map(
|
||||
FileChannel.MapMode.READ_WRITE, 0, LENGTH);
|
||||
for(int i = 0; i < LENGTH; i++)
|
||||
out.put((byte)'x');
|
||||
new LockAndModify(out, 0, 0 + LENGTH/3);
|
||||
new LockAndModify(
|
||||
out, LENGTH/2, LENGTH/2 + LENGTH/4);
|
||||
}
|
||||
private static class LockAndModify extends Thread {
|
||||
private ByteBuffer buff;
|
||||
private int start, end;
|
||||
LockAndModify(ByteBuffer mbb, int start, int end) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
mbb.limit(end);
|
||||
mbb.position(start);
|
||||
buff = mbb.slice();
|
||||
start();
|
||||
}
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
// Exclusive lock with no overlap:
|
||||
FileLock fl = fc.lock(start, end, false);
|
||||
System.out.println(
|
||||
"Locked: "+ start +" to "+ end);
|
||||
// Perform modification:
|
||||
while(buff.position() < buff.limit() - 1)
|
||||
buff.put((byte)(buff.get() + 1));
|
||||
fl.release();
|
||||
System.out.println(
|
||||
"Released: " + start + " to " + end);
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Output:
|
||||
Locked: 75497471 to 113246206
|
||||
Locked: 0 to 50331647
|
||||
Released: 75497471 to 113246206
|
||||
Released: 0 to 50331647
|
||||
*/
|
||||
|
||||
```
|
||||
|
||||
**LockAndModify** 线程类设置缓冲区并创建要修改的 **slice()**,在 **run()** 中,锁在文件通道上获取(不能在缓冲区上获取锁—只能在通道上获取锁)。**lock()** 的调用非常类似于获取对象上的线程锁——现在有了一个“临界区”,可以对文件的这部分进行独占访问。[^1]
|
||||
|
||||
当 JVM 退出或关闭获取锁的通道时,锁会自动释放,但是您也可以显式地调用 **FileLock** 对象上的**release()**,如上所示。
|
||||
|
||||
<!-- 脚注 -->
|
||||
|
||||
1. 您可以在附录:低级并发中找到关于线程的更多细节。
|
||||
=======
|
||||
<!-- 分页 -->
|
||||
|
||||
<div style="page-break-after: always;"></div>
|
||||
22
docs/book/Appendix-Object-Serialization.md
Normal file
22
docs/book/Appendix-Object-Serialization.md
Normal file
@@ -0,0 +1,22 @@
|
||||
[TOC]
|
||||
|
||||
<!-- Appendix: Object Serialization -->
|
||||
# 附录:对象序列化
|
||||
|
||||
|
||||
<!-- Finding the Class -->
|
||||
## 查找类
|
||||
|
||||
|
||||
<!-- Controlling Serialization -->
|
||||
## 控制序列化
|
||||
|
||||
|
||||
<!-- Using Persistence -->
|
||||
## 使用持久化
|
||||
|
||||
|
||||
|
||||
<!-- 分页 -->
|
||||
|
||||
<div style="page-break-after: always;"></div>
|
||||
30
docs/book/Appendix-Passing-and-Returning-Objects.md
Normal file
30
docs/book/Appendix-Passing-and-Returning-Objects.md
Normal file
@@ -0,0 +1,30 @@
|
||||
[TOC]
|
||||
|
||||
<!-- Appendix: Passing and Returning Objects -->
|
||||
# 附录:对象传递和返回
|
||||
|
||||
|
||||
<!-- Passing References -->
|
||||
## 传递引用
|
||||
|
||||
|
||||
<!-- Making Local Copies -->
|
||||
## 本地拷贝
|
||||
|
||||
|
||||
<!-- Controlling Cloneability -->
|
||||
## 控制克隆
|
||||
|
||||
|
||||
<!-- Immutable Classes -->
|
||||
## 不可变类
|
||||
|
||||
|
||||
<!-- Summary -->
|
||||
## 本章小结
|
||||
|
||||
|
||||
|
||||
<!-- 分页 -->
|
||||
|
||||
<div style="page-break-after: always;"></div>
|
||||
18
docs/book/Appendix-Programming-Guidelines.md
Normal file
18
docs/book/Appendix-Programming-Guidelines.md
Normal file
@@ -0,0 +1,18 @@
|
||||
[TOC]
|
||||
|
||||
<!-- Appendix: Programming Guidelines -->
|
||||
# 附录:编程指南
|
||||
|
||||
|
||||
<!-- Design -->
|
||||
## 设计
|
||||
|
||||
|
||||
<!-- Implementation -->
|
||||
## 实现
|
||||
|
||||
|
||||
|
||||
<!-- 分页 -->
|
||||
|
||||
<div style="page-break-after: always;"></div>
|
||||
11
docs/book/Appendix-Standard-IO.md
Normal file
11
docs/book/Appendix-Standard-IO.md
Normal file
@@ -0,0 +1,11 @@
|
||||
[TOC]
|
||||
|
||||
<!-- Appendix: Standard I/O -->
|
||||
# 附录:标准IO
|
||||
|
||||
<!-- Process Control -->
|
||||
## 执行控制
|
||||
|
||||
<!-- 分页 -->
|
||||
|
||||
<div style="page-break-after: always;"></div>
|
||||
20
docs/book/Appendix-Supplements.md
Normal file
20
docs/book/Appendix-Supplements.md
Normal file
@@ -0,0 +1,20 @@
|
||||
[TOC]
|
||||
|
||||
<!-- Appendix: Supplements -->
|
||||
# 附录:补充
|
||||
|
||||
|
||||
<!-- Downloadable Supplements -->
|
||||
## 可下载的补充
|
||||
|
||||
|
||||
<!-- Thinking in C: Foundations for Java -->
|
||||
## 通过Thinking-in-C来巩固Java基础
|
||||
|
||||
|
||||
<!-- Hands-On Java eSeminar -->
|
||||
## 动手实践
|
||||
|
||||
|
||||
<!-- 分页 -->
|
||||
<div style="page-break-after: always;"></div>
|
||||
@@ -0,0 +1,8 @@
|
||||
[TOC]
|
||||
|
||||
<!-- Appendix: The Positive Legacy of C++ and Java -->
|
||||
# 附录:C++和Java的优良传统
|
||||
|
||||
<!-- 分页 -->
|
||||
<div style="page-break-after: always;"></div>
|
||||
|
||||
19
docs/book/Appendix-Understanding-equals-and-hashCode.md
Normal file
19
docs/book/Appendix-Understanding-equals-and-hashCode.md
Normal file
@@ -0,0 +1,19 @@
|
||||
[TOC]
|
||||
|
||||
<!-- Appendix: Understanding equals() and hashCode() -->
|
||||
# 附录:理解equals和hashCode方法
|
||||
|
||||
|
||||
<!-- A Canonical equals() -->
|
||||
## equals典范
|
||||
|
||||
|
||||
<!-- Hashing and Hash Codes -->
|
||||
## 哈希和哈希码
|
||||
|
||||
|
||||
<!-- Tuning a HashMap -->
|
||||
## 调整HashMap
|
||||
|
||||
<!-- 分页 -->
|
||||
<div style="page-break-after: always;"></div>
|
||||
10
docs/book/GLOSSARY.md
Normal file
10
docs/book/GLOSSARY.md
Normal file
@@ -0,0 +1,10 @@
|
||||
## 词汇表
|
||||
|
||||
| 词汇 | 解释 |
|
||||
| ----------------| ----------|
|
||||
| **OOP** (*Object-oriented programming*) | 面向对象编程,一种编程思维模式和编程架构|
|
||||
| **UML** (*Unified Modeling Language*) | 统一建模语言,类图 |
|
||||
| **Aggregation** | 聚合,关联关系的一种,是强的关联关系|
|
||||
| **Composition** | 组合,关联关系的一种,是比聚合关系强的关系 |
|
||||
| **STL**(*the Standard Template Library*)| C++ 标准模板库|
|
||||
|
||||
Reference in New Issue
Block a user