mirror of
https://github.com/LingCoder/OnJava8.git
synced 2026-08-24 06:53:27 +08:00
3
.gitignore
vendored
3
.gitignore
vendored
@@ -20,3 +20,6 @@ _book
|
||||
._*
|
||||
*/.DS_Store
|
||||
*/._*
|
||||
|
||||
# IDEA
|
||||
/.idea/*
|
||||
16
README.md
16
README.md
@@ -25,16 +25,16 @@
|
||||
- [x] [第三章 万物皆对象](docs/book/03-Objects-Everywhere.md)
|
||||
- [x] [第四章 运算符](docs/book/04-Operators.md)
|
||||
- [x] [第五章 控制流](docs/book/05-Control-Flow.md)
|
||||
- [ ] [第六章 初始化和清理](docs/book/06-Housekeeping.md)
|
||||
- [ ] [第七章 封装](docs/book/07-Implementation-Hiding.md)
|
||||
- [x] [第六章 初始化和清理](docs/book/06-Housekeeping.md)
|
||||
- [x] [第七章 封装](docs/book/07-Implementation-Hiding.md)
|
||||
- [ ] [第八章 复用](docs/book/08-Reuse.md)
|
||||
- [ ] [第九章 多态](docs/book/09-Polymorphism.md)
|
||||
- [ ] [第十章 接口](docs/book/10-Interfaces.md)
|
||||
- [ ] [第十一章 内部类](docs/book/11-Inner-Classes.md)
|
||||
- [x] [第十一章 内部类](docs/book/11-Inner-Classes.md)
|
||||
- [ ] [第十二章 集合](docs/book/12-Collections.md)
|
||||
- [x] [第十三章 函数式编程](docs/book/13-Functional-Programming.md)
|
||||
- [x] [第十四章 流式编程](docs/book/14-Streams.md)
|
||||
- [ ] [第十五章 异常](docs/book/15-Exceptions.md)
|
||||
- [x] [第十五章 异常](docs/book/15-Exceptions.md)
|
||||
- [ ] [第十六章 代码校验](docs/book/16-Validating-Your-Code.md)
|
||||
- [ ] [第十七章 文件](docs/book/17-Files.md)
|
||||
- [ ] [第十八章 字符串](docs/book/18-Strings.md)
|
||||
@@ -55,7 +55,7 @@
|
||||
- [ ] [附录:理解equals和hashCode方法](docs/book/Appendix-Understanding-equals-and-hashCode.md)
|
||||
- [ ] [附录:集合主题](docs/book/Appendix-Collection-Topics.md)
|
||||
- [ ] [附录:并发底层原理](docs/book/Appendix-Low-Level-Concurrency.md)
|
||||
- [ ] [附录:数据压缩](docs/book/Appendix-Data-Compression.md)
|
||||
- [x] [附录:数据压缩](docs/book/Appendix-Data-Compression.md)
|
||||
- [ ] [附录:对象序列化](docs/book/Appendix-Object-Serialization.md)
|
||||
- [ ] [附录:静态语言类型检查](docs/book/Appendix-Benefits-and-Costs-of-Static-Type-Checking.md)
|
||||
- [ ] [附录:成为一名程序员](docs/book/Appendix-Becoming-a-Programmer.md)
|
||||
@@ -89,9 +89,9 @@
|
||||
|
||||
## 贡献者
|
||||
|
||||
* 主译: [LingCoder](https://github.com/LingCoder)
|
||||
* 参译: [sjsdfg](https://github.com/sjsdfg),[LortSir](https://github.com/LortSir)
|
||||
* 校对:[LingCoder](https://github.com/LingCoder),[nickChenyx](https://github.com/nickChenyx)
|
||||
* 主译:[LingCoder](https://github.com/LingCoder),[sjsdfg](https://github.com/sjsdfg)
|
||||
* 参译:[xiangflight](https://github.com/xiangflight),[LortSir](https://github.com/LortSir)
|
||||
* 校对:[LingCoder](https://github.com/LingCoder),[xiangflight](https://github.com/xiangflight),[nickChenyx](https://github.com/nickChenyx)
|
||||
|
||||
|
||||
## 翻译说明
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
本翻译项目的 GITHUB 开源地址:[https://github.com/LingCoder/OnJava8](https://github.com/LingCoder/OnJava8)
|
||||
|
||||
如果您在阅读本书的过程中有发现不明白或者错误的地方,请随时到项目地址发布 issue 或者 fork 项目后发布 pr 帮助译者改善!不胜感激!
|
||||
如果你在阅读本书的过程中有发现不明白或者错误的地方,请随时到项目地址发布 issue 或者 fork 项目后发布 pr 帮助译者改善!不胜感激!
|
||||
|
||||
## 书籍简介
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
这句话无论对于自然语言还是编程语言来说都是一样的。你所使用的编程语言会将你的思维模式固化并逐渐远离其他语言,而且往往发生在潜移默化中。Java 作为一门傲娇的语言尤其如此。
|
||||
|
||||
Java 是一门派生语言,早期语言设计者为了不想在项目中使用 C++ 而创造了这种看起来很像 C++,却比 C++ 有了改进的新语言(原始的项目并未成功)。Java 最核心的变化就是加入了“虚拟机”和“垃圾回收机制”,这两个概念在之后的章节会有详细描述。 此外,Java 还在其他方面推动了行业发展。例如,现在绝大多数编程语言都支持文档注释语法和 HTML 文档生成工具。
|
||||
Java 是一门派生语言,早期语言设计者为了不想在项目中使用 C++ 而创造了这种看起来很像 C++,却比 C++ 有了改进的新语言(原始的项目并未成功)。Java 最核心的变化就是加入了“虚拟机”和“垃圾回收机制”,这两个概念在之后的章节会有详细描述。 此外,Java 还在其他方面推动了行业发展。例如,现在绝大多数编程语言都支持文档注释语法和 HTML 文档生成工具。
|
||||
|
||||
Java 最主要的概念之一“对象”来自 SmallTalk 语言。SmallTalk 语言恪守“对象”(在下一章中描述)是编程的最基本单元。于是,万物皆对象。历经时间的检验,人们发现这种信念太过狂热。有些人甚至认为“对象”的概念是完全错误的,应该舍弃。就我个人而言,把一切事物都抽象成对象不仅是一项不必要的负担,同时还会招致许多设计朝着不好的方向发展。尽管如此,“对象”的概念依然有其闪光点。固执地要求所有东西都是一个对象(特别是一直到最底层级别)是一种设计错误;相反,完全逃避“对象”的概念似乎同样太过苛刻。
|
||||
|
||||
@@ -19,7 +19,7 @@ Java 语言曾规划设计的许多功能并未按照承诺兑现。本书中,
|
||||
|
||||
许多语言设计决策时都考虑到了复杂性,并试图降低语言的复杂性,但在设计过程中遇到了一些更棘手的问题,最终导致语言设计不可避免地“碰壁”,复杂性增加。例如,C++ 必须向后兼容 C(允许 C 程序员轻松迁移),并且效率很高。这些目标非常实用,并且也是 C++ 在编程界取得了成功的原因之一,但同时也引入了额外的复杂性,导致某些用C++编写的项目开发失败。当然,你可以责怪程序员和管理人员手艺不精,但如果有一种编程语言可以帮助你在开发过程中发现错误,那岂不是更好?
|
||||
|
||||
虽然 VB(Visual BASIC)绑定在 BASIC 上,但 BASIC 实际上并不是一种可扩展的语言。大量扩展的堆积造成 VB 的语法难以维护。Perl 向后兼容 awk、sed、grep 以及其它要替换的 Unix 工具。因此它常常被诟病产生了一堆“只写代码”(*write-only code*,写代码的人自己都看不懂的代码)。另一方面,C ++,VB,Perl 和其他语言(如 SmallTalk)在设计时重点放在了对某些复杂问题的处理上,因而在解决这些特定类型的问题方面非常成功。
|
||||
虽然 VB(Visual BASIC)绑定在 BASIC 上,但 BASIC 实际上并不是一种可扩展的语言。大量扩展的堆积造成 VB 的语法难以维护。Perl 向后兼容 awk、sed、grep 以及其它要替换的 Unix 工具。因此它常常被诟病产生了一堆“只写代码”(*write-only code*,写代码的人自己都看不懂的代码)。另一方面,C ++,VB,Perl 和其他语言(如 SmallTalk)在设计时重点放在了对某些复杂问题的处理上,因而在解决这些特定类型的问题方面非常成功。
|
||||
|
||||
通信革命使我们相互沟通更加便利。无论是一对一沟通,还是团队里的互相沟通,甚至是地球上不同地区的沟通。据说下一次革命需要的是一种全球性的思维,这种思维源于足量的人以及足量相互连接。不我不知道 Java 是否能成为这场革命的工具之一,但至少这种可能性让我觉得:我现在正在做的传道授业的事情是有意义的!
|
||||
|
||||
@@ -78,7 +78,7 @@ Java 语言曾规划设计的许多功能并未按照承诺兑现。本书中,
|
||||
|
||||
## 编码样式
|
||||
|
||||
本书中代码标识符(关键字,方法,变量和类名)以粗体,固定宽度代码字体显示。像“**class**”这种在代码中高频率出现的关键字可能让你觉得粗体有点乏味。(译者注:由于中英排版差异,中问翻译过程并未完全参照原作者的说明。具体排版格式请参考[此处](https://github.com/ruanyf/document-style-guide))其他显示为正常字体。本书文本格式尽可能遵循 Oracle 常见样式,并保证在大多数 Java 开发环境中被支持。书中我使用了自己喜欢的字体风格。Java 是一种自由的编程语言,你也可以使用 IDE(集成开发环境)工具(如 IntelliJ IDEA,Eclipse 或 NetBeans)将格式更改为适合你的格式。
|
||||
本书中代码标识符(关键字,方法,变量和类名)以粗体,固定宽度代码字体显示。像“*class”这种在代码中高频率出现的关键字可能让你觉得粗体有点乏味。(译者注:由于中英排版差异,中文翻译过程并未完全参照原作者的说明。具体排版格式请参考[此处](https://github.com/ruanyf/document-style-guide))其他显示为正常字体。本书文本格式尽可能遵循 Oracle 常见样式,并保证在大多数 Java 开发环境中被支持。书中我使用了自己喜欢的字体风格。Java 是一种自由的编程语言,你也可以使用 IDE(集成开发环境)工具(如 IntelliJ IDEA,Eclipse 或 NetBeans)将格式更改为适合你的格式。
|
||||
|
||||
本书代码文件使用自动化工具进行测试,并在最新版本的 Java 编译通过(除了那些特别标记的错误之外)。本书重点介绍并使用 Java 8 进行测试。如果你必须了解更早的语言版本,可以在 [www.OnJava8.com](http://www.OnJava8.com) 免费下载 《Thinking in Java》。
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
随着 Java 8 的出现,这门语言在许多地方发生了翻天覆地的变化。在新的版本中,代码的运用和实现上与以往不尽相同。这也促使了我时隔多年后再次创作了这本新书。《On Java 8》旨在面向已具有编程基础的开发者们。对于初学者,可以先在 [Code.org](http://Code.org) 或者 [Khan Academy](https://www.khanacademy.org/computing/computer-programming) 等网站上补充必要的前置知识。同时,[OnJava8.com](http://www.OnJava8.com) 上也有免费的 Thinking in C(《C编程思想》)专题知识。
|
||||
|
||||
与几年前我们依赖印刷媒体相比,YouTube,博客和 StackOverflow 等网站的出现让寻找答案变得简单。请结合这些学习途径和努力坚持下去。本书可作为编程入门书籍,同时也适用于想要扩展知识的在职程序员。每次在世界各地的演讲中,我都非常感谢 《*Thinking in Java*》 这本书给我带来的所有荣誉。它对于我重塑 [Reinventing Business](http://www.reinventing-business.com) 项目和促进交流是非常宝贵的。最后,写这本书的原因之一 希望这本书可以为我的这个项目众筹。似乎下一步要创建一个所谓的蓝绿色组织(Teal Organization)才合乎逻辑的。
|
||||
与几年前我们依赖印刷媒体相比,YouTube,博客和 StackOverflow 等网站的出现让寻找答案变得简单。请结合这些学习途径和努力坚持下去。本书可作为编程入门书籍,同时也适用于想要扩展知识的在职程序员。每次在世界各地的演讲中,我都非常感谢 《*Thinking in Java*》 这本书给我带来的所有荣誉。它对于我重塑 [Reinventing Business](http://www.reinventing-business.com) 项目和促进交流是非常宝贵的。最后,写这本书的原因之一 希望这本书可以为我的这个项目众筹。似乎下一步要创建一个所谓的蓝绿色组织(Teal Organization)才合乎逻辑的。
|
||||
|
||||
|
||||
## 教学目标
|
||||
@@ -75,7 +75,7 @@ Java 的普及性对于其受欢迎程度有重要意义。学习 Java 会让你
|
||||
|
||||
## 电子版权声明
|
||||
|
||||
《On Java 8》仅提供电子版,并且仅通过 [www.OnJava8.com](http://www.OnJava8.com) 提供。任何未经 <mindviewinc@gmail.com> 授权的其他来源或流传送机构都是非法的。本作品受版权保护!未经许可,请勿通过以任何方式分享或发布。您可以使用这些示例进行教学,只要不对本书非法重新出版。有关完整详细信息,请参阅示例分发中的 Copyright.txt 文件。对于视觉障碍者,电子版本有可搜索性,字体大小调整或文本到语音等诸多好处。
|
||||
《On Java 8》仅提供电子版,并且仅通过 [www.OnJava8.com](http://www.OnJava8.com) 提供。任何未经 <mindviewinc@gmail.com> 授权的其他来源或流传送机构都是非法的。本作品受版权保护!未经许可,请勿通过以任何方式分享或发布。你可以使用这些示例进行教学,只要不对本书非法重新出版。有关完整详细信息,请参阅示例分发中的 Copyright.txt 文件。对于视觉障碍者,电子版本有可搜索性,字体大小调整或文本到语音等诸多好处。
|
||||
|
||||
任何购买这本书的读者,还需要一台计算机来运行和写作代码。另外电子版在计算机上和移动设备上的显示效果俱佳,推荐使用平板设备阅读。相比购买传统纸质版的价格,平板电脑价格都足够便宜。在床上阅读电子版比看这样一本厚厚的实体书要方便得多。起初你可能会有些不习惯,但我相信很快你就会发现它带来的优点远胜过不适。我已经走过这个阶段,Google Play 图书的浏览器阅读体验非常好,包括在 Linux 和 iOS 设备上。作为一次尝试,我决定尝试通过 Google 图书进行出版。
|
||||
|
||||
@@ -112,7 +112,7 @@ Java 的普及性对于其受欢迎程度有重要意义。学习 Java 会让你
|
||||
|
||||
感谢 *Jeremy Cerise* 和 *Bill Frasure* 来到开发商务虚会预订,并随后提供了宝贵的帮助。
|
||||
|
||||
感谢所有花时间和精力来科罗拉多州克雷斯特德比特(Crested Butte,Colorado)镇参加我的研讨会,开发商务聚会和其他活动的人!你们的贡献可能不容易看到,但却非常重要!
|
||||
感谢所有花时间和精力来科罗拉多州克雷斯特德比特(Crested Butte, Colorado)镇参加我的研讨会,开发商务聚会和其他活动的人!你们的贡献可能不容易看到,但却非常重要!
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -7,13 +7,13 @@
|
||||
|
||||
> “我们没有意识到惯用语言的结构有多大的力量。可以毫不夸张地说,它通过语义反应机制奴役我们。语言表现出来并在无意识中给我们留下深刻印象的结构会自动投射到我们周围的世界。” -- Alfred Korzybski (1930)
|
||||
|
||||
计算机革命的起源来自机器。编程语言就像是那台机器。它不仅是我们思维放大的工具与另一种表达媒介,更像我们思想的一部分。语言的灵感来自其他形式的表达,如写作,绘画,雕塑,动画和电影制作。编程语言就是创建应用程序的思想结构。
|
||||
计算机革命的起源来自机器。编程语言就像是那台机器。它不仅是我们思维放大的工具与另一种表达媒介,更像是我们思想的一部分。语言的灵感来自其他形式的表达,如写作,绘画,雕塑,动画和电影制作。编程语言就是创建应用程序的思想结构。
|
||||
|
||||
面向对象编程(Object-Oriented Programming OOP)是一种编程思维方式和编码架构。本章讲述 OOP 的基本概述。如果读者对不太理解,可先行跳过本章。等你具备一定编程基础后,请务必再回头看。只有这样你才能深刻理解面向对象编程的重要性及设计方式。
|
||||
面向对象编程(Object-Oriented Programming OOP)是一种编程思维方式和编码架构。本章讲述 OOP 的基本概述。如果读者对此不太理解,可先行跳过本章。等你具备一定编程基础后,请务必再回头看。只有这样你才能深刻理解面向对象编程的重要性及设计方式。
|
||||
|
||||
## 抽象
|
||||
|
||||
所有编程语言都提供抽象机制。从某种程度上来说,问题的复杂度直接取决于抽象的类型和质量。这里的“类型”意思是:抽象的内容是什么?汇编语言是对底层机器的轻微抽象。接着出现的“命令式”语言(如 FORTRAN,BASIC 和 C)是对汇编语言的抽象。与汇编相比,这类语言已有了长足的改进,但它们的抽象原理依然要求我们着重考虑计算机的结构,而非问题本身的结构。
|
||||
所有编程语言都提供抽象机制。从某种程度上来说,问题的复杂度直接取决于抽象的类型和质量。这里的“类型”意思是:抽象的内容是什么?汇编语言是对底层机器的轻微抽象。接着出现的“命令式”语言(如 FORTRAN,BASIC 和 C)是对汇编语言的抽象。与汇编相比,这类语言已有了长足的改进,但它们的抽象原理依然要求我们着重考虑计算机的结构,而非问题本身的结构。
|
||||
|
||||
程序员必须要在机器模型(“解决方案空间”)和实际解决的问题模型(“问题空间”)之间建立起一种关联。这个过程既费精力,又脱离编程语言本身的范畴。这使得程序代码很难编写,维护代价高昂。同时还造就了一门副产业的“编程方法”学科。
|
||||
|
||||
@@ -21,26 +21,26 @@
|
||||
问题都归纳为决策链。对于这些语言,我们认为它们一部分是“基于约束”的编程,另一部分则是专为
|
||||
处理图形符号设计的(后者被证明限制性太强)。每种方法都有自己特殊的用途,适合解决某一类的问题。只要超出了它们力所能及的范围,就会显得非常笨拙。
|
||||
|
||||
面向对象的程序设计在此基础上跨出了一大步,程序员可利用一些工具表达“问题空间”内的元素。由于这种表达非常具有普遍性,所以不必受限于特定类型的问题。我们将问题空间中的元素以及它们在解决方案空间的表示物称作“对象”(**Object**)。当然,还有一些在问题空间没有对应的对象体。通过添加新的对象类型,程序可进行灵活的调整,以便与特定的问题配合。所以在阅读方案的描述代码时,会读到对问题进行表达的话语。与我们以前见过的相比,这无疑是一种更加灵活、更加强大的语言抽象方法。总之,OOP 允许我们根据问题来描述问题,而不是根据方案。然而,仍有一个联系途径回到计算机。每个对象都类似一台小计算机;它们有自己的状态,而且可要求它们进行特定的操作。与现实世界的“对象”或者“物体”相比,编程“对象”与它们也存在共通的地方:它们都有自己的特征和行为。
|
||||
面向对象的程序设计在此基础上跨出了一大步,程序员可利用一些工具表达“问题空间”内的元素。由于这种表达非常具有普遍性,所以不必受限于特定类型的问题。我们将问题空间中的元素以及它们在解决方案空间的表示称作“对象”(**Object**)。当然,还有一些在问题空间没有对应的对象体。通过添加新的对象类型,程序可进行灵活的调整,以便与特定的问题配合。所以当你在阅读描述解决方案的代码时,也是在阅读问题的表述。与我们以前见过的相比,这无疑是一种更加灵活、更加强大的语言抽象方法。总之,OOP 允许我们根据问题来描述问题,而不是根据运行解决方案的计算机。然而,它仍然与计算机有联系,每个对象都类似一台小计算机:它们有自己的状态并且可以进行特定的操作。这与现实世界的“对象”或者“物体”相似:它们都有自己的特征和行为。
|
||||
|
||||
Smalltalk 作为第一种成功的面向对象程序设计语言和 Java 的基础语言,*Alan Kay* 总结了其五大基本特征。通过这些特征,我们可理解“纯粹”的面向对象程序设计方法是什么样的:
|
||||
Smalltalk 作为第一个成功的面向对象并影响了 Java的程序设计语言 ,*Alan Kay* 总结了其五大基本特征。通过这些特征,我们可理解“纯粹”的面向对象程序设计方法是什么样的:
|
||||
|
||||
> 1. **万物皆对象**。你可以将对象想象成一种特殊的变量。它可以存储数据,可以在你对其“发出请求”时执行本身的操作。理论上讲,你可以从要解决的问题身上抽象出概念性的组件,然后在程序中将其表达为一个对象。
|
||||
> 2. **程序是一组对象,通过信息传递来告知彼此该做什么**。要请求一个对象,你需要向该对象发送信息。
|
||||
> 1. **万物皆对象**。你可以将对象想象成一种特殊的变量。它存储数据,但可以在你对其“发出请求”时执行本身的操作。理论上讲,你总是可以从要解决的问题身上抽象出概念性的组件,然后在程序中将其表示为一个对象。
|
||||
> 2. **程序是一组对象,通过消息传递来告知彼此该做什么**。要请求调用一个对象的方法,你需要向该对象发送消息。
|
||||
> 3. **每个对象都有自己的存储空间,可容纳其他对象**。或者说,通过封装现有对象,可制作出新型对象。所以,尽管对象的概念非常简单,但在程序中却可达到任意高的复杂程度。
|
||||
> 4. **每个对象都有一种类型**。根据语法,每个对象都是某个“类”的一个“实例”。其中,“类”(Class)是“类型”(Type)的同义词。一个类最重要的特征就是“能将什么信息发给它?”。
|
||||
> 5. **同一类所有对象都能接收相同的信息**。这实际是别有含义的一种说法,大家不久便能理解。由于类型为“圆”(Circle)的一个对象也属于类型为“形状”(Shape)的一个对象,所以一个圆完全能接收形状信息。这意味着可让程序代码统一指挥“形状”,令其自动控制所有符合“形状”描述的对象,其中自然包括“圆”。这一特性称为对象的“可替换性”,是OOP最重要的概念之一。
|
||||
> 4. **每个对象都有一种类型**。根据语法,每个对象都是某个“类”的一个“实例”。其中,“类”(Class)是“类型”(Type)的同义词。一个类最重要的特征就是“能将什么消息发给它?”。
|
||||
> 5. **同一类所有对象都能接收相同的消息**。这实际是别有含义的一种说法,大家不久便能理解。由于类型为“圆”(Circle)的一个对象也属于类型为“形状”(Shape)的一个对象,所以一个圆完全能接收发送给"形状”的消息。这意味着可让程序代码统一指挥“形状”,令其自动控制所有符合“形状”描述的对象,其中自然包括“圆”。这一特性称为对象的“可替换性”,是OOP最重要的概念之一。
|
||||
|
||||
*Grady Booch* 提供了对对象更简洁的描述:一个对象具有自己的状态,行为和身份。这意味着对象有自己的内部数据(由状态提供)、方法 (由特性提供),并彼此区分(每个对象在内存中都有唯一的地址)。
|
||||
*Grady Booch* 提供了对对象更简洁的描述:一个对象具有自己的状态,行为和标识。这意味着对象有自己的内部数据(提供状态)、方法 (产生行为),并彼此区分(每个对象在内存中都有唯一的地址)。
|
||||
|
||||
|
||||
## 接口
|
||||
|
||||
亚里士多德(*Aristotle*)大概是第一个认真研究“类型”的哲学家,他曾提出过“鱼和鸟类”这样的概念。所有对象都是唯一的,但同时也是具有相同的特性和型位的对象所归属的类的一部分。这种思想被首次应用于第一个面向对象编程语言 Simula-67,它在程序中使用基本关键字 **class** 来引入新的类型(class 和 type 通常可互换使用,有些人对它们进行了进一步区分,他们强调 type 决定了接口,而 class 是那个接口的一种特殊实现方式)。
|
||||
亚里士多德(*Aristotle*)大概是第一个认真研究“类型”的哲学家,他曾提出过“鱼类和鸟类”这样的概念。所有对象都是唯一的,但同时也是具有相同的特性和行为的对象所归属的类的一部分。这种思想被首次应用于第一个面向对象编程语言 Simula-67,它在程序中使用基本关键字 **class** 来引入新的类型(class 和 type 通常可互换使用,有些人对它们进行了进一步区分,他们强调 type 决定了接口,而 class 是那个接口的一种特殊实现方式)。
|
||||
|
||||
Simula 是一个很好的例子。正如这个名字所暗示的,它的作用是“模拟”(Simulate)类似“银行出纳员”这样的经典问题。在这个例子里,我们有一系列出纳员、客户、帐号以及交易等。每类成员(元素)都具有一些通用的特征:每个帐号都有一定的余额;每名出纳都能接收客户的存款;等等。与此同时,每个成员都有自己的状态;每个帐号都有不同的余额;每名出纳都有一个名字。所以在计算机程序中,能用独一无二的实体分别表示出纳员、客户、帐号以及交易。这个实体便是“对象”,而且每个对象都隶属一个特定的“类”,那个类具有自己的通用特征与行为。
|
||||
Simula 是一个很好的例子。正如这个名字所暗示的,它的作用是“模拟”(Simulate)类似“银行出纳员”这样的经典问题。在这个例子里,我们有一系列出纳员、客户、帐号、交易和货币单位等许多"对象”。每类成员(元素)都具有一些通用的特征:每个帐号都有一定的余额;每名出纳都能接收客户的存款;等等。与此同时,每个成员都有自己的状态;每个帐号都有不同的余额;每名出纳都有一个名字。所以在计算机程序中,能用独一无二的实体分别表示出纳员、客户、帐号以及交易。这个实体便是“对象”,而且每个对象都隶属一个特定的“类”,那个类具有自己的通用特征与行为。
|
||||
|
||||
因此,在面向对象的程序设计中,尽管我们真正要做的是新建各种各样的数据“类型”(Type),但几乎所有面向对象的程序设计语言都采用了 `class` 关键字。当你看到“type”这个字的时候,请同时想到 `class`;反之亦然。
|
||||
因此,在面向对象的程序设计中,尽管我们真正要做的是新建各种各样的数据“类型”(Type),但几乎所有面向对象的程序设计语言都采用了 `class` 关键字。当你看到“type”这个词的时候,请同时想到 `class`;反之亦然。
|
||||
|
||||
创建好一个类后,可根据情况生成许多对象。随后,可将那些对象作为要解决问题中存在的元素进行处理。事实上,当我们进行面向对象的程序设计时,面临的最大一项挑战性就是:如何在“问题空间”(问题实际存在的地方)的元素与“方案空间”(对实际问题进行建模的地方,如计算机)的元素之间建立理想的“一对一”的映射关系。
|
||||
|
||||
@@ -55,107 +55,121 @@ Light lt = new Light();
|
||||
lt.on();
|
||||
```
|
||||
|
||||
在这个例子中,类型/类的名称是 **Light**,可向 **Light** 对象发出的请求包括包括打开 `on`、关闭 `off`、变得更明亮 `brighten` 或者变得更暗淡 `dim`。通过声明一个引用,如 `lt` 和 `new` 关键字,我们为 **Light** 类对象创建了一个对象,再用等号将其赋给引用。
|
||||
在这个例子中,类型/类的名称是 **Light**,可向 **Light** 对象发出的请求包括包括打开 `on`、关闭 `off`、变得更明亮 `brighten` 或者变得更暗淡 `dim`。通过声明一个引用,如 `lt` 和 `new` 关键字,我们创建了一个**Light**类型的对象,再用等号将其赋给引用。
|
||||
|
||||
为了向对象发送信息,我们使用句点符号 `.` 将 `lt` 和信息名称 `on` 连接起来。可以看出,使用一些预先定义好的类时,我们在程序里采用的代码是非常简单和直观的。
|
||||
为了向对象发送消息,我们使用句点符号 `.` 将 `lt` 和消息名称 `on` 连接起来。可以看出,使用一些预先定义好的类时,我们在程序里采用的代码是非常简单直观的。
|
||||
|
||||
上图遵循 **UML**(Unified Modeling Language,统一建模语言)的格式。每个类由一个框表示,框的顶部有类型名称,框中间部分要描述的任何数据成员,以及方法(属于此对象的方法,它们接收任何发送到该对象的信息)在框的底部。通常,只有类的名称和公共方法在 **UML** 设计图中显示,因此中间部分未显示,如本例所示。如果你只对类名感兴趣,则也不需要显示方法信息。
|
||||
上图遵循 **UML**(Unified Modeling Language,统一建模语言)的格式。每个类由一个框表示,框的顶部有类型名称,框中间部分是要描述的任何数据成员,方法(属于此对象的方法,它们接收任何发送到该对象的消息)在框的底部。通常,只有类的名称和公共方法在 **UML** 设计图中显示,因此中间部分未显示,如本例所示。如果你只对类名感兴趣,则也不需要显示方法信息。
|
||||
|
||||
|
||||
## 服务提供
|
||||
|
||||
在开发或理解程序设计时,我们可以将对象看成是“服务提供者”。你的程序本身将为用户提供服务,并且它能通过调用其他对象提供的服务来实现这一点。我们的最终目标是开发或调用工具库中已有的一些对象,提供理想的服务来解决问题。
|
||||
|
||||
那么问题来了:我们该选择哪个对像来解决问题呢?例如,你正在开发一个记事本程序。你可能会想在屏幕输入一个默认的记事本对象,一个用于检测不同类型打印机并执行打印的对象。这些对象中的某些已经有了。那对于还没有的对象,我们该设计成啥样呢?这些对象需要提供哪些服务,以及还需要调用其他哪些对象?
|
||||
那么问题来了:我们该选择哪个对象来解决问题呢?例如,你正在开发一个记事本程序。*你可能会想到在屏幕输入默认的记事本对象*,一个用于检测不同类型打印机并执行打印的对象。这些对象中的某些已经有了。那对于还没有的对象,我们该设计成啥样呢?这些对象需要提供哪些服务,以及还需要调用其他哪些对象?
|
||||
|
||||
我们可以将这些问题一一分解,抽象成一组服务。软件设计的基本原则是高内聚:每个组件的内部作用明确,功能紧密相关。然而经常有人将太多功能塞进一个对象中。例如:在支票打印模块中,你需要设计一个可以同时读取文本格式又能正确识别不同打印机型号的对象。正确的做法是提供三个或更多对象:一个对象检查所有排版布局的目录;一个或一组可以识别不同打印机型号的对象展示通用的打印界面;第三个对象组合上述两个服务来完成任务。这样,每个对象都提供了一组紧密的服务。在良好的面向对象设计中,每个对象功能单一且高效。这样的程序设计可以提高我们代码的复用性,同时也方便别人阅读和理解我们的代码。只有让人知道你提供什么服务,别人才能更好地将其应用到其他模块或程序中。
|
||||
|
||||
|
||||
## 封装
|
||||
|
||||
我们可以把编程的侧重领域划分为研发和应用。应用程序员调用研发程序员构建的基础工具类来做快速开发。研发程序员开发一个工具类,该工具类仅向应用程序员公开必要的内容,并隐藏内部实现的细节。这样可以有效地避免该工具类被错误的使用和更改,从而减少程序出错的可能。彼此职责划分清晰,相互协作。当应用程序员调用研发程序员开发的工具类时,双方建立了关系。应用程序员通过使用现成的工具类组装应用程序或者构建更大的工具库。如果工具类的创建者将类的内部所有信息都公开给调用者,那么有些使用规则就不容易被遵守。因为前者无法保证后者是否按照正确的规则来使用或是改变了该工具类。只有设定访问控制,才能从根本上阻止。
|
||||
我们可以把编程的侧重领域划分为研发和应用。应用程序员调用研发程序员构建的基础工具类来做快速开发。研发程序员开发一个工具类,该工具类仅向应用程序员公开必要的内容,并隐藏内部实现的细节。这样可以有效地避免该工具类被错误的使用和更改,从而减少程序出错的可能。彼此职责划分清晰,相互协作。当应用程序员调用研发程序员开发的工具类时,双方建立了关系。应用程序员通过使用现成的工具类组装应用程序或者构建更大的工具库。如果工具类的创建者将类的内部所有信息都公开给调用者,那么有些使用规则就不容易被遵守。因为前者无法保证后者是否会按照正确的规则来使用,甚至是改变该工具类。只有设定访问控制,才能从根本上阻止这种情况的发生。
|
||||
|
||||
因此,使用访问控制的原因有以下2点:
|
||||
|
||||
1. 让应用程序员不要触摸他们不应该触摸的部分。(请注意,这也是一个哲学决策。部分编程语言认为如果程序员有需要,则应该让他们访问细节部分。);
|
||||
|
||||
2. 使类库的创建者(研发程序员)在不影响后者使用的情况下完善更新工具库。例如,我们开发了一个功能简单的工具类,后来发现可以通过优化代码来提高执行速度。假如工具类的接口和实现部分明确分开并受到保护,那我们就可以轻松的完成改造。
|
||||
2. 使类库的创建者(研发程序员)在不影响后者使用的情况下完善更新工具库。例如,我们开发了一个功能简单的工具类,后来发现可以通过优化代码来提高执行速度。假如工具类的接口和实现部分明确分开并受到保护,那我们就可以轻松地完成改造。
|
||||
|
||||
Java 有三个显式关键字来设置类中的访问权限:`public`(公开),`private`(私有)和`protected`(受保护)。这些修饰符可以明确谁能访问后面的方法、变量或类。
|
||||
Java 有三个显式关键字来设置类中的访问权限:`public`(公开),`private`(私有)和`protected`(受保护)。这些访问修饰符决定了谁能使用它们修饰的方法、变量或类。
|
||||
|
||||
1. `public` (公开) 表示任何人都可以访问和使用该元素;
|
||||
|
||||
2. `private` (私有) 除了类本身,外界无法直接访问该元素和内部方法。`private` 是你和调用者之间的屏障。任何试图访问私有成员的人都会收到编译时错误;
|
||||
2. `private` (私有) 除了类本身和类内部的方法,外界无法直接访问该元素。`private` 是类和调用者之间的屏障。任何试图访问私有成员的行为都会报编译时错误;
|
||||
|
||||
3. `protected` (受保护) 类似于 `private`,区别是继承类(下一节就会引入继承的概念)可以访问 `protected` 的成员,但不能访问 `private` 成员;
|
||||
3. `protected` (受保护) 类似于 `private`,区别是子类(下一节就会引入继承的概念)可以访问 `protected` 的成员,但不能访问 `private` 成员;
|
||||
|
||||
4. `default` (默认) 如果你不使用前面的三者,默认就是 `default` 访问权限。`default` 被称之为包访问,因为该权限下的资源可以同一包(库组件)中其他类的成员访问。
|
||||
4. `default` (默认) 如果你不使用前面的三者,默认就是 `default` 访问权限。`default` 被称为包访问,因为该权限下的资源可以被同一包(库组件)中其他类的成员访问。
|
||||
|
||||
|
||||
## 复用
|
||||
|
||||
一个类经创建和测试后,理应是可复用的。然而很多时候,由于程序员没有足够的编程经验和远见,我们的代码复用性并不强。
|
||||
|
||||
代码和设计方案的复用性是面向对象的程序设计的优点之一。我们可以通过重复使用某个 `class` 来达到这种复用性。同时,我们也可以将这个 `class` 作为另一个 `class` 的成员变量来使用。新的对象可以是由任意数量、类型的其他对象构成。这里涉及到“组合”和“聚合”的概念:
|
||||
代码和设计方案的复用性是面向对象程序设计的优点之一。我们可以通过重复使用某个类的对象来达到这种复用性。同时,我们也可以将一个类的对象作为另一个类的成员变量使用。新的类可以是由任意数量和任意类型的其他对象构成。这里涉及到“组合”和“聚合”的概念:
|
||||
|
||||
* **组合**(Composition) 经常用来表示“拥有”关系(Has-a Relationship)。例如,“汽车拥有了主机”。
|
||||
* **组合**(Composition)经常用来表示“拥有”关系(has-a relationship)。例如,“汽车拥有引擎”。
|
||||
|
||||
* **聚合**(Aggregation) 动态的 **组合**。
|
||||
* **聚合**(Aggregation) 动态的 **组合**。
|
||||
|
||||

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

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

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

|
||||
|
||||
例如,某些形状可以翻转。有些行为可能不同,比如计算形状的面积时。类型层次结构体现了形状之间的相似性和差异。以与问题相同的术语转换解决方案是有用的,因为你不需要中间模型来从问题的描述获得解决方案的描述。对于对象,类型层次结构是模型的一个重要方面,因此你可以直接从真实世界中的系统描述转到代码中的系统描述。的确,有时候,那些被训练去寻找复杂解决方案的人在面向对象设计的简单性方面有困难。从现有类型继承创建新类型。这种新类型不仅包含现有类型的所有成员(尽管私有成员被隐藏起来并且不可访问),更重要的是它复制了基类的接口。也就是说,基类对象接受的所有信息也被派生类对象接受。根据类接受的信息,我们知道类的类型,因此派生类与基类是相同的类型。
|
||||
例如,某些形状可以翻转。有些行为可能不同,比如计算形状的面积。类型层次结构体现了形状之间的相似性和差异性。以相同的术语将解决方案转换成问题是有用的,因为你不需要在问题描述和解决方案描述之间建立许多中间模型。通过使用对象,类型层次结构成为了主要模型,因此你可以直接从真实世界中对系统的描述过渡到用代码对系统进行描述。事实上,有时候,那些善于寻找复杂解决方案的人会被面向对象设计的简单性难倒。从现有类型继承创建新类型。这种新类型不仅包含现有类型的所有成员(尽管私有成员被隐藏起来并且不可访问),而且更重要的是它复制了基类的接口。也就是说,基类对象接收的所有消息也能被派生类对象接收。根据类接收的消息,我们知道类的类型,因此派生类与基类是相同的类型。
|
||||
|
||||
在前面的例子中,“圆是形状”。这种通过继承的类型等价性是理解面向对象编程含义的基本门槛之一。因为基类和派生类都具有相同的基本接口,所以伴随此接口的必定有某些具体实现。也就是说,当对象接收到特定消息时,必须有可执行代码。如果继承一个类而不做其他任何事,则来自基类接口的方法直接进入派生类。这意味着派生类和基类不仅具有相同的类型,而且具有相同的行为,这么做没什么特别意义。
|
||||
|
||||
有两种方法可以区分新的派生类与原始的基类。第一种方法很简单:在派生类中添加新方法。这些新方法不是基类接口的一部分。这意味着基类不能满足你的所有需求,所以你添加了更多的方法。继承的这种简单而原始的用途有时是解决问题的完美解决方案。然而,还是要仔细考虑是否在基类中也要有这些额外的方法。这种设计的发现与迭代过程在面向对象程序设计中会经常发生。
|
||||
|
||||
尽管继承有时意味着你要在接口中添加新方法(尤其是在以**extends**关键字表示继承的Java中),但并非总需如此。第二种也是更重要地区分派生类和基类的方法是改变现有基类方法的行为,这被称为覆盖(overriding)。要想覆盖一个方法,只需要在派生类中重新定义这个方法即可。
|
||||
|
||||
### "是一个"与"像是一个"的关系
|
||||
|
||||
对于继承可能会引发争论:继承应该只覆盖基类的方法(不应该添加基类中没有的方法)吗?如果这样的话,基类和派生类就是相同的类型了,因为它们具有相同的接口。这会造成,你可以用一个派生类对象完全替代基类对象,这叫作"纯粹替代",也经常被称作"替代原则"。在某种意义上,这是一种处理继承的理想方式。我们经常把这种基类和派生类的关系称为是一个(is-a)关系,因为可以说"圆是一个形状"。判断是否继承,就看在你的类之间有无这种is-a关系。
|
||||
|
||||
有时你在派生类添加了新的接口元素,从而扩展接口。虽然新类型仍然可以替代基类,但是这种替代不完美,原因在于基类无法访问新添加的方法。这种关系称为像是一个(is-like-a)关系。新类型不但拥有旧类型的接口,而且包含其他方法,所以不能说新旧类型完全相同。
|
||||
|
||||

|
||||
|
||||
在前面的例子中,“圆是形状”。这种通过继承的类型等价是理解面向对象编程含义的基本网关之一。因为基类和派生类都具有相同的基本接口,所以必须有一些实现来支持该接口。也就是说,当对象接收到特定信息时,必须有可执行代码。如果继承一个类并且不做其他任何事情,则来自基类接口的方法直接进入派生类。这意味着派生类的对象不仅具有相同的类型,而且具有相同的行为,这并不特别有趣。有两种方法可以区分新派生类与原始基类。第一种方法很简单:向派生类添加全新的方法。这些新方法不是基类接口的一部分。这意味着基类没有按照你想要的那样多,所以你添加了更多的方法。继承的这种简单而原始的用途有时是解决问题的完美解决方案。然而,事先还是要仔细调查自己的基础类是否真的需要这些额外的方法。
|
||||
以空调为例,假设房间里已经安装好了制冷设备的控制器,即你有了控制制冷设备的接口。想象一下,现在空调坏了,你重新安装了一个既制冷又制热的热力泵。热力泵就像是一个(is-like-a)空调,但它可以做更多。因为当初房间的控制系统被设计成只能控制制冷设备,所以它只能与新对象(热力泵)的制冷部分通信。新对象的接口已经扩展了,现有控制系统却只知道原来的接口,一旦看到这个设计,你就会发现,作为基类的制冷系统不够一般化,应该被重新命名为"温度控制系统",也应该包含制热功能,这样的话,我们就可以使用替代原则了。上图反映了在现实世界中进行设计时可能会发生的事情。
|
||||
|
||||
当你看到替代原则时,很容易会认为纯粹替代是唯一可行的方式,并且使用纯粹替代的设计是很好的。但有些时候,你会发现必须得在派生(扩展)类中添加新方法(提供新的接口)。只要仔细审视,你可以很明显地区分两种设计方式的使用场合。
|
||||
|
||||
|
||||
## 多态
|
||||
|
||||
我们在处理类的层次结构时,通常是定义对象的基类而不是对象本身。通过这种方式,我们可以编写出不局限于特定类型的代码。在上个“形状”的图例中,“方法”(method)操纵的是通用“形状”,而不关心它们是“圆”、“正方形”、“三角形”还是某种尚未定义的形状。所有的形状都可以被绘制、擦除和移动,因此“方法”向其中的任何代表“形状”的对象发送信息都不必担心对象如何处理信息。
|
||||
我们在处理类的层次结构时,通常把一个对象看成是它所属的基类,而不是把它当成具体类。通过这种方式,我们可以编写出不局限于特定类型的代码。在上个“形状”的例子中,“方法”(method)操纵的是通用“形状”,而不关心它们是“圆”、“正方形”、“三角形”还是某种尚未定义的形状。所有的形状都可以被绘制、擦除和移动,因此“方法”向其中的任何代表“形状”的对象发送消息都不必担心对象如何处理信息。
|
||||
|
||||
这样代码不受添加的新类型影响,并且添加新类型是扩展面向对象程序以处理新情况的常用方法。 举个例子来说吧:你可以通过这个通用的“形状”基类来得到一个新的“五角星”形状得子类。通过派生新的子类来扩展设计的这种能力是封装变化的基本方法之一。
|
||||
这样的代码不会受添加的新类型影响,并且添加新类型是扩展面向对象程序以处理新情况的常用方法。 例如,你可以通过通用的“形状”基类派生出新的“五角形”形状的子类,而不需要修改通用"形状"基类的方法。通过派生新的子类来扩展设计的这种能力是封装变化的基本方法之一。
|
||||
|
||||
这样的设计减少了程序的维护难度。我们把派生的对象类型统一看成是它本身的基础类型(“圆”是一种“形状”,“自行车”是“车”,“鸬鹚”也是“鸟”等等)。编译器(compiler)在编译时期无法精确的知道什么“形状”被擦除,哪一种“车”在行驶,或者是某种“鸟”在飞行。当程序接收这种信息之前程序员并不知道哪段代码会被执行。“擦除”的方法可以平等地应用到每一种可能的“形状”上。
|
||||
这种能力改善了我们的设计,且减少了软件的维护代价。如果我们把派生的对象类型统一看成是它本身的基础类型(“圆”当作“形状”,“自行车”当作“车”,“鸬鹚”当作“鸟”等等),编译器(compiler)在编译时期就无法准确地知道什么“形状”被擦除,哪一种“车”在行驶,或者是哪种“鸟”在飞行。这就是关键所在:当程序接收这种消息时,程序员并不想知道哪段代码会被执行。“绘图”的方法可以平等地应用到每种可能的“形状”上,形状会依据自身的具体类型执行恰当的代码。
|
||||
|
||||
如果不需要考虑执行了哪部分代码,那我们就能添加一个新的不同执行方式的子类而不需要更改调用它的方法。那么编译器在不确定该执行哪部分代码时是怎么做的呢?举个例子,下图的 **BirdController** 对象和通用 **Bird** 对象中,**BirdController** 不知道 **Bird** 的确切类型却还能一起工作。从 **BirdController** 的角度来看,这是很方便的,因为它不需要特殊的代码来确定 **Bird** 工作的确切类型或行为。那么,在调用 **move()** 方法时是如何保证发生正确的行为(鹅走路、苍蝇或游泳、企鹅走路或游泳)的呢?
|
||||
如果不需要知道执行了哪部分代码,那我们就能添加一个新的不同执行方式的子类而不需要更改调用它的方法。那么编译器在不确定该执行哪部分代码时是怎么做的呢?举个例子,下图的 **BirdController** 对象和通用 **Bird** 对象中,**BirdController** 不知道 **Bird** 的确切类型却还能一起工作。从 **BirdController** 的角度来看,这是很方便的,因为它不需要编写特别的代码来确定 **Bird** 对象的确切类型或行为。那么,在调用 **move()** 方法时是如何保证发生正确的行为(鹅走路、飞或游泳、企鹅走路或游泳)的呢?
|
||||
|
||||

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

|
||||
|
||||
这实际是一个非常有用的编程技巧。分析下面这行代码:
|
||||
这是一个非常令人惊奇的编程技巧。分析下面这行代码:
|
||||
|
||||
```java
|
||||
doSomething(circle);
|
||||
```
|
||||
这里将 **Circle**(圆)句柄传递给一个本来期待 **Shape**(形状)句柄的方法。由于圆也是一种几何形状,所
|
||||
以 `doSomething(circle)` 能正确地执行。也就是说,`doSomething()` 能接受任意 **Shape** 的信息。这是完全安全和合乎逻辑的事情。
|
||||
当预期接收 **Shape** 的方法被传入了 **Circle**,会发生什么。由于 **Circle** 也是一种 **Shape**,所
|
||||
以 `doSomething(circle)` 能正确地执行。也就是说,`doSomething()` 能接收任意发送给 **Shape** 的消息。这是完全安全和合乎逻辑的事情。
|
||||
|
||||
这种把子类当成其基类来处理的过程叫做“向上转型”(**Upcasting**)。在面向对象的编程里,经常利用这种方法来给程序解耦。再看下面的 `doSomething()` 代码示例:
|
||||
这种把子类当成其基类来处理的过程叫做“向上转型”(**upcasting**)。在面向对象的编程里,经常利用这种方法来给程序解耦。再看下面的 `doSomething()` 代码示例:
|
||||
|
||||
```java
|
||||
shape.erase();
|
||||
// ...
|
||||
shape.draw();
|
||||
|
||||
```
|
||||
|
||||
我们可以看到程序并未这样表达:“如果你是一个 Circle ,就这样做;如果你是一个 Square,就那样做;等等”。若那样编写代码,就需检查 Shape 所有可能的类型,如圆、矩形等等。这显然是非常麻烦的,而且每次添加了一种新的 Shape 类型后,都要相应地进行修改。在这里,我们只需说:“你是一种几何形状,我知道你能将自己删掉,即 `erase()`;请自己采取具体行动,并控制所有的细节吧。”
|
||||
我们可以看到程序并未这样表达:“如果你是一个 Circle ,就这样做;如果你是一个 Square,就那样做…...”。若那样编写代码,就需检查 Shape 所有可能的类型,如圆、矩形等等。这显然是非常麻烦的,而且每次添加了一种新的 Shape 类型后,都要相应地进行修改。在这里,我们只需说:“你是一种几何形状,我知道你能删掉 `erase()` 和绘制 `draw()` 你自己,去做吧,注意细节。”
|
||||
|
||||
尽管我们没作出任何特殊指示,程序的操作也是完全正确和恰当的。我们知道,为 Circle 调用`draw()` 时执行的代码与为一个 Square 或 Line 调用 `draw()` 时执行的代码是不同的。但在将 `draw()` 信息发给一个匿名 Shape 时,根据 Shape 句柄当时连接的实际类型,会相应地采取正确的操作。这非常神奇,因为当 Java 编译器为 `doSomething()` 编译代码时,它并不知道自己要操作的准确类型是什么。
|
||||
|
||||
尽管我们确实可以保证最终会为 Shape 调用 `erase()`、 `draw()`,但并不能确定特定的 Circle,Square 或者 Line 调用什么。最后,程序执行的操作却依然是正确的,这是怎么做到的呢?
|
||||
|
||||
将信息发给对象时,如果程序不知道接受的具体类型是什么,但最终执行是正确的,这就是对象的“多态性”(Polymorphism)。面向对象的程序设计语言是通过“动态绑定”的方式来实现对象的多态性的。编译器和运行期系统会负责对所有细节的控制;我们只需知道要做什么,以及如何利用多态性来更好的设计程序。
|
||||
发送消息给对象时,如果程序不知道接收的具体类型是什么,但最终执行是正确的,这就是对象的“多态性”(Polymorphism)。面向对象的程序设计语言是通过“动态绑定”的方式来实现对象的多态性的。编译器和运行时系统会负责对所有细节的控制;我们只需知道要做什么,以及如何利用多态性来更好地设计程序。
|
||||
|
||||
## 单继承
|
||||
## 单继承结构
|
||||
|
||||
自从 C++ 引入以来,OOP 问题变得尤为突出。是否所有的类都应该默认从一个基类继承呢?这个答案在 Java 中是肯定的。(实际上,除 C++ 以外的其他虚拟机语言也是这样。)在 Java 中这个最终的基类的名字就是 `Object`。
|
||||
自从 C++ 引入以来,一个OOP 问题变得尤为突出:是否所有的类都应该默认从一个基类继承呢?这个答案在 Java 中是肯定的(实际上,除 C++ 以外的几乎所有OOP语言中也是这样)。在 Java 中,这个最终的基类的名字就是 `Object`。
|
||||
|
||||
Java 的单继承结构有很多好处。由于所有对象都有继承自一个公共接口,因此它们最终都属于同一个基本类型。相反的,对于 C++ 所使用的多继承的方案则是不保证所有的对象都属于同一个的基类。这种方案的限制更少一点。从向后兼容的角度看,多继承的方案更符合 C 的模型。
|
||||
Java 的单继承结构有很多好处。由于所有对象都具有一个公共接口,因此它们最终都属于同一个基本类型。相反的,对于 C++ 所使用的多继承的方案则是不保证所有的对象都属于同一个基类。从向后兼容的角度看,多继承的方案更符合 C 的模型,而且受限较少。
|
||||
|
||||
对于完全面向对象编程,我们必须要构建自己的层次结构,以提供与其他 OOP 语言同样的便利。我们经常会使用到新的类库和不兼容的接口。为了整合它们而花费大气力(有可能还要用上多继承)以获得 C++ 样的“灵活性”值得吗?如果从零开始,Java 这样的替代方案会是更好的选择。
|
||||
|
||||
@@ -223,23 +236,25 @@ Java 的单继承结构有很多好处。由于所有对象都有继承自一个
|
||||
|
||||
## 集合
|
||||
|
||||
通常,我们并不知道解决某个具体问题需要的对象数量,持续时间,以及对象的存储方式。那么我们如何知悉程序在创建时需要分配的内存空间呢?
|
||||
通常,我们并不知道解决某个具体问题需要的对象数量和持续时间,以及对象的存储方式。那么我们如何知悉程序在运行时需要分配的内存空间呢?
|
||||
|
||||
在面向对象的设计中,问题的解决方案有些千篇一律:创建一个新类型的对象来引用、容纳其他的对象。当然,我们也可以使用多数编程语言都支持的“数组”(Array)。在 Java 中“集合”(Collection)的使用率更高。(也可称之为“容器”,但“集合”这个称呼更通用。)
|
||||
在面向对象的设计中,问题的解决方案有些过于轻率:创建一个新类型的对象来引用、容纳其他的对象。当然,我们也可以使用多数编程语言都支持的“数组”(array)。在 Java 中“集合”(Collection)的使用率更高。(也可称之为“容器”,但“集合”这个称呼更通用。)
|
||||
|
||||
“集合”这种类型的对象可以存储任意类型、数量的其他对象。它能根据需要自动扩容,我们不用关心过程是如何实现的。
|
||||
|
||||
还好,一般优秀的 OOP 语言都会将“集合”作为其基础包。在 C++ 中,“集合”是其标准库的一部分。通常被称为 STL(Standard Template Library,标准模板库)。SmallTalk 有一套非常完整的集合库。同样,Java 的标准库中也提供许多现成的集合类。在一些库中,一两个集合泛型就能满足我们所有的需求了。在 Java 中不同的需求对应不同种类的集合
|
||||
还好,一般优秀的 OOP 语言都会将“集合”作为其基础包。在 C++ 中,“集合”是其标准库的一部分,通常被称为 STL(Standard Template Library,标准模板库)。SmallTalk 有一套非常完整的集合库。同样,Java 的标准库中也提供许多现成的集合类。
|
||||
|
||||
在一些库中,一个或两个泛型集合被认为是对所有需求都足够好的,而在其他(Java)中,不同类型的集合对应不同的需求:常见的有 List,常用于保存序列;Map,也称为关联数组,常用于将对象与其他对象关联);Set,只能保存非重复的值;其他还包括如队列(Queue)、树(Tree)、堆(Stack)等等。从设计的角度来看,我们真正想要的是一个能够解决某个问题的集合。如果一种集合就满足所有需求,那么我们就不需要剩下的了。之所以选择集合有以下两个原因:
|
||||
在一些库中,一两个集合泛型集合就能满足我们所有的需求了,而在其他(Java)中,不同类型的集合对应不同的需求:常见的有 List,常用于保存序列;Map,也称为关联数组,常用于将对象与其他对象关联);Set,只能保存非重复的值;其他还包括如队列(Queue)、树(Tree)、堆(Stack)等等。从设计的角度来看,我们真正想要的是一个能够解决某个问题的集合。如果一种集合就满足所有需求,那么我们就不需要剩下的了。之所以选择集合有以下两个原因:
|
||||
|
||||
1. 集合可以提供不同类型的接口和外部行为。堆栈、队列的应用场景和集合、列表不同,为我们解决问题提供了灵活的方案。
|
||||
1. 集合可以提供不同类型的接口和外部行为。堆栈、队列的应用场景和集合、列表不同,它们中的一种提供的解决方案可能比其他灵活得多。
|
||||
|
||||
2. 不同的集合种类对应着不同的用途。例如,List 的两种基本类型:ArrayList 和 LinkedList。虽然两者具有相同接口和外部行为,但是在某些操作中它们的效率差别很大。在 ArrayList 中随机查找元素是很高效的,而 LinkedList 随机查找效率低下。反之,在 LinkedList 中插入元素的效率要比在 ArrayList 中高。由于底层数据结构的不同,每种集合类型在执行相同的操作时会表现出效率上的差异。
|
||||
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)。
|
||||
我们可以一开始使用 LinkedList 构建程序,在优化系统性能时改用 ArrayList。通过对 List 接口的抽象,我们可以很容易地将 LinkedList 改为 ArrayList。
|
||||
|
||||
参数化类型机制可以使得编译器能够自动识别某个 `class` 的具体类型并正确地执行. 举个例子,对集合的参数化类型机制可以让其仅接受“形状”这种类型的元素,并以“形状”类型取出元素。Java 5 版本支持了参数化类型机制,称之为“泛型”(Generic)。泛型是 Java 5 的主要特性之一。举个例子,你可以按以下方式向 ArrayList 种添加 Shape(形状):
|
||||
在 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<>();
|
||||
@@ -248,47 +263,48 @@ Java 的单继承结构有很多好处。由于所有对象都有继承自一个
|
||||
泛型的应用,让 Java 的许多标准库和组件都发生了改变。在本书的代码示例中,你也会经常看到泛型的身影。
|
||||
|
||||
|
||||
## 生命周期
|
||||
## 对象创建与生命周期
|
||||
|
||||
我们在使用对象时要注意的一个关键问题就是对象的创建和销毁方式。每个对象的生存都需要资源,尤其是内存。为了资源的重复利用,当对象不再被使用时我们应该及时释放资源,清理内存。
|
||||
我们在使用对象时要注意的一个关键问题就是对象的创建和销毁方式。每个对象的生存都需要资源,尤其是内存。为了资源的重复利用,当对象不再被使用时,我们应该及时释放资源,清理内存。
|
||||
|
||||
在简单的编程场景下,对象的清理并不是问题。我们创建对象,按需使用,最后销毁它。然而,情况往往要比这更复杂:
|
||||
|
||||
假设,我们正在为机场设计一个空中交通管制的系统(该例也适用于仓库货柜管理、影带出租或者宠物寄养仓库系统)。第一步比较简单:创建一个用来保存飞机的集合,每当有飞机进入交通管制区域时,我们就创建一个“飞机”对象并将其加入到集合中,等到飞机离开时将其从这个集合中清除。与此同时,我们还需要一个记录飞机信息的系统。这些数据的重要性靠后,可以放在系统空闲的时候再后台处理。比如,我们要记录所有飞机中的小型飞机的的信息(比如飞行计划)。此时,我们又创建了第二个集合来记录所有小型飞机。 每当创建一个“飞机”对象的时候,将其放入第一个集合;若它属于小型飞机,也必须同时将其放入第二个集合里。
|
||||
假设,我们正在为机场设计一个空中交通管制的系统(该例也适用于仓库货柜管理、影带出租或者宠物寄养仓库系统)。第一步比较简单:创建一个用来保存飞机的集合,每当有飞机进入交通管制区域时,我们就创建一个“飞机”对象并将其加入到集合中,等到飞机离开时将其从这个集合中清除。与此同时,我们还需要一个记录飞机信息的系统,也许这些数据不像主要控制功能那样引人注意。比如,我们要记录所有飞机中的小型飞机的的信息(比如飞行计划)。此时,我们又创建了第二个集合来记录所有小型飞机。 每当创建一个“飞机”对象的时候,将其放入第一个集合;若它属于小型飞机,也必须同时将其放入第二个集合里。
|
||||
|
||||
现在问题开始棘手了:我们怎么知道何时该清理这些对象呢?当某一个系统处理完成,而其他系统可能还没有处理完成。这样的问题在其他的场景下也可能发生。在 C++ 程序设计中,当使用完一个对象后,必须明确将其删除,这就让问题变复杂了。
|
||||
|
||||
这个对象的数据在哪?它的生命周期是怎么被控制的? 在 C++ 设计中采用的观点是效率第一,因此它将选择权交给了程序员。为了获得最大的运行时速度,程序员可以在编写程序时,通过将对象放在堆栈(Stack,有时称为自动变量或作用域变量)或静态存储区域(static storage area)中来确定内存占用和生存时间。这些区域的对象会被优先分配内存和释放。这种控制在某些情况下非常有用。
|
||||
对象的数据在哪?它的生命周期是怎么被控制的? 在 C++ 设计中采用的观点是效率第一,因此它将选择权交给了程序员。为了获得最大的运行时速度,程序员可以在编写程序时,通过将对象放在堆栈(Stack,有时称为自动变量或作用域变量)或静态存储区域(static storage area)中来确定内存占用和生存时间。这些区域的对象会被优先分配内存和释放。这种控制在某些情况下非常有用。
|
||||
|
||||
然而相对的,我们也牺牲了程序的灵活性。因为在编写代码时,我们必须要弄清楚对象的数量、生存时间还有类型。如果我们要用它来解决一个相当普遍的问题时(如计算机辅助设计、仓库管理或空中交通管制等),限制就太大了。
|
||||
|
||||
第二种方法是在堆内存(Heap)中动态地创建对象。在这种方式下,直到程序运行我们才能确定需要创建的对象数量、生存时间和类型。什么时候需要,什么时候在堆内存中创建。 因为内存的占用是动态管理的,所以在运行时,在堆内存上开辟空间所需的时间可能比在栈内存上要长(但也不一定)。在栈内存开辟空间通常是一条将栈指针向下移动,另一条将其向后移动的汇编指令。开辟堆内存空间的时间取决于内存机制的设计。
|
||||
第二种方法是在堆内存(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.*)此外,更好的灵活性对于问题的解决至关重要。
|
||||
动态方法有这样一个一般性的逻辑假设:对象趋向于变得复杂,因此额外的内存查找和释放对对象的创建影响不大。(原文:*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 使用动态内存分配。每次创建对象时,使用 `new` 关键字构建该对象的动态实例。这又带来另一个问题:对象的生命周期。较之堆内存,在栈内存中创建对象,编译器能够确定该对象的生命周期并自动销毁它;然而如果你在堆内存创建对象的话,编译器是不知道它的生命周期的。在 C++ 中你必须以编程方式确定何时销毁对象,否则可能导致内存泄漏。Java 的内存管理是建立在垃圾收集器上的,它能自动发现对象不再被使用并释放内存。垃圾收集器的存在带来了极大的便利,它减少了我们之前必须要跟踪的问题和编写相关代码的数量。因此,垃圾收集器提供了更高级别的保险,以防止潜在的内存泄漏问题,这个问题使得许多 C++ 项目没落。
|
||||
|
||||
Java 的垃圾收集器被设计用来解决内存释放的问题(虽然这不包括对象清理的其他方面)。垃圾收集器知道对象什么时候不再被使用并且自动释放内存。结合单继承和仅可在堆中创建对象的机制,Java 的编码过程较之 C++ 要简单得多。我们所要做的决定和要克服的障碍也会少很多!
|
||||
Java 的垃圾收集器被设计用来解决内存释放的问题(虽然这不包括对象清理的其他方面)。垃圾收集器知道对象什么时候不再被使用并且自动释放内存。结合单继承和仅可在堆中创建对象的机制,Java 的编码过程比用 C++ 要简单得多。我们所要做的决定和要克服的障碍也会少很多!
|
||||
|
||||
|
||||
## 异常处理
|
||||
|
||||
自编程语言被发明以来,程序的错误处理一直都是个难题,因为很难设计出一个好的错误处理方案。许多编程语言都忽略了这个问题,把这个问题丢给了程序类库的设计者。他们提出了在许多情况下都可以工作但很容易被规避的半途而废的措施,通常只需忽略错误。多数错误处理方案的主要问题是:它们依赖程序员之间的约定俗成而不是语言层面的限制。换句话说,如果程序员赶时间或没想起来,这些方案就很容易被忘记。
|
||||
自编程语言被发明以来,程序的错误处理一直都是个难题。因为很难设计出一个好的错误处理方案,所以许多编程语言都忽略了这个问题,把这个问题丢给了程序类库的设计者。他们提出了在许多情况下都可以工作但很容易被规避的半途而废的措施,通常只需忽略错误。多数错误处理方案的主要问题是:它们依赖程序员之间的约定俗成而不是语言层面的限制。换句话说,如果程序员赶时间或没想起来,这些方案就很容易被忘记。
|
||||
|
||||
异常处理机制将程序错误直接交给编程语言甚至是操作系统。“异常”(Exception)是一个从出错点“抛出”(thrown)后能被特定类型的异常处理程序捕获(catch)的一个对象。它不会干扰程序的正常运行,仅当程序出错的时候才被执行。这让我们的编码更简单:不用再反复检查错误了。另外,如果 **throw** 的异常类型和 **catch** 的不符,则不会触发该条件下的异常处理程序。异常的发生是不会被忽略的,它终究会在某一时刻被处理。
|
||||
异常处理机制将程序错误直接交给编程语言甚至是操作系统。“异常”(Exception)是一个从出错点“抛出”(thrown)后能被特定类型的异常处理程序捕获(catch)的一个对象。它不会干扰程序的正常运行,仅当程序出错的时候才被执行。这让我们的编码更简单:不用再反复检查错误了。另外,异常不像方法返回的错误值和方法设置用来表示发生错误的标志位那样可以被忽略。异常的发生是不会被忽略的,它终究会在某一时刻被处理。
|
||||
|
||||
最后,“异常机制”提供了一种可靠地从意外情况中恢复的方法,使得我们可以编写出更健壮的程序。有时你只要处理好抛出的异常情况并恢复程序的运行即可,无需退出。
|
||||
最后,“异常机制”提供了一种可靠地从错误状况中恢复的方法,使得我们可以编写出更健壮的程序。有时你只要处理好抛出的异常情况并恢复程序的运行即可,无需退出。
|
||||
|
||||
Java 的异常处理机制在编程语言中脱颖而出。在 Java 中,异常处理从一开始就被连接起来,因此你不得不使用它。这是 Java 语言唯一接受的错误报告方法。如果没有编写适当的异常处理代码,你将会收到一条编译时错误消息。这种保证错误一致性的方法有时会让程序的错误处理变得更容易。值得注意的是,异常处理并不是面向对象的特性。尽管在面向对象的语言中异常通常由对象表示,但是在面向对象语言之前也存在异常处理。
|
||||
Java 的异常处理机制在编程语言中脱颖而出。Java 从一开始就内置了异常处理,因此你不得不使用它。这是 Java 语言唯一接受的错误报告方法。如果没有编写适当的异常处理代码,你将会收到一条编译时错误消息。这种有保障的一致性有时会让程序的错误处理变得更容易。值得注意的是,异常处理并不是面向对象的特性。尽管在面向对象的语言中异常通常由对象表示,但是在面向对象语言之前也存在异常处理。
|
||||
|
||||
|
||||
## 本章小结
|
||||
|
||||
面向过程程序包含数据定义和函数调用。要找到程序的意图,你必须要在脑中建立一个模型,弄清函数调用和更底层的概念。这些程序往往容易混淆,因为表达式的术语更多地面向计算机而不是我们要解决的问题。这就是我们在设计程序时需要中间表示的原因。OOP 在面向过程编程的基础上增加了许多新的概念,所以有人会认为使用 Java 来编程会比同等的面向过程编程要更复杂。在这里,我想给大家一个惊喜:通常按照 Java 规范编写的程序会比面向过程程序更容易被理解。
|
||||
面向过程程序包含数据定义和函数调用。要找到程序的意图,你必须要在脑中建立一个模型,弄清函数调用和更底层的概念。这些程序令人困扰,因为它们的表示更多地面向计算机而不是我们要解决的问题,这就是我们在设计程序时需要中间表示的原因。OOP 在面向过程编程的基础上增加了许多新的概念,所以有人会认为使用 Java 来编程会比同等的面向过程编程要更复杂。在这里,我想给大家一个惊喜:通常按照 Java 规范编写的程序会比面向过程程序更容易被理解。
|
||||
|
||||
你看到的是对象的概念,这些概念是站在“问题空间”的(而不是站在计算机角度的“解决方案空间”),以及发送给对象以指示该空间中的活动的信息。面向对象编程的一个优点是:设计良好的 Java 程序代码更容易被人阅读理解。由于 Java 类库的复用性,通常程序要写的代码也会少得多。
|
||||
你看到的是对象的概念,这些概念是站在“问题空间”的(而不是站在计算机角度的“解决方案空间”),以及发送消息给对象以指示该空间中的活动。面向对象编程的一个优点是:设计良好的 Java 程序代码更容易被人阅读理解。由于 Java 类库的复用性,通常程序要写的代码也会少得多。
|
||||
|
||||
OOP 和 Java 不一定适合每个人。评估自己的需求以及与现有方案作比较是很重要的。请充分考虑后再决定是不是选择 Java。如果在可预见的未来,Java 并不能很好的满足你的特定需求,那么你应该去寻找其他替代方案(特别是,我推荐看 Python)。如果你依然选择 Java 作为你的开发语言,我希望你至少应该清楚你选择的是什么,以及为什么选择这个方向。
|
||||
<!-- 分页 -->
|
||||
|
||||
<div style="page-break-after: always;"></div>
|
||||
|
||||
|
||||
@@ -4,16 +4,16 @@
|
||||
|
||||
现在,我们来为这次阅读之旅做些准备吧!
|
||||
|
||||
在开始学习 Java 之前,你必须要先安装好 Java 和本书的源代码示例。因为考虑到可能有“专门的初学者”从本书开始学习编程,所以我会仔细解释计算机命令行 Shell 的这个过程。 如果你已经有此方面的经验了,可以跳过这段安装说明。如果你对此处描述的任何术语或过程仍不清楚,还可以通过 Google 搜索找到答案。具体的问题或困难请试着在 StackOverflow 上提问。或者去 YouTube 看有没有相关的安装说明。
|
||||
在开始学习 Java 之前,你必须要先安装好 Java 和本书的源代码示例。因为考虑到可能有“专门的初学者”从本书开始学习编程,所以我会详细地教你如何使用命令行。 如果你已经有此方面的经验了,可以跳过这段安装说明。如果你对此处描述的任何术语或过程仍不清楚,还可以通过 [Google](https://google.com/) 搜索找到答案。具体的问题或困难请试着在 [StackOverflow](https://stackoverflow.com/) 上提问。或者去 [YouTube](https://youtube.com) 看有没有相关的安装说明。
|
||||
|
||||
|
||||
## 编辑器
|
||||
|
||||
首先你需要安装一个编辑器来创建和修改本书用例里的 Java 代码。有可能你还需要一个编辑器来更改系统配置文件。
|
||||
首先你需要安装一个编辑器来创建和修改本书用例里的 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 使用的细节问题可以到她们的网站上。
|
||||
相比一些重量级的 IDE(Integrated Development Environments,集成开发环境),如Eclipse、NetBeans和IntelliJ IDEA (译者注:做项目强烈推荐IDEA),编辑器是一种更纯粹的文本编辑器。如果你已经有了一个用着顺手的 IDE,那就可以直接用了。为了方便后面的学习和统一下教学环境,我推荐大家使用 Atom 这个编辑器。大家可以在 [atom.io](https://atom.io) 上下载。
|
||||
|
||||
Atom 是一个免费开源、易于安装且跨平台(支持 Window、Mac和Linux)的文本编辑器。内置支持 Java 文件。相比 IDE 的厚重,它比较轻量级,是学习本书的理想工具。Atom 包含了许多方便的编辑功能,相信你一定会爱上它!更多关于 Atom 使用的细节问题可以到它的网站上寻找。
|
||||
|
||||
还有很多其他的编辑器。有一种亚文化的群体,他们热衷于争论哪个更好用!如果你找到一个你更喜欢的编辑器,换一种使用也没什么难度。重要的是,你要找一个用着舒服的。
|
||||
|
||||
@@ -22,23 +22,26 @@
|
||||
|
||||
如果你之前没有接触过编程,那么有可能对 Shell(命令行窗口) 不太熟悉。shell 的历史可以追溯到早期的计算时代,当时在计算机上的操作是都通过输入命令进行的,计算机通过回显响应。所有的操作都是基于文本的。
|
||||
|
||||
尽管和现在的图形用户界面相比,Shell 操作方式很原始。但是同时 shell 也为我们提供了许多有用的功特性。在学习本书的过程中,我们会经常使用到 Shell,包括现在这部分的安装,还有运行 Java 程序。
|
||||
尽管和现在的图形用户界面相比,Shell 操作方式很原始。但是同时 shell 也为我们提供了许多有用的功能特性。在学习本书的过程中,我们会经常使用到 Shell,包括现在这部分的安装,还有运行 Java 程序。
|
||||
|
||||
Mac:单击聚光灯(屏幕右上角的放大镜图标),然后键入 `terminal`。单击看起来像小电视屏幕的应用程序(您也可以单击“return”)。这就启动了你的用户下的 shell 窗口。
|
||||
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”
|
||||
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 的基础元素之一。目录用来保存文件和其他目录。目录就好比树的分支。如果书籍是你系统上的一个目录,并且它有两个其他目录作为分支,例如数学和艺术,那么我们就可以说你有一个书籍目录,它包含数学和艺术两个子目录。注意:Windows 使用 `\` 而不是 `/` 来分隔路径。
|
||||
|
||||
**Shell基本操作**
|
||||
|
||||
@@ -47,14 +50,14 @@ Mac:单击聚光灯(屏幕右上角的放大镜图标),然后键入 `ter
|
||||
```bash
|
||||
更改目录: cd <路径>
|
||||
cd .. 移动到上级目录
|
||||
pushd <路径> 记住来源的同时移动到其他目录,popd 返回上一个目录
|
||||
pushd <路径> 记住来源的同时移动到其他目录,popd 返回来源
|
||||
|
||||
目录列举: ls 列举出当前目录下所有的文件和子目录名(不包含隐藏文件),
|
||||
可以选择使用通配符 * 来缩小搜索的范围。
|
||||
示例(1): 列举所有以“.java”结尾的文件,输入ls *.java
|
||||
示例(2): 列举所有以“F”开头,“.java”结尾的文件,输入ls F*.java
|
||||
可以选择使用通配符 * 来缩小搜索范围。
|
||||
示例(1): 列举所有以“.java”结尾的文件,输入 ls *.java (Windows: dir *.java)
|
||||
示例(2): 列举所有以“F”开头,“.java”结尾的文件,输入ls F*.java (Windows: dir F*.java)
|
||||
|
||||
增加目录:
|
||||
创建目录:
|
||||
Mac/Linux 系统:mkdir
|
||||
示例:mkdir books
|
||||
Windows 系统:md
|
||||
@@ -68,9 +71,9 @@ Mac:单击聚光灯(屏幕右上角的放大镜图标),然后键入 `ter
|
||||
|
||||
移除目录:
|
||||
Mac/Linux 系统:rm -r
|
||||
示例:rm -r somefile.java
|
||||
示例:rm -r books
|
||||
Windows 系统:deltree
|
||||
示例:deltree somefile.java
|
||||
示例:deltree books
|
||||
|
||||
重复命令: !! 重复上条命令
|
||||
示例:!n 重复倒数第n条命令
|
||||
@@ -80,8 +83,8 @@ Mac:单击聚光灯(屏幕右上角的放大镜图标),然后键入 `ter
|
||||
Windows 系统:按 F7 键
|
||||
|
||||
文件解压:
|
||||
Linux/Mac 都有命令行解压程序,您可以通过互联网为Windows安装命令行解压程序。
|
||||
图形界面下(Windows 资源管理器、Mac Finder ,Linux Nautilus 或其他等效软件)右键单击该文件,
|
||||
Linux/Mac 都有命令行解压程序 unzip,你可以通过互联网为 Windows 安装命令行解压程序 unzip。
|
||||
图形界面下(Windows 资源管理器,Mac Finder,Linux Nautilus 或其他等效软件)右键单击该文件,
|
||||
在 Mac 上选择“open”,在 Linux 上选择“extract here”,或在 Windows 上选择“extract all…”。
|
||||
要了解关于 shell 的更多信息,请在维基百科中搜索 Windows shell,Mac/Linux用户可搜索 bash shell。
|
||||
|
||||
@@ -90,7 +93,7 @@ Mac:单击聚光灯(屏幕右上角的放大镜图标),然后键入 `ter
|
||||
|
||||
## Java安装
|
||||
|
||||
为了安装了和运行代码示例,首先你必须安装 JDK 1.8。本书中采用的是 JDK 1.8版本。
|
||||
为了编译和运行代码示例,首先你必须安装 JDK(Java Development Kit,JAVA 软件开发工具包)。本书中采用的是 JDK 8。
|
||||
|
||||
|
||||
**Windows**
|
||||
@@ -99,12 +102,13 @@ Mac:单击聚光灯(屏幕右上角的放大镜图标),然后键入 `ter
|
||||
2. 在命令行提示符下输入下面的命令,等待片刻,结束后 Java 安装完成并自动完成环境变量设置。
|
||||
|
||||
```bash
|
||||
choco install jdk8`.
|
||||
choco install jdk8
|
||||
```
|
||||
|
||||
**Macintosh**
|
||||
|
||||
Mac 系统自带的 Java 版本太老,为了确保本书的代码示例能被正确执行,你必须先更新它到 Java 8。我们需要管理员权限来运行下面的步骤:
|
||||
Mac 系统自带的 Java 版本太老,为了确保本书的代码示例能被正确执行,你必须将它先更新到 Java 8。我们需要管理员权限来运行下面的步骤:
|
||||
|
||||
1. 以下为 HomeBrew 的[安装说明](https://brew.sh/)。安装完成后执行命令 `brew update` 更新到最新版本
|
||||
2. 在命令行下执行下面的命令来安装 Java。
|
||||
|
||||
@@ -125,7 +129,7 @@ Mac 系统自带的 Java 版本太老,为了确保本书的代码示例能被
|
||||
* **Fedora/Redhat**:
|
||||
|
||||
```bash
|
||||
su-c "yum install java-1.8.0-openjdk"
|
||||
su-c "yum install java-1.8.0-openjdk"(注:执行引号内的内容就可以安装)
|
||||
```
|
||||
|
||||
|
||||
@@ -151,11 +155,11 @@ Java HotSpot(TM) 64-Bit Server VM (build 25.112-b15, mixed mode)
|
||||
|
||||
当 Java 安装完毕,下一步就是安装本书的代码示例了。安装步骤所有平台一致:
|
||||
|
||||
1. 在 [GitHub 仓库](https://github.com/BruceEckel/OnJava8-Examples/archive/master.zip)中下载本书代码示例
|
||||
1. 从 [GitHub 仓库](https://github.com/BruceEckel/OnJava8-Examples/archive/master.zip)中下载本书代码示例
|
||||
2. 解压到你所选目录里。
|
||||
3. 使用 Windows 资源管理器, Mac Finder, or Linux 的 Nautilus 或其他等效工具浏览,在该目录下打开 bash 命令行。
|
||||
3. 使用 Windows 资源管理器,Mac Finder,Linux 的 Nautilus 或其他等效工具浏览,在该目录下打开 Shell。
|
||||
4. 如果你在正确的目录中,你应该看到该目录中名为 gradlew 和 gradlew.bat 的文件,以及许多其他文件和目录。目录与书中的章节相对应。
|
||||
5. 在命令行中输入下面的命令运行:
|
||||
5. 在shell中输入下面的命令运行:
|
||||
|
||||
```bash
|
||||
Windows 系统:
|
||||
@@ -169,10 +173,9 @@ Java HotSpot(TM) 64-Bit Server VM (build 25.112-b15, mixed mode)
|
||||
|
||||
**注意**: 第一次运行 gradlew 命令时必须连接互联网。
|
||||
|
||||
|
||||
**Gradle基础任务**
|
||||
|
||||
本书构建的大量 Gradle 任务都可以自动运行。Gradle 设置使用约定大于配置的方式,简单设置即可具备高可用性。本书中“一起去骑行”的某些任务不适用于此或无法执行成功。以下是你通常会使用上的分级任务列表:
|
||||
本书构建的大量 Gradle 任务都可以自动运行。Gradle 使用约定大于配置的方式,简单设置即可具备高可用性。本书中“一起去骑行”的某些任务不适用于此或无法执行成功。以下是你通常会使用上的 Gradle 任务列表:
|
||||
|
||||
```bash
|
||||
编译本书中的所有 java 文件,除了部分错误示范的
|
||||
@@ -181,13 +184,14 @@ Java HotSpot(TM) 64-Bit Server VM (build 25.112-b15, mixed mode)
|
||||
编译并执行 java 文件(某些文件是库组件)
|
||||
gradlew run
|
||||
|
||||
执行所有的单元测试(在本书学习中校验自己的代码是由正确)
|
||||
执行所有的单元测试(在本书第16章会有详细介绍)
|
||||
gradlew test
|
||||
|
||||
编译且运行一个特别的示例程序
|
||||
编译并运行一个具体的示例程序
|
||||
gradlew <本书章节>:<示例名称>
|
||||
示例:gradlew objects:HelloDate
|
||||
```
|
||||
<!-- 分页 -->
|
||||
|
||||
<div style="page-break-after: always;"></div>
|
||||
|
||||
|
||||
@@ -2,28 +2,29 @@
|
||||
|
||||
# 第三章 万物皆对象
|
||||
|
||||
> 如果我们说不同的语言,我们会感觉到一个不同的世界!— Ludwig Wittgenstein (1889-1951)
|
||||
> 如果我们说另外一种不同的语言,我们会发觉一个不同的世界!— Ludwig Wittgenstein (1889-1951)
|
||||
|
||||
相比 C++ ,Java 是一种更纯粹的面向对象编程语言。虽然它们都是混合语言,但在 Java 中,设计者们认为混合的作用并非像 C++ 那般重要。混合语言允许多种编程风格,这也是 C++ 支持向后兼容 C 的原因。正因为 C++ 是 C 语言的超集,所以它也同时包含了许多 C 语言的不良特性,这使得 C++ 在某种程度上过于复杂。
|
||||
相比 C++ ,Java 是一种更纯粹的面向对象编程语言。虽然它们都是混合语言,但在 Java 中,设计者们认为混合的作用并非像在 C++ 中那般重要。混合语言允许多种编程风格,这也是 C++ 支持向后兼容 C 的原因。正因为 C++ 是 C 语言的超集,所以它也同时包含了许多 C 语言不具备的特性,这使得 C++ 在某些方面过于复杂。
|
||||
|
||||
Java 语言假设你已接触过面向对象编程。开始学习之前,我们需要将思维置于面向对象的世界。本章你将了解到 Java 语言的基本组成,学习 Java (几乎)万物皆对象的思想。
|
||||
Java 语言假设你只进行面向对象编程。开始学习之前,我们需要将思维置于面向对象的世界。本章你将了解到 Java 程序的基本组成,学习在 Java 中万物(几乎)皆对象的思想。
|
||||
|
||||
<!-- You Manipulate Objects with References -->
|
||||
|
||||
## 对象操纵
|
||||
|
||||
“名字代表什么?玫瑰即使不叫玫瑰,也依旧芬芳”。(引用自 莎士比亚,《罗密欧与朱丽叶》)。
|
||||
|
||||
所有的编程语言都会操纵内存中的元素。有时程序员必须要有意识地直接或间接地操纵它们。在 C/C++ 中,对象的操纵是通过指针来完成的。
|
||||
|
||||
Java 利用万物皆对象的思想和独特的语法方式来简化问题。虽万物皆可为对象,但我们所操纵的标识符实际上是只对象的“引用” [^1]。 举例:我们可以将这种“引用”想象成电视(对象)和遥控器(引用)之间的关系。只要拥有对象的“引用”,就可以操纵该“对象”。换成白话,我们无需直接接触电视,就可通过遥控器自由地控制电视(对象)的频道和音量。此外,没有电视机,遥控器也可以单独存在。引申来说,仅仅因为你有一个“引用”并不意味着你必然有一个关联的“对象”。
|
||||
Java 利用万物皆对象的思想和单一一致的语法方式来简化问题。虽万物皆可为对象,但我们所操纵的标识符实际上只是对对象的“引用” [^1]。 举例:我们可以用遥控器(引用)去操纵电视(对象)。只要拥有对象的“引用”,就可以操纵该“对象”。换句话说,我们无需直接接触电视,就可通过遥控器(引用)自由地控制电视(对象)的频道和音量。此外,没有电视,遥控器也可以单独存在。就是说,你仅仅有一个“引用”并不意味着你必然有一个与之关联的“对象”。
|
||||
|
||||
下面来创建一个 **String** 的引用,用于保存单词语句。代码示例:
|
||||
下面来创建一个 **String** 引用,用于保存单词或语句。代码示例:
|
||||
|
||||
```java
|
||||
String s;
|
||||
```
|
||||
|
||||
这里我们仅仅只是创建了一个 **String** 对象的引用,而非对象。直接拿来使用会出现错误:因为此时你并没有给变量 `s` 赋值--附加任何引用的对象。通常更安全的做法是:在声明变量引用的同时初始化对象信息。代码示例:
|
||||
这里我们只是创建了一个 **String** 对象的引用,而非对象。直接拿来使用会出现错误:因为此时你并没有给变量 `s` 赋值--指向任何对象。通常更安全的做法是:创建一个引用的同时进行初始化。代码示例:
|
||||
|
||||
```java
|
||||
String s = "asdf";
|
||||
@@ -32,37 +33,39 @@ Java 利用万物皆对象的思想和独特的语法方式来简化问题。虽
|
||||
Java 语法允许我们使用带双引号的文本内容来初始化字符串。同样,其他类型的对象也有相应的初始化方式。
|
||||
|
||||
<!-- You Must Create All the Objects -->
|
||||
|
||||
## 对象创建
|
||||
|
||||
“引用”用来连接“对象”。在 Java 中,通常我们使用`new`这个操作符来创建一个新的对象。`new`关键字代表:创建一个新的对象实例。所以,前面的代码实例我们也可以这样来表示:
|
||||
“引用”用来关联“对象”。在 Java 中,通常我们使用`new`操作符来创建一个新对象。`new`关键字代表:创建一个新的对象实例。所以,我们也可以这样来表示前面的代码示例:
|
||||
|
||||
```java
|
||||
String s = new String("asdf");
|
||||
```
|
||||
以上的代码示例展示了字符串对象的创建过程,以及如何初始化生成字符串。Java 本身自带了许多现成的数据类型,在此基础之上我们还可以创建自己的数据类型。类型的创建是 Java 的基本操作。在本书后面的学习中将会接触到。
|
||||
以上展示了字符串对象的创建过程,以及如何初始化生成字符串。除了 **String** 类型以外,Java 本身自带了许多现成的数据类型。除此之外,我们还可以创建自己的数据类型。事实上,这是Java程序设计中的一项基本行为。在本书后面的学习中将会接触到。
|
||||
|
||||
<!-- Where Storage Lives -->
|
||||
|
||||
### 数据存储
|
||||
|
||||
那么, 程序在运行时是如何存储的呢?尤其是内存。下面我们就来形象地描述下, Java 中数据存储的5个不同的地方:
|
||||
那么,程序在运行时是如何存储的呢?尤其是内存是怎么分配的。有5个不同的地方可以存储数据:
|
||||
|
||||
1. **寄存器** (Registers) 最快的保存区域,位于CPU内部 [^2]。然而,寄存器的数量十分有限,所以寄存器是根据需要由编译器分配。我们对其没有直接的控制权,也无法在自己的程序里找到寄存器存在的踪迹(另一方面,C/C++ 允许开发者向编译器建议寄存器的分配)。
|
||||
1. **寄存器** (Registers) 最快的存储区域,位于CPU内部 [^2]。然而,寄存器的数量十分有限,所以寄存器根据需求进行分配。我们对其没有直接的控制权,也无法在自己的程序里找到寄存器存在的踪迹(另一方面,C/C++ 允许开发者向编译器建议寄存器的分配)。
|
||||
|
||||
2. **栈内存**(Stack) 存在于常规内存(RAM)区域中,可通过栈指针获得处理器的直接支持。栈指针下移创建新内存,上移释放该内存,顺序后进先出,速度仅次于寄存器。创建程序时,Java 编译器必须准确地知道栈内保存的所有数据的“长度”以及生命周期。栈内存的这种约束限制了程序的灵活性。因此,虽然在栈内存上存在一些 Java 数据,特别是对象引用,但 Java 对象本身却是保存在堆内存的。
|
||||
2. **栈内存**(Stack) 存在于常规内存 RAM (随机访问存储器,Random Access Memory)区域中,可通过栈指针获得处理器的直接支持。栈指针下移分配内存,上移释放内存,这是一种快速有效的内存分配方法,速度仅次于寄存器。创建程序时,Java 系统必须准确地知道栈内保存的所有项的生命周期。这种约束限制了程序的灵活性。因此,虽然在栈内存上存在一些 Java 数据,特别是对象引用,但 Java 对象却是保存在堆内存的。
|
||||
|
||||
3. **堆内存**(Heap) 这是一种常规用途的内存池(也在 RAM区域),所有 Java 对象都存在于其中。与栈内存不同,编译器不需要知道对象必须在堆内存上停留多长时间。因此,用堆内存保存数据更具灵活性。创建一个对象时,只需用 `new` 命令实例化代码即可。执行这些代码时,数据会在堆内存里自动进行保存。这种灵活性是有代价的:分配和清理堆内存要比栈内存需要更多的时间(如果你甚至可以用 Java 在栈内存上创建对象,就像在C++ 中那样)。随着时间的推移,Java 的堆内存分配机制现已非常快,因此这不是一个值得关心的问题了。
|
||||
3. **堆内存**(Heap) 这是一种通用的内存池(也在 RAM区域),所有 Java 对象都存在于其中。与栈内存不同,编译器不需要知道对象必须在堆内存上停留多长时间。因此,用堆内存保存数据更具灵活性。创建一个对象时,只需用 `new` 命令实例化对象即可,当执行代码时,会自动在堆中进行内存分配。这种灵活性是有代价的:分配和清理堆内存要比栈内存需要更多的时间(如果可以用 Java 在栈内存上创建对象,就像在C++ 中那样的话)。随着时间的推移,Java 的堆内存分配机制现在已经非常快,因此这不是一个值得关心的问题了。
|
||||
|
||||
4. **常量存储** (Constant storage) 常量值通常直接放在程序代码中,因为它们永远不会改变。如需严格保护,可考虑将它们置于只读存储器(ROM)中 [^3]。
|
||||
4. **常量存储** (Constant storage) 常量值通常直接放在程序代码中,因为它们永远不会改变。如需严格保护,可考虑将它们置于只读存储器 ROM (只读存储器,Read Only Memory)中 [^3]。
|
||||
|
||||
5. **非 RAM 存储** (Non-RAM storage) 数据完全存在于程序之外,在程序未运行以及脱离程序控制后依然存在。两个主要的例子:(1)序列化对象:对象被转换为字节流,通常被发送到另一台机器;(2)持久化对象:对象被放置在磁盘上,即使程序终止,数据依然存在。这些存储的方式都是将对象转存于另一个介质中,并在需要时恢复到常规内存中。Java 为轻量级持久性提供支持。诸如 JDBC 和 Hibernate 之类的库为使用数据库存储和检索对象信息提供了更复杂的支持。
|
||||
5. **非 RAM 存储** (Non-RAM storage) 数据完全存在于程序之外,在程序未运行以及脱离程序控制后依然存在。两个主要的例子:(1)序列化对象:对象被转换为字节流,通常被发送到另一台机器;(2)持久化对象:对象被放置在磁盘上,即使程序终止,数据依然存在。这些存储的方式都是将对象转存于另一个介质中,并在需要时恢复成常规的、基于 RAM 的对象。Java 为轻量级持久化提供了支持。而诸如 JDBC 和 Hibernate 这些类库为使用数据库存储和检索对象信息提供了更复杂的支持。
|
||||
|
||||
|
||||
<!-- Special Case: Primitive Types -->
|
||||
### 基本类型的存储
|
||||
|
||||
有一组类型在 Java 中使用频率很高,这就是 Java 的基本类型。对于此类数据的存储我们需要特别对待。之所以说这么说,是因为它们的创建并不是通过 `new` 关键字来产生。通常 `new` 出来的对象都是保存在 **Heap** 内存中的, 用它来创建小、简单的基本类型的数据是不划算的。所以对于这些基本类型的创建方法, Java 使用了和 C/C++ 一样的策略。也就是说,不是使用 `new` 创建变量,而是使用一个“自动”变量。 这个变量容纳了具体的值,并置于栈内存中,能够更高效地存取。
|
||||
有一组类型在 Java 中使用频率很高,它们需要特殊对待,这就是 Java 的基本类型。之所以这么说,是因为它们的创建并不是通过 `new` 关键字来产生。通常 `new` 出来的对象都是保存在堆内存中的, 以此方式创建小而简单的变量往往是不划算的。所以对于这些基本类型的创建方法, Java 使用了和 C/C++ 一样的策略。也就是说,不是使用 `new` 创建变量,而是使用一个“自动”变量。 这个变量直接存储"值",并置于栈内存中,因此更加高效。
|
||||
|
||||
Java 预设了每种基本类型的初始内存占用大小。 这些大小标准不会随着机器环境的变化而变化。这种不变性也是Java 的跨平台的一个原因。
|
||||
Java 确定了每种基本类型的内存占用大小。 这些大小不会像其他一些语言那样随着机器环境的变化而变化。这种不变性也是 Java 更具可移植性的一个原因。
|
||||
|
||||
| 基本类型 | 大小 | 最小值 | 最大值 | 包装类型 |
|
||||
| :------: | :------: | :------: | :------: | :------: |
|
||||
@@ -76,13 +79,13 @@ Java 预设了每种基本类型的初始内存占用大小。 这些大小标
|
||||
| double | 64 bits |IEEE754 | IEEE754 | Double |
|
||||
| void | — | — | — | Void |
|
||||
|
||||
所有的数值类型都是有正/负符号的。布尔(boolean)类型的大小没有明确的规定,通常定义为采用文字 “true” 和 “false”。基本类型有自己对应的包装类型,如果你希望在堆内存里表示基本类型的数据,就需要用到它们的包装类。代码示例:
|
||||
所有的数值类型都是有正/负符号的。布尔(boolean)类型的大小没有明确的规定,通常定义为取字面值 “true” 或 “false” 。基本类型有自己对应的包装类型,如果你希望在堆内存里表示基本类型的数据,就需要用到它们的包装类。代码示例:
|
||||
|
||||
```java
|
||||
char c = 'x';
|
||||
Character ch = new Character(c);
|
||||
```
|
||||
或者你也可以使用下面的形式 基本类型自动转换成包装类型(自动装箱):
|
||||
或者你也可以使用下面的形式,基本类型自动转换成包装类型(自动装箱):
|
||||
|
||||
```java
|
||||
Character ch = new Character('x');
|
||||
@@ -96,51 +99,53 @@ char c = ch;
|
||||
个中原因将在以后的章节里解释。
|
||||
|
||||
<!-- High-Precision Numbers -->
|
||||
|
||||
### 高精数值的存储
|
||||
|
||||
在 Java 中有两种类型的数据可用于高精度的计算。它们是 `BigInteger` 和 `BigDecimal`。尽管它们大致可以划归为“包装类型”,但是它们并没有相应的基本类型形式。
|
||||
在 Java 中有两种类型的数据可用于高精度的计算。它们是 `BigInteger` 和 `BigDecimal`。尽管它们大致可以划归为“包装类型”,但是它们并没有对应的基本类型。
|
||||
|
||||
这两个类都有自己特殊的“方法”,对应于我们针对基本类型数值执行的操作。也就是说,能对 int 或 float 做的运算,在 BigInteger 和 BigDecimal 这里也同样可以做一样可以,只不过必须要通过调用它们的方法来实现而非运算符。此外,由于涉及到的计算量更多,所以运算速度会慢一些。诚然,我们牺牲了速度,但换来了精度。
|
||||
这两个类包含的方法提供的操作,与对基本类型执行的操作相似。也就是说,能对 int 或 float 做的运算,在 BigInteger 和 BigDecimal 这里也同样可以,只不过必须要通过调用它们的方法来实现而非运算符。此外,由于涉及到的计算量更多,所以运算速度会慢一些。诚然,我们牺牲了速度,但换来了精度。
|
||||
|
||||
BigInteger 支持任意精度的整数。可用于精确表示任意大小的整数值,同时在运算过程中不会丢失精度。
|
||||
BigDecimal 支持任意精度的定点数字。例如,可用它进行精确的币值计算。至于具体使用什么方法,更多详情,请参考 JDK 官方文档。
|
||||
BigDecimal 支持任意精度的定点数字。例如,可用它进行精确的货币计算。
|
||||
|
||||
关于这两个类的详细信息,请参考 JDK 官方文档。
|
||||
|
||||
<!-- Arrays in Java -->
|
||||
|
||||
### 数组的存储
|
||||
|
||||
|
||||
许多编程语言都支持数组类型。在 C 和 C++ 中使用数组是危险的,因为那些数组只是内存块。如果程序访问了其内存块之外的数组或在初始化之前使用该段内存(常见编程错误),则结果是不可预测的。
|
||||
许多编程语言都支持数组类型。在 C 和 C++ 中使用数组是危险的,因为那些数组只是内存块。如果程序访问了内存块之外的数组或在初始化之前使用该段内存(常见编程错误),则结果是不可预测的。
|
||||
|
||||
Java 的设计主要目标之一是安全性,因此许多困扰 C 和 C++ 程序员的问题不会在 Java 中再现。在 Java 中,数组使用前需要被初始化,并且不能访问数组长度以外数据。这种长度检查的代价是每个阵列都有少量的内存开销以及在运行时验证索引的额外时间,但是这种安全性的前提对于提高的生产率是值得的。(并且 Java 经常可以优化这些操作)。
|
||||
|
||||
当我们创建对象数组时,实际上是创建了一个数据的引用,并且每个引用的初始值都为 **null** 。在使用该数组之前,我们必须为每个引用分配一个对象 。如果我们尝试使用为**null**的引用,则会在运行时报告该问题。因此,在 Java 中就防止了数组操作的典型错误。
|
||||
|
||||
我们还可创建基本类型的数组。编译器通过将该数组的内存归零来保证初始化。本书稍后将详细介绍数组,特别是在数组章节中。
|
||||
Java 的设计主要目标之一是安全性,因此许多困扰 C 和 C++ 程序员的问题不会在 Java 中再现。在 Java 中,数组使用前需要被初始化,并且不能访问数组长度以外的数据。这种范围检查,是以每个数组上少量的内存开销及运行时检查下标的额外时间为代价的,但由此换来的安全性和效率的提高是值得的。(并且 Java 经常可以优化这些操作)。
|
||||
|
||||
当我们创建对象数组时,实际上是创建了一个引用数组,并且每个引用的初始值都为 **null** 。在使用该数组之前,我们必须为每个引用指定一个对象 。如果我们尝试使用为 **null** 的引用,则会在运行时报错。因此,在 Java 中就防止了数组操作的常规错误。
|
||||
|
||||
我们还可创建基本类型的数组。编译器通过将该数组的内存全部置零来保证初始化。本书稍后将详细介绍数组,特别是在数组章节中。
|
||||
|
||||
<!-- Comments -->
|
||||
|
||||
## 代码注释
|
||||
|
||||
|
||||
Java 中有两种类型的注释。第一种是传统的 C 风格的注释,以 `/*` 开头,可以跨越多行,到 `*/ ` 结束。**注意**,许多程序员在多行注释的每一行开头添加 `*`,所以你经常会看到:
|
||||
Java 中有两种类型的注释。第一种是传统的 C 风格的注释,以 `/*` 开头,可以跨越多行,到 `*/ ` 结束。注意,许多程序员在多行注释的每一行开头添加 `*`,所以你经常会看到:
|
||||
|
||||
```java
|
||||
/* This is a comment
|
||||
* that continues
|
||||
* across lines
|
||||
/* 这是
|
||||
* 跨越多行的
|
||||
* 注释
|
||||
*/
|
||||
```
|
||||
|
||||
但请记住, `/*` 和 `*/` 之间的内容都是被忽略的。所以你将其改为下面的风格也是没有区别的。
|
||||
但请记住, `/*` 和 `*/` 之间的内容都是被忽略的。所以你将其改为下面这样也是没有区别的。
|
||||
|
||||
```java
|
||||
/* This is a comment that
|
||||
continues across lines */
|
||||
/* 这是跨越多
|
||||
行的注释 */
|
||||
```
|
||||
|
||||
第二种注释形式来自 C++ 。它是单行注释,以 `//` 开头并一直持续到行结束。这种注释方便且常用,因为它很直观和简单。所以你经常看到:
|
||||
第二种注释形式来自 C++ 。它是单行注释,以 `//` 开头并一直持续到行结束。这种注释方便且常用,因为直观简单。所以你经常看到:
|
||||
|
||||
```java
|
||||
// 这是单行注释
|
||||
@@ -149,27 +154,27 @@ continues across lines */
|
||||
<!-- You Never Need to Destroy an Object -->
|
||||
## 对象清理
|
||||
|
||||
在一些编程语言中,管理存储的生命周期需要大量的工作。一个变量需要存续多久?如果我们想销毁它,应该什么时候去做呢?存储生命周期的混乱会导致许多错误,本小结将会向你介绍 Java 是如何通过释放存储来简化这个问题的。
|
||||
在一些编程语言中,管理变量的生命周期需要大量的工作。一个变量需要存活多久?如果我们想销毁它,应该什么时候去做呢?变量生命周期的混乱会导致许多bug,本小结向你介绍 Java 是如何通过释放存储来简化这个问题的。
|
||||
|
||||
<!-- Scoping -->
|
||||
### 作用域
|
||||
|
||||
大多数程序语言都有作用域的概念。这将确定在该范围内定义的名称的可见性和生存周期。在 C、C++ 和 Java 中,作用域是由大括号 `{}` 的位置决定的。下面是 Java 代码作用域的一个示例:
|
||||
大多数程序语言都有作用域的概念。作用域决定了在该范围内定义的变量名的可见性和生存周期。在 C、C++ 和 Java 中,作用域是由大括号 `{}` 的位置决定的。例如:
|
||||
|
||||
```java
|
||||
{
|
||||
int x = 12;
|
||||
// 仅 x 变量可用
|
||||
{
|
||||
int q = 96;
|
||||
// x 和 q 变量皆可用
|
||||
}
|
||||
// 仅 x 变量可用
|
||||
// 变量 q 不在作用域内
|
||||
// 仅 x 变量可用
|
||||
{
|
||||
int q = 96;
|
||||
// x 和 q 变量皆可用
|
||||
}
|
||||
// 仅 x 变量可用
|
||||
// 变量 q 不在作用域内
|
||||
}
|
||||
```
|
||||
|
||||
Java 的变量只有在其作用域内才可用。缩进使得 Java 代码更易于阅读。由于 Java 是一种自由形式的语言,额外的空格、制表符和回车并不会影响程序的生成结果。在 Java 中,你不能执行以下操作,即使这在 C 和 C++ 中是合法的:
|
||||
Java 的变量只有在其作用域内才可用。缩进使得 Java 代码更易于阅读。由于 Java 是一种自由格式的语言,额外的空格、制表符和回车并不会影响程序的执行结果。在 Java 中,你不能执行以下操作,即使这在 C 和 C++ 中是合法的:
|
||||
|
||||
```java
|
||||
{
|
||||
@@ -180,50 +185,53 @@ Java 的变量只有在其作用域内才可用。缩进使得 Java 代码更易
|
||||
}
|
||||
```
|
||||
|
||||
在上例中, Java 便编译器会在提示变量 x 已经被定义过了。因此,在 C/C++ 中可以于更大作用域中“隐藏”变量的能力在 Java 中是不被允许的。 因为 Java 的设计者认为这样的定义会混淆编程。
|
||||
在上例中, Java 编译器会在提示变量 x 已经被定义过了。因此,在 C/C++ 中将一个较大作用域的变量"隐藏"起来的做法,在 Java 中是不被允许的。 因为 Java 的设计者认为这样做会导致程序混乱。
|
||||
|
||||
<!-- Scope of Objects -->
|
||||
|
||||
### 对象作用域
|
||||
|
||||
Java 对象与基本类型具有不同的生命周期。当我们使用 `new` 关键字来创建 Java 对象时,它的生命周期将会超出作用域的末尾。因此,下面这段代码示例
|
||||
Java 对象与基本类型具有不同的生命周期。当我们使用 `new` 关键字来创建 Java 对象时,它的生命周期将会超出作用域。因此,下面这段代码示例:
|
||||
|
||||
```java
|
||||
{
|
||||
String s = new String("a string");
|
||||
}
|
||||
// 作用域结束
|
||||
// 作用域终点
|
||||
```
|
||||
上例中,变量 s 的范围在标注的地方结束了。但是,引用的字符串对象依然还在占用内存。在这段代码中,变量 s 的唯一引用超出了作用域,因此它无法在作用域外被访问。在后面的章节中,你我们还会学习怎么在编程中传递和复制对象的引用。
|
||||
上例中,引用 s 在作用域终点就结束了。但是,引用 s 指向的字符串对象依然还在占用内存。在这段代码中,我们无法在这个作用域之后访问这个对象,因为唯一对它的引用 s 已超出了作用域的范围。在后面的章节中,我们还会学习怎么在编程中传递和复制对象的引用。
|
||||
|
||||
只要程序需要, `new` 出来的对象就不会被销毁。 相比在 C++ 编码中操作内存可能会出现的诸多问题,这些困扰在 Java 中都不复存在了。在 C++ 中你不仅要确保对象的内存在你操作的范围内存在,还必须在完成后销毁对象。
|
||||
只要你需要, `new` 出来的对象就会一直存活下去。 相比在 C++ 编码中操作内存可能会出现的诸多问题,这些困扰在 Java 中都不复存在了。在 C++ 中你不仅要确保对象的内存在你操作的范围内存在,还必须在使用完它们之后,将其销毁。
|
||||
|
||||
那么问题来了:我们在 Java 中并没有去主动清理这些对象,那么它是如何避免 C++ 中出现的内存泄漏和程序终止的问题呢?答案是:Java 的垃圾收集器会检查所有 `new` 出来的对象并判断哪些不再可达,继而释放那些被占用的内存。至此,我们不必再自己操作回收内存了。取而代之,只需简单创建对象即可。当其不再被需要时,能自行被垃圾收集器释放。垃圾回收机制有效防止了因程序员忘记释放内存而造成的“内存泄漏”问题。
|
||||
那么问题来了:我们在 Java 中并没有主动清理这些对象,那么它是如何避免 C++ 中出现的内存被填满从而阻塞程序的问题呢?答案是:Java 的垃圾收集器会检查所有 `new` 出来的对象并判断哪些不再可达,继而释放那些被占用的内存,供其他新的对象使用。也就是说,我们不必担心内存回收的问题了。你只需简单创建对象即可。当其不再被需要时,能自行被垃圾收集器释放。垃圾回收机制有效防止了因程序员忘记释放内存而造成的“内存泄漏”问题。
|
||||
|
||||
<!-- Creating New Data Types: class -->
|
||||
|
||||
## 类的创建
|
||||
|
||||
### 类型
|
||||
|
||||
如果一切都是对象,那么我们用什么来表示对象类的具体展现和行为呢?顾名思义,你可能很自然地想到 `type` 关键字。但是,事实上大多数面向对象的语言都使用 `class` 关键字类来描述一种新的对象。 通常在 `class` 关键字的后面的紧跟类的的名称。如下代码示例:
|
||||
如果一切都是对象,那么是什么决定了某一类对象的外观和行为呢?换句话说,是什么确定了对象的类型?你可能很自然地想到 `type` 关键字。但是,事实上大多数面向对象的语言都使用 `class` 关键字类来描述一种新的对象。 通常在 `class` 关键字的后面的紧跟类的的名称。如下代码示例:
|
||||
|
||||
```java
|
||||
class ATypeName {
|
||||
class ATypeName {
|
||||
// 这里是类的内部
|
||||
}
|
||||
```
|
||||
|
||||
在上例中,我们介绍了如何创建一个新的类型,尽管这个类里只有一行注释。但是我们一样可以通过 `new` 关键字来创建一个对象。如下:
|
||||
在上例中,我们引入了一个新的类型,尽管这个类里只有一行注释。但是我们一样可以通过 `new` 关键字来创建一个这种类型的对象。如下:
|
||||
|
||||
```java
|
||||
ATypeName a = new ATypeName();
|
||||
```
|
||||
|
||||
到现在为止,我们还不能用这个对象来做什么事,比如发送一些有趣的信息啊。除非我们在这个类里定义一些方法。
|
||||
到现在为止,我们还不能用这个对象来做什么事(即不能向它发送任何有意义的消息),除非我们在这个类里定义一些方法。
|
||||
|
||||
<!-- Fields -->
|
||||
|
||||
### 属性
|
||||
|
||||
当我们创建好了一个类之后,我们可以往类里存放两种类型的元素。方法(method)和属性(field)。类的属性可以是基本类型。如果类的属性是对象的话,那么必须要初始化该引用将其关联到一个实际的对象上(通过之前介绍的创建对象的方法)。每个对象都会为其属性保留独立的存储空间。通常,属性不再对象之间做共享。下面是一个包含部分属性的类的代码示例:
|
||||
当我们创建好一个类之后,我们可以往类里存放两种类型的元素:方法(method)和属性(field)。类的属性可以是基本类型,也可以是引用类型。如果类的属性是对某个对象的引用,那么必须要初始化该引用将其关联到一个实际的对象上(通过之前介绍的创建对象的方法)。每个对象都有用来存储其属性的空间。通常,属性不在对象间共享。下面是一个具有某些属性的类的代码示例:
|
||||
|
||||
```java
|
||||
class DataOnly {
|
||||
@@ -233,13 +241,13 @@ class DataOnly {
|
||||
}
|
||||
```
|
||||
|
||||
除非持有数据,不然这个类不能做任何事。在此之前,我们可以通过下面的代码示例来创建它的对象:
|
||||
这个类除了存储数据之外什么也不能做。但是,我们仍然可以通过下面的代码来创建它的一个对象:
|
||||
|
||||
```java
|
||||
DataOnly data = new DataOnly();
|
||||
```
|
||||
|
||||
我们必须通过这个对象的引用来指定属性值。格式:对象名称.方法名称或成员名称。代码示例:
|
||||
我们必须通过这个对象的引用来指定属性值。格式:对象名称.方法名称或属性名称。代码示例:
|
||||
|
||||
```java
|
||||
data.i = 47;
|
||||
@@ -253,10 +261,10 @@ class DataOnly {
|
||||
myPlane.leftTank.capacity = 100;
|
||||
```
|
||||
|
||||
你可以用这种方式嵌套许多对象(尽管这样的设计会带来混淆)。
|
||||
|
||||
你可以用这种方式嵌套许多对象(尽管这样的设计会带来混乱)。
|
||||
|
||||
<!-- Default Values for Primitive Members -->
|
||||
|
||||
### 基本类型默认值
|
||||
|
||||
如果类的成员变量(属性)是基本类型,那么在类初始化时,这些类型将会被赋予一个初始值。
|
||||
@@ -264,9 +272,9 @@ class DataOnly {
|
||||
| 基本类型 | 初始值 |
|
||||
| :-----: |:-----: |
|
||||
| boolean | false |
|
||||
| char | \u0000 |
|
||||
| byte | 0 |
|
||||
| short |0 |
|
||||
| char | \u0000 (null) |
|
||||
| byte | (byte) 0 |
|
||||
| short |(short) 0 |
|
||||
| int | 0 |
|
||||
| long | 0L |
|
||||
| float | 0.0f |
|
||||
@@ -280,60 +288,60 @@ class DataOnly {
|
||||
int x;
|
||||
```
|
||||
|
||||
这里的变量 x 不会自动初始化为0,因而在使用变量 x 之前,程序员有责任主动地为其赋值(和 C 、C++ 一致)。如果我们忘记了这一步,在 Java 中将会提示我们“编译时错误,该变量尚未被初始化”。 这一点做的比 C++ 要更好,在后者中,编译器只是提示警告,而在 Java 中则直接报错。
|
||||
这里的变量 x 不会自动初始化为0,因而在使用变量 x 之前,程序员有责任主动地为其赋值(和 C 、C++ 一致)。如果我们忘记了这一步, Java 将会提示我们“编译时错误,该变量可能尚未被初始化”。 这一点做的比 C++ 更好,在后者中,编译器只是提示警告,而在 Java 中则直接报错。
|
||||
|
||||
|
||||
<!-- Methods, Arguments,and Return Values -->
|
||||
### 方法使用
|
||||
|
||||
在许多语言(如 C 和 C++)中,术语函数(function)用于描述命名子程序。在 Java 中,我们使用术语方法(method)来表示“做某事的方式”。
|
||||
在许多语言(如 C 和 C++)中,使用术语 *函数* (function) 用来命名子程序。在 Java 中,我们使用术语 *方法*(method)来表示“做某事的方式”。
|
||||
|
||||
在 Java 中,方法决定着对象能接收哪些信息。方法的基础部分包含名称、参数、返回类型、方法体。格式如:
|
||||
在 Java 中,方法决定对象能接收哪些消息。方法的基本组成部分包括名称、参数、返回类型、方法体。格式如:
|
||||
|
||||
```java
|
||||
[返回类型][方法名](/*参数列表*/){
|
||||
// 方法体
|
||||
[返回类型] [方法名](/*参数列表*/){
|
||||
// 方法体
|
||||
}
|
||||
```
|
||||
|
||||
#### 返回类型
|
||||
|
||||
方法的返回类型表明了当你调用它时会返回的结果类型。参数列表则显示了可被传递到方法内部的参数类型及名称。方法的名称和参数列表被统称为**方法签名**(signature of the method)。签名作为方法的唯一性标识。
|
||||
方法的返回类型表明了当你调用它时会返回的结果类型。参数列表则显示了可被传递到方法内部的参数类型及名称。方法名和参数列表统称为**方法签名**(signature of the method)。签名作为方法的唯一标识。
|
||||
|
||||
Java 中的方法只能作为类的一部分创建。它只能被对象所调用 [^4],并且该对象必须有权限来执行调用。若对象调用错误的方法,则程序将在编译时报错。
|
||||
|
||||
我们可以通过在对象名的后面跟上 `.` 符号+方法名及其参数来调用一个方法。代码示例:
|
||||
我们可以像下面这样调用一个对象的方法:
|
||||
|
||||
```java
|
||||
[对象引用].[方法名](参数1, 参数2, 参数3);
|
||||
```
|
||||
|
||||
若方法不带参数,例如一个对象 `a` 的方法 `f` 不带参数并返回 **int** 型结果,我们可以如下表示。代码示例:
|
||||
若方法不带参数,例如一个对象引用 `a` 的方法 `f` 不带参数并返回 **int** 型结果,我们可以如下表示:
|
||||
|
||||
```java
|
||||
int x = a.f();
|
||||
```
|
||||
|
||||
上例中方法 `f` 的返回值必须兼容接收的变量 `x` 。这种调用方法的行为有时被称为向对象传递信息。面向对象编程可以被总结为:向对象传递信息。
|
||||
上例中方法 `f` 的返回值类型必须和变量 `x` 的类型兼容 。调用方法的行为有时被称为向对象发送消息。面向对象编程可以总结为:向对象发送消息。
|
||||
|
||||
<!-- The Argument List -->
|
||||
|
||||
#### 参数列表
|
||||
|
||||
方法参数列表指定传递给方法的信息。正如你可能猜到的,这些信息 —— 就像 Java 中的其他所有信息 —— 采用对象的形式。参数列表必须指定对象类型和每个对象的名称。同样,我们并没有直接处理对象,而是在传递对象引用 [^5] 。但是引用的类型必须是正确的。如果方法需要 String 参数,则必须传入 String,否则编译器将报错。
|
||||
方法参数列表指定了传递给方法的信息。正如你可能猜到的,这些信息就像 Java 中的其他所有信息 ,以对象的形式传递。参数列表必须指定每个对象的类型和名称。同样,我们并没有直接处理对象,而是在传递对象引用 [^5] 。但是引用的类型必须是正确的。如果方法需要 String 参数,则必须传入 String,否则编译器将报错。
|
||||
|
||||
```java
|
||||
int storage(String s) {
|
||||
|
||||
return s.length() * 2;
|
||||
}
|
||||
```
|
||||
|
||||
此方法计算并返回某个字符串的长度。参数 `s` 的类型为 **String** 。将 字符串变量 s 传递给 `storage()` 后,我们可以将其视为任何其他对象一样 —— 我们可以想起传递信息。在这里,我们调用 `length()` 方法,它是一个 String 方法,返回字符串长度。字符串中每个字符的大小为16位或两个字节。您还可以看到 **return** 关键字,它执行两项操作。首先,它意味着“方法执行结束”。其次,如果方法有返回值,那么该值就位于 **return** 语句之后。这里,返回值是通过计算
|
||||
此方法计算并返回某个字符串所占的字节数。参数 `s` 的类型为 **String** 。将 s 传递给 `storage()` 后,我们可以把它看作和任何其他对象一样,可以向它发送消息。在这里,我们调用 `length()` 方法,它是一个 String 方法,返回字符串中的字符数。字符串中每个字符的大小为16位或2个字节。你还看到了 **return** 关键字,它执行两项操作。首先,它意味着“方法执行结束”。其次,如果方法有返回值,那么该值就紧跟 **return** 语句之后。这里,返回值是通过计算
|
||||
|
||||
```java
|
||||
s.length() * 2
|
||||
```
|
||||
产生的。在方法中,我们可以返回任意的数据。如果我们不想方法返回什么数据,则可以通过给方法标识 `void` 来表明这是一个无需返回值的方法。 代码示例:
|
||||
产生的。在方法中,我们可以返回任何类型的数据。如果我们不想方法返回数据,则可以通过给方法标识 `void` 来表明这是一个无需返回值的方法。 代码示例:
|
||||
|
||||
```java
|
||||
boolean flag() {
|
||||
@@ -353,65 +361,67 @@ void nothing2() {
|
||||
}
|
||||
```
|
||||
|
||||
当返回类型为 **void** 时, **return** 关键字仅用于退出方法,因此在方法结束处的 **return** 可被省略。我们可以随时从方法中返回。若方法返回类型为非 `void`,则编译器会强制返回相应类型的值。
|
||||
当返回类型为 **void** 时, **return** 关键字仅用于退出方法,因此在方法结束处的 **return** 可被省略。我们可以随时从方法中返回,但若方法返回类型为非 `void`,则编译器会强制我们返回相应类型的值。
|
||||
|
||||
上面的描述可能会让你感觉程序只不过是一堆包含各种方法的对象,将对象作为方法参数来传递信息给其他的对象。从表面上来看的确如此。但在下一章的运算符中我们将会学习如何在方法中做出决策来完成更底层、详细的工作。对于本章,知道如何传递信息就够了。
|
||||
上面的描述可能会让你感觉程序只不过是一堆包含各种方法的对象,在这些方法中,将对象作为参数并发送消息给其他对象。大部分情况下确实如此。但在下一章的运算符中我们将会学习如何在方法中做出决策来完成更底层、详细的工作。对于本章,知道如何发送消息就够了。
|
||||
|
||||
|
||||
<!-- Writing a Java Program -->
|
||||
## 程序编写
|
||||
|
||||
在看到第一个 Java 程序之前,我们还必须了解其他几个问题。
|
||||
在看到第一个 Java 程序之前,我们还必须理解其他几个问题。
|
||||
|
||||
#### 命名可见性
|
||||
|
||||
命名控制在任何一门编程语言中都是一个问题。如果程序员在两个模块中使用相同的命名,那么如何区分这两个名称,并防止两个名称发生“冲突”呢?在 C 语言编程中这是很具有挑战性的,因为程序通常是一个无法管理的名称海洋。C++ 将函数嵌套在类中,它们不能与嵌套在其他类中的函数名冲突。然而,C++ 继续允许全局数据和全局函数,因此仍有可能发生冲突。为了解决这个问题,C++ 使用附加的关键字引入命名空间。
|
||||
命名控制在任何一门编程语言中都是一个问题。如果你在两个模块中使用相同的命名,那么如何区分这两个名称,并防止两个名称发生“冲突”呢?在 C 语言编程中这是很具有挑战性的,因为程序通常是一个无法管理的名称海洋。C++ 将函数嵌套在类中,所以它们不会和嵌套在其他类中的函数名冲突。然而,C++ 还是允许全局数据和全局函数,因此仍有可能发生冲突。为了解决这个问题,C++ 使用附加的关键字引入了*命名空间*。
|
||||
|
||||
Java 采取了一种新的方法避免了以上这些问题:为一个库生成一个明确的名称,Java 创建者希望我们反向使用自己的网络域名,因为域名通常是唯一的。自从我的域名为 MindviewInc.com 开始,我就将我的 foibles 工具库命名为 com.mindviewinc.utility.foibles。根据你的域名的反向信息,`.`代表着一个子目录。
|
||||
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 的开发过程中,他们发现这会导致问题,所以现在整个包名都是小写的。此机制意味着所有文件都自动存在于自己的命名空间中,文件中的每个类都具有唯一标识符。这样,该语言可以防止名称冲突。
|
||||
在 Java 1.0 和 Java 1.1 中,域扩展名 com,edu,org,net 等按惯例大写,因此类库中会出现这样类似的名称:Com.mindviewinc.utility.foibles。然而,在 Java 2 的开发过程中,他们发现这会导致问题,所以现在整个包名都是小写的。此机制意味着所有文件都自动存在于自己的命名空间中,文件中的每个类都具有唯一标识符。这样,Java 语言可以防止名称冲突。
|
||||
|
||||
使用反向 URL 是一种新的命名空间方法,在此之前尚未有其他语言这么做过。Java 中有许多这些“创造性”地解决问题的方法。正如你想象,如果我们未经测试就添加一个功能并用于生产,那么在将来发现该功能的问题再想纠正,通常为时已晚。(有些问题错误得足以从语言中删除。)
|
||||
使用反向 URL 是一种新的命名空间方法,在此之前尚未有其他语言这么做过。Java 中有许多这些“创造性”地解决问题的方法。正如你想象,如果我们未经测试就添加一个功能并用于生产,那么在将来发现该功能的问题再想纠正,通常为时已晚(有些错误太严重了就得从语言中删除新功能。)
|
||||
|
||||
使用反向 URL 将命名空间与文件路径相关联不会导致BUG,但它却给源代码管理带来麻烦。例如在 `com.mindviewinc.utility.foiles` 这样的目录结构中,我们创建了 `com`、`mindviewinc` 空目录。它们存在的唯一目的就是用来表示这个反向的 URL。
|
||||
使用反向 URL 将命名空间与文件路径相关联不会导致BUG,但它却给源代码管理带来麻烦。例如在 `com.mindviewinc.utility.foibles` 这样的目录结构中,我们创建了 `com`、`mindviewinc` 空目录。它们存在的唯一目的就是用来表示这个反向的 URL。
|
||||
|
||||
这种方式似乎为我们在编写 Java 程序中的某个问题打开了大门。空目录填充了深层次结构,它们不仅用于表示反向 URL,还用于捕获其他信息。这些长路径基本上用于存储有关目录中的内容的数据。如果你希望以最初设计的方式使用目录,这种方法可以从“令人沮丧”到“令人抓狂”,对于产生的 Java 代码,你基本上不得不使用专门为此设计的 IDE 来管理代码。例如 NetBeans,Eclipse 或 IntelliJ IDEA。实际上,这些 IDE 都为我们管理和创建深度空目录层次结构。
|
||||
这种方式似乎为我们在编写 Java 程序中的某个问题打开了大门。空目录填充了深层次结构,它们不仅用于表示反向 URL,还用于捕获其他信息。这些长路径基本上用于存储有关目录中的内容的数据。如果你希望以最初设计的方式使用目录,这种方法可以从“令人沮丧”到“令人抓狂”,对于生产级的 Java 代码,你必须使用专门为此设计的 IDE 来管理代码。例如 NetBeans,Eclipse 或 IntelliJ IDEA。实际上,这些 IDE 都为我们管理和创建深层次空目录结构。
|
||||
|
||||
对于这本书中的例子,我不想让深层次的层次结构给你的学习带来额外的麻烦,这实际上需要你在开始之前学习熟悉一种重量级的 IDE。所以,我们的每个章节的示例都位于一个浅的子目录中,以章节标题为名。这导致我偶尔会与遵循深度层次方法的工具发生冲突。
|
||||
对于这本书中的例子,我不想让深层次结构给你的学习带来额外的麻烦,这实际上需要你在开始之前学习熟悉一种重量级的 IDE。所以,我们的每个章节的示例都位于一个浅的子目录中,以章节标题为名。这导致我偶尔会与遵循深层次方法的工具发生冲突。
|
||||
|
||||
<!-- Using Other Components -->
|
||||
#### 使用其他组件
|
||||
|
||||
|
||||
无论何时在程序中使用预定义的类,编译器都必须找到该类。在一般情况下,该类已存在于被调用的源代码文件中。此时我们使用该类 —— 即使该类未在文件中稍后定义(Java 消除了所谓的“前向引用”问题)。而那些存在于其他文件中的类怎么样?你可能认为编译器应该足够智能去找到它,但这样是有问题的。想象一下,假如你要使用某个类,但目录中存在多个同名的类(可能用途不同)。或者更糟糕的是,假设你正在编写程序,并且在构建它时,你将向库中添加一个与现有类名称冲突的新类。
|
||||
无论何时在程序中使用预先定义好的类,编译器都必须找到该类。最简单的情况下,该类存在于被调用的源代码文件中。此时我们使用该类 —— 即使该类在文件的后面才会被定义(Java 消除了所谓的“前向引用”问题)。而如果一个类位于其他文件中,又会怎样呢?你可能认为编译器应该足够智能去找到它,但这样是有问题的。想象一下,假如你要使用某个类,但目录中存在多个同名的类(可能用途不同)。或者更糟糕的是,假设你正在编写程序,在构建过程中,你想将某个新类添加到类库中,但却与已有的类名称冲突。
|
||||
|
||||
要解决此问题,你必须通过使用 **import** 关键字来告诉 Java 编译器具体要使用的类。**import** 表示编译器引入一个包,它是一个类库。(在其他语言中,库可以包含函数和数据以及类,但请记住,Java 中的所有活动都在类中进行。)大多数时候,我们都在使用 Java 标准库中的组件。有了这些,你不用担心长的反向域名。你只用说,例如:
|
||||
要解决此问题,你必须通过使用 **import** 关键字来告诉 Java 编译器具体要使用的类。**import** 指示编译器导入一个包,也就是一个类库(在其他语言中,一个库不仅包含类,还可能包括函数和数据,但请记住 Java 中的所有代码都必须写在类里)。大多数时候,我们都在使用 Java 标准库中的组件。有了这些构件,你就不必写一长串的反转域名。例如:
|
||||
|
||||
```java
|
||||
import java.util.ArrayList;
|
||||
```
|
||||
|
||||
上例可以告诉编译器使用位于标准库 util 下的 ArrayList 类。在 util 包含许多类,我们可以使用通配符 `*` 来导入其中部分类,无需显式导入。代码示例:
|
||||
上例可以告诉编译器使用位于标准库 **util** 下的 ArrayList 类。但是,**util** 中包含许多类,我们可以使用通配符 `*` 来导入其中部分类,而无需显式得逐一声明这些类。代码示例:
|
||||
|
||||
```java
|
||||
import java.util.*;
|
||||
```
|
||||
|
||||
本书中的示例很小,为简单起见,我们通常会使用 `.*` 形式略过导入。然而,许多教程书籍都会要求程序员单独导入每个类。
|
||||
|
||||
本书中的示例很小,为简单起见,我们通常会使用 `.*` 形式略过导入。然而,许多教程书籍都会要求程序员逐一导入每个类。
|
||||
|
||||
<!-- The static Keyword -->
|
||||
|
||||
#### static关键字
|
||||
|
||||
类是对象的外观及行为方式的描述。通常只有在使用 `new` 关键字之后程序才能被分配存储空间以及使用其方法。这种方式在两种情况下是不足的。
|
||||
类是对象的外观及行为方式的描述。通常只有在使用 `new` 创建那个类的对象后,数据存储空间才被分配,对象的方法才能供外界调用。这种方式在两种情况下是不足的。
|
||||
|
||||
1. 有时你只需要为特定字段分配一个共享存储空间,无论该类创建了多少个对象,或者即使没有创建任何对象;
|
||||
1. 有时你只想为特定字段(注:也称为属性、域)分配一个共享存储空间,而不去考虑究竟要创建多少对象,甚至根本就不创建对象。
|
||||
|
||||
2. 创建一个与此类本身任何对象无关的方法。也就是说,即使没有创建对象,也能调用该方法。
|
||||
2. 创建一个与此类的任何对象无关的方法。也就是说,即使没有创建对象,也能调用该方法。
|
||||
|
||||
**static** 关键字(从 C++ 采用)就符合我们的要求。当我们说某些东西是静态的时,它意味着该字段或方法不依赖于任何特定的对象实例 。 即使我们从未创建过该类的对象,也可以调用其静态方法或访问静态字段。相反,对于普通的非静态字段和方法,我们必须要先创建一个对象并使用该对象来访问该字段或方法,因为非静态字段和方法必须与替对象关联 [^6] 。
|
||||
**static** 关键字(从 C++ 采用)就符合上述两点要求。当我们说某个事物是静态时,就意味着该字段或方法不依赖于任何特定的对象实例 。 即使我们从未创建过该类的对象,也可以调用其静态方法或访问静态字段。相反,对于普通的非静态字段和方法,我们必须要先创建一个对象并使用该对象来访问字段或方法,因为非静态字段和方法必须与特定对象关联 [^6] 。
|
||||
|
||||
一些面向对象的语言使用类数据(class data)和类方法(class method)这样的术语来表述静态。静态的数据意味着该数据和方法仅存在于类中,而非类的任何实例对象中。有时 Java 文献也使用这些术语。我们可以通过在类的属性或方法前添加 `static` 修饰来表示这是一个静态属性或静态方法。 代码示例:
|
||||
一些面向对象的语言使用类数据(class data)和类方法(class method),表示静态数据和方法只是作为类,而不是类的某个特定对象而存在的。有时 Java 文献也使用这些术语。
|
||||
|
||||
我们可以在类的属性或方法前添加 `static` 关键字来表示这是一个静态属性或静态方法。 代码示例:
|
||||
|
||||
```java
|
||||
class StaticTest {
|
||||
@@ -426,15 +436,15 @@ StaticTest st1 = new StaticTest();
|
||||
StaticTest st2 = new StaticTest();
|
||||
```
|
||||
|
||||
`st1.i` 和 `st2.i` 的值都是 47,因为它们属于同一段内存。引用静态变量有两种方法。在前面的示例中,我们可以通过一个对象来命名它;例如,`st2.i`。同时,你也可以通过它的类名直接调用它(这是非静态成员不能执行的操作):
|
||||
`st1.i` 和 `st2.i` 指向同一块存储空间,因此它们的值都是 47。引用静态变量有两种方法。在前面的示例中,我们通过一个对象来去定位它,例如 `st2.i`。我们也可以通过类名直接引用它,这种方式对于非静态成员则不行:
|
||||
|
||||
```java
|
||||
StaticTest.i ++;
|
||||
StaticTest.i++;
|
||||
```
|
||||
|
||||
`++` 运算符将会使变量结果 + 1。此时 `st1.i` 和 `st2.i` 的值就变成了 48 了。
|
||||
`++` 运算符将会使变量结果 + 1。此时 `st1.i` 和 `st2.i` 的值都变成了 48。
|
||||
|
||||
使用类名直接引用静态变量的首选方法,因为它强调了变量的静态属性。类似的逻辑也适用于静态方法。我们可以通过对象引用静态方法,就像使用任何方法一样,也可以使用特殊的附加语法 classname.method()来直接调用静态属性或方法 [^7]。 代码示例:
|
||||
使用类名直接引用静态变量是首选方法,因为它强调了变量的静态属性。类似的逻辑也适用于静态方法。我们可以通过对象引用静态方法,就像使用任何方法一样,也可以通过特殊的语法方式 `Classname.method()` 来直接调用静态属性或方法 [^7]。 代码示例:
|
||||
|
||||
```java
|
||||
class Incrementable {
|
||||
@@ -444,7 +454,7 @@ class Incrementable {
|
||||
}
|
||||
```
|
||||
|
||||
上例中 `Incrementable` 类调用静态方法 `increment()`。后者再使用 `++` 运算符递增静态变量 **int** `i`。我们依然可以先实例化对象再调用该方法。 代码示例:
|
||||
上例中,`Incrementable` 的 `increment()` 方法通过 `++` 运算符将静态数据 `i` 加1。我们依然可以先实例化对象再调用该方法。 代码示例:
|
||||
|
||||
```java
|
||||
Incrementable sf = new Incrementable();
|
||||
@@ -457,14 +467,13 @@ sf.increment();
|
||||
Incrementable.increment();
|
||||
```
|
||||
|
||||
相比非静态的对象,`static` 属性改变了创建数据的方式。同样,当 `static` 关键字修饰方法时,它允许我们无需创建对象就可以直接通过类的引用来调用该方法。正如我们所知,`static` 关键字的这些特性对于应用程序入口点的 main() 方法尤为重要。
|
||||
应用于字段的 `static` 肯定会更改创建数据的方式 —— `static` 针对每个类和非 `static` 针对每个对象。当应用于方法时,`static` 允许您在不创建对象的情况下调用该方法。正如您将看到的,在定义作为运行应用程序入口点的main()方法时,这是非常重要的。
|
||||
|
||||
相比非静态的对象,`static` 属性改变了数据创建的方式。同样,当 `static` 关键字修饰方法时,它允许我们无需创建对象就可以直接通过类的引用来调用该方法。正如我们所知,`static` 关键字的这些特性对于应用程序入口点的 `main()` 方法尤为重要。
|
||||
|
||||
<!-- Your First Java Program -->
|
||||
|
||||
## 小试牛刀
|
||||
|
||||
最后,我们来开始编写第一个完整的程序。我们使用 Java 标准库来展示一个字符串和日期。
|
||||
最后,我们开始编写第一个完整的程序。我们使用 Java 标准库中的 **Date** 类来展示一个字符串和日期。
|
||||
|
||||
```java
|
||||
|
||||
@@ -480,31 +489,31 @@ public class HelloDate {
|
||||
|
||||
```
|
||||
|
||||
在这本书中,代码块的第一行,我将使用注释行,其中包含文件的路径信息(使用本章的目录名对象),后跟文件名。我的工具可以根据这些信息自动提取和测试书籍的代码,你也可以通过参考第一行注释信息轻松地在 Github 库中找到相应的代码示例。
|
||||
在这本书中,所有代码示例的第一行都是注释行,其中包含文件的路径信息(比如本章的目录名是 **objects**),后跟文件名。我的工具可以根据这些信息自动提取和测试书籍的代码,你也可以通过参考第一行注释轻松地在 Github 库中找到对应的代码示例。
|
||||
|
||||
如果你想在代码中使用到一些额外的库,那么你需要在程序文件的开始处使用 **import** 关键字来导入它们。之所以说是额外的,因为有一些库已经默认自动包含到每个文件里了。例如:`java.lang` 包。
|
||||
如果你想在代码中使用一些额外的类库,那么就必须在程序文件的开始处使用 **import** 关键字来导入它们。之所以说是额外的,因为有一些类库已经默认自动导入到每个文件里了。例如:`java.lang` 包。
|
||||
|
||||
现在打开你的浏览器在 [Oracle](https://www.oracle.com/) 上查看文档。如果你还没有在 [Oracle](https://www.oracle.com/) 网站上下载 JDK 文档,那就趁现在 [^8] 。查看包列表,你会看到 Java 附带的所有不同的类库。
|
||||
现在打开你的浏览器在 [Oracle](https://www.oracle.com/) 上查看文档。如果你还没有从 [Oracle](https://www.oracle.com/) 网站上下载 JDK 文档,那现在就去 [^8] 。查看包列表,你会看到 Java 附带的所有不同的类库。
|
||||
|
||||
选择 `java.lang`。这里显示的是该库中所有类的列表。由于 `java.lang` 隐式包含在每个 Java代码文件中,因此这些类是自动可用的。`java.lang` 中没有列出 **Date** 类,所以我们必须将其导入库才能使用它。如果你不清楚某个类名或者想查看所有的类,可以在 Java 文档中选择“Tree”。
|
||||
选择 `java.lang` ,你会看到该库中所有类的列表。由于 `java.lang` 隐式包含在每个 Java代码文件中,因此这些类是自动可用的。`java.lang` 类库中没有 **Date** 类,所以我们必须导入其他的类库(即 Date 所在的类库)。如果你不清楚某个类所在的类库或者想查看类库中所有的类,那么可以在 Java 文档中选择 “Tree” 查看。
|
||||
|
||||
现在,我们可以找到 Java 附带的每个类。使用浏览器的“查找”功能查找 “Date”。搜索结果中将会列出 java.util.Date,显而易见,它在 util 库中,所以我们必须导入 java.util.* 才能使用 Date。
|
||||
现在,我们可以找到 Java 附带的每个类。使用浏览器的“查找”功能查找 **Date**,搜索结果中将会列出 **java.util.Date**,我们就知道了 **Date** 在 **util** 库中,所以必须导入 **java.util.*** 才能使用 **Date**。
|
||||
|
||||
如果你在文档中选择 `java.lang`,然后选择 **System**,你会看到 System 类中有几个字段,如果你选择 `out`,你会发现它是一个静态的 **PrintStream** 对象。 所以,即使我们没有使用 new 创建, `out` 对象就已经存在并可以使用。 `out` 对象可以执行的操作取决于 PrintStream 。 其在文档中的描述中显示为超链接,如果单击该链接,我们将可以看到 PrintStream 所对应的方法列表。(更多详情,将在本书后面介绍。) 现在我们重点说的是 println() 这个方法。 它的作用是 “将信息输出到控制台,并以换行符结束。” 既然如此,我们可以这样编码来输出信息到控制台。 代码示例:
|
||||
如果你在文档中选择 **java.lang**,然后选择 **System**,你会看到 **System** 类中有几个字段,如果你选择了 **out**,你会发现它是一个静态的 **PrintStream** 对象。 所以,即使我们不使用 **new** 创建, **out** 对象就已经存在并可以使用。 **out** 对象可以执行的操作取决于它的类型: **PrintStream** ,其在文档中是一个超链接,如果单击该链接,我们将可以看到 **PrintStream** 对应的方法列表(更多详情,将在本书后面介绍)。 现在我们重点说的是 **println()** 这个方法。 它的作用是 “将信息输出到控制台,并以换行符结束”。既然如此,我们可以这样编码来输出信息到控制台。 代码示例:
|
||||
|
||||
```java
|
||||
System.out.println("A String of things");
|
||||
```
|
||||
|
||||
每个 java 源文件中允许有多个类。与此同时,源文件的名称必须要和其中一个类名相同,否则编译器将会报错。每个独立的程序应该包含一个 main 方法作为程序运行的入口。其方法签名和返回类型如下。代码示例:
|
||||
每个 java 源文件中允许有多个类。同时,源文件的名称必须要和其中一个类名相同,否则编译器将会报错。每个独立的程序应该包含一个 `main()` 方法作为程序运行的入口。其方法签名和返回类型如下。代码示例:
|
||||
|
||||
```java
|
||||
public static void main(String[] args) {
|
||||
}
|
||||
```
|
||||
|
||||
关键字 `public` 表示方法可以被外界访问到。( 更多详情将在 **隐藏实现** 章节讲到)
|
||||
main() 方法的参数是一个 字符串(**String**) 数组。 参数 `args` 并没有在当前的程序中使用到,但是 Java 虚拟机强制要求必须要有。 这是因为它们被用于保存命令行中的参数。
|
||||
关键字 **public** 表示方法可以被外界访问到。( 更多详情将在 **隐藏实现** 章节讲到)
|
||||
**main()** 方法的参数是一个 字符串(**String**) 数组。 参数 **args** 并没有在当前的程序中使用到,但是 Java 编译器强制要求必须要有, 这是因为它们被用于接收从命令行输入的参数。
|
||||
|
||||
下面我们来看一段有趣的代码:
|
||||
|
||||
@@ -512,9 +521,9 @@ main() 方法的参数是一个 字符串(**String**) 数组。 参数 `args
|
||||
System.out.println(new Date());
|
||||
```
|
||||
|
||||
上面的示例中,我们创建了一个日期(**Date**)型对象并将其转化为字符串类型并输出到控制台中。 一旦这一行语句执行完毕,我们就不再需要该日期对象了。这时, Java 的垃圾回收器就可以将其占用的内存回收,我们无需去主动清除它们。
|
||||
上面的示例中,我们创建了一个日期(**Date**)类型的对象并将其转化为字符串类型,输出到控制台中。 一旦这一行语句执行完毕,我们就不再需要该日期对象了。这时,Java 垃圾回收器就可以将其占用的内存回收,我们无需去主动清除它们。
|
||||
|
||||
查看 JDK 文档,我们可以看到, **System** 类下还有很多其他有用的方法。( Java 的牛逼之处还在于,它拥有一个庞大的标准库资源。) 代码示例:
|
||||
查看 JDK 文档时,我们可以看到在 **System** 类下还有很多其他有用的方法( Java 的牛逼之处还在于,它拥有一个庞大的标准库资源)。代码示例:
|
||||
|
||||
```java
|
||||
// objects/ShowProperties.java
|
||||
@@ -554,33 +563,34 @@ os.arch=amd64
|
||||
java.io.tmpdir=C:\Users\Bruce\AppData\Local\Temp\
|
||||
```
|
||||
|
||||
上例主方法中的第一行,会输出所有的系统属性,也就是环境信息。 **list()** 方法将结果发送给它的参数 **System.out** 在本书的后面,我们还会接触到将结果输出到其他地方,如文件中。另外,我们还可以请求特定的属性。该例中我们使用到了 **user.name** 和 **java.library.path**。 末尾的 “/* Output:”标记表示此文件生成的输出的开头。本书中产生输出的大多数示例将会以包含此注释形式的输出,因此我们可以看到输出并知道它是正确的。带有这个标签允许在使用编译器检查并执行后将输出自动更新到本书的文本中。
|
||||
`main()` 方法中的第一行会输出所有的系统属性,也就是环境信息。 **list()** 方法将结果发送给它的参数 **System.out**。在本书的后面,我们还会接触到将结果输出到其他地方,例如文件中。另外,我们还可以请求特定的属性。该例中我们使用到了 **user.name** 和 **java.library.path**。
|
||||
|
||||
<!-- Compiling and Running -->
|
||||
|
||||
### 编译和运行
|
||||
|
||||
要编译和运行本书中的代码示例,首先必须具有 Java 编程环境。 第二章的示例中描述了安装过程。如果你遵循这些说明,那么你将会在不受 Oracle 的限制的条件下用到 Java 开发人员工具包(JDK)。如果你使用其他开发系统,请查看该系统的文档以确定如何编译和运行程序。 第二章还介绍了如何安装本书的示例。
|
||||
要编译和运行本书中的代码示例,首先必须具有 Java 编程环境。 第二章的示例中描述了安装过程。如果你遵循这些说明,那么你将会在不受 Oracle 的限制的条件下用到 Java 开发工具包(JDK)。如果你使用其他开发系统,请查看该系统的文档以确定如何编译和运行程序。 第二章还介绍了如何安装本书的示例。
|
||||
|
||||
移动到 objects 子目录下并键入:
|
||||
移动到子目录 **objects** 下并键入:
|
||||
|
||||
``` bash
|
||||
javac HelloDate.java
|
||||
```
|
||||
|
||||
此命令不应生成任何响应。如果我们收到任何类型的错误消息,则表示未正确安装 JDK,并且必须检查这些问题。
|
||||
此命令不应产生任何响应。如果我们收到任何类型的错误消息,则表示未正确安装 JDK,那就得检查这些问题。
|
||||
|
||||
此外,仅仅是执行的话,则可以键入:
|
||||
若执行不报错的话,此时可以键入:
|
||||
|
||||
```java
|
||||
java HelloDate
|
||||
```
|
||||
我们将会获得信息反馈,日期输出。这是我们编译和运行本书中每个程序(包含main())的过程 [^9]。但是,本书的源代码在根目录中也有一个名为 **build.gradle** 的文件,其中包含用于自动构建,测试和运行本书文件的 **Gradle** 配置。当您第一次运行 `gradlew` 命令时,**Gradle** 将自动安装(前提已安装Java)。
|
||||
我们将会得到正确的日期输出。这是我们编译和运行本书中每个程序(包含 `main()` 方法)的过程 [^9]。此外,本书的源代码在根目录中也有一个名为 **build.gradle** 的文件,其中包含用于自动构建,测试和运行本书文件的 **Gradle** 配置。当你第一次运行 `gradlew` 命令时,**Gradle** 将自动安装(前提是已安装Java)。
|
||||
|
||||
<!-- Coding Style -->
|
||||
## 编码风格
|
||||
|
||||
|
||||
Java 的代码约定规范(Code Conventions for the Java Programming Language)[^10] 要求类名的首字母大写。 如果类名是由多个单词构成的,则每个单词的首字母都应大写(不采用下划线来分隔)例如:
|
||||
Java 编程语言编码规范(Code Conventions for the Java Programming Language)[^10] 要求类名的首字母大写。 如果类名是由多个单词构成的,则每个单词的首字母都应大写(不采用下划线来分隔)例如:
|
||||
|
||||
```java
|
||||
class AllTheColorsOfTheRainbow {
|
||||
@@ -588,7 +598,7 @@ class AllTheColorsOfTheRainbow {
|
||||
```
|
||||
|
||||
|
||||
这种风格也被称之为“驼峰式”。对于几乎所有其他方法,字段(成员变量)和对象引用名称 - 除了标识符的首字母是小写之外其他规范与类的命名一致。代码示例:
|
||||
有时称这种命名风格叫“驼峰命名法”。对于几乎所有其他方法,字段(成员变量)和对象引用名都采用驼峰命名的方式,但是它们的首字母不需要大写。代码示例:
|
||||
|
||||
```java
|
||||
class AllTheColorsOfTheRainbow {
|
||||
@@ -601,13 +611,13 @@ class AllTheColorsOfTheRainbow {
|
||||
|
||||
```
|
||||
|
||||
在 Oracle 的库中,开放式花括号的位置同样遵循和本书中相同的规范。
|
||||
在 Oracle 的官方类库中,花括号的位置同样遵循和本书中上述示例相同的规范。
|
||||
|
||||
|
||||
## 本章小结
|
||||
|
||||
|
||||
本章向您展示了简单的 Java 程序编写以及该语言相关的基本概念。到目前为止,我们的示例都只是些简单的顺序执行。在接下来的两章里,我们将会接触到 Java 的一些基本操作符,以及如何去控制程序执行的流程。
|
||||
本章向你展示了简单的 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),并且一切都是值传递。所以你不是通过引用传递,而是“通过值传递对象引用。人们可以质疑我的这种解释的准确性,但我认为我的方法简化了对概念的理解而又没对语言造成伤害(嗯,语言专家可能会说我骗你,但我会说我只是对此进行了适当的抽象。)
|
||||
|
||||
@@ -8,23 +8,23 @@
|
||||
|
||||
Java 是从 C++ 的基础上做了一些改进和简化发展而成的。对于 C/C++ 程序员来说,Java 的运算符并不陌生。如果你已了解 C 或 C++,大可以跳过本章和下一章,直接阅读 Java 与 C/C++ 不同的地方。
|
||||
|
||||
如果理解这两章的内容对你来说还有点困难,那么我推荐你先了解下 《Thinking in C》 再继续后面的学习。 这本书现在可以在 [www.OnJava8.com](http://www.OnJava8.com]) 上免费下载。它的内容包含音频讲座、幻灯片、练习和解决方案,专门用于帮助你快速掌握学习 Java 所需的基础知识。
|
||||
如果理解这两章的内容对你来说还有点困难,那么我推荐你先了解下 《Thinking in C》 再继续后面的学习。 这本书现在可以在 [www.OnJava8.com](http://www.OnJava8.com]) 上免费下载。它的内容包含音频讲座、幻灯片、练习和解答,专门用于帮助你快速掌握学习 Java 所需的基础知识。
|
||||
|
||||
|
||||
<!-- Using-Java-Operators -->
|
||||
## 使用说明
|
||||
|
||||
|
||||
运算符接受一个或多个参数并生成新值。这个参数与普通方法调用的形式是不同的,但效果是相同的。加法 `+`,减法 `-`,乘法 `*`,除法 `/` 以及赋值 `=` 在任何编程语言中的工作方式都是类似的。所有运算符都能根据自己的运算对象生成一个值。除此以外,一个运算符可改变运算对象的值,这叫作“副作用”(**Side Effect**)。运算符最常见的用途就是修改自己的运算对象,从而产生副作用。但要注意生成的值亦可由没有副作用的运算符生成。
|
||||
运算符接受一个或多个参数并生成新值。这个参数与普通方法调用的形式不同,但效果是相同的。加法 `+`,减法 `-`,乘法 `*`,除法 `/` 以及赋值 `=` 在任何编程语言中的工作方式都是类似的。所有运算符都能根据自己的运算对象生成一个值。除此以外,一些运算符可改变运算对象的值,这叫作“副作用”(**Side Effect**)。运算符最常见的用途就是修改自己的运算对象,从而产生副作用。但要注意生成的值亦可由没有副作用的运算符生成。
|
||||
|
||||
几乎所有运算符都只能操作基本类型(Primitives)。唯一的例外是 `=`、`==` 和 `!=`,它们能操作所有对象(这也是令人混淆的一个地方)。除此以外,**String** 类支持 `+` 和 `+=`。
|
||||
|
||||
|
||||
<!-- Precedence -->
|
||||
|
||||
## 优先级
|
||||
|
||||
|
||||
运算符的优先级决定了存在多个运算符时一个表达式各部分的计算顺序。Java 对计算顺序作出了特别的规定。其中,最简单的规则就是乘法和除法在加法和减法之前完成。程序员经常都会忘记其他优先级规则,所以应该用括号明确规定计算顺序。代码示例:
|
||||
运算符的优先级决定了存在多个运算符时一个表达式各部分的运算顺序。Java 对运算顺序作出了特别的规定。其中,最简单的规则就是乘法和除法在加法和减法之前完成。程序员经常都会忘记其他优先级规则,所以应该用括号明确规定运算顺序。代码示例:
|
||||
|
||||
```java
|
||||
// operators/Precedence.java
|
||||
@@ -51,14 +51,14 @@ public class Precedence {
|
||||
|
||||
我们注意到,在 `System.out.println()` 语句中使用了 `+` 运算符。 但是在这里 `+` 代表的意思是字符串连接符。编译器会将 `+` 连接的非字符串尝试转换为字符串。上例中的输出结果说明了 a 和 b 都已经被转化成了字符串。
|
||||
|
||||
|
||||
<!-- Assignment -->
|
||||
|
||||
## 赋值
|
||||
|
||||
|
||||
运算符的赋值是由符号 `=` 完成的。它代表着获取 `=` 右边的值并赋给左边的变量。右边可以是任何常量、变量或者是可产生一个返回值的表达式。但左边必须是一个明确的、已命名的变量。也就是说,必须要有一个物理的空间来存放右边的值。举个例子来说,可将一个常数赋给一个变量( A = 4 ),但不可将任何东西赋给一个常数(比如不能 4 = A)。
|
||||
运算符的赋值是由符号 `=` 完成的。它代表着获取 `=` 右边的值并赋给左边的变量。右边可以是任何常量、变量或者可产生一个返回值的表达式。但左边必须是一个明确的、已命名的变量。也就是说,必须要有一个物理的空间来存放右边的值。举个例子来说,可将一个常数赋给一个变量( A = 4 ),但不可将任何东西赋给一个常数(比如不能 4 = A)。
|
||||
|
||||
基本类型的赋值都是直接的,而不像对象,赋予的只是其内存的引用。举个例子,a = b ,如果 b 是基本类型,那么 赋值操作会将 b 的值复制一根给变量 a, 此后若 a 的值发生改变是不会影响到 b 的。作为一名程序员,着应该成为我们的常识。
|
||||
基本类型的赋值都是直接的,而不像对象,赋予的只是其内存的引用。举个例子,a = b ,如果 b 是基本类型,那么 赋值操作会将 b 的值复制一份给变量 a, 此后若 a 的值发生改变是不会影响到 b 的。作为一名程序员,这应该成为我们的常识。
|
||||
|
||||
如果是为对象赋值,那么结果就不一样了。对一个对象进行操作时,我们实际上操作的是它的引用。所以我们将右边的对象赋予给左边时,赋予的只是该对象的引用。此时,两者指向的堆中的对象还是同一个。代码示例:
|
||||
|
||||
@@ -96,9 +96,9 @@ public class Assignment {
|
||||
3: t1.level: 27, t2.level: 27
|
||||
```
|
||||
|
||||
这是个简单的 `Tank` 类,通过 main 方法 `new` 出了 2 个实例对象。 两个对象的 `level` 属性分别被赋予了不同的值。 然后,t2 的值被赋予给 t1。在许多编程语言里,预期的结果是 t1 和 t2 的值会一直相对独立。但是,在 Java 中,由于赋予的只是对象的引用,改变 t1 也就改变了 t2。 这是因为 t1 和 t2 此时指向的是堆中同一个对象。(t1 原始对象的引用在 t2 赋值给其时被丢失,它就将会在垃圾回收时被清理)。
|
||||
这是一个简单的 `Tank` 类,在 `main()` 方法创建了 2 个实例对象。 两个对象的 `level` 属性分别被赋予不同的值。 然后,t2 的值被赋予给 t1。在许多编程语言里,预期的结果是 t1 和 t2 的值会一直相对独立。但是,在 Java 中,由于赋予的只是对象的引用,改变 t1 也就改变了 t2。 这是因为 t1 和 t2 此时指向的是堆中同一个对象。(t1 原始对象的引用在 t2 赋值给其时丢失,它引用的对象会在垃圾回收时被清理)。
|
||||
|
||||
这种现象通常称为别名(aliasing),这是 Java 处理对象的一种基本方式。但是假若你不想这里出现这样的混淆的话,你可以这么做。代码示例:
|
||||
这种现象通常称为别名(aliasing),这是 Java 处理对象的一种基本方式。但是假若你不想出现这里的别名引起混淆的话,你可以这么做。代码示例:
|
||||
|
||||
```java
|
||||
t1.level = t2.level;
|
||||
@@ -116,16 +116,16 @@ t1.level = t2.level;
|
||||
// 正在传递的对象可能不是你之前使用的
|
||||
class Letter {
|
||||
char c;
|
||||
}
|
||||
}
|
||||
|
||||
public class PassObject {
|
||||
static void f(Letter y) {
|
||||
y.c = 'z';
|
||||
}
|
||||
|
||||
y.c = 'z';
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
Letter x = new Letter();
|
||||
x.c = 'a';
|
||||
x.c = 'a';
|
||||
System.out.println("1: x.c: " + x.c);
|
||||
f(x);
|
||||
System.out.println("2: x.c: " + x.c);
|
||||
@@ -140,15 +140,15 @@ public class PassObject {
|
||||
2: x.c: z
|
||||
```
|
||||
|
||||
在许多编程语言中,方法 `f()` 似乎在内部复制其参数 **Letter y**。但是一旦传递了一个引用,那么实际上 `y.c ='z';` 是在方法 `f()` 之外改变对象。别名现象以及其解决方案是个复杂的问题,在附录中有包含:[对象传递和返回](./Appendix-Passing-and-Returning-Objects.md)。意识到这一点,我们可以警惕类似的陷阱。
|
||||
|
||||
在许多编程语言中,方法 `f()` 似乎会在内部复制其参数 **Letter y**。但是一旦传递了一个引用,那么实际上 `y.c ='z';` 是在方法 `f()` 之外改变对象。别名现象以及其解决方案是个复杂的问题,在附录中有包含:[对象传递和返回](./Appendix-Passing-and-Returning-Objects.md)。意识到这一点,我们可以警惕类似的陷阱。
|
||||
|
||||
<!-- Mathematical Operators -->
|
||||
|
||||
## 算术运算符
|
||||
|
||||
Java 的基本算术运算符与其他大多编程语言是相同的。其中包括加号 `+`、减号 `-`、除号 `/`、乘号 `*` 以及模数 `%`(从整数除法中获得余数)。整数除法会直接砍掉小数,而不是进位。
|
||||
Java 的基本算术运算符与其他大多编程语言是相同的。其中包括加号 `+`、减号 `-`、除号 `/`、乘号 `*` 以及取模 `%`(从整数除法中获得余数)。整数除法会直接砍掉小数,而不是进位。
|
||||
|
||||
Java 也用一种简写形式同时进行运算和赋值操作,由运算符后跟等号表示,并且与语言中的所有运算符一致(只要有意义)。为了将 4 的值赋予给变量 x 同时将结果赋予给 x , 可用 x += 4 来表示。下面带来代码更多代码示例:
|
||||
Java 也用一种与 C++ 相同的简写形式同时进行运算和赋值操作,由运算符后跟等号表示,并且与语言中的所有运算符一致(只要有意义)。 可用 x += 4 来表示:将 x 的值加上4的结果再赋值给 x。更多代码示例:
|
||||
|
||||
|
||||
```java
|
||||
@@ -232,8 +232,8 @@ u /= v : 9.940527
|
||||
|
||||
为了生成随机数字,程序首先创建一个 **Random** 对象。不带参数的 **Random** 对象会利用当前的时间用作随机数生成器的“种子”(seed),从而为程序的每次执行生成不同的输出。在本书的示例中,重要的是每个示例末尾的输出尽可能一致,以便可以使用外部工具进行验证。所以我们通过在创建 **Random** 对象时提供种子(随机数生成器的初始化值,其始终为特定种子值产生相同的序列),让程序每次执行都生成相同的随机数,如此以来输出结果就是可验证的 [^1]。 若需要生成随机值,可删除代码示例中的种子参数。该对象通过调用方法 `nextInt()` 和 `nextFloat()`(还可以调用 `nextLong()` 或 `nextDouble()`),使用 **Random** 对象生成许多不同类型的随机数。`nextInt()` 的参数设置生成的数字的上限,下限为零,为了避免零除的可能性,结果偏移1。
|
||||
|
||||
|
||||
<!-- Unary Minus and Plus Operators -->
|
||||
|
||||
### 一元加减运算符
|
||||
|
||||
一元加 `+` 减 `-` 运算符的操作和二元是相同的。编译器可自动识别使用何种方式解析运算:
|
||||
@@ -254,16 +254,16 @@ x = a * -b;
|
||||
x = a * (-b);
|
||||
```
|
||||
|
||||
一元减号可以得到数据的负值。一元加号的作用相反,不过它唯一能影响的就是把较小的数值类型自动转换为了 **int** 类型。
|
||||
|
||||
一元减号可以得到数据的负值。一元加号的作用相反,不过它唯一能影响的就是把较小的数值类型自动转换为 **int** 类型。
|
||||
|
||||
<!-- Auto-Increment-and-Decrement -->
|
||||
|
||||
## 递增和递减
|
||||
|
||||
|
||||
和 C 语言类似,Java 提供了许多快捷运算方式。快捷运算可使代码可读性,可写性都更强。其中包括递增 `++` 和递减 `--`,意为“增加或减少一个单位”。举个例子来说,假设 a 是一个 **int** 类型的值,则表达式 `++a` 就等价于 `a = a + 1`。 递增和递减运算符不仅可以修改变量,还可以生成变量的值。
|
||||
和 C 语言类似,Java 提供了许多快捷运算方式。快捷运算可使代码可读性,可写性都更强。其中包括递增 `++` 和递减 `--`,意为“增加或减少一个单位”。举个例子来说,假设 a 是一个 **int** 类型的值,则表达式 `++a` 就等价于 `a = a + 1`。 递增和递减运算符不仅可以修改变量,还可以生成变量的值。
|
||||
|
||||
每种类型的运算符,都有两个版本可供选用;通常将其称为“前缀版”和“后缀版”。“前递增”表示 `++` 运算符位于变量或表达式的前面;而“后递增”表示 `++` 运算符位于变量或表达式的后面。类似地,“前递减”意味着 `--` 运算符位于变量或表达式的前面;而“后递减”意味着 `--` 运算符位于变量或表达式的后面。对于前递增和前递减(如 `++a` 或 `--a`),会先执行递增/减运算,再返回值。而对于后递增和后递减(如 `a++` 或 `a--`),会先返回值,再执行递增/减运算。代码示例:
|
||||
每种类型的运算符,都有两个版本可供选用;通常将其称为“前缀”和“后缀”。“前递增”表示 `++` 运算符位于变量或表达式的前面;而“后递增”表示 `++` 运算符位于变量的后面。类似地,“前递减”意味着 `--` 运算符位于变量的前面;而“后递减”意味着 `--` 运算符位于变量的后面。对于前递增和前递减(如 `++a` 或 `--a`),会先执行递增/减运算,再返回值。而对于后递增和后递减(如 `a++` 或 `a--`),会先返回值,再执行递增/减运算。代码示例:
|
||||
|
||||
```java
|
||||
// operators/AutoInc.java
|
||||
@@ -294,21 +294,22 @@ i--: 2
|
||||
i: 1
|
||||
```
|
||||
|
||||
对于前缀形式,我们将在执行递增/减操作后获取值;使用后缀形式,我们将在执行递增/减操作之前获取值。它们是唯一具有“副作用”的运算符(除那些涉及赋值的以外) —— 它们既改变操作数又改变值。
|
||||
对于前缀形式,我们将在执行递增/减操作后获取值;使用后缀形式,我们将在执行递增/减操作之前获取值。它们是唯一具有“副作用”的运算符(除那些涉及赋值的以外) —— 它们修改了操作数的值。
|
||||
|
||||
C++ 名称来自于递增运算符,同时也代表着“比 C 更进一步”。在早期的 Java 演讲中,*Bill Joy*(Java 作者之一)说“**Java = C ++ --**”(C++ 减减)。这意味着 Java 是在 C++ 的基础上减少了许多不必要的东西,因此语言更简单。随着进一步地学习,我们会发现 Java 的确有许多地方相对 C++ 来说更简便,但是在其他方面,难度并不会比 C++ 小多少。
|
||||
C++ 名称来自于递增运算符,暗示着“比 C 更进一步”。在早期的 Java 演讲中,*Bill Joy*(Java 作者之一)说“**Java = C++ --**”(C++ 减减),意味着 Java 在 C++ 的基础上减少了许多不必要的东西,因此语言更简单。随着进一步地学习,我们会发现 Java 的确有许多地方相对 C++ 来说更简便,但是在其他方面,难度并不会比 C++ 小多少。
|
||||
|
||||
|
||||
<!-- Relational-Operators -->
|
||||
## 关系运算符
|
||||
|
||||
|
||||
关系运算符会通过产生一个布尔(**boolean**)结果来表示被操作的数值之间的关系。如果关系为真,则结果为 **true**,如果关系非真,则结果为 **false**。关系运算符包括小于 `<`,大于 `>`,小于或等于 `<=`,大于或等于 `>=`,等价 `==` 和不等价 `!=`。`==` 和 `!=` 可与所有基本类型搭配使用。但与其他类型的比较就不太适合了,因为布尔值只能表示 **true** 或 **false**,所以比较它们之间的“大于”或“小于”没有意义。
|
||||
关系运算符会通过产生一个布尔(**boolean**)结果来表示操作数之间的关系。如果关系为真,则结果为 **true**,如果关系为假,则结果为 **false**。关系运算符包括小于 `<`,大于 `>`,小于或等于 `<=`,大于或等于 `>=`,等于 `==` 和不等于 `!=`。`==` 和 `!=` 可用于所有基本类型,但其他运算符不能用于基本类型 **boolean**,因为布尔值只能表示 **true** 或 **false**,所以比较它们之间的“大于”或“小于”没有意义。
|
||||
|
||||
<!-- Testing Object Equivalence -->
|
||||
|
||||
### 测试对象等价
|
||||
|
||||
关系运算符 `==` 和 `!=` 同样适用于所有对象之间的比较运算,但产生的结果却经常混淆 Java 的初学者。下面是代码示例:
|
||||
关系运算符 `==` 和 `!=` 同样适用于所有对象之间的比较运算,但它们比较的内容却经常困扰 Java 的初学者。下面是代码示例:
|
||||
|
||||
```java
|
||||
// operators/Equivalence.java
|
||||
@@ -329,13 +330,34 @@ true
|
||||
false
|
||||
```
|
||||
|
||||
表达式 `System.out.println(n1 == n2)` 将会输出比较的结果。因为两个 **Integer** 对象相同,所以先输出 **true**,再输出 **false**。但是,尽管对象的内容一样,对象的引用却不一样。`==` 和 `!=` 比较的是对象引用,所以输出实际上应该是先输出 **false**,再输出 **true**(译者注:如果你把47改成128,那么打印的结果就是这样,因为 Integer 内部维护着一个 IntegerCache 的缓存,默认缓存范围是 [-128, 127],所以 [-128, 127] 之间的值用 `==` 和 `!=` 比较也能能到正确的结果,但是不推荐用关系运算符比较,具体见 JDK 中的 Integer 类源码)。
|
||||
|
||||
那么怎么比较两个对象的内容是否相同呢?你必须使用所有对象(不包括基本类型)中都存在的 `equals()` 方法,下面是如何使用 `equals()` 方法的示例:
|
||||
|
||||
```java
|
||||
// operators/EqualsMethod.java
|
||||
public class EqualsMethod {
|
||||
public static void main(String[] args) {
|
||||
Integer n1 = 47;
|
||||
Integer n2 = 47;
|
||||
System.out.println(n1.equals(n2));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
输出结果:
|
||||
|
||||
```java
|
||||
true
|
||||
```
|
||||
|
||||
上例的结果看起来是我们所期望的。但其实事情并非那么简单。下面我们来创建自己的类:
|
||||
|
||||
```java
|
||||
// operators/EqualsMethod2.java
|
||||
// 默认的 equals() 方法没有比较内容
|
||||
class Value {
|
||||
int i;
|
||||
int i;
|
||||
}
|
||||
|
||||
public class EqualsMethod2 {
|
||||
@@ -350,15 +372,16 @@ public class EqualsMethod2 {
|
||||
|
||||
输出结果:
|
||||
|
||||
```
|
||||
```java
|
||||
false
|
||||
```
|
||||
|
||||
上例的结果再次令人困惑:结果是错误的。原因: `equals()` 的默认行为是比较对象的引用而非具体内容。因此,除非你在新类中重写 `equals()` 方法,否则我们将获取不到想要的结果。不幸的是,在学习 [复用](./08-Reuse.md)(**Reuse**) 章节后我们才能接触到“覆盖”(**Override**),并且直到 [附录:集合主题](./Appendix-Collection-Topics.md),才能知道定义 **equals()** 方法的正确方式,但是现在明白 `equals()` 行为方式也可能为你节省一些时间。
|
||||
上例的结果再次令人困惑:结果是 **false**。原因: `equals()` 的默认行为是比较对象的引用而非具体内容。因此,除非你在新类中覆写 `equals()` 方法,否则我们将获取不到想要的结果。不幸的是,在学习 [复用](./08-Reuse.md)(**Reuse**) 章节后我们才能接触到“覆写”(**Override**),并且直到 [附录:集合主题](./Appendix-Collection-Topics.md),才能知道定义 `equals()` 方法的正确方式,但是现在明白 `equals()` 行为方式也可能为你节省一些时间。
|
||||
|
||||
大多数 Java 库类通过覆写 `equals()` 方法比较对象的内容而不是其引用。
|
||||
|
||||
<!-- Logical-Operators -->
|
||||
<!-- Logical Operators -->
|
||||
|
||||
## 逻辑运算符
|
||||
|
||||
每个逻辑运算符 `&&` (**AND**)、`||`(**OR**)和 `!`(**非**)根据参数的逻辑关系生成布尔值 `true` 或 `false`。下面的代码示例使用了关系运算符和逻辑运算符:
|
||||
@@ -407,14 +430,12 @@ i != j is true
|
||||
(i < 10) || (j < 10) is false
|
||||
```
|
||||
|
||||
在 Java 逻辑运算中,我们不能像 C/C++ 那样使用非布尔值, 而仅能使用 **AND**、**OR**、**NOT**。
|
||||
下面是一次错误的尝试: ~~a || -~~。 但是,后续表达式使用关系比较生成布尔值,然后对结果使用逻辑运算结果。请注意,如果在预期为 **String** 类型的位置使用 **boolean** 类型的值,则结果会自动转为适当的文本格式。
|
||||
|
||||
我们可以将前一个程序中 **int** 的定义替换为除 **boolean** 之外的任何其他基本数据类型。
|
||||
但请注意,**float** 类型的数值比较非常严格。只要最小位部分的数字不同则两个数值之间的比较仍然是“非等”的;只要数字最小位是大于 0 的,那么它就不等于 0。
|
||||
在 Java 逻辑运算中,我们不能像 C/C++ 那样使用非布尔值, 而仅能使用 **AND**、**OR**、**NOT**。上面的例子中,我们将使用非布尔值的表达式注释掉了(你可以看到表达式前面是 //-)。但是,后续的表达式使用关系比较生成布尔值,然后对结果使用了逻辑运算。请注意,如果在预期为 **String** 类型的位置使用 **boolean** 类型的值,则结果会自动转为适当的文本格式(即 "true" 或 "false" 字符串)。
|
||||
|
||||
我们可以将前一个程序中 **int** 的定义替换为除 **boolean** 之外的任何其他基本数据类型。但请注意,**float** 类型的数值比较非常严格,只要两个数字的最小位不同则两个数仍然不相等;只要数字最小位是大于 0 的,那么它就不等于 0。
|
||||
|
||||
<!-- Short-Circuiting -->
|
||||
|
||||
### 短路
|
||||
|
||||
逻辑运算符支持一种称为“短路”(short-circuiting)的现象。整个表达式会在运算到可以明确结果时就停止并返回结果,这意味着该逻辑表达式的后半部分不会被执行到。代码示例:
|
||||
@@ -465,12 +486,12 @@ expression is false
|
||||
test1(0)&& test2(2)&& test3(2)
|
||||
```
|
||||
|
||||
可能你的预期是程序会执行 3 个 **test** 方法并返回。我们来分析一下:第一个方法的结果返回 `true`,因此表达式会继续走下去。紧接着,第二个方法的返回结果是 `false`。这就代表这整个表达式的结果肯定为 `false`,所以就没有必要再判断剩下的表达式部分了。
|
||||
可能你的预期是程序会执行 3 个 **test** 方法并返回。我们来分析一下:第一个方法的结果返回 `true`,因此表达式会继续走下去。紧接着,第二个方法的返回结果是 `false`。这就代表这整个表达式的结果肯定为 `false`,所以就没有必要再判断剩下的表达式部分了。
|
||||
|
||||
所以,运用“短路”可以节省部分不必要的运算,从而提高程序潜在的性能。
|
||||
|
||||
|
||||
<!-- Literals -->
|
||||
|
||||
## 字面值常量
|
||||
|
||||
|
||||
@@ -540,22 +561,23 @@ bli: 101111101011111010111110101111
|
||||
bll: 101111101011111010111110101111
|
||||
```
|
||||
|
||||
在文本值的后面添加字面值常量可以让编译器识别该文本值的类型。对于 **Long** 型数值,结尾使用大写 `L` 或小写 `l` 皆可(不推荐使用 `l`,因为容易与阿拉伯数值 1 混淆)。大写 `F` 或小写 `f` 表示 **float** 浮点数。大写 `D` 或小写 `d` 表示 **double** 双精度。
|
||||
在文本值的后面添加字符可以让编译器识别该文本值的类型。对于 **Long** 型数值,结尾使用大写 `L` 或小写 `l` 皆可(不推荐使用 `l`,因为容易与阿拉伯数值 1 混淆)。大写 `F` 或小写 `f` 表示 **float** 浮点数。大写 `D` 或小写 `d` 表示 **double** 双精度。
|
||||
|
||||
|
||||
十六进制(以 16 为基数),适用于所有整型数据类型,由前导 `0x` 或 `0x` 表示,后跟 0-9 或 a-f (大写或小写)。如果我们在初始化某个类型的数值时,赋值超出其范围,那么编译器会报错(不管值的数字形式如何)。在上例的代码中,**char**、**byte** 和 **short** 的值已经是最大了。如果超过这些值,编译器将自动转型为 **int**,并且我们需要声明强制转换(强制转换将在本章后面定义)。这告诉我们已越过该类型的范围界限。
|
||||
十六进制(以 16 为基数),适用于所有整型数据类型,由前导 `0x` 或 `0X` 表示,后跟 0-9 或 a-f (大写或小写)。如果我们在初始化某个类型的数值时,赋值超出其范围,那么编译器会报错(不管值的数字形式如何)。在上例的代码中,**char**、**byte** 和 **short** 的值已经是最大了。如果超过这些值,编译器将自动转型为 **int**,并且提示我们需要声明强制转换(强制转换将在本章后面定义),意味着我们已越过该类型的范围界限。
|
||||
|
||||
八进制(以 8 为基数)由 0~7 之间的数字和前导零 `0` 表示。
|
||||
|
||||
Java 7 引入了二进制的字面值常量,由前导 `0B` 或 `0B` 表示,它可以初始化所有的整数类型。
|
||||
Java 7 引入了二进制的字面值常量,由前导 `0b` 或 `0B` 表示,它可以初始化所有的整数类型。
|
||||
|
||||
使用整型数值类型时,显示其二进制形式会很有用。在 Long 型和 Integer 型中这很容易实现,调用其静态的 `toBinaryString()` 方法即可。 但是请注意,若将较小的类型传递给 **Integer.**`tobinarystring()` 时,类型将自动转换为 **int**。
|
||||
|
||||
<!-- Underscores in Literals -->
|
||||
|
||||
### 下划线
|
||||
|
||||
|
||||
Java 7 中有一个深思熟虑的补充:我们可以在数字文字中包含下划线 `_`,以使结果更清晰。这对于大数值的分组数字特别有用。代码示例:
|
||||
Java 7 中有一个深思熟虑的补充:我们可以在数字字面量中包含下划线 `_`,以使结果更清晰。这对于大数值的分组特别有用。代码示例:
|
||||
|
||||
|
||||
```java
|
||||
@@ -590,11 +612,10 @@ public class Underscores {
|
||||
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` 而不是换行符。
|
||||
|
||||
[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 -->
|
||||
|
||||
### 指数计数法
|
||||
|
||||
指数总是采用一种我认为很不直观的记号方法:
|
||||
@@ -622,11 +643,11 @@ public class Exponents {
|
||||
4.7E48
|
||||
```
|
||||
|
||||
在科学与工程学领域,“e”代表自然对数的基数,约等于 2.718 (Java 一种更精确的 **double** 值采用 **Math.E** 的形式)。它在类似 “1.39×e 的 -47 次方”这样的指数表达式中使用,意味着“1.39×2.718 的-47次方”。然而,自 FORTRAN 语言发明后,人们自然而然地觉得e 代表 “10几次幂”。这种做法显得颇为古怪,因为 FORTRAN 最初面向的是科学与工程设计领域。
|
||||
在科学与工程学领域,**e** 代表自然对数的基数,约等于 2.718 (Java 里用一种更精确的 **double** 值 **Math.E** 来表示自然对数)。指数表达式 "1.39 x e-43",意味着“1.39 × 2.718 的-43次方”。然而,自 FORTRAN 语言发明后,人们自然而然地觉得e 代表 “10的几次幂”。这种做法显得颇为古怪,因为 FORTRAN 最初是为科学与工程领域设计的。
|
||||
|
||||
理所当然,它的设计者应对这样的混淆概念持谨慎态度 [^2]。但不管怎样,这种特别的表达方法在 C,C++ 以及现在的 Java 中顽固地保留下来了。所以倘若习惯 e 作为自然对数的基数使用,那么在 Java 中看到类似“1.39e-47f”这样的表达式时,请转换你的思维,从程序设计的角度思考它;它真正的含义是“1.39×10 的-47次方”。
|
||||
理所当然,它的设计者应对这样的混淆概念持谨慎态度 [^2]。但不管怎样,这种特别的表达方法在 C,C++ 以及现在的 Java 中顽固地保留下来了。所以倘若习惯 e 作为自然对数的基数使用,那么在 Java 中看到类似“1.39e-43f”这样的表达式时,请转换你的思维,从程序设计的角度思考它;它真正的含义是“1.39 × 10的-43次方”。
|
||||
|
||||
注意如果编译器能够正确地识别类型,就不必使用尾随字面值常量。对于下述语句:
|
||||
注意如果编译器能够正确地识别类型,就不必使用后缀字符。对于下述语句:
|
||||
|
||||
```java
|
||||
long n3 = 200;
|
||||
@@ -634,33 +655,33 @@ long n3 = 200;
|
||||
|
||||
它并不存在含糊不清的地方,所以 200 后面的 L 大可省去。然而,对于下述语句:
|
||||
|
||||
```
|
||||
float f4 = 1e-47f; //10 的幂数
|
||||
```java
|
||||
float f4 = 1e-43f; //10 的幂数
|
||||
```
|
||||
|
||||
编译器通常会将指数作为 **double** 类型来处理,所以假若没有这个尾随的 `f`,编辑器就会报错,提示我们应该将 **double** 型转换成 **float** 型。
|
||||
|
||||
编译器通常会将指数作为 **double** 类型来处理,所以假若没有这个后缀字符 `f`,编译器就会报错,提示我们应该将 **double** 型转换成 **float** 型。
|
||||
|
||||
<!-- Bitwise-Operators -->
|
||||
## 按位运算符
|
||||
|
||||
## 位运算符
|
||||
|
||||
|
||||
按位运算符允许我们操作一个整数基本类型中的单个“比特”位,即二进制位。按位运算符会对两个自变量中对应的位执行布尔代数,并最终生成一个结果。
|
||||
位运算符允许我们操作一个整型数字中的单个二进制位。位运算符会对两个整数对应的位执行布尔代数,从而产生结果。
|
||||
|
||||
按位运算来源自 C 语言的底层操作。我们经常要直接操纵硬件,频繁设置硬件寄存器内的二进制位。Java 的设计初衷是电视机顶盒嵌入式开发,所以这种底层的操作被仍被保留了下来。然而,随着操作系统的进步,现今已没必要过于频繁地进行按位运算了。
|
||||
位运算源自 C 语言的底层操作。我们经常要直接操纵硬件,频繁设置硬件寄存器内的二进制位。Java 的设计初衷是电视机顶盒嵌入式开发,所以这种底层的操作仍被保留了下来。但是,你可能不会使用太多位运算。
|
||||
|
||||
若两个输入位都是 1,则按位“与运算符” `&` 在输出位里生成一个 1 ;否则生成 0。若两个输入位里至少有一个是 1,则按位“或运算符” `|` 在输出位里生成一个 1;只有在两个输入位都是 0 的情况下,它才会生成一个 0。若两个输入位的某一个是 1,但不全都是 1,那么按位“异或运算符” `^` 才会在输出位里生成一个 1。按位“非运算符” `~` 属于一元运算符;它只对一个自变量进行操作(其他所有运算符都是二元运算符)。按位非运算生成与输入位的相反的值。例如,若输入 0,则输出 1;输入 1,则输出 0。
|
||||
若两个输入位都是 1,则按位“与运算符” `&` 运算后结果是 1,否则结果是 0。若两个输入位里至少有一个是 1,则按位“或运算符” `|` 运算后结果是 1;只有在两个输入位都是 0 的情况下,运算结果才是 0。若两个输入位的某一个是 1,另一个不是 1,那么按位“异或运算符” `^` 运算后结果才是 1。按位“非运算符” `~` 属于一元运算符;它只对一个自变量进行操作(其他所有运算符都是二元运算符)。按位非运算后结果与输入位相反。例如输入 0,则输出 1;输入 1,则输出 0。
|
||||
|
||||
按位运算符和逻辑运算符都使用了同样的字符,只不过数量不同。“按位”短“移位”长。按位运算符可与等号 `=` 联合使用以接收结果及赋值:`&=`,`|=` 和 `^=` 都是合法的(由于 `~` 是一元运算符,所以不可与 `=` 联合使用)。
|
||||
位运算符和逻辑运算符都使用了同样的字符,只不过数量不同。位短,所以位运算符只有一个字符。位运算符可与等号 `=` 联合使用以接收结果及赋值:`&=`,`|=` 和 `^=` 都是合法的(由于 `~` 是一元运算符,所以不可与 `=` 联合使用)。
|
||||
|
||||
我们将 **Boolean** 类型被视为“单位值”(one-bit value)对待,所以它多少有些独特的地方。我们可以执行按位“与”、“或”、“异或”,但不能执行按位“非”(大概是为了避免与逻辑“非”混淆)。对于布尔值,按位运算符具有与逻辑运算符相同的效果,只是它们不会中途“短路”。此外,针对布尔值进行的按位运算为我们新增了一个“异或”逻辑运算符,它并未包括在“逻辑”运算符的列表中。在移位表达式中,禁止使用布尔运算,原因将在下面解释。
|
||||
我们将 **Boolean** 类型被视为“单位值”(one-bit value),所以它多少有些独特的地方。我们可以对 boolean 型变量执行与、或、异或运算,但不能执行非运算(大概是为了避免与逻辑“非”混淆)。对于布尔值,位运算符具有与逻辑运算符相同的效果,只是它们不会中途“短路”。此外,针对布尔值进行的位运算为我们新增了一个“异或”逻辑运算符,它并未包括在逻辑运算符的列表中。在移位表达式中,禁止使用布尔值,原因将在下面解释。
|
||||
|
||||
|
||||
<!-- Shift Operators -->
|
||||
## 移位运算符
|
||||
|
||||
|
||||
移位运算符面向的运算对象也是二进制的“位”。可单独用它们处理整数类型(基本类型的一种)。左移位运算符 `<<` 能将其左边的运算对象向左移动右侧指定的位数(在低位补 0)。右移位运算符 `>>` 则相反。位移运算符有“正”、“负”值”:若值为正,则在高位插入 0;若值为负,则在高位插入 1。Java 也添加了一种“不分正负”的右移位运算符(>>>),它使用了“零扩展”(zero extension):无论正负,都在高位插入 0。这一运算符是 C/C++ 没有的。
|
||||
移位运算符面向的运算对象也是二进制的“位”。它们只能用于处理整数类型(基本类型的一种)。左移位运算符 `<<` 能将其左边的运算对象向左移动右侧指定的位数(在低位补 0)。右移位运算符 `>>` 则相反。右移位运算符有“正”、“负”值:若值为正,则在高位插入 0;若值为负,则在高位插入 1。Java 也添加了一种“不分正负”的右移位运算符(>>>),它使用了“零扩展”(zero extension):无论正负,都在高位插入 0。这一运算符是 C/C++ 没有的。
|
||||
|
||||
如果移动 **char**、**byte** 或 **short**,则会在移动发生之前将其提升为 **int**,结果为 **int**。仅使用右侧的 5 个低阶位。这可以防止我们移动超过 **int** 范围的位数。若对一个 **long** 值进行处理,最后得到的结果也是 **long**。
|
||||
|
||||
@@ -816,7 +837,7 @@ i >>> 5, int: 97591828, binary:
|
||||
```
|
||||
|
||||
|
||||
结尾的两个方法 `printBinaryInt()` 和 `printBinaryLong()` 它们分别操作一个 **int** 和 **long** 值,并转换为二进制格式输出,同时附有简要的文字说明。除了演示 **int** 和 **long** 的所有位运算符的效果之外,本示例还显示 **int** 和 **long** 的最小值、最大值、+1 和 -1 值,以便我们了解它们的形式。注意高位代表符号:0 表示正,1 表示负。上面显示了 **int** 部分的输出。数字的二进制表示称为有符号的两个补数。
|
||||
结尾的两个方法 `printBinaryInt()` 和 `printBinaryLong()` 分别操作一个 **int** 和 **long** 值,并转换为二进制格式输出,同时附有简要的文字说明。除了演示 **int** 和 **long** 的所有位运算符的效果之外,本示例还显示 **int** 和 **long** 的最小值、最大值、+1 和 -1 值,以便我们了解它们的形式。注意高位代表符号:0 表示正,1 表示负。上面显示了 **int** 部分的输出。数字的二进制表示称为有符号的两个补数。
|
||||
|
||||
|
||||
<!-- Ternary-if-else-Operator -->
|
||||
@@ -841,9 +862,9 @@ static int ternary(int i) {
|
||||
|
||||
static int standardIfElse(int i) {
|
||||
if(i < 10)
|
||||
return i * 100;
|
||||
return i * 100;
|
||||
else
|
||||
return i * 10;
|
||||
return i * 10;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
@@ -901,16 +922,16 @@ x, y, z (summed) = 3
|
||||
0
|
||||
```
|
||||
|
||||
**注意**:上例中第 1 输出语句的执行结果是 `012` 而并非 `3`,这是因为编译器将其分别转换为其字符串形式然后与字符串变量 **s** 连接。在第 2 条输出语句中,编译器将开头的变量转换为了字符串,由此可以看出,这种转换与数据的位置无无关系,只要当中有一条数据是字符串类型,其他非字符串数据都将被转换为字符串形式并连接。最后一条输出语句,我们可以看出 `+=` 运算符可以拼接其右侧的字符串连接结果并重赋值给自身变量 `s`。括号 `()` 可以控制表达式的计算顺序,以便在显示 **int** 之前对其进行实际求和。
|
||||
**注意**:上例中第 1 输出语句的执行结果是 `012` 而并非 `3`,这是因为编译器将其分别转换为其字符串形式然后与字符串变量 **s** 连接。在第 2 条输出语句中,编译器将开头的变量转换为了字符串,由此可以看出,这种转换与数据的位置无关,只要当中有一条数据是字符串类型,其他非字符串数据都将被转换为字符串形式并连接。最后一条输出语句,我们可以看出 `+=` 运算符可以拼接其右侧的字符串连接结果并重赋值给自身变量 `s`。括号 `()` 可以控制表达式的计算顺序,以便在显示 **int** 之前对其进行实际求和。
|
||||
|
||||
请注意主方法中的最后一个例子:我们经常会看到一个空字符串 `""` 跟着一个基本类型的数据。这样可以隐式地将其转换为字符串,以代替繁琐的显式调用方法(如这里可以使用 **Integer.toString()**)。
|
||||
|
||||
|
||||
<!-- Common-Pitfalls-When-Using-Operators -->
|
||||
|
||||
## 常见陷阱
|
||||
|
||||
|
||||
使用运算符时很容易犯的一个错误是,在还没搞清楚表达式的计算方式时就试图忽略括号 `()`。在 Java 中也一样。 在 C++ 中你甚至可能凡这样极端的错误.代码示例:
|
||||
使用运算符时很容易犯的一个错误是,在还没搞清楚表达式的计算方式时就试图忽略括号 `()`。在 Java 中也一样。 在 C++ 中你甚至可能犯这样极端的错误.代码示例:
|
||||
|
||||
```java
|
||||
while(x = y) {
|
||||
@@ -949,7 +970,7 @@ public class Casting {
|
||||
|
||||
诚然,你可以这样地去转换一个数值类型的变量。但是上例这种做法是多余的:因为编译器会在必要时自动提升 **int** 型数据为 **long** 型。
|
||||
|
||||
当然,为了程序逻辑清晰或提醒自己留意,我们也可以显式地类型转换。在其他情况下,类型转换型只有在代码编译时才显出其重要性。在 C/C++ 中,类型转换有时会让人头痛。在 Java 里,类型转换则是一种比较安全的操作。但是,若将数据类型进行“向下转换”(**Narrowing Conversion**)的操作(将容量较大的数据类型转换成容量较小的类型),可能会发生信息丢失的危险。此时,编译器会强迫我们进行造型,好比在提醒我们:该操作可能危险,若您坚持让我这么做,那么对不起,请明确需要转换的类型。 对于“向上转换”(**Widening conversion**),则不必进行显式的类型转换,因为较大类型的数据肯定能容纳较小类型的数据,不会造成任何信息的丢失。
|
||||
当然,为了程序逻辑清晰或提醒自己留意,我们也可以显式地类型转换。在其他情况下,类型转换型只有在代码编译时才显出其重要性。在 C/C++ 中,类型转换有时会让人头痛。在 Java 里,类型转换则是一种比较安全的操作。但是,若将数据类型进行“向下转换”(**Narrowing Conversion**)的操作(将容量较大的数据类型转换成容量较小的类型),可能会发生信息丢失的危险。此时,编译器会强迫我们进行转型,好比在提醒我们:该操作可能危险,若你坚持让我这么做,那么对不起,请明确需要转换的类型。 对于“向上转换”(**Widening conversion**),则不必进行显式的类型转换,因为较大类型的数据肯定能容纳较小类型的数据,不会造成任何信息的丢失。
|
||||
|
||||
除了布尔类型的数据, Java 允许任何基本类型的数据转换为另一种基本类型的数据。此外,类是不能进行类型转换的。为了将一个类转换为另一个类型,需要使用特殊的方法(后面将会学习到如何在父子类之间进行向上/向下转型,例如,“橡树”可以转换为“树”,反之亦然。而对于“岩石”是无法转换为“树”的)。
|
||||
|
||||
@@ -1008,28 +1029,28 @@ public class RoundingNumbers {
|
||||
Math.round(above): 1
|
||||
Math.round(below): 0
|
||||
Math.round(fabove): 1
|
||||
`Math.round(fbelow): 0
|
||||
Math.round(fbelow): 0
|
||||
```
|
||||
|
||||
自从 `round()` 方法成为了 `java.lang` 的一部分,我们就无需通过 `import` 来使用了。
|
||||
因为 `round()` 方法是 `java.lang` 的一部分,所以我们无需通过 `import` 就可以使用。
|
||||
|
||||
<!-- Promotion -->
|
||||
### 类型提升
|
||||
|
||||
|
||||
你会发现,如果我们对小于 **int** 的基元数据类型(即 **char**、**byte** 或 **short**)执行任何算术或按位操作,这些值会在执行操作之前类型提升为 **int**,并且结果值的类型为 **int**。若想重新使用到较小的类型,必须使用强制转换(由于重新分配回一个较小的类型,结果可能会丢失精度)。通常,表达式中最大的数据类型是决定表达式结果的数据类型。**float** 型和 **double** 型相乘,结果是 **double** 型的;**int** 和 **long** 相加,结果是 **long** 型。
|
||||
你会发现,如果我们对小于 **int** 的基本数据类型(即 **char**、**byte** 或 **short**)执行任何算术或按位操作,这些值会在执行操作之前类型提升为 **int**,并且结果值的类型为 **int**。若想重新使用较小的类型,必须使用强制转换(由于重新分配回一个较小的类型,结果可能会丢失精度)。通常,表达式中最大的数据类型是决定表达式结果的数据类型。**float** 型和 **double** 型相乘,结果是 **double** 型的;**int** 和 **long** 相加,结果是 **long** 型。
|
||||
|
||||
|
||||
<!-- Java-Has-No-sizeof -->
|
||||
## Java没有sizeof
|
||||
|
||||
|
||||
在 C/C++ 中,经常 需要用到 `sizeof()` 方法来获取数据被分配的字符大小。在 C/C++ 中,`sizeof() `最常见的应用就是“移植”。不同数据在不同机器上可能有不同的大小,所以在进行大小敏感的运算时,程序员必须对这些类型有多大做到心中有数。例如,一台计算机可用 32 位来保存整数,而另一台只用 16 位保存。显然,在第一台机器中,程序可保存更大的值。所以,移植是令 C/C++ 程序员颇为头痛的一个问题。
|
||||
|
||||
Java 不需要` sizeof()` 方法来满足这种需求,因为所有类型的默认大小在不同平台间是相同的。我们不必考虑这个层次的移植问题 —— Java 本身就是一种“与平台无关”的语言。
|
||||
在 C/C++ 中,经常需要用到 `sizeof()` 方法来获取数据项被分配的字节大小。C/C++ 中使用 `sizeof()` 最有说服力的原因是为了移植性,不同数据在不同机器上可能有不同的大小,所以在进行大小敏感的运算时,程序员必须对这些类型有多大做到心中有数。例如,一台计算机可用 32 位来保存整数,而另一台只用 16 位保存。显然,在第一台机器中,程序可保存更大的值。所以,移植是令 C/C++ 程序员颇为头痛的一个问题。
|
||||
|
||||
Java 不需要 ` sizeof()` 方法来满足这种需求,因为所有类型的大小在不同平台上是相同的。我们不必考虑这个层次的移植问题 —— Java 本身就是一种“与平台无关”的语言。
|
||||
|
||||
<!-- A-Compendium-of-Operators -->
|
||||
|
||||
## 运算符总结
|
||||
|
||||
上述示例分别向我们展示了哪些基本类型能被用于特定的运算符。基本上,下面的代码示例是对上述所有示例的重复,只不过概括了所有的基本类型。这个文件能被正确地编译,因为我已经把编译不通过的那部分用注释 `//` 过滤了。代码示例:
|
||||
@@ -1453,7 +1474,7 @@ public class AllOps {
|
||||
|
||||
**注意** :**boolean** 类型的的运算是受限的。你能为其赋值 `true` 或 `false`,也可测试它的值是否是 `true` 或 `false`。但你不能对其作加减等其他运算。
|
||||
|
||||
在 **char** , **byte** 和 **short** 类型中,我们可以看到算术运算符的“类型转换”效果。我们必须要显式强制类型转换才能将结果重新赋值为原始类型。对于 **int** 类型的运算则不用转换,因为默认就是 **int** 型。虽然我们不用再停下来思考这一切是否安全,但是两个大的 int 型相乘时,结果有可能超出 **int** 型的范围,这种情况下结果会发生溢出。下面的代码示例:
|
||||
在 **char**,**byte** 和 **short** 类型中,我们可以看到算术运算符的“类型转换”效果。我们必须要显式强制类型转换才能将结果重新赋值为原始类型。对于 **int** 类型的运算则不用转换,因为默认就是 **int** 型。虽然我们不用再停下来思考这一切是否安全,但是两个大的 int 型整数相乘时,结果有可能超出 **int** 型的范围,这种情况下结果会发生溢出。下面的代码示例:
|
||||
|
||||
```java
|
||||
// operators/Overflow.java
|
||||
@@ -1477,7 +1498,7 @@ bigger = -4
|
||||
|
||||
编译器没有报错或警告,运行时一切看起来都无异常。诚然,Java 是优秀的,但是还不足够优秀。
|
||||
|
||||
对于 **char**,**byte** 或者 **short**,混合赋值并不需要类型转换。即使为它们执行转型操作,也会获得与直接算术运算相同的结果。另外,省略类型转换可以使代码显得更加简练。总之,除 **boolean** 以外,其他任何两种基本类型间都可进行类型转换。当我们进行向下转换类型时,需要注意结果的范围是否溢出,否则我们就很可能在不知不觉种丢失精度。
|
||||
对于 **char**,**byte** 或者 **short**,混合赋值并不需要类型转换。即使为它们执行转型操作,也会获得与直接算术运算相同的结果。另外,省略类型转换可以使代码显得更加简练。总之,除 **boolean** 以外,其他任何两种基本类型间都可进行类型转换。当我们进行向下转换类型时,需要注意结果的范围是否溢出,否则我们就很可能在不知不觉中丢失精度。
|
||||
|
||||
|
||||
<!-- Summary -->
|
||||
@@ -1487,11 +1508,7 @@ bigger = -4
|
||||
|
||||
[^1]: 我在 *Pomona College* 大学读过两年本科,在那里 47 被称之为“魔法数字”(*magic number*),详见 [维基百科](https://en.wikipedia.org/wiki/47_(number)) 。
|
||||
|
||||
|
||||
|
||||
[^2]: *John Kirkham* 说过, “自 1960 年我开始在 IBM 1620 上开始编程起,至 1970 年之间,FORTRAN 一直都是一种全大写的编程语言。这可能是因为许多早期的输入设备都是旧的电传打字机,使用了5 位波特码,没有小写字母的功能。
|
||||
|
||||
指数符号中的 e 也总是大写的,并且从未与自然对数底数 e 混淆,自然对数底数 e 总是小写的。 e 简单地代表指数,通常 10 是基数。那时,八进制也被程序员广泛使用。虽然我从未见过它的用法,但如果我看到一个指数符号的八进制数,我会认为它是以 8 为基数的。我记得第一次看到指数使用小写字母 e 是在 20 世纪 70 年代末,我也发现它令人困惑。这个问题出现的时候,小写字母悄悄进入了 Fortran。如果你真的想使用自然对数底,我们实际上有一些函数要使用,但是它们都是大写的。
|
||||
[^2]: *John Kirkham* 说过:“自 1960 年我开始在 IBM 1620 上开始编程起,至 1970 年之间,FORTRAN 一直都是一种全大写的编程语言。这可能是因为许多早期的输入设备都是旧的电传打字机,使用了5 位波特码,没有小写字母的功能。指数符号中的 e 也总是大写的,并且从未与自然对数底数 e 混淆,自然对数底数 e 总是小写的。 e 简单地代表指数,通常 10 是基数。那时,八进制也被程序员广泛使用。虽然我从未见过它的用法,但如果我看到一个指数符号的八进制数,我会认为它是以 8 为基数的。我记得第一次看到指数使用小写字母 e 是在 20 世纪 70 年代末,我也发现它令人困惑。这个问题出现的时候,小写字母悄悄进入了 Fortran。如果你真的想使用自然对数底,我们实际上有一些函数要使用,但是它们都是大写的。”
|
||||
|
||||
|
||||
<!-- 分页 -->
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
# 第五章 控制流
|
||||
|
||||
> 程序必须在执行过程中控制它的世界并做出选择。 在 Java 中,你需要使用执行控制语句来做出选择。
|
||||
> 程序必须在执行过程中控制它的世界并做出选择。 在 Java 中,你需要执行控制语句来做出选择。
|
||||
|
||||
Java 使用了 C 的所有执行控制语句,因此对于熟悉 C/C++ 编程的人来说,这部分内容轻车熟路。 大多数面向过程编程语言都有共通的某种控制语句。在 Java 中,涉及的关键字包括 **if-else,while,do-while,for,return,break** 和选择语句 **switch**。 Java 并不支持备受诟病的 **goto**(尽管它在某些特殊场景中依然是最行之有效的方法)。 尽管如此,在 Java 中我们仍旧可以进行类似的逻辑跳转,但较之典型的 **goto** 用法限制更多。
|
||||
Java 使用了 C 的所有执行控制语句,因此对于熟悉 C/C++ 编程的人来说,这部分内容轻车熟路。大多数面向过程编程语言都有共通的某种控制语句。在 Java 中,涉及的关键字包括 **if-else,while,do-while,for,return,break** 和选择语句 **switch**。 Java 并不支持备受诟病的 **goto**(尽管它在某些特殊场景中依然是最行之有效的方法)。 尽管如此,在 Java 中我们仍旧可以进行类似的逻辑跳转,但较之典型的 **goto** 用法限制更多。
|
||||
|
||||
|
||||
## true和flase
|
||||
@@ -48,7 +48,7 @@ else
|
||||
“statement”
|
||||
```
|
||||
|
||||
布尔表达式(Boolean-expression)必须生成 **boolean** 类型的结果,执行语句 `statement` 既可以是只包含单个分号 `;` 的简单语句,也可以是包含大括号 `{}` 的复合语句 —— 封闭在大括号内的一组简单语句。 凡本书中提及的“statement”一词,皆表示类似的执行语句。
|
||||
布尔表达式(Boolean-expression)必须生成 **boolean** 类型的结果,执行语句 `statement` 既可以是以分号 `;` 结尾的一条简单语句,也可以是包含在大括号 `{}` 内的的复合语句 —— 封闭在大括号内的一组简单语句。 凡本书中提及“statement”一词,皆表示类似的执行语句。
|
||||
|
||||
下面是一个有关 **if-else** 语句的例子。`test()` 方法可以告知你两个数值之间的大小关系。代码示例:
|
||||
|
||||
@@ -86,12 +86,12 @@ public class IfElse {
|
||||
|
||||
<sub>**注解**:`else if` 并非新关键字,它仅是 `else` 后紧跟的一条新 `if` 语句。</sub>
|
||||
|
||||
Java 和 C/C++ 同属“自由格式”的编程语言,但通常我们会在 Java 控制流程语句中采用首尾缩进的规范,以便代码更具可读性。
|
||||
Java 和 C/C++ 同属“自由格式”的编程语言,但通常我们会在 Java 控制流程语句中采用首部缩进的规范,以便代码更具可读性。
|
||||
|
||||
<!--Iteration Statements-->
|
||||
## 迭代语句
|
||||
|
||||
**while**,**do-while** 和 **for** 用来控制循环语句(有时也称迭代语句)。 只有控制循环的布尔表达式计算结果为 `false`,循环语句才会停止。
|
||||
**while**,**do-while** 和 **for** 用来控制循环语句(有时也称迭代语句)。只有控制循环的布尔表达式计算结果为 `false`,循环语句才会停止。
|
||||
|
||||
|
||||
### while
|
||||
@@ -138,12 +138,9 @@ true, Inside 'while'
|
||||
false, Exited 'while'
|
||||
```
|
||||
|
||||
|
||||
`condition()` 方法使用到了 **Math** 库的**静态**方法 `random()`。该方法的作用是产生 0 和 1 之间 (包括 0,但不包括 1) 的一个 **double** 值。
|
||||
|
||||
`result` 的值是通过判断
|
||||
|
||||
**result** 的值是通过比较运算符 `<` 来产生的 **boolean** 类型的结果。当控制台输出 **boolean** 型值时,会自动将其转换未对应的文字形式 `true` 或 `false`。此处 `while` 条件表达式代表:“仅在 `condition()` 返回 `false` 时停止循环”。
|
||||
**result** 的值是通过比较运算符 `<` 产生的 **boolean** 类型的结果。当控制台输出 **boolean** 型值时,会自动将其转换为对应的文字形式 `true` 或 `false`。此处 `while` 条件表达式代表:“仅在 `condition()` 返回 `false` 时停止循环”。
|
||||
|
||||
|
||||
### do-while
|
||||
@@ -206,12 +203,13 @@ value: 106 character: j
|
||||
|
||||
传统的面向过程语言如 C 需要先在代码块(block)前定义好所有变量才能够使用。这样编译器才能在创建块时,为这些变量分配内存空间。在 Java 和 C++ 中,我们可以在整个块使用变量声明,并且可以在需要时才定义变量。 这种自然的编码风格使我们的代码更容易被人理解 [^1]。
|
||||
|
||||
上例使用了 **java.lang.Character** 包装类,该类不仅包含了基本类型 `char` 的值,还封装了一些有用的方法。例如这里就用到了静态方法 `isLowerCase()` 来判断字符否为小写。
|
||||
上例使用了 **java.lang.Character** 包装类,该类不仅包含了基本类型 `char` 的值,还封装了一些有用的方法。例如这里就用到了静态方法 `isLowerCase()` 来判断字符是否为小写。
|
||||
|
||||
<!--The Comma Operator-->
|
||||
|
||||
#### 逗号操作符
|
||||
|
||||
在 Java 中逗号运算符(这里并非指我们平常用于分隔定义和方法参数的逗号分隔符)仅有一种用法:在 **for** 循环的初始化和步进控制中定义多个变量。我们可以使用逗号分隔多个语句,并按顺序计算这些语句。**注意**:哟啊求定义的变量类型相同。代码示例:
|
||||
在 Java 中逗号运算符(这里并非指我们平常用于分隔定义和方法参数的逗号分隔符)仅有一种用法:在 **for** 循环的初始化和步进控制中定义多个变量。我们可以使用逗号分隔多个语句,并按顺序计算这些语句。**注意**:要求定义的变量类型相同。代码示例:
|
||||
|
||||
```java
|
||||
// control/CommaOperator.java
|
||||
@@ -239,7 +237,7 @@ i = 4 j = 8
|
||||
|
||||
## for-in语法
|
||||
|
||||
Java 5 引入了更为简洁的“增强版 **for** 循环”语法来操纵数组和集合。(更多细节,可参考 [数组](./21-Arrays.md) 和 [集合](./12-Collections.md) 章节内容)。,大部分文档也称其为 **for-each** 语法,但因为了不与 Java 8 新添的 `forEach()` 产生混淆,因此我称之为 **for-in** 循环。 (Python 已有类似的先例,如:**for x in sequence**)。**注意**:你可能会在其他地方看到不同叫法。
|
||||
Java 5 引入了更为简洁的“增强版 **for** 循环”语法来操纵数组和集合。(更多细节,可参考 [数组](./21-Arrays.md) 和 [集合](./12-Collections.md) 章节内容)。大部分文档也称其为 **for-each** 语法,但因为了不与 Java 8 新添的 `forEach()` 产生混淆,因此我称之为 **for-in** 循环。 (Python 已有类似的先例,如:**for x in sequence**)。**注意**:你可能会在其他地方看到不同叫法。
|
||||
|
||||
**for-in** 无需你去创建 **int** 变量和步进来控制循环计数。 下面我们来遍历获取 **float** 数组中的元素。代码示例:
|
||||
|
||||
@@ -290,7 +288,7 @@ for(float x : f) {
|
||||
|
||||
public class ForInString {
|
||||
public static void main(String[] args) {
|
||||
for(char c : "An African Swallow".toCharArray())
|
||||
for(char c: "An African Swallow".toCharArray())
|
||||
System.out.print(c + " ");
|
||||
}
|
||||
}
|
||||
@@ -302,10 +300,9 @@ public class ForInString {
|
||||
A n A f r i c a n S w a l l o w
|
||||
```
|
||||
|
||||
很快我们能在 [集合](./12-Collections.md) 章节里学习到,
|
||||
**for-in** 循环适用于任何可迭代(*iterable*)的 对象。
|
||||
很快我们能在 [集合](./12-Collections.md) 章节里学习到,**for-in** 循环适用于任何可迭代(*iterable*)的 对象。
|
||||
|
||||
通常, **for** 循环语句都会在一个整型数值序列中步进。代码示例:
|
||||
通常,**for** 循环语句都会在一个整型数值序列中步进。代码示例:
|
||||
|
||||
```java
|
||||
for(int i = 0; i < 100; i++)
|
||||
@@ -324,7 +321,7 @@ public class ForInInt {
|
||||
public static void main(String[] args) {
|
||||
for(int i : range(10)) // 0..9
|
||||
System.out.print(i + " ");
|
||||
System.out.println();
|
||||
System.out.println();
|
||||
for(int i : range(5, 10)) // 5..9
|
||||
System.out.print(i + " ");
|
||||
System.out.println();
|
||||
@@ -346,7 +343,6 @@ public class ForInInt {
|
||||
20 17 14 11 8
|
||||
```
|
||||
|
||||
|
||||
`range()` 方法已被 [重载](./06-Housekeeping.md#方法重载)(重载:同名方法,参数列表或类型不同)。上例中 `range()` 方法有多种重载形式:第一种产生从 0 至范围上限(不包含)的值;第二种产生参数一至参数二(不包含)范围内的整数值;第三种形式有一个步进值,因此它每次的增量为该值;第四种 `range()` 表明还可以递减。`range()` 无参方法是该生成器最简单的版本。有关内容会在本书稍后介绍。
|
||||
|
||||
`range()` 的使用提高了代码可读性,让 **for-in** 循环在本书中适应更多的代码示例场景。
|
||||
@@ -359,7 +355,7 @@ public class ForInInt {
|
||||
|
||||
在 Java 中有几个关键字代表无条件分支,这意味无需任何测试即可发生。这些关键字包括 **return**,**break**,**continue** 和跳转到带标签语句的方法,类似于其他语言中的 **goto**。
|
||||
|
||||
**return** 关键字有两方面的作用:1.指定一个方法返回值(在方法返回类型非 **void** 的情况下);2.退出当前方法,并返回作用 1 中值。我们可以利用 `return` 的这些特点来改写上例 `IfElse.java` 文件朱姑中的 `test()` 方法。代码示例:
|
||||
**return** 关键字有两方面的作用:1.指定一个方法返回值(在方法返回类型非 **void** 的情况下);2.退出当前方法,并返回作用 1 中值。我们可以利用 `return` 的这些特点来改写上例 `IfElse.java` 文件中的 `test()` 方法。代码示例:
|
||||
|
||||
```java
|
||||
// control/TestWithReturn.java
|
||||
@@ -395,9 +391,9 @@ public class TestWithReturn {
|
||||
|
||||
## break和continue
|
||||
|
||||
在任何迭代语句的主体内,都可以使用 **break** 和 **continue** 来控制循环的流程。 其中,**break** 表示跳出当前循环体。 而 **continue** 表示停止本次循环,开始下一次循环。
|
||||
在任何迭代语句的主体内,都可以使用 **break** 和 **continue** 来控制循环的流程。 其中,**break** 表示跳出当前循环体。而 **continue** 表示停止本次循环,开始下一次循环。
|
||||
|
||||
下例向大家展示 **break** 和 **continue** 在 **for**、**while** 循环中的使运用。代码示例:
|
||||
下例向大家展示 **break** 和 **continue** 在 **for**、**while** 循环中的使用。代码示例:
|
||||
|
||||
```java
|
||||
// control/BreakAndContinue.java
|
||||
@@ -443,10 +439,7 @@ public class BreakAndContinue {
|
||||
|
||||
<sub>**[1]** 在这个 **for** 循环中,`i` 的值永远不会达到 100,因为一旦 `i` 等于 74,**break** 语句就会中断循环。通常,只有在不知道中断条件何时满足时,才需要 **break**。因为 `i` 不能被 9 整除,**continue** 语句就会使循环从头开始。这使 **i** 递增)。如果能够整除,则将值显示出来。</sub>
|
||||
<sub>**[2]** 使用 **for-in** 语法,结果相同。</sub>
|
||||
<sub>**[3]** 无限 **while** 循环。循环内的 **break** 语句可中止循环。**注意**,**continue** 语句可将控制权移回循环的顶部,而不会执行 **continue** 之后的任何操作。 因此,只有当 `i` 的值可被 10 整除时才会输出。在输出中,显示值 0,因为 `0%9` 产生 0。</sub>
|
||||
|
||||
|
||||
还有一种无限循环的形式: `for(;;)`。 在编译器看来,它与 `while(true)` 无异,使用哪种完全取决于你的编程品味。
|
||||
<sub>**[3]** 无限 **while** 循环。循环内的 **break** 语句可中止循环。**注意**,**continue** 语句可将控制权移回循环的顶部,而不会执行 **continue** 之后的任何操作。 因此,只有当 `i` 的值可被 10 整除时才会输出。在输出中,显示值 0,因为 `0%9` 产生 0。还有一种无限循环的形式: `for(;;)`。 在编译器看来,它与 `while(true)` 无异,使用哪种完全取决于你的编程品味。</sub>
|
||||
|
||||
<!--The Infamous “Goto”-->
|
||||
## 臭名昭著的goto
|
||||
@@ -457,7 +450,7 @@ public class BreakAndContinue {
|
||||
|
||||
正如上述提及的经典情况,我们不应走向两个极端。问题不在 **goto**,而在于过度使用 **goto**。在极少数情况下,**goto** 实际上是控制流程的最佳方式。
|
||||
|
||||
尽管 **goto** 仍是 Java 的一个保留字,但其并未被到正式启用。可以说, Java 中并不支持 **goto**。然而,在 **break** 和 **continue** 这两个关键字的身上,我们仍能看出一些 **goto** 的影子。它们并不属于一次跳转,而是中断循环语句的一种方法。之所以把它们纳入 **goto** 问题中一起讨论,是由于它们使用了相同的机制:标签。
|
||||
尽管 **goto** 仍是 Java 的一个保留字,但其并未被正式启用。可以说, Java 中并不支持 **goto**。然而,在 **break** 和 **continue** 这两个关键字的身上,我们仍能看出一些 **goto** 的影子。它们并不属于一次跳转,而是中断循环语句的一种方法。之所以把它们纳入 **goto** 问题中一起讨论,是由于它们使用了相同的机制:标签。
|
||||
|
||||
“标签”是后面跟一个冒号的标识符。代码示例:
|
||||
|
||||
@@ -708,7 +701,7 @@ z, 122: consonant
|
||||
...
|
||||
```
|
||||
|
||||
由于 `Random.nextInt(26)` 会产生 0 到 26 之间的一个值,所以在其上加上一个偏移量 `a`,即可产生小写字母。在 **case** 语句中,使用单引号引起的字符也会产生用于比较的整数值。
|
||||
由于 `Random.nextInt(26)` 会产生 0 到 25 之间的一个值,所以在其上加上一个偏移量 `a`,即可产生小写字母。在 **case** 语句中,使用单引号引起的字符也会产生用于比较的整数值。
|
||||
|
||||
请注意 **case** 语句能够堆叠在一起,为一段代码形成多重匹配,即只要符合多种条件中的一种,就执行那段特别的代码。这时也应该注意将 **break** 语句置于特定 **case** 的末尾,否则控制流程会继续往下执行,处理后面的 **case**。在下面的语句中:
|
||||
|
||||
@@ -718,8 +711,8 @@ int c = rand.nextInt(26) + 'a';
|
||||
|
||||
此处 `Random.nextInt()` 将产生 0~25 之间的一个随机 **int** 值,它将被加到 `a` 上。这表示 `a` 将自动被转换为 **int** 以执行加法。为了把 `c` 当作字符打印,必须将其转型为 **char**;否则,将会输出整数。
|
||||
|
||||
|
||||
<!-- Switching on Strings -->
|
||||
|
||||
## switch字符串
|
||||
|
||||
Java 7 增加了在字符串上 **switch** 的用法。 下例展示了从一组 **String** 中选择可能值的传统方法,以及新式方法:
|
||||
@@ -830,4 +823,5 @@ java RandomBounds upper
|
||||
[^2]: **注意**,此处观点似乎难以让人信服,很可能只是一个因认知偏差而造成的[因果关系谬误](https://en.wikipedia.org/wiki/Correlation_does_not_imply_causation)的例子。</sup>
|
||||
|
||||
<!-- 分页 -->
|
||||
|
||||
<div style="page-break-after: always;"></div>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,27 +3,690 @@
|
||||
<!-- Implementation Hiding -->
|
||||
# 第七章 封装
|
||||
|
||||
访问控制(或者隐藏实现)与"最初的实现不恰当"有关。
|
||||
|
||||
所有优秀的作者——包括这些编写软件的人——都知道一件好的作品都是经过反复打磨才变得优秀的。如果你把一段代码置于某个位置一段时间,过一会重新来看,你可能发现更好的实现方式。这是重构的原动力之一,重构就是重写可工作的代码,使之更加可读,易懂,因而更易维护。
|
||||
|
||||
但是,在修改和完善代码的愿望下,也存在巨大的压力。通常,客户端程序员希望你的代码在某些方面保持不变。所以你想修改代码,但他们希望代码保持不变。由此引出了面向对象设计中的一个基本问题:"如何区分变动的事物和不变的事物"。
|
||||
|
||||
这个问题对于类库而言尤其重要。类库的使用者必须依赖他们所使用的那部分类库,并且知道如果使用了类库的新版本,不需要改写代码。另一方面,类库的开发者必须有修改和改进类库的自由,并保证客户代码不会受这些改动影响。
|
||||
|
||||
这可以通过约定解决。例如,类库开发者必须同意在修改类库中的一个类时,不会移除已有的方法,因为那样将会破坏客户端程序员的代码。与之相反的情况更加复杂。在有成员属性的情况下,类库开发者如何知道哪些属性被客户端程序员使用?这同样会发生在那些只为实现类库类而创建的方法上,它们也不是设计成可供客户端程序员调用的。如果类库开发者想删除旧的实现,添加新的实现,结果会怎样呢?任何这些成员的改动都可能破环客户端程序员的代码。因此类库开发者会被束缚,不能修改任何事物。
|
||||
|
||||
为了解决这一问题,Java 提供了访问修饰符供类库开发者指明哪些对于客户端程序员是可用的,哪些是不可用的。访问控制权限的等级,从"最大权限"到"最小权限"依次是:**public**,**protected**,包访问权限(没有关键字)和 **private**。根据上一段的内容,你可能会想,作为一名类库设计者,你会尽可能将一切都设为 **private**,仅向客户端程序员暴露你愿意他们使用的方法。这就是你通常所做的,尽管这与使用其他语言(尤其是 C)编程和访问不受任何限制的人们的直觉相违背。
|
||||
|
||||
然而,构建类库的概念和对类库组件的访问控制仍然不完善。其中仍然存在问题就是如何将类库组件捆绑到一个内聚到类库单元中。Java 中通过 package 关键字加以控制,类是在相同包下还是不同包下会影响访问修饰符。所以在这章开始,你将会学习如何将类库组件置于同一个包下,之后你就能明白访问修饰符的全部含义。
|
||||
|
||||
<!-- package: the Library Unit -->
|
||||
|
||||
## 包的概念
|
||||
|
||||
包内包含一组类,它们被组织在一个单独的命名空间下。
|
||||
|
||||
例如,标准 Java 发布中有一个工具库,它被组织在 **java.util** 命名空间下。**java.util** 中含有一个类,叫做 **ArrayList**。使用 **ArrayList** 的一种方式是用其全名 **java.util.ArrayList**。
|
||||
|
||||
```java
|
||||
// hiding/FullQualification.java
|
||||
|
||||
public class FullQualification {
|
||||
public static void main(String[] args) {
|
||||
java.util.ArrayList list = new java.util.ArrayList();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这种方式使得程序冗长乏味,因此你可以换一种方式,使用 **import** 关键字。如果需要导入某个类,就需要在 **import** 语句中声明:
|
||||
|
||||
```java
|
||||
// hiding/SingleImport.java
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class SingleImport {
|
||||
public static void main(String[] args) {
|
||||
ArrayList list = new ArrayList();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
现在你就可以不加限定词,直接使用 **ArrayList** 了。但是对于 **java.util** 包下的其他类,你还是不能用。要导入其中所有的类,只需使用 ***** ,就像本书中其他示例那样:
|
||||
```java
|
||||
import java.util.*
|
||||
```
|
||||
|
||||
之所以使用导入,是为了提供一种管理命名空间的机制。所有类名之间都是相互隔离的。类 **A** 中的方法 `f()` 不会与类 **B** 中具有相同签名的方法 `f()` 冲突。但是如果类名冲突呢?假设你创建了一个 **Stack** 类,打算安装在一台已经有别人所写的 **Stack** 类的机器上,该怎么办呢?这种类名的潜在冲突,正是我们需要在 Java 中对命名空间进行完全控制的原因。为了解决冲突,我们为每个类创建一个唯一标识符组合。
|
||||
|
||||
到目前为止的大部分示例都只存在单个文件,并为本地使用的,所以尚未收到包名的干扰。但是,这些示例其实已经位于包中了,叫做"未命名"包或默认包。这当然是一种选择,为了简单起见,本书其余部分会尽可能采用这种方式。但是,如果你打算为相同机器上的其他 Java 程序创建友好的类库或程序时,就必须仔细考虑以防类名冲突。
|
||||
|
||||
一个 Java 源代码文件称为一个*编译单元*(有时也称*翻译单元*)。每个编译单元的文件名后缀必须是 **.java**。在编译单元中可以有一个 **public** 类,它的类名必须与文件名相同(包括大小写,但不包括后缀名 **.java**)。每个编译单元中只能有一个 **public** 类,否则编译器不接受。如果这个编译单元中还有其他类,那么在包之外是无法访问到这些类的,因为它们不是 **public** 类,此时它们支持主 **public** 类。
|
||||
|
||||
### 代码组织
|
||||
|
||||
当编译一个 **.java** 文件时,**.java** 文件的每个类都会有一个输出文件。每个输出的文件名和 **.java** 文件中每个类的类名相同,只是后缀名是 **.class**。因此,在编译少量的 **.java** 文件后,会得到大量的 **.class** 文件。如果你使用过编译型语言,那么你可能习惯编译后产生一个中间文件(通常称为"obj"文件),然后与使用链接器(创建可执行文件)或类库生成器(创建类库)产生的其他同类文件打包到一起的情况。这不是 Java 工作的方式。在 Java 中,可运行程序是一组 **.class** 文件,它们可以打包压缩成一个 Java 文档文件(JAR,使用 **jar** 文档生成器)。Java 解释器负责查找、加载和解释这些文件。
|
||||
|
||||
类库是一组类文件。每个源文件通常都含有一个 **public** 类和任意数量的非 **public** 类,因此每个文件都有一个构件。如果把这些组件集中在一起,就需要使用关键字 **package**。
|
||||
|
||||
如果你使用了 **package** 语句,它必须是文件中除了注释之外的第一行代码。当你如下这样写:
|
||||
|
||||
```java
|
||||
package hiding;
|
||||
```
|
||||
|
||||
意味着这个编译单元是一个名为 **hiding** 类库的一部分。换句话说,你正在声明的编译单元中的 **public** 类名称位于名为 **hiding** 的保护伞下。任何人想要使用该名称,必须指明完整的类名或者使用 **import** 关键字导入 **hiding**。(注意,Java 包名按惯例一律小写,即使中间的单词也需要小写,与驼峰命名不同)
|
||||
|
||||
例如,假设文件名是 **MyClass.java**,这意味着文件中只能有一个 **public** 类,且类名必须是 MyClass(大小写也与文件名相同):
|
||||
|
||||
```java
|
||||
// hiding/mypackage/MyClass.java
|
||||
package hiding.mypackage
|
||||
|
||||
public class MyClass {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
现在,如果有人想使用 **MyClass** 或 **hiding.mypackage** 中的其他 **public** 类,就必须使用关键字 **import** 来使 **hiding.mypackage** 中的名称可用。还有一种选择是使用完整的名称:
|
||||
|
||||
```java
|
||||
// hiding/QualifiedMyClass.java
|
||||
|
||||
public class QualifiedMyClass {
|
||||
public static void main(String[] args) {
|
||||
hiding.mypackage.MyClass m = new hiding.mypackage.MyClass();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
关键字 **import** 使之更简洁:
|
||||
|
||||
```java
|
||||
// hiding/ImportedMyClass.java
|
||||
import hiding.mypackage.*;
|
||||
|
||||
public class ImportedMyClass {
|
||||
public static void main(String[] args) {
|
||||
MyClass m = new MyClass();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**package** 和 **import** 这两个关键字将单一的全局命名空间分隔开,从而避免名称冲突。
|
||||
|
||||
### 创建独一无二的包名
|
||||
|
||||
你可能注意到,一个包从未真正被打包成单一的文件,它可以由很多 **.class** 文件构成,因而事情就变得有点复杂了。为了避免这种情况,一种合乎逻辑的做法是将特定包下的所有 **.class** 文件都放在一个目录下。也就是说,利用操作系统的文件结构的层次性。这是 Java 解决解决混乱问题的一种方式;稍后你还会在我们介绍 **jar** 工具时看到另一种方式。
|
||||
|
||||
将所有的文件放在一个子目录还解决了其他的两个问题:创建独一无二的包名和查找可能隐藏于目录结构某处的类。这是通过将 **.class** 文件所在的路径位置编码成 **package** 名称来实现的。按照惯例,**package** 名称是类的创建者的反顺序的 Internet 域名。如果你遵循惯例,因为 Internet 域名是独一无二的,所以你的 **package** 名称也应该是独一无二的,不会发生名称冲突。如果你没有自己的域名,你就得构造一组不大可能与他人重复的组合(比如你的姓名),来创建独一无二的 package 名称。如果你打算发布 Java 程序代码,那么花些力气去获取一个域名是值得的。
|
||||
|
||||
此技巧的第二部分是把 **package** 名称分解成你机器上的一个目录,所以当 Java 解释器必须要加载一个 .class 文件时,它能定位到 **.class** 文件所在的位置。首先,它找出环境变量 **CLASSPATH**(通过操作系统设置,有时也能通过 Java 的安装程序或基于 Java 的工具设置)。**CLASSPATH** 包含一个或多个目录,用作查找 .**class** 文件的根目录。从根目录开始,Java 解释器获取包名并将每个句点替换成反斜杠,生成一个基于根目录的路径名(包名 foo.bar.baz 变成 foo\bar\baz 或 foo/bar/baz 或其它,取决于你的操作系统)。然后这个路径与 **CLASSPATH** 的不同项连接,解释器就在这些目录中查找与你所创建的类名称相关的 **.class** 文件(解释器还会查找某些涉及 Java 解释器所在位置的标准目录)。
|
||||
|
||||
为了理解这点,比如说我的域名 **MindviewInc.com**,将之反转并全部改为小写后就是 **com.mindviewinc**,这将作为我创建的类的独一无二的全局名称。(com、edu、org等扩展名之前在 Java 包中都是大写,但是 Java 2 之后都统一用小写。)我决定再创建一个名为 **simple** 的类库,从而细分名称:
|
||||
|
||||
```java
|
||||
package com.mindviewinc.simple;
|
||||
```
|
||||
|
||||
这个包名可以用作下面两个文件的命名空间保护伞:
|
||||
|
||||
```java
|
||||
// com/mindviewinc/simple/Vector.java
|
||||
// Creating a package
|
||||
package com.mindviewinc.simple;
|
||||
|
||||
public class Vector {
|
||||
public Vector() {
|
||||
System.out.println("com.mindviewinc.simple.Vector");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
如前所述,**package** 语句必须是文件的第一行非注释代码。第二个文件看上去差不多:
|
||||
|
||||
```java
|
||||
// com/mindviewinc/simple/List.java
|
||||
// Creating a package
|
||||
package com.mindviewinc.simple;
|
||||
|
||||
public class List {
|
||||
System.out.println("com.mindview.simple.List");
|
||||
}
|
||||
```
|
||||
|
||||
这两个文件都位于我机器上的子目录中,如下:
|
||||
|
||||
```
|
||||
C:\DOC\Java\com\mindviewinc\simple
|
||||
```
|
||||
|
||||
(注意,本书的每个文件的第一行注释都指明了文件在源代码目录树中的位置——供本书的自动代码提取工具使用。)
|
||||
|
||||
如果你回头看这个路径,会看到包名 **com.mindviewinc.simple**,但是路径的第一部分呢?CLASSPATH 环境变量会处理它。我机器上的环境变量部分如下:
|
||||
|
||||
```
|
||||
CLASSPATH=.;D:\JAVA\LIB;C:\DOC\Java
|
||||
```
|
||||
|
||||
CLASSPATH 可以包含多个不同的搜索路径。
|
||||
|
||||
但是在使用 JAR 文件时,有点不一样。你必须在类路径写清楚 JAR 文件的实际名称,不能仅仅是 JAR 文件所在的目录。因此,对于一个名为 **grape.jar** 的 JAR 文件,类路径应包括:
|
||||
|
||||
```
|
||||
CLASSPATH=.;D\JAVA\LIB;C:\flavors\grape.jar
|
||||
```
|
||||
|
||||
一旦设置好类路径,下面的文件就可以放在任意目录:
|
||||
|
||||
```java
|
||||
// hiding/LibTest.java
|
||||
// Uses the library
|
||||
import com.mindviewinc.simple.*;
|
||||
|
||||
public class LibTest {
|
||||
public static void main(String[] args) {
|
||||
Vector v = new Vector();
|
||||
List l = new List();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
输出:
|
||||
|
||||
```
|
||||
com.mindviewinc.simple.Vector
|
||||
com.mindviewinc.simple.List
|
||||
```
|
||||
|
||||
当编译器遇到导入 **simple** 库的 **import** 语句时,它首先会在 CLASSPATH 指定的目录中查找子目录 **com/mindviewinc/simple**,然后从已编译的文件中找出名称相符者(对 **Vector** 而言是 **Vector.class**,对 **List** 而言是 **List.class**)。注意,这两个类和其中要访问的方法都必须是 **public** 修饰的。
|
||||
|
||||
对于 Java 新手而言,设置 CLASSPATH 是一件麻烦的事(我最初使用时这么觉得),后面版本的 JDK 更加智能。你会发现当你安装好 JDK 时,即使不设置 CLASSPATH,也能够编译和运行基本的 Java 程序。但是,为了编译和运行本书的代码示例(从[https://github.com/BruceEckel/OnJava8-examples](https://github.com/BruceEckel/OnJava8-examples) 取得),你必须将本书程序代码树的基本目录加入到 CLASSPATH 中( gradlew 命令管理自身的 CLASSPATH,所以如果你想直接使用 javac 和 java,不用 Gradle 的话,就需要设置 CLASSPATH)。
|
||||
|
||||
### 冲突
|
||||
|
||||
如果通过 ***** 导入了两个包含相同名字类名的类库,会发生什么?例如,假设程序如下:
|
||||
|
||||
```java
|
||||
import com.mindviewinc.simple.*;
|
||||
import java.util.*;
|
||||
```
|
||||
|
||||
因为 **java.util.*** 也包含了 **Vector** 类,这就存在潜在的冲突。但是只要你不写导致冲突的代码,就不会有问题——这样很好,否则就得做很多类型检查工作来防止那些根本不会出现的冲突。
|
||||
|
||||
现在如果要创建一个 Vector 类,就会出现冲突:
|
||||
|
||||
```java
|
||||
Vector v = new Vector();
|
||||
```
|
||||
|
||||
这里的 **Vector** 类指的是谁呢?编译器不知道,读者也不知道。所以编译器报错,强制你明确指明。对于标准的 Java 类 **Vector**,你可以这么写:
|
||||
|
||||
```java
|
||||
java.util.Vector v = new java.util.Vector();
|
||||
```
|
||||
|
||||
这种写法完全指明了 **Vector** 类的位置(配合 CLASSPATH),那么就没有必要写 **import java.util.*** 语句,除非使用其他来自 **java.util** 中的类。
|
||||
|
||||
或者,可以导入单个类以防冲突——只要不在同一个程序中使用有冲突的名字(若使用了有冲突的名字,必须明确指明全名)。
|
||||
|
||||
### 定制工具库
|
||||
|
||||
具备了以上知识,现在就可以创建自己的工具库来减少重复的程序代码了。
|
||||
|
||||
一般来说,我会使用反转后的域名来命名要创建的工具包,比如 **com.mindviewinc.util**,但为了简化,这里我把工具包命名为 **onjava**。
|
||||
|
||||
比如,下面是"控制流"一章中使用到的 `range()` 方法,采用了 for-in 语法进行简单的遍历:
|
||||
|
||||
```java
|
||||
// onjava/Range.java
|
||||
// Array creation methods that can be used without
|
||||
// qualifiers, using static imports:
|
||||
package onjava;
|
||||
|
||||
public class Range {
|
||||
// Produce a sequence [0,n)
|
||||
public static int[] range(int n) {
|
||||
int[] result = new int[n];
|
||||
for (int i = 0; i < n; i++) {
|
||||
result[i] = i;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
// Produce a sequence [start..end)
|
||||
public static int[] range(int start, int end) {
|
||||
int sz = end - start;
|
||||
int[] result = new int[sz];
|
||||
for (int i = 0; i < sz; i++) {
|
||||
result[i] = start + i;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
// Produce sequence [start..end) incrementing by step
|
||||
public static int[] range(int start, int end, int step) {
|
||||
int sz = (end - start) / step;
|
||||
int[] result = new int[sz];
|
||||
for (int i = 0; i < sz; i++) {
|
||||
result[i] = start + (i * step);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这个文件的位置一定是在某个以一个 CLASSPATH 位置开始,然后接着是 **onjava** 的目录下。编译完之后,就可以在系统的任何地方使用 **import static** 语句来使用这些方法了。
|
||||
|
||||
从现在开始,无论何时你创建了有用的新工具,都可以把它加入到自己的类库中。在本书中,你将会看到更多的组件加入到 **onjava** 库。
|
||||
|
||||
### 使用 import 改变行为
|
||||
|
||||
Java 没有 C 的*条件编译*功能,该功能使你不必更改任何程序代码而能够切换开关产生不同的行为。Java 之所以去掉此功能,可能是因为 C 在绝大多数情况下使用该功能解决跨平台问题:程序代码的不同部分要根据不同的平台来编译。而 Java 自身就是跨平台设计的,这个功能就没有必要了。
|
||||
|
||||
但是,条件编译还有其他的用途。调试是一个很常见的用途,调试功能在开发过程中是开启的,在发布的产品中是禁用的。可以通过改变导入的 **package** 来实现这一目的,修改的方法是将程序中的代码从调试版改为发布版。这个技术可用于任何种类的条件代码。
|
||||
|
||||
### 使用包的忠告
|
||||
|
||||
当创建一个包时,包名就隐含了目录结构。这个包必须位于包名指定的目录中,该目录必须在以 CLASSPATH 开始的目录中可以查询到。 最初使用关键字 **package** 可能会有点不顺,因为除非遵守"包名对应目录路径"的规则,否则会收到很多意外的运行时错误信息如找不到特定的类,即使这个类就位于同一目录中。如果你收到类似信息,尝试把 **package** 语句注释掉,如果程序能运行的话,你就知道问题出现在哪里了。
|
||||
|
||||
注意,编译过的代码通常位于与源代码的不同目录中。这是很多工程的标准,而且集成开发环境(IDE)通常会自动为我们做这些。必须保证 JVM 通过 CLASSPATH 能找到编译后的代码。
|
||||
|
||||
<!-- Java Access Specifiers -->
|
||||
|
||||
## 访问权限修饰符
|
||||
|
||||
Java 访问权限修饰符 **public**,**protected** 和 **private** 位于定义的类名,属性名和方法名之前。每个访问权限修饰符只能控制它所修饰的对象。
|
||||
|
||||
如果不提供访问修饰符,就意味着"包访问权限"。所以无论如何,万物都有某种形式的访问控制权。接下来的几节中,你将学习各种类型的访问权限。
|
||||
|
||||
### 包访问权限
|
||||
|
||||
本章之前的所有示例要么使用 **public** 访问修饰符,要么就没使用修饰符(默认访问)。默认访问权限没有关键字,通常被称为包访问权限(有时也称为 friendly)。这意味着当前包中的所有其他类都可以访问那个成员。对于这个包之外的类,这个成员看上去是 **private** 的。由于一个编译单元(即一个文件)只能隶属于一个包,所以通过包访问权限,位于同一编译单元中的所有类彼此之间都是可访问的。
|
||||
|
||||
包访问权限可以把相关类聚到一个包下,以便它们能轻易地相互访问。包里的类给它们的包访问权限的成员赋予了相互访问的权限,所以你"拥有”了包内的程序代码。只有你拥有的代码才能访问你拥有的其他代码是有意义的。包访问为把类聚在一个包中提供了理由。在许多语言中,在文件中组织定义的方式是任意的,但是在 Java 中你被强制以一种合理的方式组织它们。另外,你可以把不能访问当前包里的类的其他类排除在外。
|
||||
|
||||
类控制着哪些代码有权访问自己的成员。其他包中的代码不能一上来就说"嗨,我是 **Bob** 的朋友!",然后想看到 **Bob** 的 **protected**,包访问权限和 **private** 成员。取得对成员的访问权的唯一方式是:
|
||||
|
||||
1. 使成员成为 public。那么无论是谁,无论在哪,都可以访问它。
|
||||
2. 赋予成员默认包访问权限,不用加任何访问修饰符,然后将其他类放在相同的包内。这样,其他类就可以访问该成员。
|
||||
3. 在"复用"这一章你将看到,继承的类既可以访问 **public** 成员,也可以访问 **protected** 成员(但不能访问 **private** 成员)。只有当两个类处于同一个包内,它才可以访问包访问权限的成员。但现在不用担心继承和 **protected**。
|
||||
4. 提供访问器(accessor)和修改器(mutator)方法(有时也称为"get/set" 方法),从而读取和改变值。
|
||||
|
||||
### public: 接口访问权限
|
||||
|
||||
当你使用关键字 **public**,就意味着紧随 public 后声明的成员对于每个人都是可用的,尤其是使用类库的客户端程序员更是如此。假设定义了一个包含下面编译单元的 **dessert** 包:
|
||||
|
||||
```java
|
||||
// hiding/dessert/Cookie.java
|
||||
// Creates a library
|
||||
package hiding.dessert;
|
||||
|
||||
public class Cookie {
|
||||
public Cookie() {
|
||||
System.out.println("Cookie constructor");
|
||||
}
|
||||
|
||||
void bite() {
|
||||
System.out.println("bite");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
记住,**Cookie.java** 文件产生的类文件必须位于名为 **dessert** 的子目录中,该子目录在 **hiding** (表明本书的"封装"章节)下,它必须在 CLASSPATH 的几个目录之下。不要错误地认为 Java 总是会将当前目录视作查找行为的起点之一。如果你的 CLASSPATH 中没有 `.`,Java 就不会查找单独当前目录。
|
||||
|
||||
现在,使用 **Cookie** 创建一个程序:
|
||||
```java
|
||||
// hiding/Dinner.java
|
||||
// Uses the library
|
||||
import hiding.dessert.*;
|
||||
|
||||
public class Dinner {
|
||||
public static void main(String[] args) {
|
||||
Cookie x = new Cookie();
|
||||
// -x.bite(); // Can't access
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
输出:
|
||||
|
||||
```
|
||||
Cookie constructor
|
||||
```
|
||||
|
||||
你可以创建一个 **Cookie** 对象,因为它构造器和类都是 **public** 的。(后面会看到更多 **public** 的概念)但是,在 **Dinner.java** 中无法访问到 **Cookie** 对象中的 `bite()` 方法,因为 `bite()` 只提供了包访问权限,因而在 **dessert** 包之外无法访问,编译器禁止你使用它。
|
||||
|
||||
### 默认包
|
||||
|
||||
你可能惊讶地发现,以下代码尽管看上去破坏了规则,但是仍然可以编译:
|
||||
|
||||
```java
|
||||
// hiding/Cake.java
|
||||
// Accesses a class in a separate compilation unit
|
||||
class Cake {
|
||||
public static void main(String[] args) {
|
||||
Pie x = new Pie();
|
||||
x.f();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
输出:
|
||||
|
||||
```
|
||||
Pie.f()
|
||||
```
|
||||
|
||||
同一目录下的第二个文件:
|
||||
|
||||
```java
|
||||
// hiding/Pie.java
|
||||
// The other class
|
||||
class Pie {
|
||||
void f() {
|
||||
System.out.println("Pie.f()");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
最初看上去这两个文件毫不相关,但在 **Cake** 中可以创建一个 **Pie** 对象并调用它的 `f()` 方法。(注意,你的 CLASSPATH 中一定得有 `.`,这样文件才能编译)通常会认为 **Pie** 和 `f()` 具有包访问权限,因此不能被 **Cake** 访问。它们的确具有包访问权限,这是部分正确。**Cake.java** 可以访问它们是因为它们在相同的目录中且没有给自己设定明确的包名。Java 把这样的文件看作是隶属于该目录的默认包中,因此它们为该目录中所有的其他文件都提供了包访问权限。
|
||||
|
||||
### private: 你无法访问
|
||||
|
||||
关键字 **private** 意味着除了包含该成员的类,其他任何类都无法访问这个成员。同一包中的其他类无法访问 **private** 成员,因此这等于说是自己隔离自己。另一方面,让许多人合作创建一个包也是有可能的。使用 **private**,你可以自由地修改那个被修饰的成员,无需担心会影响同一包下的其他类。
|
||||
|
||||
默认的包访问权限通常提供了足够的隐藏措施;记住,使用类的客户端程序员无法访问包访问权限成员。这样做很好,因为默认访问权限是一种我们常用的权限(同时也是一种在忘记添加任何访问权限时自动得到的权限)。因此,通常考虑的是把哪些成员声明成 **public** 供客户端程序员使用。所以,最初不常使用关键字 **private**,因为程序没有它也可以照常工作。然而,使用 **private** 是非常重要的,尤其是在多线程环境中。(在"并发编程"一章中将看到)。
|
||||
|
||||
以下是一个使用 **private** 的例子:
|
||||
|
||||
```java
|
||||
// hiding/IceCream.java
|
||||
// Demonstrates "private" keyword
|
||||
|
||||
class Sundae {
|
||||
private Sundae() {}
|
||||
static Sundae makeASundae() {
|
||||
return new Sundae();
|
||||
}
|
||||
}
|
||||
|
||||
public class IceCream {
|
||||
public static void main(String[] args) {
|
||||
//- Sundae x = new Sundae();
|
||||
Sundae x = Sundae.makeASundae();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
以上展示了 **private** 的用武之地:控制如何创建对象,防止别人直接访问某个特定的构造器(或全部构造器)。例子中,你无法通过构造器创建一个 **Sundae** 对象,而必须调用 `makeASundae()` 方法创建对象。
|
||||
|
||||
任何可以肯定只是该类的"助手"方法,都可以声明为 **private**,以确保不会在包中的其他地方误用它,也防止了你会去改变或删除它。将方法声明为 **private** 确保了你拥有这种选择权。
|
||||
|
||||
对于类中的 **private** 属性也是一样。除非必须公开底层实现(这种情况很少见),否则就将属性声明为 **private**。然而,不能因为类中某个对象的引用是 **private**,就认为其他对象也无法拥有该对象的 **public** 引用(参见附录:对象传递和返回)。
|
||||
|
||||
### protected: 继承访问权限
|
||||
|
||||
要理解 **protected** 的访问权限,我们在内容上需要作一点跳跃。首先,在介绍本书"复用"章节前,你不必真正理解本节的内容。但为了内容的完整性,这里作了简要介绍,举了个使用 **protected** 的例子。
|
||||
|
||||
关键字 **protected** 处理的是继承的概念,通过继承可以利用一个现有的类——我们称之为基类,然后添加新成员到现有类中而不必碰现有类。我们还可以改变类的现有成员的行为。为了从一个类中继承,需要声明新类 extends 一个现有类,像这样:
|
||||
|
||||
```java
|
||||
class Foo extends Bar {}
|
||||
```
|
||||
|
||||
类定义的其他部分看起来是一样的。
|
||||
|
||||
如果你创建了一个新包,并从另一个包继承类,那么唯一能访问的就是被继承类的 **public** 成员。(如果在同一个包中继承,就可以操作所有的包访问权限的成员。)有时,基类的创建者会希望某个特定成员能被继承类访问,但不能被其他类访问。这时就需要使用 **protected**。**protected** 也提供包访问权限,也就是说,相同包内的其他类可以访问 **protected** 元素。
|
||||
|
||||
回顾下先前的文件 **Cookie.java**,下面的类不能调用包访问权限的方法 `bite()`:
|
||||
|
||||
```java
|
||||
// hiding/ChocolateChip.java
|
||||
// Can't use package-access member from another package
|
||||
import hiding.dessert.*;
|
||||
|
||||
public class ChocolateChip extends Cookie {
|
||||
public ChocolateChip() {
|
||||
System.out.println("ChocolateChip constructor");
|
||||
}
|
||||
|
||||
public void chomp() {
|
||||
//- bite(); // Can't access bite
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
ChocolateChip x = new ChocolateChip();
|
||||
x.chomp();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
输出:
|
||||
|
||||
```
|
||||
Cookie constructor
|
||||
ChocolateChip constructor
|
||||
```
|
||||
|
||||
如果类 **Cookie** 中存在一个方法 `bite()`,那么它的任何子类中都存在 `bite()` 方法。但是因为 `bite()` 具有包访问权限并且位于另一个包中,所以我们在这个包中无法使用它。你可以把它声明为 **public**,但这样一来每个人都能访问它,这可能也不是你想要的。如果你将 **Cookie** 改成如下这样:
|
||||
|
||||
```java
|
||||
// hiding/cookie2/Cookie.java
|
||||
package hiding.cookie2;
|
||||
|
||||
public class Cookie {
|
||||
public Cookie() {
|
||||
System.out.println("Cookie constructor");
|
||||
}
|
||||
|
||||
protected void bite() {
|
||||
System.out.println("bite");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这样,`bite()` 对于所有继承 **Cookie** 的类,都是可访问的:
|
||||
|
||||
```java
|
||||
// hiding/ChocolateChip2.java
|
||||
import hiding.cookie2.*;
|
||||
|
||||
public class ChocolateChip2 extends Cookie {
|
||||
public ChocoalteChip2() {
|
||||
System.out.println("ChocolateChip2 constructor");
|
||||
}
|
||||
|
||||
public void chomp() {
|
||||
bite(); // Protected method
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
ChocolateChip2 x = new ChocolateChip2();
|
||||
x.chomp();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
输出:
|
||||
|
||||
```
|
||||
Cookie constructor
|
||||
ChocolateChip2 constructor
|
||||
bite
|
||||
```
|
||||
|
||||
尽管 `bite()` 也具有包访问权限,但它不是 **public** 的。
|
||||
|
||||
### 包访问权限 Vs Public 构造器
|
||||
|
||||
当你定义一个具有包访问权限的类时,你可以在类中定义一个 public 构造器,编译器不会报错:
|
||||
|
||||
```java
|
||||
// hiding/packageaccess/PublicConstructor.java
|
||||
package hiding.packageaccess;
|
||||
|
||||
class PublicConstructor {
|
||||
public PublicConstructor() {}
|
||||
}
|
||||
```
|
||||
|
||||
有一个 Checkstyle 工具,你可以运行命令 **gradlew hiding:checkstyleMain** 使用它,它会指出这种写法是虚假的,而且从技术上来说是错误的。实际上你不能从包外访问到这个 **public** 构造器:
|
||||
|
||||
```java
|
||||
// hiding/CreatePackageAccessObject.java
|
||||
// {WillNotCompile}
|
||||
import hiding.packageaccess.*;
|
||||
|
||||
public class CreatePackageAcessObject {
|
||||
public static void main(String[] args) {
|
||||
new PublicConstructor();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
如果你编译下这个类,会得到编译错误信息:
|
||||
|
||||
```
|
||||
CreatePackageAccessObject.java:6:error:
|
||||
PublicConstructor is not public in hiding.packageaccess;
|
||||
cannot be accessed from outside package
|
||||
new PublicConstructor();
|
||||
^
|
||||
1 error
|
||||
```
|
||||
|
||||
因此,在一个具有包访问权限的类中定义一个 **public** 的构造器并不能真的使这个构造器成为 **public**,在声明的时候就应该标记为编译时错误。
|
||||
|
||||
<!-- Interface and Implementation -->
|
||||
|
||||
## 接口和实现
|
||||
|
||||
访问控制通常被称为实现的隐藏。将数据和方法包装进类中并把具体实现隐藏被称作是封装。其结果就是一个同时带有特征和行为的数据类型。
|
||||
|
||||
出于两个重要的原因,访问控制在数据类型内部划定了边界。第一个原因是确立客户端程序员可以使用和不能使用的边界。可以在结构中建立自己的内部机制而不必担心客户端程序员偶尔将内部实现作为他们可以使用的接口的一部分。
|
||||
|
||||
这直接引出了第二个原因:将接口与实现分离。如果在一组程序中使用结构,而客户端程序员只能向 **public** 接口发送消息的话,那么就可以自由地修改任何不是 **public** 的事物(例如包访问权限,protected,或 private 修饰的事物),却不会破坏客户端代码。
|
||||
|
||||
为了清晰起见,你可以采用一种创建类的风格:**public** 成员放在类的开头,接着是 **protected** 成员,包访问权限成员,最后是 **private** 成员。这么做的好处是类的使用者可以从头读起,首先会看到对他们而言最重要的部分(public 成员,因为可以从文件外访问它们),直到遇到非 **public** 成员时停止阅读,下面就是内部实现了:
|
||||
|
||||
```java
|
||||
// hiding/OrganizedByAccess.java
|
||||
|
||||
public class OrganizedByAccess {
|
||||
public void pub1() {/* ... */}
|
||||
public void pub2() {/* ... */}
|
||||
public void pub3() {/* ... */}
|
||||
private void priv1() {/* ... */}
|
||||
private void priv2() {/* ... */}
|
||||
private void priv3() {/* ... */}
|
||||
private int i;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
这么做只能是程序阅读起来稍微容易一些,因为实现和接口还是混合在一起。也就是说,你仍然能看到源代码——实现部分,因为它就在类中。另外,javadoc 提供的注释文档功能降低了程序代码的可读性对客户端程序员的重要性。将接口展现给类的使用者实际上是类浏览器的任务,类浏览器会展示所有可用的类,并告诉你如何使用它们(比如说那些成员可用)。在 Java 中,JDK 文档起到了类浏览器的作用。
|
||||
|
||||
<!-- Class Access -->
|
||||
|
||||
## 类访问权限
|
||||
|
||||
访问权限修饰符也可以用于确定类库中的哪些类对于类库的使用者是可用的。如果希望某个类可以被客户端程序员使用,就把关键字 **public** 作用于整个类的定义。这甚至控制着客户端程序员能否创建类的对象。
|
||||
|
||||
为了控制一个类的访问权限,修饰符必须出现在关键字 **class** 之前:
|
||||
|
||||
```java
|
||||
public class Widget {
|
||||
```
|
||||
|
||||
如果你的类库名是 **hiding**,那么任何客户端程序员都可以通过如下声明访问 **Widget**:
|
||||
|
||||
```java
|
||||
import hiding.Widget;
|
||||
```
|
||||
|
||||
或者
|
||||
|
||||
```java
|
||||
import hiding.*;
|
||||
```
|
||||
|
||||
这里有一些额外的限制:
|
||||
|
||||
1. 每个编译单元(即每个文件)中只能有一个 **public** 类。这表示,每个编译单元有一个公共的接口用 **public** 类表示。该接口可以包含许多支持包访问权限的类。一旦一个编译单元中出现一个以上的 **public** 类,编译就会报错。
|
||||
2. **public** 类的名称必须与含有该编译单元的文件名相同,包括大小写。所以对于 **Widget** 来说,文件名必须是 **Widget.java**,不能是 **widget.java** 或 **WIDGET.java**。再次强调,如果名字不匹配,编译器会报错。
|
||||
3. 虽然不是很常见,但是编译单元内没有 **public** 类也是可能的。这时可以随意命名文件(尽管随意命名会让代码的阅读者和维护者感到困惑)。
|
||||
|
||||
如果获取了一个在 **hiding** 包中的类,只用来完成 **Widget** 或 **hiding** 包下一些其他 **public** 类所要执行的任务,怎么办呢? 你不想自找麻烦为客户端程序员创建说明文档,并且你认为不久后会完全改变原有方案并将旧版本删除,替换成新版本。为了保留此灵活性,需要确保客户端程序员不依赖隐藏在 **hiding** 中的任何特定细节,那么把 **public** 关键字从类中去掉,给予它包访问权限,就可以了。
|
||||
|
||||
当你创建了一个包访问权限的类,把类中的属性声明为 **private** 仍然是有意义的——应该尽可能将所有属性都声明为 **private**,但是通常把方法声明成与类(包访问权限)相同的访问权限也是合理的。由于一个包访问权限的类只能被用于包内,除非你被强制将某些方法声明为 **public**,这种情况下,编译器会告诉你。
|
||||
|
||||
注意,类既不能是 **private** 的(这样除了该类自身,任何类都不能访问它),也不能是 **protected** 的。所以对于类的访问权限只有两种选择:包访问权限或者 **public**。为了防止类被外界访问,可以将所有的构造器声明为 **private**,这样只有你自己能创建对象(在类的 static 成员中):
|
||||
|
||||
```java
|
||||
// hiding/Lunch.java
|
||||
// Demonstrates class access specifiers. Make a class
|
||||
// effectively private with private constructors:
|
||||
|
||||
class Soup1 {
|
||||
private Soup1() {}
|
||||
|
||||
public static Soup1 makeSoup() { // [1]
|
||||
return new Soup1();
|
||||
}
|
||||
}
|
||||
|
||||
class Soup2 {
|
||||
private Soup2() {}
|
||||
|
||||
private static Soup2 ps1 = new Soup2(); // [2]
|
||||
|
||||
public static Soup2 access() {
|
||||
return ps1;
|
||||
}
|
||||
|
||||
public void f() {}
|
||||
}
|
||||
// Only one public class allowed per file:
|
||||
public class Lunch {
|
||||
void testPrivate() {
|
||||
// Can't do this! Private constructor:
|
||||
//- Soup1 soup = new Soup1();
|
||||
}
|
||||
|
||||
void testStatic() {
|
||||
Soup1 soup = Soup1.makeSoup();
|
||||
}
|
||||
|
||||
void testSingleton() {
|
||||
Soup2.access().f();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
可以像 [1] 那样通过 **static** 方法创建对象,也可以像 [2] 那样先创建一个静态对象,当用户需要访问它时返回对象的引用即可。
|
||||
|
||||
到目前为止,大部分的方法要么返回 void,要么返回基本类型,所以 [1] 处的定义乍看之下会有点困惑。方法名(**makeSoup**)前面的 **Soup1** 表明了方法返回的类型。到目前为止,这里经常是 **void**,即不返回任何东西。然而也可以返回对象的引用,就像这里一样。这个方法返回了对 **Soup1** 类对象的引用。
|
||||
|
||||
**Soup1** 和 **Soup2** 展示了如何通过嫁给你所有的构造器声明为 **private** 的方式防止直接创建某个类的对象。记住,如果你不显式地创建构造器,编译器会自动为你创建一个无参构造器(没有参数的构造器)。如果我们编写了无参构造器,那么编译器就不会自动创建构造器了。将构造器声明为 **private**,那么谁也无法创建该类的对象了。但是现在别人该怎么使用这个类呢?上述例子给出了两个选择。在 **Soup1** 中,有一个 **static** 方法,它的作用是创建一个新的 **Soup1** 对象并返回对象的引用。如果想要在返回引用之前在 **Soup1** 上做一些额外操作,或是记录创建了多少个 **Soup1** 对象(可以用来限制数量),这种做法是有用的。
|
||||
|
||||
**Soup2** 用到了所谓的*设计模式*。这种模式叫做*单例模式*,因为它只允许创建类的一个对象。**Soup2** 类的对象是作为 **Soup2** 的 **static** **private** 成员而创建的,所以有且只有一个,你只能通过 **public** 修饰的 `access()` 方法访问到这个对象。
|
||||
|
||||
<!-- Summary -->
|
||||
|
||||
## 本章小结
|
||||
|
||||
无论在什么样的关系中,划定一些供各成员共同遵守的界限是很重要的。当你创建了一个类库,也就与该类库的使用者产生了联系,他们是类库的客户端程序员,需要使用你的类库创建应用或更大的类库。
|
||||
|
||||
没有规则,客户端程序员就可以对类的所有成员为所欲为,即使你希望他们不要操作部分成员。这种情况下,所有事物都是公开的。
|
||||
|
||||
本章讨论了类库是如何通过类构建的:首先,介绍了将一组类打包到类库的方式,其次介绍了类如何控制对其成员的访问。
|
||||
|
||||
据估计,用 C 语言开发项目,当代码量达到 5 万行和 10 万行时就会出现问题,因为 C 语言只有单一的命名空间,名称开始冲突造成额外的管理开销。在 Java 中,关键字 **package**,包命名模式和关键字 **import** 给了你对于名称的完全控制权,因此可以轻易地避免名称冲突的问题。
|
||||
|
||||
控制成员访问权限有两个原因。第一个原因是使用户不要接触他们不该接触的部分,这部分对于类内部来说是必要的,但是不属于客户端程序员所需接口的一部分。因此将方法和属性声明为 **private** 对于客户端程序员来说是一种服务,可以让他们清楚地看到什么是重要的,什么可以忽略。这可以简化他们对类的理解。
|
||||
|
||||
第二个也是最重要的原因是为了让类库设计者更改类内部的工作方式,而不用担心会影响到客户端程序员。比如最初以某种方式创建一个类,随后发现如果更改代码结构可以极大地提高运行速度。如果接口与实现被明确地隔离和保护,你可以实现这一目的,而不必强制客户端程序员重新编写代码。访问权限控制确保客户端程序员不会依赖某个类的底层实现的任何部分。
|
||||
|
||||
当你具备更改底层实现的能力时,不但可以自由地改善设计,还可能会随意地犯错。无论如何细心地计划和设计,都有可能犯错。当了解到犯错是相对安全的时候,你可以更加放心地实验,更快地学会,更快地完成项目。
|
||||
|
||||
类的 **public** 接口是用户真正看到的,所以在分析和设计阶段决定这部分接口是最重要的部分。尽管如此,你仍然有改变的空间。如果最初没有创建出正确的接口,可以添加更多的方法,主要你不删除那些客户端程序员已经在他们的代码中使用的东西。
|
||||
|
||||
注意到访问权限控制关注的是类库创建者和外部使用者之间的关系,一种交流方式。很多情况下,事实并非如此。例如,你自己编写了所有的代码,或者在一个小组中工作,所有的东西都放在同一个包下。这些情况下,交流方式则是另外一种,此时严格地遵循访问权限规则也许不是最佳选择,默认(包)访问权限也许就足够好了。
|
||||
|
||||
<!-- 分页 -->
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ i = 0 f = 0.0 source = Constructed
|
||||
|
||||
```
|
||||
|
||||
这两个类中定义的一个方法是特殊的: **toString()**。每个非原始类型对象都有一个 **toString()** 方法,在编译器需要字符串但它有对象的特殊情况下调用该方法。因此,在[1]中,编译器看到您试图“添加”一个 **WaterSource** 类型字符串对象 。因为您只能将一个字符串添加到另一个字符串中,所以它说,“我将通过调用 **toString()** 将 **source** 转换成一个字符串。然后,它可以组合这两个字符串并将结果字符串传递给 **System.out.println()**。要对创建的任何类允许这种行为,只需要编写一个 **toString()** 方法。在 **toString()** 上使用 **@Override** 注释来告诉编译器,以确保正确地覆盖。**@Override** 是可选的,但它有助于验证您没有拼写错误 (或者更微妙地说,大小写字母输入错误)。类中的基本类型的字段自动初始化为零,正如 **object Everywhere** 一章中所述。但是对象引用被初始化为 **null**,如果您尝试调用其任何一个方法,您将得到一个异常(一个运行时错误)。
|
||||
这两个类中定义的一个方法是特殊的: **toString()**。每个非原始类型对象都有一个 **toString()** 方法,在编译器需要字符串但它有对象的特殊情况下调用该方法。因此,在[1]中,编译器看到你试图“添加”一个 **WaterSource** 类型字符串对象 。因为你只能将一个字符串添加到另一个字符串中,所以它说,“我将通过调用 **toString()** 将 **source** 转换成一个字符串。然后,它可以组合这两个字符串并将结果字符串传递给 **System.out.println()**。要对创建的任何类允许这种行为,只需要编写一个 **toString()** 方法。在 **toString()** 上使用 **@Override** 注释来告诉编译器,以确保正确地覆盖。**@Override** 是可选的,但它有助于验证你没有拼写错误 (或者更微妙地说,大小写字母输入错误)。类中的基本类型的字段自动初始化为零,正如 **object Everywhere** 一章中所述。但是对象引用被初始化为 **null**,如果你尝试调用其任何一个方法,你将得到一个异常(一个运行时错误)。
|
||||
|
||||
编译器不只是为每个引用创建一个默认对象,这是有意义的,因为在许多情况下,这会导致不必要的开销。初始化引用有四种方法:
|
||||
|
||||
@@ -144,7 +144,7 @@ castille = Constructed
|
||||
|
||||
```
|
||||
|
||||
在 **Bath** 构造函数中,在初始化发生。当您不在定义处初始化时,仍然不能保证在向对象引用发送消息之前执行任何初始化——如果您试图对未初始化的引用调用方法,则未初始化的引用将产生运行时异常。
|
||||
在 **Bath** 构造函数中,在初始化发生。当你不在定义处初始化时,仍然不能保证在向对象引用发送消息之前执行任何初始化——如果你试图对未初始化的引用调用方法,则未初始化的引用将产生运行时异常。
|
||||
|
||||
当调用 **toString()** 时,它将赋值 s4,以便在使用字段的时候进行初始化。
|
||||
|
||||
@@ -154,7 +154,7 @@ castille = Constructed
|
||||
|
||||
继承是所有面向对象语言的一个组成部分。事实证明,在创建类时总是要继承,因为除非显式地继承其他类,否则就隐式地继承 Java 的标准根类对象(Object)。
|
||||
|
||||
组合的语法很明显,但是继承使用了一种特殊的语法。当您继承时,您说,“这个新类与那个旧类类似。您可以在类主体的左大括号前的代码中声明这一点,使用关键字 **extends** 后跟基类的名称。当您这样做时,您将自动获得基类中的所有字段和方法。这里有一个例子:
|
||||
组合的语法很明显,但是继承使用了一种特殊的语法。当你继承时,你说,“这个新类与那个旧类类似。你可以在类主体的左大括号前的代码中声明这一点,使用关键字 **extends** 后跟基类的名称。当你这样做时,你将自动获得基类中的所有字段和方法。这里有一个例子:
|
||||
|
||||
```java
|
||||
// reuse/Detergent.java
|
||||
@@ -210,21 +210,21 @@ Cleanser dilute() apply() scrub()
|
||||
|
||||
这演示了一些特性。首先,在 **Cleanser append()** 方法中,使用 += 操作符将字符串连接到 **s**,这是 Java 设计人员“重载”来处理字符串的操作符之一 (还有 + )。
|
||||
|
||||
第二,**Cleanser** 和 **Detergent** 都包含一个 **main()** 方法。您可以为每个类创建一个 main() ; 这允许对每个类进行简单的测试。当您完成测试时,不需要删除 **main()** ; 您可以将其留在以后的测试中。即使程序中有很多类,惟一 运行的 **main()** 也是在命令行上调用的 **main()**。这里,当您使用 **java Detergent** 时候,调用了 **Detergent.main()**。但是您也可以使用 **java Cleanser** 来调用 **Cleanser.main()** ,即使 **Cleanser** 不是一个公共类。即使类具只有包访问权,也可以访问 **public main()**。
|
||||
第二,**Cleanser** 和 **Detergent** 都包含一个 **main()** 方法。你可以为每个类创建一个 main() ; 这允许对每个类进行简单的测试。当你完成测试时,不需要删除 **main()** ; 你可以将其留在以后的测试中。即使程序中有很多类,惟一 运行的 **main()** 也是在命令行上调用的 **main()**。这里,当你使用 **java Detergent** 时候,调用了 **Detergent.main()**。但是你也可以使用 **java Cleanser** 来调用 **Cleanser.main()** ,即使 **Cleanser** 不是一个公共类。即使类具只有包访问权,也可以访问 **public main()**。
|
||||
|
||||
在这里,**Detergent.main() **显式地调用 **Cleanser.main()** ,从命令行传递相同的参数(当然,您可以传递任何字符串数组)。
|
||||
在这里,**Detergent.main() **显式地调用 **Cleanser.main()** ,从命令行传递相同的参数(当然,你可以传递任何字符串数组)。
|
||||
|
||||
**Cleanser** 中的所有方法都是公开的。请记住,如果不使用任何访问说明符,则成员默认为包访问,这只允许访问包成员。因此,在这个包中,如果没有访问说明符,任何人都可以使用这些方法。例如,**Detergent** 就没有问题。但是,如果要从 **Cleanser** 继承其他包中的类,则该类只能访问公共成员。因此,为了允许继承,一般规则是所有字段为私有,所有方法为公共。(受保护成员也允许派生类访问;你以后会知道的。)在特定的情况下,您必须进行调整,但这是一个有用的指南。
|
||||
**Cleanser** 中的所有方法都是公开的。请记住,如果不使用任何访问说明符,则成员默认为包访问,这只允许访问包成员。因此,在这个包中,如果没有访问说明符,任何人都可以使用这些方法。例如,**Detergent** 就没有问题。但是,如果要从 **Cleanser** 继承其他包中的类,则该类只能访问公共成员。因此,为了允许继承,一般规则是所有字段为私有,所有方法为公共。(受保护成员也允许派生类访问;你以后会知道的。)在特定的情况下,你必须进行调整,但这是一个有用的指南。
|
||||
|
||||
**Cleanser** 的接口中有一组方法: **append()**、**dilute()**、**apply()**、**scrub()** 和 **toString()**。因为 **Detergent** 是从 **Cleanser** 派生的(通过 **extends** 关键字),所以它会在其接口中自动获取所有这些方法,即使您没有在 **Detergent** 中看到所有这些方法的显式定义。你那么,可以把继承看作是重用类。如在 **scrub()** 中所见,可以使用基类中定义的方法并修改它。在这里,您可以从新版本中的基类中调用该方法。但是在 **scrub()** 内部,不能简单地调用 **scrub()**,因为这会产生递归调用。为了解决这个问题,Java的 **super** 关键字引用了当前类继承的“超类”(基类)。因此表达式 **super.scrub()** 调用方法 **scrub()** 的基类版本。
|
||||
**Cleanser** 的接口中有一组方法: **append()**、**dilute()**、**apply()**、**scrub()** 和 **toString()**。因为 **Detergent** 是从 **Cleanser** 派生的(通过 **extends** 关键字),所以它会在其接口中自动获取所有这些方法,即使你没有在 **Detergent** 中看到所有这些方法的显式定义。你那么,可以把继承看作是重用类。如在 **scrub()** 中所见,可以使用基类中定义的方法并修改它。在这里,你可以从新版本中的基类中调用该方法。但是在 **scrub()** 内部,不能简单地调用 **scrub()**,因为这会产生递归调用。为了解决这个问题,Java的 **super** 关键字引用了当前类继承的“超类”(基类)。因此表达式 **super.scrub()** 调用方法 **scrub()** 的基类版本。
|
||||
|
||||
继承时,您不受限于使用基类的方法。您还可以像向类添加任何方法一样向派生类添加新方法:只需定义它。方法 **foam()** 就是一个例子。**Detergent.main()** 中可以看到,对于 **Detergent** 对象,您可以调用 **Cleanser** 和 **Detergent** 中可用的所有方法 (如 **foam()** )。
|
||||
继承时,你不受限于使用基类的方法。你还可以像向类添加任何方法一样向派生类添加新方法:只需定义它。方法 **foam()** 就是一个例子。**Detergent.main()** 中可以看到,对于 **Detergent** 对象,你可以调用 **Cleanser** 和 **Detergent** 中可用的所有方法 (如 **foam()** )。
|
||||
|
||||
<!-- Initializing the Base Class -->
|
||||
|
||||
### 初始化基类
|
||||
|
||||
现在涉及到两个类:基类和派生类。想象派生类生成的结果对象可能会让人感到困惑。从外部看,新类与基类具有相同的接口,可能还有一些额外的方法和字段。但是继承并不只是复制基类的接口。当您创建派生类的对象时,它包含基类的子对象。这个子对象与您自己创建基类的对象是一样的。只是从外部看,基类的子对象被包装在派生类的对象中。
|
||||
现在涉及到两个类:基类和派生类。想象派生类生成的结果对象可能会让人感到困惑。从外部看,新类与基类具有相同的接口,可能还有一些额外的方法和字段。但是继承并不只是复制基类的接口。当你创建派生类的对象时,它包含基类的子对象。这个子对象与你自己创建基类的对象是一样的。只是从外部看,基类的子对象被包装在派生类的对象中。
|
||||
|
||||
必须正确初始化基类子对象,而且只有一种方法可以保证这一点 : 通过调用基类构造函数在构造函数中执行初始化,该构造函数具有执行基类初始化所需的所有适当信息和特权。Java 自动在派生类构造函数中插入对基类构造函数的调用。下面的例子展示了三个层次的继承:
|
||||
|
||||
@@ -263,7 +263,7 @@ Cartoon constructor
|
||||
|
||||
```
|
||||
|
||||
构造从基类“向外”进行,因此基类在派生类构造函数能够访问它之前进行初始化。即使不为 **Cartoon** 创建构造函数,编译器也会为您合成一个无参数构造函数,调用基类构造函数。尝试删除 **Cartoon** 构造函数来查看这个。
|
||||
构造从基类“向外”进行,因此基类在派生类构造函数能够访问它之前进行初始化。即使不为 **Cartoon** 创建构造函数,编译器也会为你合成一个无参数构造函数,调用基类构造函数。尝试删除 **Cartoon** 构造函数来查看这个。
|
||||
|
||||
<!-- Constructors with Arguments -->
|
||||
|
||||
@@ -316,7 +316,7 @@ Chess constructor
|
||||
|
||||
## 委托
|
||||
|
||||
Java不直接支持的第三种重用关系称为委托。这介于继承和组合之间,因为您将一个成员对象放在正在构建的类中(比如组合),但同时又在新类中公开来自成员对象的所有方法(比如继承)。例如,宇宙飞船需要一个控制模块:
|
||||
Java不直接支持的第三种重用关系称为委托。这介于继承和组合之间,因为你将一个成员对象放在正在构建的类中(比如组合),但同时又在新类中公开来自成员对象的所有方法(比如继承)。例如,宇宙飞船需要一个控制模块:
|
||||
|
||||
```java
|
||||
// reuse/SpaceShipControls.java
|
||||
@@ -361,7 +361,7 @@ DerivedSpaceShip extends SpaceShipControls {
|
||||
|
||||
```
|
||||
|
||||
然而, **DerivedSpaceShip** 并不是真正的“一种” **SpaceShipControls** ,即使您“告诉” **DerivedSpaceShip** 调用 **forward()** 。更准确地说,一艘宇宙飞船包含了 **SpaceShipControls **,同时 **SpaceShipControls** 中的所有方法都暴露在宇宙飞船中。委托解决了这个难题:
|
||||
然而, **DerivedSpaceShip** 并不是真正的“一种” **SpaceShipControls** ,即使你“告诉” **DerivedSpaceShip** 调用 **forward()** 。更准确地说,一艘宇宙飞船包含了 **SpaceShipControls **,同时 **SpaceShipControls** 中的所有方法都暴露在宇宙飞船中。委托解决了这个难题:
|
||||
|
||||
```java
|
||||
// reuse/SpaceShipDelegation.java
|
||||
@@ -407,7 +407,7 @@ public class SpaceShipDelegation {
|
||||
|
||||
```
|
||||
|
||||
方法被转发到底层 **control** 对象,因此接口与继承的接口是相同的。但是,您对委托有更多的控制,因为您可以选择只在成员对象中提供方法的子集。
|
||||
方法被转发到底层 **control** 对象,因此接口与继承的接口是相同的。但是,你对委托有更多的控制,因为你可以选择只在成员对象中提供方法的子集。
|
||||
|
||||
虽然Java语言不支持委托,但是开发工具常常支持。例如,上面的例子是使用 JetBrains Idea IDE 自动生成的。
|
||||
|
||||
@@ -415,7 +415,7 @@ public class SpaceShipDelegation {
|
||||
|
||||
## 结合组合与继承
|
||||
|
||||
您将经常同时使用组合和继承。下面的例子展示了使用继承和组合创建类,以及必要的构造函数初始化:
|
||||
你将经常同时使用组合和继承。下面的例子展示了使用继承和组合创建类,以及必要的构造函数初始化:
|
||||
|
||||
```java
|
||||
// reuse/PlaceSetting.java
|
||||
@@ -503,13 +503,13 @@ PlaceSetting constructor
|
||||
|
||||
```
|
||||
|
||||
尽管编译器强制您初始化基类,并要求您在构造函数的开头就初始化基类,但它并不监视您以确保您初始化了成员对象。注意类是如何干净地分离的。你甚至不需要方法重用代码的源代码。您最多只导入一个包。(这对于继承和组合都是正确的。)
|
||||
尽管编译器强制你初始化基类,并要求你在构造函数的开头就初始化基类,但它并不监视你以确保你初始化了成员对象。注意类是如何干净地分离的。你甚至不需要方法重用代码的源代码。你最多只导入一个包。(这对于继承和组合都是正确的。)
|
||||
|
||||
<!-- Guaranteeing Proper Cleanup -->
|
||||
|
||||
### 保证适当的清理
|
||||
|
||||
Java 没有 c++ 中析构函数的概念,析构函数是在对象被销毁时自动调用的方法。原因可能是,在Java中,实践只会忘记对象而不是销毁它们,从而允许垃圾收集器根据需要回收内存。通常这是可以的,但是有时您的类可能在其生命周期中执行一些需要清理的活动。Housekeeping 章节解释说,您不能知道何时调用垃圾收集器,甚至不能知道是否调用了它。因此,如果您想为类清理一些东西,您必须显式地编写一个特殊的方法来完成它,并确保客户机程序员知道他们必须调用这个方法。最重要的是——正如在 Exceptions 章节中描述的——您必须通过在 **finally **子句中放置此类清理来防止异常。
|
||||
Java 没有 c++ 中析构函数的概念,析构函数是在对象被销毁时自动调用的方法。原因可能是,在Java中,实践只会忘记对象而不是销毁它们,从而允许垃圾收集器根据需要回收内存。通常这是可以的,但是有时你的类可能在其生命周期中执行一些需要清理的活动。Housekeeping 章节解释说,你不能知道何时调用垃圾收集器,甚至不能知道是否调用了它。因此,如果你想为类清理一些东西,你必须显式地编写一个特殊的方法来完成它,并确保客户机程序员知道他们必须调用这个方法。最重要的是——正如在 Exceptions 章节中描述的——你必须通过在 **finally **子句中放置此类清理来防止异常。
|
||||
|
||||
请考虑一个在屏幕上绘制图片的计算机辅助设计系统的例子:
|
||||
|
||||
@@ -635,11 +635,11 @@ Shape dispose
|
||||
|
||||
这个系统中的所有东西都是某种 **Shape** (它本身是一种 **Object**,因为它是从根类隐式继承的) 。除了使用 **super** 调用该方法的基类版本外,每个类还覆盖 **Shapes dispose()** 方法。特定的 **Shape** 类——Circle、**Triangle** 和 **Line**,都有 “draw” 构造函数,尽管在对象的生命周期中调用的任何方法都可以负责做一些需要清理的事情。每个类都有自己的 **dispose()** 方法来将非内存的内容恢复到对象存在之前的状态。
|
||||
|
||||
在 **main()** 中,有两个关键字是您以前没有见过的,在 exception 一章之前不会详细解释: **try** 和 **finally**。try关键字表示后面的块 (用花括号分隔 )是一个受保护的区域,这意味着它得到了特殊处理。其中一个特殊处理是,无论 **try** 块如何退出,在这个保护区域之后的 **finally** 子句中的代码总是被执行。(通过异常处理,可以用许多不同寻常的方式留下 **try** 块。)这里,**finally** 子句的意思是,“无论发生什么,始终调用 **dispose()** for **x**。”
|
||||
在 **main()** 中,有两个关键字是你以前没有见过的,在 exception 一章之前不会详细解释: **try** 和 **finally**。try关键字表示后面的块 (用花括号分隔 )是一个受保护的区域,这意味着它得到了特殊处理。其中一个特殊处理是,无论 **try** 块如何退出,在这个保护区域之后的 **finally** 子句中的代码总是被执行。(通过异常处理,可以用许多不同寻常的方式留下 **try** 块。)这里,**finally** 子句的意思是,“无论发生什么,始终调用 **dispose()** for **x**。”
|
||||
|
||||
在清理方法 (在本例中是 **dispose()** ) 中,还必须注意基类和成员对象清理方法的调用顺序,以防一个子对象依赖于另一个子对象。首先,按创建的相反顺序执行特定于类的所有清理工作。(一般来说,这要求基类元素仍然是可访问的。) 然后调用基类清理方法,如这所示。
|
||||
|
||||
在很多情况下,清理问题不是问题;您只需要让垃圾收集器来完成这项工作。但是,当您必须执行显式清理时,就需要勤奋和注意,因为在垃圾收集方面没有什么可以依赖的。可能永远不会调用垃圾收集器。如果是,它可以按照它想要的任何顺序回收对象。除了内存回收外,您不能依赖垃圾收集来做任何事情。如果希望进行清理,可以使用自己的清理方法,不要使用 **finalize()**。
|
||||
在很多情况下,清理问题不是问题;你只需要让垃圾收集器来完成这项工作。但是,当你必须执行显式清理时,就需要勤奋和注意,因为在垃圾收集方面没有什么可以依赖的。可能永远不会调用垃圾收集器。如果是,它可以按照它想要的任何顺序回收对象。除了内存回收外,你不能依赖垃圾收集来做任何事情。如果希望进行清理,可以使用自己的清理方法,不要使用 **finalize()**。
|
||||
|
||||
<!-- Name Hiding -->
|
||||
|
||||
@@ -692,9 +692,9 @@ doh(Milhouse)
|
||||
|
||||
````
|
||||
|
||||
**Bart** 的所有重载方法在 **Bart** 中都是可用的,尽管 **Bart** 引入了一种新的重载方法。在下一章中您将看到,使用与基类中完全相同的签名和返回类型覆盖相同名称的方法要常见得多。否则就会令人困惑。
|
||||
**Bart** 的所有重载方法在 **Bart** 中都是可用的,尽管 **Bart** 引入了一种新的重载方法。在下一章中你将看到,使用与基类中完全相同的签名和返回类型覆盖相同名称的方法要常见得多。否则就会令人困惑。
|
||||
|
||||
您已经看到了Java 5 **@Override **注释,它不是关键字,但是可以像使用关键字一样使用它。当你打算重写一个方法时,你可以选择添加这个注释,如果你不小心用了重载而不是重写,编译器会产生一个错误消息:
|
||||
你已经看到了Java 5 **@Override **注释,它不是关键字,但是可以像使用关键字一样使用它。当你打算重写一个方法时,你可以选择添加这个注释,如果你不小心用了重载而不是重写,编译器会产生一个错误消息:
|
||||
|
||||
```java
|
||||
// reuse/Lisa.java
|
||||
@@ -711,7 +711,7 @@ class Lisa extends Homer {
|
||||
|
||||
```
|
||||
|
||||
**{WillNotCompile}** 标记将该文件排除在本书的 **Gradle** 构建之外,但是如果您手工编译它,您将看到:方法不会覆盖超类中的方法, **@Override** 注释防止您意外地重载。
|
||||
**{WillNotCompile}** 标记将该文件排除在本书的 **Gradle** 构建之外,但是如果你手工编译它,你将看到:方法不会覆盖超类中的方法, **@Override** 注释防止你意外地重载。
|
||||
|
||||
<!-- Choosing Composition vs. Inheritance -->
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -97,7 +97,7 @@ public class Strategize {
|
||||
|
||||
```
|
||||
hello there?
|
||||
HELLO THERE!'
|
||||
HELLO THERE!
|
||||
Hello
|
||||
Hello there Hello there
|
||||
```
|
||||
@@ -370,7 +370,7 @@ Help!
|
||||
|
||||
**[5]** `assist()` 是静态内部类中的静态方法。
|
||||
|
||||
**[6]** 我们将 **Describe** 对象的方法引用赋值给 **Callable** , 它没有 `show()` 方法,而是 `call()` 方法。 但是,Java 似乎接受用这个看似奇怪的赋值,因为方法引用符合 **Callable** 的 `call()` 方法的签名。
|
||||
**[6]** 我们将 **Describe** 对象的方法引用赋值给 **Callable** ,它没有 `show()` 方法,而是 `call()` 方法。 但是,Java 似乎接受用这个看似奇怪的赋值,因为方法引用符合 **Callable** 的 `call()` 方法的签名。
|
||||
|
||||
**[7]** 我们现在可以通过调用 `call()` 来调用 `show()`,因为 Java 将 `call()` 映射到 `show()`。
|
||||
|
||||
@@ -1518,4 +1518,4 @@ Lambda 表达式和方法引用并没有将 Java 转换成函数式语言,而
|
||||
|
||||
|
||||
<!-- 分页 -->
|
||||
<div style="page-break-after: always;"></div>
|
||||
<div style="page-break-after: always;"></div>
|
||||
|
||||
@@ -87,7 +87,7 @@ Java 设计者面临着这样一个难题:现存的大量类库不仅为 Java
|
||||
|
||||
问题是,接口部分怎么改造呢?特别是涉及集合类接口的部分。如果你想把一个集合转换为流,直接向接口添加新方法会破坏所有老的接口实现类。
|
||||
|
||||
Java 8 采用的解决方案是:在[接口](10-Interfaces.md)中添加被 `default`(`默认`)修饰的方法。通过这种方案,设计者们可以将流式(*stream*)方法平滑地嵌入到现有类中。流方法预置的操作几乎已满足了我们平常所有的需求。流操作的类型有三种:创建流,修改流元素(中间操作, Intermediate Operations),消费流元素(终端操作, Terminal Operations)。最后一种类型通常意味着收集流元素(通常是到集合中)。
|
||||
Java 8 采用的解决方案是:在[接口](10-Interfaces.md)中添加被 `default`(`默认`)修饰的方法。通过这种方案,设计者们可以将流式(*stream*)方法平滑地嵌入到现有类中。流方法预置的操作几乎已满足了我们平常所有的需求。流操作的类型有三种:创建流,修改流元素(中间操作, Intermediate Operations),消费流元素(终端操作, Terminal Operations)。最后一种类型通常意味着收集流元素(通常是到集合中)。
|
||||
|
||||
下面我们来看下每种类型的流操作。
|
||||
|
||||
@@ -769,7 +769,7 @@ Well WELL well it IT it s S s so SO so
|
||||
|
||||
### 流元素排序
|
||||
|
||||
在 `Randoms.java` 中,我们熟识了 `sorted()` 的默认比较器实现。其实它还有另一种形式的实现:传入一个 **Comparator** 参数。代码示例:
|
||||
在 `Randoms.java` 中,我们熟识了 `sorted()` 的默认比较器实现。其实它还有另一种形式的实现:传入一个 **Comparator** 参数。代码示例:
|
||||
|
||||
```java
|
||||
// streams/SortedComparator.java
|
||||
@@ -835,19 +835,19 @@ public class Prime {
|
||||
467 479 487 491 499 503 509 521 523 541
|
||||
```
|
||||
|
||||
`rangeClosed() ` 包含了上限值。如果不能整除,即余数不等于 0,则 `noneMatch()` 操作返回 ture,如果出现任何等于 0 的则返回 false。 `noneMatch()` 操作在第一次失败之后就会退出,而不是进行全部尝试。
|
||||
`rangeClosed()` 包含了上限值。如果不能整除,即余数不等于 0,则 `noneMatch()` 操作返回 ture,如果出现任何等于 0 的则返回 `false`。 `noneMatch()` 操作在首次失败之后就会退出,而不是尝试匹配所有。
|
||||
|
||||
### 应用操作到所有元素
|
||||
### 应用函数到元素
|
||||
|
||||
`map(Function) `:将 `Function` 操作应用在输入流的每一个元素中,并将返回值传递到输出流中。
|
||||
- `map(Function)`:将函数操作应用在输入流的元素中,并将返回值传递到输出流中。
|
||||
|
||||
`mapToInt(ToIntFunction)`:操作同上,但结果是 `IntStream`。
|
||||
- `mapToInt(ToIntFunction)`:操作同上,但结果是 **IntStream**。
|
||||
|
||||
`mapToLong(ToLongFunction)`:操作同上,但结果是 `LongStream`。
|
||||
- `mapToLong(ToLongFunction)`:操作同上,但结果是 **LongStream**。
|
||||
|
||||
`mapToDouble(ToDoubleFunction)` : 操作同上,但结果是 `DoubleStream`。
|
||||
- `mapToDouble(ToDoubleFunction)`:操作同上,但结果是 **DoubleStream**。
|
||||
|
||||
在这里,我们使用 `map()` 映射多种函数到一个字符串流中:
|
||||
在这里,我们使用 `map()` 映射多种函数到一个字符串流中。代码示例:
|
||||
|
||||
```java
|
||||
// streams/FunctionMap.java
|
||||
@@ -906,13 +906,13 @@ class FunctionMap {
|
||||
5
|
||||
```
|
||||
|
||||
在“Increment”测试中,我们使用 `Integer.parseInt()` 去试图将一个字符串转化为整数。如果字符串不能转化成为整数就会抛出一个 `NumberFormatException` 异常,我们只需回过头来将原始字符串放回到输出流中。
|
||||
在“Increment”的测试中,我们使用 **Integer.**`parseInt()` 去试图将一个字符串转化为整数。如果字符串不能转化成为整数就会抛出一个 **NumberFormatException** 异常,我们只需回过头来将原始字符串放回到输出流中。
|
||||
|
||||
在以上例子中,` map()` 将一个字符串映射为另一个字符串,但是我们完全可以产生和接收类型完全不同的类型,从而改变流的数据类型。这里是一个例子:
|
||||
在以上例子中,`map()` 将一个字符串映射为另一个字符串,但是我们完全可以产生和接收类型完全不同的类型,从而改变流的数据类型。下面代码示例:
|
||||
|
||||
```java
|
||||
// streams/FunctionMap2.java
|
||||
// Different input and output types
|
||||
// Different input and output types (不同的输入输出类型)
|
||||
import java.util.*;
|
||||
import java.util.stream.*;
|
||||
class Numbered {
|
||||
@@ -945,13 +945,13 @@ Numbered(11)
|
||||
Numbered(13)
|
||||
```
|
||||
|
||||
我们获取了许多 `int` 类型整数,并通过构造器 `Numbered::new` 将它们转化成为 `Numbered` 类型。
|
||||
我们将获取到的整数通过构造器 `Numbered::new` 转化成为 `Numbered` 类型。
|
||||
|
||||
如果使用 `Function` 产生的结果是数值类型的一种,你必须使用相似的 `mapTo`-operations 操作进行替代:
|
||||
如果使用 **Function** 返回的结果是数值类型的一种,我们必须使用合适的 `mapTo数值类型` 进行替代。代码示例:
|
||||
|
||||
```java
|
||||
// streams/FunctionMap3.java
|
||||
// Producing numeric output streams
|
||||
// Producing numeric output streams( 产生数值输出流)
|
||||
import java.util.*;
|
||||
import java.util.stream.*;
|
||||
class FunctionMap3 {
|
||||
@@ -979,11 +979,11 @@ class FunctionMap3 {
|
||||
17.000000 1.900000 0.230000
|
||||
```
|
||||
|
||||
不幸的是,Java 设计者并没有尽最大的努力来消除原始类型。
|
||||
不幸的是,Java 设计者并没有尽最大努力来消除基本类型。
|
||||
|
||||
### 在 map() 期间组合流
|
||||
### 在 `map()` 期间组合流
|
||||
|
||||
假如你有一个即将到来的元素流,并且你打算对流元素使用 `map()` 函数。你已经找到了那些你在其他地方找不到的可爱的函数功能,但是这里有一个问题:这个函数功能产生一个流。你想要的只是产生一个元素的流,但是你生成的是一个元素流的流。
|
||||
假设你有一个传入的元素流,并且你打算对流元素使用 `map()` 函数。现在你已经找到了一些可爱并独一无二的函数功能,但是问题来了:这个函数功能是产生一个流。你想要的只是产生流的元素,但是你生成的是一个元素流的流。
|
||||
|
||||
`flatMap()` 做了两件事情:它获取你的流产生( stream-producing)函数,并将其应用于新到的元素(正如 `map()` 所做的),然后获取每一个流并将其“展平”为元素。所以它的输出只是元素。
|
||||
|
||||
@@ -1357,7 +1357,7 @@ Null
|
||||
- `map(Function)`:如果 **Optional** 不为空,则将 **Function** 应用于 **Optional** 的内容,并将结果返回。否则,直接返回 **Optional.empty**。
|
||||
- `flatMap(Function)`:如同 `map()` , 但是提供的映射函数将结果包装在 **Optional** 对象中,因此 `flatMap()` 不会在最后进行任何包装。
|
||||
|
||||
如上方法都不适用于数值型 **Optional**。普通流过滤器会在 **Predicate** 返回 false 时删除流元素。`Optional.filter()` 当 **Predicate** 失败时不会删除 **Optional**——而是把它留保留下来, 但将其转化为空:
|
||||
如上方法都不适用于数值型 **Optional**。普通流过滤器会在 **Predicate** 返回 false 时删除流元素。`Optional.filter()` 当 **Predicate** 失败时不会删除 **Optional**——而是把它留保留下来,但将其转化为空:
|
||||
|
||||
```java
|
||||
// streams/OptionalFilter.java
|
||||
@@ -1835,11 +1835,11 @@ public class MapCollector {
|
||||
{688=W, 309=C, 293=B, 761=N, 858=N, 668=G, 622=F, 751=N}
|
||||
```
|
||||
|
||||
**Pair** 只是一个基础的数据对象。**RandomPair** 创建了随机生成的 **Pair** 对象流。如果我们能以某种方式组合两个流, 那就再好不过了, 但 Java 在这个问题上与我们斗争。所以我创建了一个整数流,并且使用 `mapToObj()` 将其转化成为 **Pair** 流。 **capChars** 随机生成的大写字母迭代器从流开始,然后 `iterator()` 方法允许我们在 `stream()` 方法中使用它。就我所知, 这是组合多个流以生成新的对象流的唯一方法。
|
||||
**Pair** 只是一个基础的数据对象。**RandomPair** 创建了随机生成的 **Pair** 对象流。如果我们能以某种方式组合两个流,那就再好不过了,但 Java 在这个问题上与我们斗争。所以我创建了一个整数流,并且使用 `mapToObj()` 将其转化成为 **Pair** 流。 **capChars** 随机生成的大写字母迭代器从流开始,然后 `iterator()` 方法允许我们在 `stream()` 方法中使用它。就我所知,这是组合多个流以生成新的对象流的唯一方法。
|
||||
|
||||
在这里,我们只使用最简单形式的 `Collectors.toMap()`,这个方法值需要一个可以从流中获取键值对的函数。还有其他重载形式,其中一种形式是在遇到键值冲突时,需要一个函数来处理这种情况。
|
||||
|
||||
在大多数情况下,你可以在 `java.util.stream.Collectors`寻找到你想要的预先定义好的 **Collector**。在少数情况下当你找不到想要的时候,你可以使用第二种形式的 ` collect()`。 我基本上把它留作更高级的练习, 但是这里有一个例子给出了基本想法:
|
||||
在大多数情况下,你可以在 `java.util.stream.Collectors`寻找到你想要的预先定义好的 **Collector**。在少数情况下当你找不到想要的时候,你可以使用第二种形式的 ` collect()`。 我基本上把它留作更高级的练习,但是这里有一个例子给出了基本想法:
|
||||
|
||||
```java
|
||||
// streams/SpecialCollector.java
|
||||
@@ -1933,7 +1933,7 @@ Lambda 表达式中的第一个参数 `fr0` 是上一次调用 `reduce()` 的结
|
||||
- `anyMatch(Predicate)`:如果流中的一个元素根据提供的 **Predicate** 返回 true 时,结果返回为 true。这个操作将会在第一个 true 之后短路;也就是不会在发生 true 之后继续执行计算。
|
||||
- `noneMatch(Predicate)`:如果流的每个元素根据提供的 **Predicate** 都返回 false 时,结果返回为 true。这个操作将会在第一个 true 之后短路;也就是不会在发生 true 之后继续执行计算。
|
||||
|
||||
你已经在 `Prime.java` 中看到了 `noneMatch()` 的示例;` allMatch()` 和 `anyMatch()` 的用法基本上是等同的。让我们探究短路行为。为了创建消除冗余代码的 ` show()` 方法,我们必须首先发现如何概括地描述所有三个匹配器操作, 然后将其转换为称为 **Matcher** 的接口:
|
||||
你已经在 `Prime.java` 中看到了 `noneMatch()` 的示例;` allMatch()` 和 `anyMatch()` 的用法基本上是等同的。让我们探究短路行为。为了创建消除冗余代码的 ` show()` 方法,我们必须首先发现如何概括地描述所有三个匹配器操作,然后将其转换为称为 **Matcher** 的接口:
|
||||
|
||||
```java
|
||||
// streams/Matching.java
|
||||
@@ -2086,7 +2086,7 @@ you
|
||||
- `average()` :求取流元素平均值。
|
||||
- `max()` 和 `min()`:因为这些操作在数字流上面,所以不需要 **Comparator**。
|
||||
- `sum()`:对所有流元素进行求和。
|
||||
- `summaryStatistics()`:生成可能有用的数据。目前还不太清楚他们为什么觉得有必要这样做, 但是你可以直接使用方法产生所有的数据。
|
||||
- `summaryStatistics()`:生成可能有用的数据。目前还不太清楚他们为什么觉得有必要这样做,但是你可以直接使用方法产生所有的数据。
|
||||
|
||||
```java
|
||||
// streams/NumericStreamInfo.java
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,23 +2,639 @@
|
||||
|
||||
<!-- File -->
|
||||
# 第十七章 文件
|
||||
在丑陋的Java I/O编程方式诞生多年以后,Java终于简化了文件读写的基本操作。这种"困难方式"的全部细节都在[Appendix: I/O Streams]()。如果你读过这个部分,就会认同Java设计者毫不在意他们的使用者的体验这一观念。打开并读取文件对于大多数编程语言来是非常常用的,由于I/O糟糕的设计以至于
|
||||
很少有人能够在不依赖其他参考代码的情况下完成打开文件的操作。
|
||||
|
||||
好像Java设计者终于意识到了Java使用者多年来的痛苦,在Java7中对此引入了巨大的改进。这些新元素被放在**java.nio.file**包下面,过去人们通常把**nio**中的**n**理解为**new**即新的**io**,现在更应该当成是**non-blocking**非阻塞**io**(**io**就是*input/output输入/输出*)。**java.nio.file**库终于将Java文件操作带到与其他编程语言相同的水平。最重要的是Java8新增的streams与文件结合使得文件操作编程变得更加优雅。我们将看一下文件操作的两个基本组件:
|
||||
1. 文件或者目录的路径;
|
||||
2. 文件本身。
|
||||
|
||||
<!-- File and Directory Paths -->
|
||||
## 文件和目录路径
|
||||
### `Paths`
|
||||
一个**Path**对象表示一个文件或者目录的路径,是一个跨操作系统(OS)和文件系统的抽象,目的是在构造路径时不必关注底层操作系统,代码可以在不进行修改的情况下运行在不同的操作系统上。**java.nio.file.Paths**类包含一个重载方法**static get()**,该方法方法接受一系列**Strings**字符串或一个*统一资源标识符*(URI)作为参数,并且进行转换返回一个**Path**对象:
|
||||
```java
|
||||
// files/PathInfo.java
|
||||
import java.nio.file.*;
|
||||
import java.net.URI;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
public class PathInfo {
|
||||
static void show(String id, Object p) {
|
||||
System.out.println(id + ": " + p);
|
||||
}
|
||||
|
||||
static void info(Path p) {
|
||||
show("toString", p);
|
||||
show("Exists", Files.exists(p));
|
||||
show("RegularFile", Files.isRegularFile(p));
|
||||
show("Directory", Files.isDirectory(p));
|
||||
show("Absolute", p.isAbsolute());
|
||||
show("FileName", p.getFileName());
|
||||
show("Parent", p.getParent());
|
||||
show("Root", p.getRoot());
|
||||
System.out.println("******************");
|
||||
}
|
||||
public static void main(String[] args) {
|
||||
System.out.println(System.getProperty("os.name"));
|
||||
info(Paths.get("C:", "path", "to", "nowhere", "NoFile.txt"));
|
||||
Path p = Paths.get("PathInfo.java");
|
||||
info(p);
|
||||
Path ap = p.toAbsolutePath();
|
||||
info(ap);
|
||||
info(ap.getParent());
|
||||
try {
|
||||
info(p.toRealPath());
|
||||
} catch(IOException e) {
|
||||
System.out.println(e);
|
||||
}
|
||||
URI u = p.toUri();
|
||||
System.out.println("URI: " + u);
|
||||
Path puri = Paths.get(u);
|
||||
System.out.println(Files.exists(puri));
|
||||
File f = ap.toFile(); // Don't be fooled
|
||||
}
|
||||
}
|
||||
|
||||
/* 输出:
|
||||
Windows 10
|
||||
toString: C:\path\to\nowhere\NoFile.txt
|
||||
Exists: false
|
||||
RegularFile: false
|
||||
Directory: false
|
||||
Absolute: true
|
||||
FileName: NoFile.txt
|
||||
Parent: C:\path\to\nowhere
|
||||
Root: C:\
|
||||
******************
|
||||
toString: PathInfo.java
|
||||
Exists: true
|
||||
RegularFile: true
|
||||
Directory: false
|
||||
Absolute: false
|
||||
FileName: PathInfo.java
|
||||
Parent: null
|
||||
Root: null
|
||||
******************
|
||||
toString: C:\Users\Bruce\Documents\GitHub\onjava\
|
||||
ExtractedExamples\files\PathInfo.java
|
||||
Exists: true
|
||||
RegularFile: true
|
||||
Directory: false
|
||||
Absolute: true
|
||||
FileName: PathInfo.java
|
||||
Parent: C:\Users\Bruce\Documents\GitHub\onjava\
|
||||
ExtractedExamples\files
|
||||
Root: C:\
|
||||
******************
|
||||
toString: C:\Users\Bruce\Documents\GitHub\onjava\
|
||||
ExtractedExamples\files
|
||||
Exists: true
|
||||
RegularFile: false
|
||||
Directory: true
|
||||
Absolute: true
|
||||
FileName: files
|
||||
Parent: C:\Users\Bruce\Documents\GitHub\onjava\
|
||||
ExtractedExamples
|
||||
Root: C:\
|
||||
******************
|
||||
toString: C:\Users\Bruce\Documents\GitHub\onjava\
|
||||
ExtractedExamples\files\PathInfo.java
|
||||
Exists: true
|
||||
RegularFile: true
|
||||
Directory: false
|
||||
Absolute: true
|
||||
FileName: PathInfo.java
|
||||
Parent: C:\Users\Bruce\Documents\GitHub\onjava\
|
||||
ExtractedExamples\files
|
||||
Root: C:\
|
||||
******************
|
||||
URI: file:///C:/Users/Bruce/Documents/GitHub/onjava/
|
||||
ExtractedExamples/files/PathInfo.java
|
||||
true
|
||||
*/
|
||||
```
|
||||
|
||||
我已经在这一章第一个程序的**main()**方法添加了第一行用于展示操作系统的名称,因此你可以看到不同操作系统之间存在哪些差异。理想情况下,差别会相对较小,并且使用**/**或者**\\**路径分隔符进行分隔。你可以看到我运行在Windows 10上的程序输出。
|
||||
|
||||
当**toString()**方法生成完整形式的路径,你可以看到**getFileName()** 方法总是返回当前文件名。
|
||||
通过使用**Files**工具类(我们接下类将会更多的使用它),可以测试一个文件是否存在,测试是否是一个"真正"的文件还是一个目录等等。"Nofile.txt"这个示例展示我们描述的文件可能并不在指定的位置;这样可以允许你创建一个新的路径。"PathInfo.java"存在于当前目录中,最初它只是没有路径的文件名,但它仍然被检测为"存在"。一旦我们将其转换为绝对路径,我们将会得到一个从"C:"盘(因为我们是在Windows机器下进行测试)开始的完整路径,现在它也拥有一个父路径。“真实”路径的定义在文档中有点模糊,因为它取决于具体的文件系统。例如,如果文件名不区分大小写,即使路径由于大小写的缘故而不是完全相同,也可能得到肯定的匹配结果。在这样的平台上,**toRealPath()** 将返回实际情况下的**Path**,并且还会删除任何冗余元素。
|
||||
|
||||
这里你会看到**URI**看起来只能用于描述文件,实际上**URI**可以用于描述更多的东西;通过[维基百科]()可以了解更多细节。现在我们成功地将**URI**转为一个**Path**对象。
|
||||
|
||||
最后,你会在**Path**中看到一些有点欺骗的东西,这就是调用**toFile()**方法会生成一个**File**对象。听起来似乎可以得到一个类似文件的东西(毕竟被称为**File**),但是这个方法的存在仅仅是为了向后兼容。虽然看上去应该被称为"路径",实际上却应该表示目录或者文件本身。这是个非常草率并且令人困惑的命名,但是由于**java.nio.file**的存在我们可以安全的忽略它的存在。
|
||||
|
||||
### 选取路径部分片段
|
||||
**Path**对象可以非常容易的生成路径的某一部分:
|
||||
|
||||
```java
|
||||
// files/PartsOfPaths.java
|
||||
import java.nio.file.*;
|
||||
|
||||
public class PartsOfPaths {
|
||||
public static void main(String[] args) {
|
||||
System.out.println(System.getProperty("os.name"));
|
||||
Path p = Paths.get("PartsOfPaths.java").toAbsolutePath();
|
||||
for(int i = 0; i < p.getNameCount(); i++)
|
||||
System.out.println(p.getName(i));
|
||||
System.out.println("ends with '.java': " +
|
||||
p.endsWith(".java"));
|
||||
for(Path pp : p) {
|
||||
System.out.print(pp + ": ");
|
||||
System.out.print(p.startsWith(pp) + " : ");
|
||||
System.out.println(p.endsWith(pp));
|
||||
}
|
||||
System.out.println("Starts with " + p.getRoot() + " " + p.startsWith(p.getRoot()));
|
||||
}
|
||||
}
|
||||
|
||||
/* 输出:
|
||||
Windows 10
|
||||
Users
|
||||
Bruce
|
||||
Documents
|
||||
GitHub
|
||||
on-java
|
||||
ExtractedExamples
|
||||
files
|
||||
PartsOfPaths.java
|
||||
ends with '.java': false
|
||||
Users: false : false
|
||||
Bruce: false : false
|
||||
Documents: false : false
|
||||
GitHub: false : false
|
||||
on-java: false : false
|
||||
ExtractedExamples: false : false
|
||||
files: false : false
|
||||
PartsOfPaths.java: false : true
|
||||
Starts with C:\ true
|
||||
*/
|
||||
|
||||
```
|
||||
可以通过**getName()**来索引**Path**的各个部分,直到达到上限**getNameCount()**。**Path**也继承了**Iterable**接口,因此我们也可以通过增强的for循环进行遍历。请注意,即使路径以 **.java**结尾,使用**endsWith()** 方法也会返回**false**。这是因为使用**endsWith()** 比较的是整个路径部分,而不会包含文件路径的后缀。通过使用**startsWith()** 和**endsWith()**也可以完成路径的遍历。但是我们可以看到,遍历**Path**对象并不包含根路径,只有使用
|
||||
**startsWith()**检测根路径时才会返回**true**。
|
||||
|
||||
### 路径分析
|
||||
**Files**工具类包含一系列完整的方法用于获得**Path**相关的信息。
|
||||
```java
|
||||
// files/PathAnalysis.java
|
||||
import java.nio.file.*;
|
||||
import java.io.IOException;
|
||||
|
||||
public class PathAnalysis {
|
||||
static void say(String id, Object result) {
|
||||
System.out.print(id + ": ");
|
||||
System.out.println(result);
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
System.out.println(System.getProperty("os.name"));
|
||||
Path p = Paths.get("PathAnalysis.java").toAbsolutePath();
|
||||
say("Exists", Files.exists(p));
|
||||
say("Directory", Files.isDirectory(p));
|
||||
say("Executable", Files.isExecutable(p));
|
||||
say("Readable", Files.isReadable(p));
|
||||
say("RegularFile", Files.isRegularFile(p));
|
||||
say("Writable", Files.isWritable(p));
|
||||
say("notExists", Files.notExists(p));
|
||||
say("Hidden", Files.isHidden(p));
|
||||
say("size", Files.size(p));
|
||||
say("FileStore", Files.getFileStore(p));
|
||||
say("LastModified: ", Files.getLastModifiedTime(p));
|
||||
say("Owner", Files.getOwner(p));
|
||||
say("ContentType", Files.probeContentType(p));
|
||||
say("SymbolicLink", Files.isSymbolicLink(p));
|
||||
if(Files.isSymbolicLink(p))
|
||||
say("SymbolicLink", Files.readSymbolicLink(p));
|
||||
if(FileSystems.getDefault().supportedFileAttributeViews().contains("posix"))
|
||||
say("PosixFilePermissions",
|
||||
Files.getPosixFilePermissions(p));
|
||||
}
|
||||
}
|
||||
|
||||
/* 输出:
|
||||
Windows 10
|
||||
Exists: true
|
||||
Directory: false
|
||||
Executable: true
|
||||
Readable: true
|
||||
RegularFile: true
|
||||
Writable: true
|
||||
notExists: false
|
||||
Hidden: false
|
||||
size: 1631
|
||||
FileStore: SSD (C:)
|
||||
LastModified: : 2017-05-09T12:07:00.428366Z
|
||||
Owner: MINDVIEWTOSHIBA\Bruce (User)
|
||||
ContentType: null
|
||||
SymbolicLink: false
|
||||
*/
|
||||
```
|
||||
在调用最后一个测试方法**getPosixFilePermissions()** 之前我们需要确认一下当前文件系统是否支持**Posix**接口,否则会抛出运行时异常。
|
||||
|
||||
### **Paths**的增减修改
|
||||
我们必须能通过对**Path**对象增加或者删除一部分来构造一个新的**Path**对象。我们使用**relativize()**构造一个路径与给定路径的相对路径,使用**resolve()**解析为一个新的**Path**对象(不一定是“可发现”的名称)。对于下面代码中的示例,我使用**relativize()** 方法从所有的输出中移除根路径,部分原因是为了示范,部分原因是为了简化输出结果,这说明你可以使用该方法将绝对路径转为相对路径。
|
||||
这个版本的代码中包含**id**,以便于跟踪输出结果:
|
||||
|
||||
```java
|
||||
// files/AddAndSubtractPaths.java
|
||||
import java.nio.file.*;
|
||||
import java.io.IOException;
|
||||
|
||||
public class AddAndSubtractPaths {
|
||||
static Path base = Paths.get("..", "..", "..").toAbsolutePath().normalize();
|
||||
|
||||
static void show(int id, Path result) {
|
||||
if(result.isAbsolute())
|
||||
System.out.println("(" + id + ")r " + base.relativize(result));
|
||||
else
|
||||
System.out.println("(" + id + ") " + result);
|
||||
try {
|
||||
System.out.println("RealPath: " + result.toRealPath());
|
||||
} catch(IOException e) {
|
||||
System.out.println(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println(System.getProperty("os.name"));
|
||||
System.out.println(base);
|
||||
Path p = Paths.get("AddAndSubtractPaths.java").toAbsolutePath();
|
||||
show(1, p);
|
||||
Path convoluted = p.getParent().getParent()
|
||||
.resolve("strings").resolve("..")
|
||||
.resolve(p.getParent().getFileName());
|
||||
show(2, convoluted);
|
||||
show(3, convoluted.normalize());
|
||||
Path p2 = Paths.get("..", "..");
|
||||
show(4, p2);
|
||||
show(5, p2.normalize());
|
||||
show(6, p2.toAbsolutePath().normalize());
|
||||
Path p3 = Paths.get(".").toAbsolutePath();
|
||||
Path p4 = p3.resolve(p2);
|
||||
show(7, p4);
|
||||
show(8, p4.normalize());
|
||||
Path p5 = Paths.get("").toAbsolutePath();
|
||||
show(9, p5);
|
||||
show(10, p5.resolveSibling("strings"));
|
||||
show(11, Paths.get("nonexistent"));
|
||||
}
|
||||
}
|
||||
|
||||
/* 输出:
|
||||
Windows 10
|
||||
C:\Users\Bruce\Documents\GitHub
|
||||
(1)r onjava\
|
||||
ExtractedExamples\files\AddAndSubtractPaths.java
|
||||
RealPath: C:\Users\Bruce\Documents\GitHub\onjava\
|
||||
ExtractedExamples\files\AddAndSubtractPaths.java
|
||||
(2)r on-java\ExtractedExamples\strings\..\files
|
||||
RealPath: C:\Users\Bruce\Documents\GitHub\onjava\
|
||||
ExtractedExamples\files
|
||||
(3)r on-java\ExtractedExamples\files
|
||||
RealPath: C:\Users\Bruce\Documents\GitHub\onjava\
|
||||
ExtractedExamples\files
|
||||
(4) ..\..
|
||||
RealPath: C:\Users\Bruce\Documents\GitHub\on-java
|
||||
(5) ..\..
|
||||
RealPath: C:\Users\Bruce\Documents\GitHub\on-java
|
||||
(6)r on-java
|
||||
RealPath: C:\Users\Bruce\Documents\GitHub\on-java
|
||||
(7)r on-java\ExtractedExamples\files\.\..\..
|
||||
RealPath: C:\Users\Bruce\Documents\GitHub\on-java
|
||||
(8)r on-java
|
||||
RealPath: C:\Users\Bruce\Documents\GitHub\on-java
|
||||
(9)r on-java\ExtractedExamples\files
|
||||
RealPath: C:\Users\Bruce\Documents\GitHub\onjava\
|
||||
ExtractedExamples\files
|
||||
(10)r on-java\ExtractedExamples\strings
|
||||
RealPath: C:\Users\Bruce\Documents\GitHub\onjava\
|
||||
ExtractedExamples\strings
|
||||
(11) nonexistent
|
||||
java.nio.file.NoSuchFileException:
|
||||
C:\Users\Bruce\Documents\GitHub\onjava\
|
||||
ExtractedExamples\files\nonexistent
|
||||
*/
|
||||
```
|
||||
我还为**toRealPath()** 添加了进一步的测试,这是为了扩展和规则化,防止路径不存在以免产生运行时异常。
|
||||
|
||||
<!-- File Systems -->
|
||||
|
||||
<!-- Directories -->
|
||||
## 目录
|
||||
**Files**工具类类包含大部分我们需要的目录操作和文件操作方法。出于某种原因,它们没有包含删除目录树相关的方法,因此我们将实现并将其添加到**onjava**库中。
|
||||
```java
|
||||
// onjava/RmDir.java
|
||||
package onjava;
|
||||
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.io.IOException;
|
||||
|
||||
public class RmDir {
|
||||
public static void rmdir(Path dir) throws IOException {
|
||||
Files.walkFileTree(dir, new SimpleFileVisitor<Path>() {
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||
Files.delete(file);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
|
||||
Files.delete(dir);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
删除目录树的方法实现依赖于**Files.walkFileTree()**,"walking"意味着遍历整个子目录和文件。*Visitor*设计模式提供了一种标准机制来访问集合中的某个对象,然后你需要提供在每个对象上执行的操作。
|
||||
此操作的定义取决于实现的**FileVisitor**的四个抽象方法,包括:
|
||||
1. **preVisitDirectory()**:在访问目录中条目之前在目录上运行。
|
||||
2. **visitFile()**:运行目录中的每一个文件。
|
||||
3. **visitFileFailed()**:调用无法访问的文件。
|
||||
4. **postVisitDirectory()**:在访问目录中条目之后在目录上运行,包括所有的子目录。
|
||||
|
||||
为了简化,**java.nio.file.SimpleFileVisitor** 提供了所有方法的默认实现。这样,在我们的匿名内部类中,我们只需要重写非标准行为的方法:**visitFile()** 和**postVisitDirectory()** 实现删除文件和删除目录。两者都应该返回标志位决定是否继续访问(这样就可以继续访问,直到找到所需要的)。
|
||||
|
||||
作为探索目录操作的一部分,现在我们可以有条件地删除已存在的目录。在以下例子中,**makeVariant()** 接受基本目录测试,并通过旋转部件列表生成不同的子目录路径。这些旋转与路径分隔符粘**sep** 使用**String.join()**贴在一起,然后返回一个**Path**对象。
|
||||
|
||||
```java
|
||||
// files/Directories.java
|
||||
import java.util.*;
|
||||
import java.nio.file.*;
|
||||
import onjava.RmDir;
|
||||
|
||||
public class Directories {
|
||||
static Path test = Paths.get("test");
|
||||
static String sep = FileSystems.getDefault().getSeparator();
|
||||
static List<String> parts = Arrays.asList("foo", "bar", "baz", "bag");
|
||||
|
||||
static Path makeVariant() {
|
||||
Collections.rotate(parts, 1);
|
||||
return Paths.get("test", String.join(sep, parts));
|
||||
}
|
||||
|
||||
static void refreshTestDir() throws Exception {
|
||||
if(Files.exists(test))
|
||||
RmDir.rmdir(test);
|
||||
if(!Files.exists(test))
|
||||
Files.createDirectory(test);
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
refreshTestDir();
|
||||
Files.createFile(test.resolve("Hello.txt"));
|
||||
Path variant = makeVariant();
|
||||
// Throws exception (too many levels):
|
||||
try {
|
||||
Files.createDirectory(variant);
|
||||
} catch(Exception e) {
|
||||
System.out.println("Nope, that doesn't work.");
|
||||
}
|
||||
populateTestDir();
|
||||
Path tempdir = Files.createTempDirectory(test, "DIR_");
|
||||
Files.createTempFile(tempdir, "pre", ".non");
|
||||
Files.newDirectoryStream(test).forEach(System.out::println);
|
||||
System.out.println("*********");
|
||||
Files.walk(test).forEach(System.out::println);
|
||||
}
|
||||
|
||||
static void populateTestDir() throws Exception {
|
||||
for(int i = 0; i < parts.size(); i++) {
|
||||
Path variant = makeVariant();
|
||||
if(!Files.exists(variant)) {
|
||||
Files.createDirectories(variant);
|
||||
Files.copy(Paths.get("Directories.java"),
|
||||
variant.resolve("File.txt"));
|
||||
Files.createTempFile(variant, null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 输出:
|
||||
Nope, that doesn't work.
|
||||
test\bag
|
||||
test\bar
|
||||
test\baz
|
||||
test\DIR_5142667942049986036
|
||||
test\foo
|
||||
test\Hello.txt
|
||||
*********
|
||||
test
|
||||
test\bag
|
||||
test\bag\foo
|
||||
test\bag\foo\bar
|
||||
test\bag\foo\bar\baz
|
||||
test\bag\foo\bar\baz\8279660869874696036.tmp
|
||||
test\bag\foo\bar\baz\File.txt
|
||||
test\bar
|
||||
test\bar\baz
|
||||
test\bar\baz\bag
|
||||
test\bar\baz\bag\foo
|
||||
test\bar\baz\bag\foo\1274043134240426261.tmp
|
||||
test\bar\baz\bag\foo\File.txt
|
||||
test\baz
|
||||
test\baz\bag
|
||||
test\baz\bag\foo
|
||||
test\baz\bag\foo\bar
|
||||
test\baz\bag\foo\bar\6130572530014544105.tmp
|
||||
test\baz\bag\foo\bar\File.txt
|
||||
test\DIR_5142667942049986036
|
||||
test\DIR_5142667942049986036\pre7704286843227113253.non
|
||||
test\foo
|
||||
test\foo\bar
|
||||
test\foo\bar\baz
|
||||
test\foo\bar\baz\bag
|
||||
test\foo\bar\baz\bag\5412864507741775436.tmp
|
||||
test\foo\bar\baz\bag\File.txt
|
||||
test\Hello.txt
|
||||
*/
|
||||
```
|
||||
首先,**refreshTestDir()**用于检测**test**目录是否已经存在。果是这样,则使用我们新工具类**rmdir()** 删除其整个目录。检查是否**exists**是多余的,但我想说明一点,因为如果你对于已经存在的目录调用**createDirectory()** 将会抛出异常。**createFile()** 使用参数**Path** 创建一个空文件; **resolve()** 将文件名添加到测试路径的末尾。
|
||||
|
||||
我们尝试使用**createDirectory()** 来创建多级路径,但是这样会抛出异常,因为这个方法只能创建单级路劲。我已经将**populateTestDir()**作为一个单独的方法,因为它将在后面的例子中被重用。对于每一个变量**variant**,我们都能使用**createDirectories()** 创建完整的目录路径,然后使用此文件的副本以不同的目标名称填充该终端目录。然后我们使用**createTempFile()** 生成一个临时文件。
|
||||
|
||||
在调用**populateTestDir()** 之后,我们在**test**目录下面下面创建一个临时目录。请注意,**createTempDirectory()** 只有名称的前缀选项。与**createTempFile()** 不同,我们再次使用它将临时文件放入新的临时目录中。你可以从输出中看到,如果未指定后缀,将默认使用".tmp"作为后缀。
|
||||
|
||||
为了展示结果,我们首先使用看起来很有可能的**newDirectoryStream()**,但事实证明这个方法只是返回**test** 目录内容的Stream流,并没有更多的内容。要获取目录树的全部内容的流,请使用**Files.walk()**。
|
||||
|
||||
<!-- File Systems -->
|
||||
## 文件系统
|
||||
为了完整起见,我们需要一种方法查找有关文件系统的其他信息。在这里,我们使用ileSystems工具类获取"默认"文件系统,但你同样也可以在**Path**对象上调用**getFileSystem()** 以获取创建该**Path** 的文件系统。你可以获得给定*URI*的文件系统,还可以构建新的文件系统(对于支持它的操作系统)。
|
||||
```java
|
||||
// files/FileSystemDemo.java
|
||||
import java.nio.file.*;
|
||||
|
||||
public class FileSystemDemo {
|
||||
static void show(String id, Object o) {
|
||||
System.out.println(id + ": " + o);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println(System.getProperty("os.name"));
|
||||
FileSystem fsys = FileSystems.getDefault();
|
||||
for(FileStore fs : fsys.getFileStores())
|
||||
show("File Store", fs);
|
||||
for(Path rd : fsys.getRootDirectories())
|
||||
show("Root Directory", rd);
|
||||
show("Separator", fsys.getSeparator());
|
||||
show("UserPrincipalLookupService",
|
||||
fsys.getUserPrincipalLookupService());
|
||||
show("isOpen", fsys.isOpen());
|
||||
show("isReadOnly", fsys.isReadOnly());
|
||||
show("FileSystemProvider", fsys.provider());
|
||||
show("File Attribute Views",
|
||||
fsys.supportedFileAttributeViews());
|
||||
}
|
||||
}
|
||||
/* 输出:
|
||||
Windows 10
|
||||
File Store: SSD (C:)
|
||||
Root Directory: C:\
|
||||
Root Directory: D:\
|
||||
Separator: \
|
||||
UserPrincipalLookupService:
|
||||
sun.nio.fs.WindowsFileSystem$LookupService$1@15db9742
|
||||
isOpen: true
|
||||
isReadOnly: false
|
||||
FileSystemProvider:
|
||||
sun.nio.fs.WindowsFileSystemProvider@6d06d69c
|
||||
File Attribute Views: [owner, dos, acl, basic, user]
|
||||
*/
|
||||
```
|
||||
一个**FileSystem**对象也能生成**WatchService** 和**WatchService** 对象,将会在接下来两章中详细讲解。
|
||||
|
||||
<!-- Watching a Path -->
|
||||
## 路径监听
|
||||
通过**WatchService** 可以设置一个进程对目录中的更改做出响应。在这个例子中,**delTxtFiles()** 作为一个单独的任务执行,该任务将遍历整个目录并删除以 **.txt** 结尾的所有文件,**WatchService** 会对文件删除操作做出反应:
|
||||
|
||||
```java
|
||||
// files/PathWatcher.java
|
||||
// {ExcludeFromGradle}
|
||||
import java.io.IOException;
|
||||
import java.nio.file.*;
|
||||
import static java.nio.file.StandardWatchEventKinds.*;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
public class PathWatcher {
|
||||
static Path test = Paths.get("test");
|
||||
|
||||
static void delTxtFiles() {
|
||||
try {
|
||||
Files.walk(test)
|
||||
.filter(f ->
|
||||
f.toString()
|
||||
.endsWith(".txt"))
|
||||
.forEach(f -> {
|
||||
try {
|
||||
System.out.println("deleting " + f);
|
||||
Files.delete(f);
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
Directories.refreshTestDir();
|
||||
Directories.populateTestDir();
|
||||
Files.createFile(test.resolve("Hello.txt"));
|
||||
WatchService watcher = FileSystems.getDefault().newWatchService();
|
||||
test.register(watcher, ENTRY_DELETE);
|
||||
Executors.newSingleThreadScheduledExecutor()
|
||||
.schedule(PathWatcher::delTxtFiles,
|
||||
250, TimeUnit.MILLISECONDS);
|
||||
WatchKey key = watcher.take();
|
||||
for(WatchEvent evt : key.pollEvents()) {
|
||||
System.out.println("evt.context(): " + evt.context() +
|
||||
"\nevt.count(): " + evt.count() +
|
||||
"\nevt.kind(): " + evt.kind());
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Output:
|
||||
deleting test\bag\foo\bar\baz\File.txt
|
||||
deleting test\bar\baz\bag\foo\File.txt
|
||||
deleting test\baz\bag\foo\bar\File.txt
|
||||
deleting test\foo\bar\baz\bag\File.txt
|
||||
deleting test\Hello.txt
|
||||
evt.context(): Hello.txt
|
||||
evt.count(): 1
|
||||
evt.kind(): ENTRY_DELETE
|
||||
*/
|
||||
```
|
||||
|
||||
**delTxtFiles()** 中的**try** 代码块看起来有些多余,因为它们捕获的是同一种类型的异常,外部的**try** 语句似乎已经足够了。然而出于某种原因,Java要求两者都必须存在(这也可能是一个bug)。还要注意的是在**filter()** 中,我们必须显式地使用**f.toString()** 转为字符串,否则我们调用**endsWith()** 将会与整个**Path**对象进行比较,而不是路径名称字符串的一部分进行比较。
|
||||
|
||||
一旦我们从**FileSystem** 中得到了**WatchService** 对象,我们将其注册到**tets** 路径以及我们感兴趣的项目的变量参数列表中,可以选择**ENTRY_CREATE**,**ENTRY_DELETE** 或**ENTRY_MODIFY**(其中创建和删除不属于修改)。
|
||||
|
||||
因为接下来对**watcher.take()** 的调用会在发生某些事情之前停止所有操作,所以我们希望**deltxtfiles()** 能够并行运行以便生成我们感兴趣的事件。为了实现这个目的,我通过调用**Executors.newSingleThreadScheduledExecutor()**产生一个**ScheduledExecutorService** 对象,然后调用**schedule()** 方法传递所需函数的方法引用,设置以及在运行之前应该等待的时间。
|
||||
|
||||
此时,**watcher.take()** 将等待并阻塞在这里。当目标事件发生时,会返回一个包含**WatchEvent**的**Watchkey**对象。展示的这三种方法是对**WatchEvent** 执行的全部操作。
|
||||
|
||||
查看输出的具体内容。即使我们正在删除以 **.txt** 结尾的文件,在**hello.txt** 被删除之前,**WatchService** 也不会被触发。你可能认为,如果说"监视这个目录",自然会包含整个目录和下面子目录,但实际上的:只会监视给定的目录,而不是下面的所有内容。如果需要监视整个树目录,必须在整个树的每个子目录上放置一个**Watchservice**。
|
||||
|
||||
```java
|
||||
// files/TreeWatcher.java
|
||||
// {ExcludeFromGradle}
|
||||
import java.io.IOException;
|
||||
import java.nio.file.*;
|
||||
import static java.nio.file.StandardWatchEventKinds.*;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
public class TreeWatcher {
|
||||
|
||||
static void watchDir(Path dir) {
|
||||
try {
|
||||
WatchService watcher =
|
||||
FileSystems.getDefault().newWatchService();
|
||||
dir.register(watcher, ENTRY_DELETE);
|
||||
Executors.newSingleThreadExecutor().submit(() -> {
|
||||
try {
|
||||
WatchKey key = watcher.take();
|
||||
for(WatchEvent evt : key.pollEvents()) {
|
||||
System.out.println(
|
||||
"evt.context(): " + evt.context() +
|
||||
"\nevt.count(): " + evt.count() +
|
||||
"\nevt.kind(): " + evt.kind());
|
||||
System.exit(0);
|
||||
}
|
||||
} catch(InterruptedException e) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
Directories.refreshTestDir();
|
||||
Directories.populateTestDir();
|
||||
Files.walk(Paths.get("test"))
|
||||
.filter(Files::isDirectory)
|
||||
.forEach(TreeWatcher::watchDir);
|
||||
PathWatcher.delTxtFiles();
|
||||
}
|
||||
}
|
||||
|
||||
/* Output:
|
||||
deleting test\bag\foo\bar\baz\File.txt
|
||||
deleting test\bar\baz\bag\foo\File.txt
|
||||
evt.context(): File.txt
|
||||
evt.count(): 1
|
||||
evt.kind(): ENTRY_DELETE
|
||||
*/
|
||||
```
|
||||
|
||||
在**watchDir()** 方法中给**WatchSevice**提供参数**ENTRY_DELETE**,并启动一个独立的线程来监视该**atchserviceW**。这里我们没有使用**schedule()** 进行启动,而是使用**submit()** 启动线程。我们遍历整个目录树,并将**watchDir()** 应用于每个子目录。现在,当我们运行**deltxtfiles()** 时,其中一个**Watchservice**s 会检测到每一次文件删除。
|
||||
|
||||
<!-- Finding Files -->
|
||||
## 文件查找
|
||||
|
||||
@@ -70,7 +70,7 @@ Triangle.draw()
|
||||
|
||||
基类中包含 `draw()` 方法,它通过传递 `this` 参数传递给 `System.out.println()`,间接地使用 `toString()` 打印类标识符(注意:这里将 `toString()` 声明为了 `abstract`,以此强制继承者覆盖改方法,并防止对 `Shape` 的实例化)。如果某个对象出现在字符串表达式中(涉及"+"和字符串对象的表达式),`toString()` 方法就会被自动调用,以生成表示该对象的 `String`。每个派生类都要覆盖(从 `Object` 继承来的)`toString()` 方法,这样 `draw()` 在不同情况下就打印出不同的消息(多态)。
|
||||
|
||||
这个例子中,在把 `Shape` 对象放入 `Stream<Shape>` 中时就会进行向上转型(隐式),但在向上转型的时候也丢失了这些对象的具体类型。对 `steam` 而言,它们只是 `Shape` 对象。
|
||||
这个例子中,在把 `Shape` 对象放入 `Stream<Shape>` 中时就会进行向上转型(隐式),但在向上转型的时候也丢失了这些对象的具体类型。对 `stream` 而言,它们只是 `Shape` 对象。
|
||||
|
||||
严格来说,`Stream<Shape>` 实际上是把放入其中的所有对象都当做 `Object` 对象来持有,只是取元素时会自动将其类型转为 `Shape`。这也是 RTTI 最基本的使用形式,因为在 Java 中,所有类型转换的正确性检查都是在运行时进行的。这也正是 RTTI 的含义所在:在运行时,识别一个对象的类型。
|
||||
|
||||
@@ -551,7 +551,305 @@ public class ClassCasts {
|
||||
|
||||
Java类库中另一个没有任何用处的特性就是 `Class.asSubclass()`,该方法允许你将一个 `Class` 对象转型为更加具体的类型。
|
||||
|
||||
<!-- Checking Before a Cast -->
|
||||
## 类型转换前先做检查
|
||||
|
||||
直到现在,我们已知的RTTI类型包括:
|
||||
|
||||
1. 传统的类型转换,如 “`(Shape)`”,由RTTI确保转换的正确性,如果执行了一个错误的类型转换,就会抛出一个 `ClassCastException` 异常。
|
||||
|
||||
2. 代表对象类型的 `Class` 对象. 通过查询 `Class` 对象可以获取运行时所需的信息.
|
||||
|
||||
在C++中,经典的类型转换 “`(Shape)`” 并不使用 RTTI. 它只是简单地告诉编译器将这个对象作为新的类型对待. 而 Java 会进行类型检查,这种类型转换一般被称作“类型安全的向下转型”。之所以称作“向下转型”,是因为传统上类继承图是这么画的。将 `Circle` 转换为 `Shape` 是一次向上转型, 将 `Shape` 转换为 `Circle` 是一次向下转型。但是, 因为我们知道 `Circle` 肯定是一个 `Shape`,所以编译器允许我们自由地做向上转型的赋值操作,且不需要任何显示的转型操作。当你给编译器一个 `Shape` 的时候,编译器并不知道它到底是什么类型的 `Shape`——它可能是 `Shape`,也可能是 `Shape` 的子类型,例如 `Circle`、`Square`、`Triangle` 或某种其他的类型。在编译期,编译器只能知道它是 `Shape`。因此,你需要使用显式的类型转换,以告知编译器你想转换的特定类型,否则编译器就不允许你执行向下转型赋值。 (编译器将会检查向下转型是否合理,因此它不允许向下转型到实际上不是待转型类型的子类的类型上)。
|
||||
|
||||
RTTI 在 Java 中还有第三种形式,那就是关键字 `instanceof`。它返回一个布尔值,告诉我们对象是不是某个特定类型的实例,可以用提问的方式使用它,就像这个样子:
|
||||
|
||||
```java
|
||||
if(x instanceof Dog)
|
||||
((Dog)x).bark();
|
||||
```
|
||||
|
||||
在将 `x` 转型为 `Dog` 之前,`if` 语句会先检查 `x` 是否是 `Dog` 类型的对象。进行向下转型前,如果没有其他信息可以告诉你这个对象是什么类型,那么使用 `instanceof` 是非常重要的,否则会得到一个 `ClassCastException` 异常。
|
||||
|
||||
一般,可能想要查找某种类型(比如要找三角形,并填充为紫色),这时可以轻松地使用 `instanceof` 来计数所有对象。举个例子,假如你有一个类的继承体系,描述了 `Pet`(以及它们的主人,在后面一个例子中会用到这个特性)。在这个继承体系中的每个 `Individual` 都有一个 `id` 和一个可选的名字。尽管下面的类都继承自 `Individual`,但是 `Individual` 类复杂性较高,因此其代码将放在[附录:容器](./Appendix-Collection-Topics)中进行解释说明。正如你所看到的,此处并不需要去了解 `Individual` 的代码——你只需了解你可以创建其具名或不具名的对象,并且每个 `Individual` 都有一个 `id()` 方法,如果你没有为 `Individual` 提供名字,`toString()` 方法只产生类型名。
|
||||
|
||||
下面是继承自 `Individual` 的类的继承体系:
|
||||
|
||||
```java
|
||||
// typeinfo/pets/Person.java
|
||||
package typeinfo.pets;
|
||||
|
||||
public class Person extends Individual {
|
||||
public Person(String name) { super(name); }
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
// typeinfo/pets/Pet.java
|
||||
package typeinfo.pets;
|
||||
|
||||
public class Pet extends Individual {
|
||||
public Pet(String name) { super(name); }
|
||||
public Pet() { super(); }
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
// typeinfo/pets/Dog.java
|
||||
package typeinfo.pets;
|
||||
|
||||
public class Dog extends Pet {
|
||||
public Dog(String name) { super(name); }
|
||||
public Dog() { super(); }
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
// typeinfo/pets/Mutt.java
|
||||
package typeinfo.pets;
|
||||
|
||||
public class Mutt extends Dog {
|
||||
public Mutt(String name) { super(name); }
|
||||
public Mutt() { super(); }
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
```java
|
||||
// typeinfo/pets/Pug.java
|
||||
package typeinfo.pets;
|
||||
|
||||
public class Pug extends Dog {
|
||||
public Pug(String name) { super(name); }
|
||||
public Pug() { super(); }
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
// typeinfo/pets/Cat.java
|
||||
package typeinfo.pets;
|
||||
|
||||
public class Cat extends Pet {
|
||||
public Cat(String name) { super(name); }
|
||||
public Cat() { super(); }
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
// typeinfo/pets/EgyptianMau.java
|
||||
package typeinfo.pets;
|
||||
|
||||
public class EgyptianMau extends Cat {
|
||||
public EgyptianMau(String name) { super(name); }
|
||||
public EgyptianMau() { super(); }
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
// typeinfo/pets/Manx.java
|
||||
package typeinfo.pets;
|
||||
|
||||
public class Manx extends Cat {
|
||||
public Manx(String name) { super(name); }
|
||||
public Manx() { super(); }
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
// typeinfo/pets/Cymric.java
|
||||
package typeinfo.pets;
|
||||
|
||||
public class Cymric extends Manx {
|
||||
public Cymric(String name) { super(name); }
|
||||
public Cymric() { super(); }
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
// typeinfo/pets/Rodent.java
|
||||
package typeinfo.pets;
|
||||
|
||||
public class Rodent extends Pet {
|
||||
public Rodent(String name) { super(name); }
|
||||
public Rodent() { super(); }
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
// typeinfo/pets/Rat.java
|
||||
package typeinfo.pets;
|
||||
|
||||
public class Rat extends Rodent {
|
||||
public Rat(String name) { super(name); }
|
||||
public Rat() { super(); }
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
// typeinfo/pets/Mouse.java
|
||||
package typeinfo.pets;
|
||||
|
||||
public class Mouse extends Rodent {
|
||||
public Mouse(String name) { super(name); }
|
||||
public Mouse() { super(); }
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
// typeinfo/pets/Hamster.java
|
||||
package typeinfo.pets;
|
||||
|
||||
public class Hamster extends Rodent {
|
||||
public Hamster(String name) { super(name); }
|
||||
public Hamster() { super(); }
|
||||
}
|
||||
```
|
||||
|
||||
我们必须显式地为每一个子类编写无参构造器。因为我们有一个带一个参数的构造器,所以编译器不会自动地为我们加上无参构造器。
|
||||
|
||||
接下来,我们需要一个类,它可以随机地创建不同类型的宠物,同时,它还可以创建宠物数组和持有宠物的 `List`。为了这个类更加普遍适用,我们将其定义为抽象类:
|
||||
|
||||
```java
|
||||
// typeinfo/pets/PetCreator.java
|
||||
// Creates random sequences of Pets
|
||||
package typeinfo.pets;
|
||||
import java.util.*;
|
||||
import java.util.function.*;
|
||||
|
||||
public abstract
|
||||
class PetCreator implements Supplier<Pet> {
|
||||
private Random rand = new Random(47);
|
||||
// The List of the different types of Pet to create:
|
||||
public abstract List<Class<? extends Pet>> types();
|
||||
public Pet get() { // Create one random Pet
|
||||
int n = rand.nextInt(types().size());
|
||||
try {
|
||||
return types().get(n).newInstance();
|
||||
} catch(InstantiationException |
|
||||
IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
抽象的 `types()` 方法需要子类来实现,以此来获取 `Class` 对象构成的 `List`(这是模板方法设计模式的一种变体)。注意,其中类的类型被定义为“任何从 `Pet` 导出的类型”,因此 `newInstance()` 不需要转型就可以产生 `Pet`。`get()` 随机的选取出一个 `Class` 对象,然后可以通过 `Class.newInstance()` 来生成该类的新实例。
|
||||
|
||||
在调用 `newInstance()` 时,可能会出现两种异常。在紧跟 `try` 语句块后面的 `catch` 子句中可以看到对它们的处理。异常的名字再次成为了一种对错误类型相对比较有用的解释(`IllegalAccessException` 违反了 Java 安全机制,在本例中,表示默认构造器为 `private` 的情况)。
|
||||
|
||||
当你导出 `PetCreator` 的子类时,你需要为 `get()` 方法提供 `Pet` 类型的 `List`。`types()` 方法会简单地返回一个静态 `List` 的引用。下面是使用 `forName()` 的一个具体实现:
|
||||
|
||||
```java
|
||||
// typeinfo/pets/ForNameCreator.java
|
||||
package typeinfo.pets;
|
||||
import java.util.*;
|
||||
|
||||
public class ForNameCreator extends PetCreator {
|
||||
private static List<Class<? extends Pet>> types =
|
||||
new ArrayList<>();
|
||||
// Types you want randomly created:
|
||||
private static String[] typeNames = {
|
||||
"typeinfo.pets.Mutt",
|
||||
"typeinfo.pets.Pug",
|
||||
"typeinfo.pets.EgyptianMau",
|
||||
"typeinfo.pets.Manx",
|
||||
"typeinfo.pets.Cymric",
|
||||
"typeinfo.pets.Rat",
|
||||
"typeinfo.pets.Mouse",
|
||||
"typeinfo.pets.Hamster"
|
||||
};
|
||||
@SuppressWarnings("unchecked")
|
||||
private static void loader() {
|
||||
try {
|
||||
for(String name : typeNames)
|
||||
types.add(
|
||||
(Class<? extends Pet>)Class.forName(name));
|
||||
} catch(ClassNotFoundException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
static { loader(); }
|
||||
@Override
|
||||
public List<Class<? extends Pet>> types() {
|
||||
return types;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`loader()` 方法使用 `Class.forName()` 创建了 `Class` 对象的 `List`。这可能会导致 `ClassNotFoundException`,因为你传入的是一个 `String`,它不能再编译期间被确认是否合理。由于 `Pet` 相关的文件在 `typeinfo` 包里面,所以使用它们的时候需要填写完整的包名。
|
||||
|
||||
为了使得 `List` 装入的是具体的 `Class` 对象,转型是必须的,它会产生一个编译时警告。`loader()` 方法是分开编写的,然后它被放入到一个静态代码块里,因为 `@SuppressWarning` 注解不能够直接放置在静态代码块之上。
|
||||
|
||||
为了对 `Pet` 进行计数,我们需要一个能跟踪不同类型的 `Pet` 的工具。`Map` 的是这个需求的首选,我们将 `Pet` 类型名作为键,将保存 `Pet` 数量的 `Integer` 作为值。通过这种方式,你就看可以询问:“有多少个 `Hamster` 对象?”我们可以使用 `instanceof` 来对 `Pet` 进行计数:
|
||||
|
||||
```java
|
||||
// typeinfo/PetCount.java
|
||||
// Using instanceof
|
||||
import typeinfo.pets.*;
|
||||
import java.util.*;
|
||||
|
||||
public class PetCount {
|
||||
static class Counter extends HashMap<String,Integer> {
|
||||
public void count(String type) {
|
||||
Integer quantity = get(type);
|
||||
if(quantity == null)
|
||||
put(type, 1);
|
||||
else
|
||||
put(type, quantity + 1);
|
||||
}
|
||||
}
|
||||
public static void
|
||||
countPets(PetCreator creator) {
|
||||
Counter counter = new Counter();
|
||||
for(Pet pet : Pets.array(20)) {
|
||||
// List each individual pet:
|
||||
System.out.print(
|
||||
pet.getClass().getSimpleName() + " ");
|
||||
if(pet instanceof Pet)
|
||||
counter.count("Pet");
|
||||
if(pet instanceof Dog)
|
||||
counter.count("Dog");
|
||||
if(pet instanceof Mutt)
|
||||
counter.count("Mutt");
|
||||
if(pet instanceof Pug)
|
||||
counter.count("Pug");
|
||||
if(pet instanceof Cat)
|
||||
counter.count("Cat");
|
||||
if(pet instanceof EgyptianMau)
|
||||
counter.count("EgyptianMau");
|
||||
if(pet instanceof Manx)
|
||||
counter.count("Manx");
|
||||
if(pet instanceof Cymric)
|
||||
counter.count("Cymric");
|
||||
if(pet instanceof Rodent)
|
||||
counter.count("Rodent");
|
||||
if(pet instanceof Rat)
|
||||
counter.count("Rat");
|
||||
if(pet instanceof Mouse)
|
||||
counter.count("Mouse");
|
||||
if(pet instanceof Hamster)
|
||||
counter.count("Hamster");
|
||||
}
|
||||
// Show the counts:
|
||||
System.out.println();
|
||||
System.out.println(counter);
|
||||
}
|
||||
public static void main(String[] args) {
|
||||
countPets(new ForNameCreator());
|
||||
}
|
||||
}
|
||||
/* Output:
|
||||
Rat Manx Cymric Mutt Pug Cymric Pug Manx Cymric Rat
|
||||
EgyptianMau Hamster EgyptianMau Mutt Mutt Cymric Mouse
|
||||
Pug Mouse Cymric
|
||||
{EgyptianMau=2, Pug=3, Rat=2, Cymric=5, Mouse=2, Cat=9,
|
||||
Manx=7, Rodent=5, Mutt=3, Dog=6, Pet=20, Hamster=1}
|
||||
*/
|
||||
```
|
||||
|
||||
在 `countPets()` 中,一个简短的静态方法 `Pets.array()` 生产出了一个随机动物的集合。每个 `Pet` 都被 `instanceof` 检测到并数了一遍。
|
||||
|
||||
`instanceof` 有一个严格的限制:只可以将它与命名类型进行比较,而不能与 `Class` 对象作比较。在前面的例子中,你可能会觉得写出一大堆 `instanceof` 表达式很乏味,事实也是如此。但是,也没有办法让 `instanceof` 聪明起来,让它能够自动地创建一个 `Class` 对象的数组,然后将目标与这个数组中的对象逐一进行比较(稍后会看到一种替代方案)。其实这并不是那么大的限制,如果你在程序中写了大量的 `instanceof`,那就说明你的设计可能存在瑕疵。
|
||||
|
||||
|
||||
## 类型转换检测
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ public class Testable {
|
||||
}
|
||||
```
|
||||
|
||||
被注解标注的方法和其他的方法没有任何区别。在这个例子中,注解 `@Test` 可以和任何修饰符共同用于方法,注入 **public**、**static** 或 **void**。用语法的角度上看,注解和使用方式和修饰符的使用方式一致。
|
||||
被注解标注的方法和其他的方法没有任何区别。在这个例子中,注解 `@Test` 可以和任何修饰符共同用于方法,诸如 **public**、**static** 或 **void**。从语法的角度上看,注解的使用方式和修饰符的使用方式一致。
|
||||
|
||||
### 定义注解
|
||||
|
||||
@@ -459,7 +459,7 @@ CREATE TABLE MEMBER(
|
||||
|
||||
每一个你编写的注解都需要处理器,但是 **javac** 可以非常容易的将多个注解处理器合并在一起。你可以指定多个需要处理的类,并且你可以添加监听器用于监听注解处理完成后接到通知。
|
||||
|
||||
本节中的示例将帮助您开始学习,但如果您必须深入学习,请做好反复学习,大量访问 Google 和StackOverflow 的准备。
|
||||
本节中的示例将帮助你开始学习,但如果你必须深入学习,请做好反复学习,大量访问 Google 和StackOverflow 的准备。
|
||||
|
||||
### 最简单的处理器
|
||||
|
||||
@@ -1806,4 +1806,4 @@ API 的提供方和框架将会将注解作为他们工具的一部分。通过
|
||||
|
||||
<div style="page-break-after: always;"></div>
|
||||
|
||||
[^3 ]: The Java designers coyly suggest that a mirror is where you find a reflection.
|
||||
[^3 ]: The Java designers coyly suggest that a mirror is where you find a reflection.
|
||||
|
||||
@@ -3,15 +3,51 @@
|
||||
<!-- Appendix: Becoming a Programmer -->
|
||||
# 附录:成为一名程序员
|
||||
|
||||
>我分别于2003,2006,2007和2009年撰写的博客文章混搭
|
||||
|
||||
<!-- How I Got Started in Programming -->
|
||||
## 如何开始
|
||||
|
||||
这是一条相当漫长和曲折的道路。我在高一学代数时(1971年),有个非常古怪的老师有一台计算机,还弄到了一台配有一个300波特的音频电话耦合器的ASR-33电传打字机,我学会了如何执行命令并得到响应,以及一个可以在高中区使用的HP-1000计算机上的帐户。我们能够创建和运行BASIC程序并将它们保存在打孔磁带上。我对此非常着迷,所以尽可能地把它带回家后在晚上写程序。我写了一个赛马模拟游戏--HOSRAC.BAS,用星号来代表马的移动,由于是在纸上打印输出,所以需要一点想象力。
|
||||
|
||||
我的朋友丹尼尔(和设计我的书的封面是同一个人)有一个兄弟,他有段时间通过向酒吧和餐馆提供弹球机来赚钱。他有一台投币式街机(老虎机),最早的《乓》游戏之一,我对此全然不知,到现在我还忍受不了这东西(现在我几乎不玩电脑游戏,这样看来我可能是个没有幽默的人,但似乎编程比玩电脑游戏更有趣、更具挑战性。)
|
||||
|
||||
后来我在高中参与了摄影和新闻工作,在大学的第一年就主修新闻学。我觉得自己已经从学校学到了足够多的东西,又转修了物理学。后来我在加州大学欧文分校完成了物理学位,如果我当时选择了一个特定的工程领域,修了足够的工程课就能拿到双专业,但我试图走得更远一些,所以最后我获得的本科学位是 "应用物理"。作为一名本科生,我多多少少学习了一些可以自娱自乐,但又没有任何深度的计算机编程课程。我个人认为在这些课程细细熏陶下,帮我打下了一定的基础,但事实我理解的这些东西没有任何深度。我不知道计算机、编译器或解释器有什么区别(只是对编译器和解释器一点点的理解)。对我来说计算机是绝对可靠的,而且我从来没有想过在程序语言和操作系统中会有出现错误的可能。
|
||||
|
||||
后来我去了在加州州立理工大学攻读研究生,主要有三点原因
|
||||
|
||||
1. 我真的非常喜欢物理学这个领域
|
||||
|
||||
2. 他们接受了我,甚至给了我一份教学工作和奖学金
|
||||
|
||||
3. 出乎意料的是他们给我的工作时间不止一个夏天
|
||||
|
||||
而我完全没做好上班的准备。
|
||||
|
||||
作为一名物理专业的学生,我学习的是太阳能发电系统,当时太阳能发电系统很大 (如果你的房子上装了太阳能或生意上是关于太阳能系统,加州就会给予税收抵免,因此也兴起很多生意),加州理工大学也承诺会在工程系开设相应的课程。然而因为学校没有提供必要的课程,要想获得在太阳能工程的学位得花好几年时间。所以我学习了研究生其他的工程课,包括介绍机械,太阳能,电气和电子工程。我上的课是非电气工程专业的电气工程导论。最常见的研究生工程课程是计算机工程专业,所以最后我拿了那个学位。我还上了艺术课,几门舞蹈课,还有一些计算机科学课程 (Pascal和数据结构),在计算机工程中,我终于弄清楚了处理器的工作流程,从那以后我一直带着一个处理器在身上。这些就是我学的计算机基础知识。
|
||||
|
||||
刚开始工作的时候,凭借着一堆硬件和相对简单低水平的编程,做了一名计算机工程师。因为C语言似乎是理想的嵌入式系统语言,于是我开始自学,并慢慢开始了解更多关于编程语言的东西。我们在这家公司从源代码构建编译器,这让我大开眼界。 (想象一下一个编译器只是另一个软件的一部分!)
|
||||
|
||||
当我去华盛顿大学海洋学院为Tom Keffer后来创建了“疯狗浪”)工作时,我们决定使用C++。我只有一本Stroustrup写的非初学者书可以参考,最终不得不通过检查C++预处理器生成的中间C代码来了解语言的功能。这个过程非常痛苦,但学习的效果很好。从那以后我就用相同的方式学习,因为它让我学习了如何剖析一种语言,并看到它本质的能力,与此同时开始有了批判性思维。
|
||||
|
||||
我并没有理解清楚所有的概念。只是在之后的日子里不断反复,我所知道的一切需要时间才能消化吸收。如果我现在能很容易地理解一个新概念,那只是因为它是我已经知道的积累概念的一个变种。在加州理工大学招收非计算机本科学历的计算机科学研究生项目中,学生们曾经说他们花了一年的时间才弄清楚他们对计算机的困惑(他们正在沉浸程序之中)。当人们学习计算机时,他们往往会对自己抱有不切实际的期望,通常是他们听说学计算机编程的好处,就希望在几周内找到一份高薪的工作。但是,最好的学习过程是先对计算机感兴趣,随着时间的推移,学习的越来越多,自然的就开始自学。
|
||||
|
||||
这些就是我主要做的事,尽管我通过学计算机工程有还算扎实的基础,但我没上过编程课,而是通过自学。在此期间我也在不断地学习新事物,在这个行业里,不断学习是非常重要的一部分。
|
||||
|
||||
<!-- A Career in Computing -->
|
||||
## 码农生涯
|
||||
|
||||
|
||||
### 延伸阅读
|
||||
* [Teach Yourself Programming In Ten Years](http://norvig.com/21-days.html), by Peter Norvig.
|
||||
* [How To Be A Programmer](http://samizdat.mines.edu/howto/HowToBeAProgrammer.html), by Robert Read.
|
||||
* A [speech by Steve Jobs](http://news.stanford.edu/news/2005/june15/jobs-061505.html) to inspire a group of graduating college students.
|
||||
* Kathy Sierra: [Does College Matter](https://headrush.typepad.com/creating_passionate_users/2005/07/does_college_ma.html)?
|
||||
* Paul Graham [on College](http://www.paulgraham.com/college.html).
|
||||
* Joel Spolsky: [Advice for Computer Science College Students](https://www.joelonsoftware.com/2005/01/02/advice-for-computer-science-college-students/).
|
||||
* James Shore: [Five Design Skills Every Programmer Should Have](https://www.jamesshore.com/Blog/Five-Design-Skills.html).
|
||||
* Steve Yegge: [The Truth About Interviewing](http://steve-yegge.blogspot.com/2006/03/truth-about-interviewing.html).
|
||||
|
||||
<!-- The Mythical 5% -->
|
||||
## 百分之五的神话
|
||||
|
||||
@@ -27,6 +63,12 @@
|
||||
<!-- Do What You Love -->
|
||||
## 做你喜欢的事
|
||||
|
||||
*“1960年,一位研究人员对1500名商学院学生进行了访谈,并将他们分为两类:那些为了钱财来这里上学的人,1245人,以及那些打算利用学位做他们非常关心的事情的人,255人。二十年后,研究人员再次访谈了这些毕业生,发现其中有101位百万富翁,除了其中一位,所有百万富翁都来自追求他们喜欢做的事的那255人!”*
|
||||
|
||||
“现在你可能觉得你对巴洛克时期的冰岛诗歌,或者蝴蝶收集,或者高尔夫,抑或是对社会正义的热情,会因为要养家糊口而让你和你喜欢做的事分道扬镳,并非一定要如此。弗拉基米尔·纳博科夫(Vladimir Nabokov)是本世纪最伟大的小说家之一,他对蝴蝶收藏的热情远远超过写作。事实上,他的第一个大学教学工作是关于鳞翅类昆虫。在过去40年里,对40万美国群众的研究表明,即使是部分的、零散的追求培养你的激情,也可以帮助你充分利用你目前的能力,激励你培养新的能力。”--摘自《The Other 90%》 Robert K.Cooper
|
||||
|
||||
当然你可以看Po Bronson写的《 What Should I Do With My Life?》这本书,对这些想法进行更多的探索。
|
||||
|
||||
<!-- 分页 -->
|
||||
|
||||
<div style="page-break-after: always;"></div>
|
||||
@@ -3,17 +3,249 @@
|
||||
<!-- Appendix: Data Compression -->
|
||||
# 附录:数据压缩
|
||||
|
||||
Java I/O 类库提供了可以读写压缩格式流的类。你可以将其他 I/O 类包装起来用于提供压缩功能。
|
||||
|
||||
这些类不是从 **Reader** 和 **Writer** 类派生的,而是 **InputStream** 和 **OutputStream** 层级结构的一部分。这是由于压缩库处理的是字节,而不是字符。但是,你可能会被迫混合使用两种类型的流(请记住,你可以使用 **InputStreamReader** 和 **OutputStreamWriter**,这两个类可以在字节类型和字符类型之间轻松转换)。
|
||||
|
||||
| 压缩类 | 功能 |
|
||||
| ------------------------ | ------------------------------------------------------------ |
|
||||
| **CheckedInputStream** | `getCheckSum()` 可以对任意 **InputStream** 计算校验和(而不只是解压) |
|
||||
| **CheckedOutputStream** | `getCheckSum()` 可以对任意 **OutputStream** 计算校验和(而不只是压缩) |
|
||||
| **DeflaterOutputStream** | 压缩类的基类 |
|
||||
| **ZipOutputStream** | **DeflaterOutputStream** 类的一种,用于压缩数据到 Zip 文件结构 |
|
||||
| **GZIPOutputStream** | **DeflaterOutputStream** 类的一种,用于压缩数据到 GZIP 文件结构 |
|
||||
| **InflaterInputStream** | 解压类的基类 |
|
||||
| **ZipInputStream** | **InflaterInputStream** 类的一种,用于解压 Zip 文件结构的数据 |
|
||||
| **GZIPInputStream** | **InflaterInputStream** 类的一种,用于解压 GZIP 文件结构的数据 |
|
||||
|
||||
尽管存在很多压缩算法,但是 Zip 和 GZIP 可能是最常见的。你可以使用许多用于读取和写入这些格式的工具,来轻松操作压缩数据。
|
||||
|
||||
<!-- Simple Compression with GZIP -->
|
||||
## 使用Gzip简单压缩
|
||||
|
||||
## 使用 Gzip 简单压缩
|
||||
|
||||
<!-- Multifile Storage with Zip -->
|
||||
## 使用zip多文件存储
|
||||
|
||||
GZIP 接口十分简单,因此当你有一个需要压缩的数据流(而不是一个包含不同数据分片的容器)时,使用 GZIP 更为合适。如下是一个压缩单个文件的示例:
|
||||
|
||||
```java
|
||||
// compression/GZIPcompress.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.
|
||||
// {java GZIPcompress GZIPcompress.java}
|
||||
// {VisuallyInspectOutput}
|
||||
|
||||
public class GZIPcompress {
|
||||
public static void main(String[] args) {
|
||||
if (args.length == 0) {
|
||||
System.out.println(
|
||||
"Usage: \nGZIPcompress file\n" +
|
||||
"\tUses GZIP compression to compress " +
|
||||
"the file to test.gz");
|
||||
System.exit(1);
|
||||
}
|
||||
try (
|
||||
InputStream in = new BufferedInputStream(
|
||||
new FileInputStream(args[0]));
|
||||
BufferedOutputStream out =
|
||||
new BufferedOutputStream(
|
||||
new GZIPOutputStream(
|
||||
new FileOutputStream("test.gz")))
|
||||
) {
|
||||
System.out.println("Writing file");
|
||||
int c;
|
||||
while ((c = in.read()) != -1)
|
||||
out.write(c);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
System.out.println("Reading file");
|
||||
try (
|
||||
BufferedReader in2 = new BufferedReader(
|
||||
new InputStreamReader(new GZIPInputStream(
|
||||
new FileInputStream("test.gz"))))
|
||||
) {
|
||||
in2.lines().forEach(System.out::println);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
使用压缩类非常简单,你只需要把你的输出流包装在 **GZIPOutputStream** 或 **ZipOutputStream** 中,将输入流包装在 **GZIPInputStream** 或 **ZipInputStream**。其他的一切就只是普通的 I/O 读写。这是面向字符流和面向字节流的混合示例;in 使用 Reader 类,而 **GZIPOutputStreams** 构造函数只能接受 **OutputStream** 对象,而不能接受 **Writer** 对象。当打开文件的时候,**GZIPInputStream** 会转换成为 **Reader**。
|
||||
|
||||
## 使用 zip 多文件存储
|
||||
|
||||
支持 Zip 格式的库比 GZIP 库更广泛。有了它,你可以轻松存储多个文件,甚至还有一个单独的类可以轻松地读取 Zip 文件。该库使用标准 Zip 格式,因此它可以与当前可在 Internet 上下载的所有 Zip 工具无缝协作。以下示例与前一个示例具有相同的形式,但它可以根据需要处理任意数量的命令行参数。此外,它还显示了 **Checksum** 类计算和验证文件的校验和。有两种校验和类型:Adler32(更快)和 CRC32(更慢但更准确)。
|
||||
|
||||
```java
|
||||
// compression/ZipCompress.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.
|
||||
// Uses Zip compression to compress any
|
||||
// number of files given on the command line
|
||||
// {java ZipCompress ZipCompress.java}
|
||||
// {VisuallyInspectOutput}
|
||||
public class ZipCompress {
|
||||
public static void main(String[] args) {
|
||||
try (
|
||||
FileOutputStream f =
|
||||
new FileOutputStream("test.zip");
|
||||
CheckedOutputStream csum =
|
||||
new CheckedOutputStream(f, new Adler32());
|
||||
ZipOutputStream zos = new ZipOutputStream(csum);
|
||||
BufferedOutputStream out =
|
||||
new BufferedOutputStream(zos)
|
||||
) {
|
||||
zos.setComment("A test of Java Zipping");
|
||||
// No corresponding getComment(), though.
|
||||
for (String arg : args) {
|
||||
System.out.println("Writing file " + arg);
|
||||
try (
|
||||
InputStream in = new BufferedInputStream(
|
||||
new FileInputStream(arg))
|
||||
) {
|
||||
zos.putNextEntry(new ZipEntry(arg));
|
||||
int c;
|
||||
while ((c = in.read()) != -1)
|
||||
out.write(c);
|
||||
}
|
||||
out.flush();
|
||||
}
|
||||
// Checksum valid only after the file is closed!
|
||||
System.out.println(
|
||||
"Checksum: " + csum.getChecksum().getValue());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
// Now extract the files:
|
||||
System.out.println("Reading file");
|
||||
try (
|
||||
FileInputStream fi =
|
||||
new FileInputStream("test.zip");
|
||||
CheckedInputStream csumi =
|
||||
new CheckedInputStream(fi, new Adler32());
|
||||
ZipInputStream in2 = new ZipInputStream(csumi);
|
||||
BufferedInputStream bis =
|
||||
new BufferedInputStream(in2)
|
||||
) {
|
||||
ZipEntry ze;
|
||||
while ((ze = in2.getNextEntry()) != null) {
|
||||
System.out.println("Reading file " + ze);
|
||||
int x;
|
||||
while ((x = bis.read()) != -1)
|
||||
System.out.write(x);
|
||||
}
|
||||
if (args.length == 1)
|
||||
System.out.println(
|
||||
"Checksum: " + csumi.getChecksum().getValue());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
// Alternative way to open and read Zip files:
|
||||
try (
|
||||
ZipFile zf = new ZipFile("test.zip")
|
||||
) {
|
||||
Enumeration e = zf.entries();
|
||||
while (e.hasMoreElements()) {
|
||||
ZipEntry ze2 = (ZipEntry) e.nextElement();
|
||||
System.out.println("File: " + ze2);
|
||||
// ... and extract the data as before
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
对于要添加到存档的每个文件,必须调用 `putNextEntry()` 并传递 **ZipEntry** 对象。 **ZipEntry** 对象包含一个扩展接口,用于获取和设置 Zip 文件中该特定条目的所有可用数据:名称,压缩和未压缩大小,日期,CRC 校验和,额外字段数据,注释,压缩方法以及它是否是目录条目。但是,即使 Zip 格式有设置密码的方法,Java 的 Zip 库也不支持。虽然 **CheckedInputStream** 和 **CheckedOutputStream** 都支持 Adler32 和 CRC32 校验和,但 **ZipEntry** 类仅支持 CRC 接口。这是对基础 Zip 格式的限制,但它可能会限制你使用更快的 Adler32。
|
||||
|
||||
要提取文件,**ZipInputStream** 有一个 `getNextEntry()` 方法,这个方法在有文件存在的情况下调用,会返回下一个 **ZipEntry**。作为一个更简洁的替代方法,你可以使用 **ZipFile** 对象读取该文件,该对象具有方法 entries() 返回一个包裹 **ZipEntries** 的 **Enumeration**。
|
||||
|
||||
要读取校验和,你必须以某种方式访问关联的 **Checksum** 对象。这里保留了对 **CheckedOutputStream** 和 **CheckedInputStream** 对象的引用,但你也可以保持对 **Checksum** 对象的引用。 Zip 流中的一个令人困惑的方法是 `setComment()`。如 **ZipCompress** 所示。在 Java 中,你可以在编写文件时设置注释,但是没有办法恢复 **ZipInputStream** 中的注释。注释似乎仅通过 **ZipEntry** 在逐个条目的基础上完全支持。
|
||||
|
||||
使用 GZIP 或 Zip 库时,你不仅被限制于文件——你可以压缩任何内容,包括通过网络连接发送的数据。
|
||||
|
||||
|
||||
|
||||
<!-- Java Archives (Jars) -->
|
||||
## Java的jar
|
||||
|
||||
## Java 的 jar
|
||||
|
||||
Zip 格式也用于 JAR(Java ARchive)文件格式,这是一种将一组文件收集到单个压缩文件中的方法,就像 Zip 一样。但是,与 Java 中的其他所有内容一样,JAR 文件是跨平台的,因此你不必担心平台问题。你还可以将音频和图像文件像类文件一样包含在其中。
|
||||
|
||||
JAR 文件由一个包含压缩文件集合的文件和一个描述它们的“清单(manifest)”组成。(你可以创建自己的清单文件;否则,jar 程序将为你执行此操作。)你可以在 JDK 文档中,找到更多关于 JAR 清单的信息。
|
||||
|
||||
JDK 附带的 jar 工具会自动压缩你选择的文件。你可以在命令行上调用它:
|
||||
|
||||
```shell
|
||||
jar [options] destination [manifest] inputfile(s)
|
||||
```
|
||||
|
||||
选项是一组字母(不需要连字符或任何其他指示符)。 Unix / Linux 用户会注意到这些选项与 tar 命令选项的相似性。这些是:
|
||||
|
||||
| 选项 | 功能 |
|
||||
| ---------- | ------------------------------------------------------------ |
|
||||
| **c** | 创建一个新的或者空的归档文件 |
|
||||
| **t** | 列出内容目录 |
|
||||
| **x** | 提取所有文件 |
|
||||
| **x** file | 提取指定的文件 |
|
||||
| **f** | 这代表着,“传递文件的名称。”如果你不使用它,jar 假定它的输入将来自标准输入,或者,如果它正在创建一个文件,它的输出将转到标准输出。 |
|
||||
| **m** | 代表第一个参数是用户创建的清单文件的名称。 |
|
||||
| **v** | 生成详细的输出用于表述 jar 所作的事情 |
|
||||
| **0** | 仅存储文件;不压缩文件(用于创建放在类路径中的 JAR 文件)。 |
|
||||
| **M** | 不要自动创建清单文件 |
|
||||
|
||||
如果放入 JAR 文件的文件中包含子目录,则会自动添加该子目录,包括其所有子目录等。还会保留路径信息。
|
||||
|
||||
以下是一些调用 jar 的典型方法。以下命令创建名为 myJarFile 的 JAR 文件。 jar 包含当前目录中的所有类文件,以及自动生成的清单文件:
|
||||
|
||||
```shell
|
||||
jar cf myJarFile.jar *.class
|
||||
```
|
||||
|
||||
下一个命令与前面的示例类似,但它添加了一个名为 myManifestFile.mf 的用户创建的清单文件。 :
|
||||
|
||||
```shell
|
||||
jar cmf myJarFile.jar myManifestFile.mf *.class
|
||||
```
|
||||
|
||||
这个命令输出了 myJarFile.jar 中的文件目录:
|
||||
|
||||
```shell
|
||||
jar tf myJarFile.jar
|
||||
```
|
||||
|
||||
如下添加了一个“verbose”的标志,用于生成更多关于 myJarFile.jar 中文件的详细信息:
|
||||
|
||||
```shell
|
||||
jar tvf myJarFile.jar
|
||||
```
|
||||
|
||||
假设 audio,classes 和 image 都是子目录,它将所有子目录组合到文件 myApp.jar 中。还包括“verbose”标志,以便在 jar 程序工作时提供额外的反馈:
|
||||
|
||||
```shell
|
||||
jar cvf myApp.jar audio classes image
|
||||
```
|
||||
|
||||
如果你在创建 JAR 文件时使用了 0(零) 选项,该文件将会被替换在你的类路径(CLASSPATH)中:
|
||||
|
||||
```shell
|
||||
CLASSPATH="lib1.jar;lib2.jar;"
|
||||
```
|
||||
|
||||
然后 Java 可以搜索到 lib1.jar 和 lib2.jar 的类文件。
|
||||
|
||||
jar 工具不像 Zip 实用程序那样通用。例如,你无法将文件添加或更新到现有 JAR 文件;只能从头开始创建 JAR 文件。
|
||||
|
||||
此外,你无法将文件移动到 JAR 文件中,在移动文件时将其删除。
|
||||
|
||||
但是,在一个平台上创建的 JAR 文件可以通过任何其他平台上的 jar 工具透明地读取(这个问题有时会困扰 Zip 实用程序)。
|
||||
|
||||
<!-- 分页 -->
|
||||
|
||||
|
||||
Reference in New Issue
Block a user