移除jupyter

This commit is contained in:
LingCoder
2020-05-29 04:06:26 +08:00
parent 659df0150d
commit 2c4185761e
46 changed files with 0 additions and 79061 deletions

View File

@@ -1,408 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# 第一章 对象的概念\n",
"\n",
"> “我们没有意识到惯用语言的结构有多大的力量。可以毫不夸张地说,它通过语义反应机制奴役我们。语言表现出来并在无意识中给我们留下深刻印象的结构会自动投射到我们周围的世界。” -- Alfred Korzybski (1930)\n",
"\n",
"计算机革命的起源来自机器。编程语言就像是那台机器。它不仅是我们思维放大的工具与另一种表达媒介,更像是我们思想的一部分。语言的灵感来自其他形式的表达,如写作,绘画,雕塑,动画和电影制作。编程语言就是创建应用程序的思想结构。\n",
"\n",
"面向对象编程Object-Oriented Programming OOP是一种编程思维方式和编码架构。本章讲述 OOP 的基本概述。如果读者对此不太理解,可先行跳过本章。等你具备一定编程基础后,请务必再回头看。只有这样你才能深刻理解面向对象编程的重要性及设计方式。\n",
"\n",
"## 抽象\n",
"\n",
"所有编程语言都提供抽象机制。从某种程度上来说,问题的复杂度直接取决于抽象的类型和质量。这里的“类型”意思是:抽象的内容是什么?汇编语言是对底层机器的轻微抽象。接着出现的“命令式”语言(如 FORTRANBASIC 和 C是对汇编语言的抽象。与汇编相比这类语言已有了长足的改进但它们的抽象原理依然要求我们着重考虑计算机的结构而非问题本身的结构。\n",
"\n",
"程序员必须要在机器模型(“解决方案空间”)和实际解决的问题模型(“问题空间”)之间建立起一种关联。这个过程既费精力,又脱离编程语言本身的范畴。这使得程序代码很难编写,维护代价高昂。同时还造就了一个副产业“编程方法”学科。\n",
"\n",
"为机器建模的另一个方法是为要解决的问题制作模型。对一些早期语言来说,如 LISP 和 APL它们的做法是“从不同的角度观察世界”——“所有问题都归纳为列表”或“所有问题都归纳为算法”。PROLOG 则将所有\n",
"问题都归纳为决策链。对于这些语言,我们认为它们一部分是“基于约束”的编程,另一部分则是专为\n",
"处理图形符号设计的(后者被证明限制性太强)。每种方法都有自己特殊的用途,适合解决某一类的问题。只要超出了它们力所能及的范围,就会显得非常笨拙。\n",
"\n",
"面向对象的程序设计在此基础上跨出了一大步,程序员可利用一些工具表达“问题空间”内的元素。由于这种表达非常具有普遍性,所以不必受限于特定类型的问题。我们将问题空间中的元素以及它们在解决方案空间的表示称作“对象”(**Object**。当然还有一些在问题空间没有对应的对象体。通过添加新的对象类型程序可进行灵活的调整以便与特定的问题配合。所以当你在阅读描述解决方案的代码时也是在阅读问题的表述。与我们以前见过的相比这无疑是一种更加灵活、更加强大的语言抽象方法。总之OOP 允许我们根据问题来描述问题,而不是根据运行解决方案的计算机。然而,它仍然与计算机有联系,每个对象都类似一台小计算机:它们有自己的状态并且可以进行特定的操作。这与现实世界的“对象”或者“物体”相似:它们都有自己的特征和行为。\n",
"\n",
"Smalltalk 作为第一个成功的面向对象并影响了 Java 的程序设计语言 *Alan Kay* 总结了其五大基本特征。通过这些特征,我们可理解“纯粹”的面向对象程序设计方法是什么样的:\n",
"\n",
"> 1. **万物皆对象**。你可以将对象想象成一种特殊的变量。它存储数据,但可以在你对其“发出请求”时执行本身的操作。理论上讲,你总是可以从要解决的问题身上抽象出概念性的组件,然后在程序中将其表示为一个对象。\n",
"> 2. **程序是一组对象,通过消息传递来告知彼此该做什么**。要请求调用一个对象的方法,你需要向该对象发送消息。\n",
"> 3. **每个对象都有自己的存储空间,可容纳其他对象**。或者说,通过封装现有对象,可制作出新型对象。所以,尽管对象的概念非常简单,但在程序中却可达到任意高的复杂程度。\n",
"> 4. **每个对象都有一种类型**。根据语法每个对象都是某个“类”的一个“实例”。其中“类”Class是“类型”Type的同义词。一个类最重要的特征就是“能将什么消息发给它”。\n",
"> 5. **同一类所有对象都能接收相同的消息**。这实际是别有含义的一种说法大家不久便能理解。由于类型为“圆”Circle的一个对象也属于类型为“形状”Shape的一个对象所以一个圆完全能接收发送给\"形状”的消息。这意味着可让程序代码统一指挥“形状”令其自动控制所有符合“形状”描述的对象其中自然包括“圆”。这一特性称为对象的“可替换性”是OOP最重要的概念之一。\n",
"\n",
"*Grady Booch* 提供了对对象更简洁的描述:一个对象具有自己的状态,行为和标识。这意味着对象有自己的内部数据(提供状态)、方法 (产生行为),并彼此区分(每个对象在内存中都有唯一的地址)。\n",
"\n",
"## 接口\n",
"\n",
"亚里士多德(*Aristotle*)大概是第一个认真研究“类型”的哲学家,他曾提出过“鱼类和鸟类”这样的概念。所有对象都是唯一的,但同时也是具有相同的特性和行为的对象所归属的类的一部分。这种思想被首次应用于第一个面向对象编程语言 Simula-67它在程序中使用基本关键字 **class** 来引入新的类型class 和 type 通常可互换使用,有些人对它们进行了进一步区分,他们强调 type 决定了接口,而 class 是那个接口的一种特殊实现方式)。\n",
"\n",
"Simula 是一个很好的例子。正如这个名字所暗示的它的作用是“模拟”Simulate类似“银行出纳员”这样的经典问题。在这个例子里我们有一系列出纳员、客户、帐号、交易和货币单位等许多\"对象”。每类成员(元素)都具有一些通用的特征:每个帐号都有一定的余额;每名出纳都能接收客户的存款;等等。与此同时,每个成员都有自己的状态;每个帐号都有不同的余额;每名出纳都有一个名字。所以在计算机程序中,能用独一无二的实体分别表示出纳员、客户、帐号以及交易。这个实体便是“对象”,而且每个对象都隶属一个特定的“类”,那个类具有自己的通用特征与行为。\n",
"\n",
"因此在面向对象的程序设计中尽管我们真正要做的是新建各种各样的数据“类型”Type但几乎所有面向对象的程序设计语言都采用了 `class` 关键字。当你看到 “type” 这个词的时候,请同时想到 `class`;反之亦然。\n",
"\n",
"创建好一个类后,可根据情况生成许多对象。随后,可将那些对象作为要解决问题中存在的元素进行处理。事实上,当我们进行面向对象的程序设计时,面临的最大一项挑战是:如何在“问题空间”(问题实际存在的地方)的元素与“方案空间”(对实际问题进行建模的地方,如计算机)的元素之间建立理想的“一对一”的映射关系。\n",
"\n",
"那么如何利用对象完成真正有用的工作呢必须有一种办法能向对象发出请求令其解决一些实际的问题比如完成一次交易、在屏幕上画一些东西或者打开一个开关等等。每个对象仅能接受特定的请求。我们向对象发出的请求是通过它的“接口”Interface定义的对象的“类型”或“类”则规定了它的接口形式。“类型”与“接口”的对应关系是面向对象程序设计的基础。\n",
"\n",
"下面让我们以电灯泡为例:\n",
"\n",
"![reader](../images/reader.png)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"java"
],
"id": ""
}
},
"outputs": [],
"source": [
"Light lt = new Light();\n",
"lt.on();"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"在这个例子中,类型/类的名称是 **Light**,可向 **Light** 对象发出的请求包括打开 `on`、关闭 `off`、变得更明亮 `brighten` 或者变得更暗淡 `dim`。通过声明一个引用,如 `lt` 和 `new` 关键字,我们创建了一个 **Light** 类型的对象,再用等号将其赋给引用。\n",
"\n",
"为了向对象发送消息,我们使用句点符号 `.` 将 `lt` 和消息名称 `on` 连接起来。可以看出,使用一些预先定义好的类时,我们在程序里采用的代码是非常简单直观的。\n",
"\n",
"上图遵循 **UML**Unified Modeling Language统一建模语言的格式。每个类由一个框表示框的顶部有类型名称框中间部分是要描述的任何数据成员方法属于此对象的方法它们接收任何发送到该对象的消息在框的底部。通常只有类的名称和公共方法在 **UML** 设计图中显示,因此中间部分未显示,如本例所示。如果你只对类名感兴趣,则也不需要显示方法信息。\n",
"\n",
"## 服务提供\n",
"\n",
"在开发或理解程序设计时,我们可以将对象看成是“服务提供者”。你的程序本身将为用户提供服务,并且它能通过调用其他对象提供的服务来实现这一点。我们的最终目标是开发或调用工具库中已有的一些对象,提供理想的服务来解决问题。\n",
"\n",
"那么问题来了:我们该选择哪个对象来解决问题呢?例如,你正在开发一个记事本程序。*你可能会想到在屏幕输入默认的记事本对象*,一个用于检测不同类型打印机并执行打印的对象。这些对象中的某些已经有了。那对于还没有的对象,我们该设计成啥样呢?这些对象需要提供哪些服务,以及还需要调用其他哪些对象?\n",
"\n",
"我们可以将这些问题一一分解,抽象成一组服务。软件设计的基本原则是高内聚:每个组件的内部作用明确,功能紧密相关。然而经常有人将太多功能塞进一个对象中。例如:在支票打印模块中,你需要设计一个可以同时读取文本格式又能正确识别不同打印机型号的对象。正确的做法是提供三个或更多对象:一个对象检查所有排版布局的目录;一个或一组可以识别不同打印机型号的对象展示通用的打印界面;第三个对象组合上述两个服务来完成任务。这样,每个对象都提供了一组紧密的服务。在良好的面向对象设计中,每个对象功能单一且高效。这样的程序设计可以提高我们代码的复用性,同时也方便别人阅读和理解我们的代码。只有让人知道你提供什么服务,别人才能更好地将其应用到其他模块或程序中。\n",
"\n",
"## 封装\n",
"\n",
"我们可以把编程的侧重领域划分为研发和应用。应用程序员调用研发程序员构建的基础工具类来做快速开发。研发程序员开发一个工具类,该工具类仅向应用程序员公开必要的内容,并隐藏内部实现的细节。这样可以有效地避免该工具类被错误的使用和更改,从而减少程序出错的可能。彼此职责划分清晰,相互协作。当应用程序员调用研发程序员开发的工具类时,双方建立了关系。应用程序员通过使用现成的工具类组装应用程序或者构建更大的工具库。如果工具类的创建者将类的内部所有信息都公开给调用者,那么有些使用规则就不容易被遵守。因为前者无法保证后者是否会按照正确的规则来使用,甚至是改变该工具类。只有设定访问控制,才能从根本上阻止这种情况的发生。\n",
"\n",
"因此,使用访问控制的原因有以下两点:\n",
"\n",
"1. 让应用程序员不要触摸他们不应该触摸的部分。(请注意,这也是一个哲学决策。部分编程语言认为如果程序员有需要,则应该让他们访问细节部分。);\n",
"\n",
"2. 使类库的创建者(研发程序员)在不影响后者使用的情况下完善更新工具库。例如,我们开发了一个功能简单的工具类,后来发现可以通过优化代码来提高执行速度。假如工具类的接口和实现部分明确分开并受到保护,那我们就可以轻松地完成改造。\n",
"\n",
"Java 有三个显式关键字来设置类中的访问权限:`public`(公开),`private`(私有)和`protected`(受保护)。这些访问修饰符决定了谁能使用它们修饰的方法、变量或类。\n",
"\n",
" 1. `public`(公开)表示任何人都可以访问和使用该元素;\n",
"\n",
" 2. `private`(私有)除了类本身和类内部的方法,外界无法直接访问该元素。`private` 是类和调用者之间的屏障。任何试图访问私有成员的行为都会报编译时错误;\n",
"\n",
" 3. `protected`(受保护)类似于 `private`,区别是子类(下一节就会引入继承的概念)可以访问 `protected` 的成员,但不能访问 `private` 成员;\n",
"\n",
" 4. `default`(默认)如果你不使用前面的三者,默认就是 `default` 访问权限。`default` 被称为包访问,因为该权限下的资源可以被同一包(库组件)中其他类的成员访问。\n",
"\n",
"## 复用\n",
"\n",
"一个类经创建和测试后,理应是可复用的。然而很多时候,由于程序员没有足够的编程经验和远见,我们的代码复用性并不强。\n",
"\n",
"代码和设计方案的复用性是面向对象程序设计的优点之一。我们可以通过重复使用某个类的对象来达到这种复用性。同时,我们也可以将一个类的对象作为另一个类的成员变量使用。新的类可以是由任意数量和任意类型的其他对象构成。这里涉及到“组合”和“聚合”的概念:\n",
"\n",
"* **组合**Composition经常用来表示“拥有”关系has-a relationship。例如“汽车拥有引擎”。\n",
"\n",
"* **聚合**Aggregation动态的**组合**。\n",
"\n",
"![UML-example](../images/1545758268350.png)\n",
"\n",
"上图中实心三角形指向“ **Car** ”表示 **组合** 的关系;如果是 **聚合** 关系,可以使用空心三角形。\n",
"\n",
"**译者注**:组合和聚合都属于关联关系的一种,只是额外具有整体-部分的意义。至于是聚合还是组合,需要根据实际的业务需求来判断。可能相同超类和子类,在不同的业务场景,关联关系会发生变化。只看代码是无法区分聚合和组合的,具体是哪一种关系,只能从语义级别来区分。聚合关系中,整件不会拥有部件的生命周期,所以整件删除时,部件不会被删除。再者,多个整件可以共享同一个部件。组合关系中,整件拥有部件的生命周期,所以整件删除时,部件一定会跟着删除。而且,多个整件不可以同时共享同一个部件。这个区别可以用来区分某个关联关系到底是组合还是聚合。两个类生命周期不同步,则是聚合关系,生命周期同步就是组合关系。)\n",
"\n",
"使用“组合”关系给我们的程序带来极大的灵活性。通常新建的类中,成员对象会使用 `private` 访问权限,这样应用程序员则无法对其直接访问。我们就可以在不影响客户代码的前提下,从容地修改那些成员。我们也可以在“运行时\"改变成员对象从而动态地改变程序的行为,这进一步增大了灵活性。下面一节要讲到的“继承”并不具备这种灵活性,因为编译器对通过继承创建的类进行了限制。\n",
"\n",
"在面向对象编程中经常重点强调“继承”。在新手程序员的印象里,或许先入为主地认为“继承应当随处可见”。沿着这种思路产生的程序设计通常拙劣又复杂。相反,在创建新类时首先要考虑“组合”,因为它更简单灵活,而且设计更加清晰。等我们有一些编程经验后,一旦需要用到继承,就会明显意识到这一点。\n",
"\n",
"## 继承\n",
"\n",
"“继承”给面向对象编程带来极大的便利。它在概念上允许我们将各式各样的数据和功能封装到一起,这样便可恰当表达“问题空间”的概念,而不用受制于必须使用底层机器语言。\n",
"\n",
"通过使用 `class` 关键字,这些概念形成了编程语言中的基本单元。遗憾的是,这么做还是有很多麻烦:在创建了一个类之后,即使另一个新类与其具有相似的功能,你还是得重新创建一个新类。但我们若能利用现成的数据类型,对其进行“克隆”,再根据情况进行添加和修改,情况就显得理想多了。“继承”正是针对这个目标而设计的。但继承并不完全等价于克隆。在继承过程中,若原始类(正式名称叫作基类、超类或父类)发生了变化,修改过的“克隆”类(正式名称叫作继承类或者子类)也会反映出这种变化。\n",
"\n",
"![Inheritance-example](../images/1545763399825.png)\n",
"\n",
"这个图中的箭头从派生类指向基类。正如你将看到的,通常有多个派生类。类型不仅仅描述一组对象的约束,它还涉及其他类型。两种类型可以具有共同的特征和行为,但是一种类型可能包含比另一种类型更多的特征,并且还可以处理更多的消息(或者以不同的方式处理它们)。继承通过基类和派生类的概念来表达这种相似性。基类包含派生自它的类型之间共享的所有特征和行为。创建基类以表示思想的核心。从基类中派生出其他类型来表示实现该核心的不同方式。\n",
"\n",
"![1545764724202](../images/1545764724202.png)\n",
"\n",
"例如,垃圾回收机对垃圾进行分类。基类是“垃圾”。每块垃圾都有重量、价值等特性,它们可以被切碎、熔化或分解。在此基础上,可以通过添加额外的特性(瓶子有颜色,钢罐有磁性)或行为(铝罐可以被压碎)派生出更具体的垃圾类型。此外,一些行为可以不同(纸张的价值取决于它的类型和状态)。使用继承,你将构建一个类型层次结构,来表示你试图解决的某种类型的问题。第二个例子是常见的“形状”例子,可能用于计算机辅助设计系统或游戏模拟。基类是“形状”,每个形状都有大小、颜色、位置等等。每个形状可以绘制、擦除、移动、着色等。由此,可以派生出(继承出)具体类型的形状——圆形、正方形、三角形等等——每个形状可以具有附加的特征和行为。\n",
"\n",
"![1545764780795](../images/1545764780795.png)\n",
"\n",
"例如,某些形状可以翻转。有些行为可能不同,比如计算形状的面积。类型层次结构体现了形状之间的相似性和差异性。以相同的术语将解决方案转换成问题是有用的,因为你不需要在问题描述和解决方案描述之间建立许多中间模型。通过使用对象,类型层次结构成为了主要模型,因此你可以直接从真实世界中对系统的描述过渡到用代码对系统进行描述。事实上,有时候,那些善于寻找复杂解决方案的人会被面向对象设计的简单性难倒。从现有类型继承创建新类型。这种新类型不仅包含现有类型的所有成员(尽管私有成员被隐藏起来并且不可访问),而且更重要的是它复制了基类的接口。也就是说,基类对象接收的所有消息也能被派生类对象接收。根据类接收的消息,我们知道类的类型,因此派生类与基类是相同的类型。\n",
"\n",
"在前面的例子中,“圆是形状”。这种通过继承的类型等价性是理解面向对象编程含义的基本门槛之一。因为基类和派生类都具有相同的基本接口,所以伴随此接口的必定有某些具体实现。也就是说,当对象接收到特定消息时,必须有可执行代码。如果继承一个类而不做其他任何事,则来自基类接口的方法直接进入派生类。这意味着派生类和基类不仅具有相同的类型,而且具有相同的行为,这么做没什么特别意义。\n",
"\n",
"有两种方法可以区分新的派生类与原始的基类。第一种方法很简单:在派生类中添加新方法。这些新方法不是基类接口的一部分。这意味着基类不能满足你的所有需求,所以你添加了更多的方法。继承的这种简单而原始的用途有时是解决问题的完美解决方案。然而,还是要仔细考虑是否在基类中也要有这些额外的方法。这种设计的发现与迭代过程在面向对象程序设计中会经常发生。\n",
"\n",
"尽管继承有时意味着你要在接口中添加新方法(尤其是在以 **extends** 关键字表示继承的 Java 中),但并非总需如此。第二种也是更重要地区分派生类和基类的方法是改变现有基类方法的行为,这被称为覆盖 (overriding)。要想覆盖一个方法,只需要在派生类中重新定义这个方法即可。\n",
"\n",
"### \"是一个\"与\"像是一个\"的关系\n",
"\n",
"对于继承可能会引发争论:继承应该只覆盖基类的方法(不应该添加基类中没有的方法)吗?如果这样的话,基类和派生类就是相同的类型了,因为它们具有相同的接口。这会造成,你可以用一个派生类对象完全替代基类对象,这叫作\"纯粹替代\",也经常被称作\"替代原则\"。在某种意义上这是一种处理继承的理想方式。我们经常把这种基类和派生类的关系称为是一个is-a关系因为可以说\"圆是一个形状\"。判断是否继承,就看在你的类之间有无这种 is-a 关系。\n",
"\n",
"有时你在派生类添加了新的接口元素,从而扩展接口。虽然新类型仍然可以替代基类,但是这种替代不完美,原因在于基类无法访问新添加的方法。这种关系称为像是一个(is-like-a)关系。新类型不但拥有旧类型的接口,而且包含其他方法,所以不能说新旧类型完全相同。\n",
"\n",
"![1545764820176](../images/1545764820176.png)\n",
"\n",
"以空调为例假设房间里已经安装好了制冷设备的控制器即你有了控制制冷设备的接口。想象一下现在空调坏了你重新安装了一个既制冷又制热的热力泵。热力泵就像是一个is-like-a空调但它可以做更多。因为当初房间的控制系统被设计成只能控制制冷设备所以它只能与新对象(热力泵)的制冷部分通信。新对象的接口已经扩展了,现有控制系统却只知道原来的接口,一旦看到这个设计,你就会发现,作为基类的制冷系统不够一般化,应该被重新命名为\"温度控制系统\",也应该包含制热功能,这样的话,我们就可以使用替代原则了。上图反映了在现实世界中进行设计时可能会发生的事情。\n",
"\n",
"当你看到替代原则时,很容易会认为纯粹替代是唯一可行的方式,并且使用纯粹替代的设计是很好的。但有些时候,你会发现必须得在派生(扩展)类中添加新方法(提供新的接口)。只要仔细审视,你可以很明显地区分两种设计方式的使用场合。\n",
"\n",
"## 多态\n",
"\n",
"我们在处理类的层次结构时通常把一个对象看成是它所属的基类而不是把它当成具体类。通过这种方式我们可以编写出不局限于特定类型的代码。在上个“形状”的例子中“方法”method操纵的是通用“形状”而不关心它们是“圆”、“正方形”、“三角形”还是某种尚未定义的形状。所有的形状都可以被绘制、擦除和移动因此“方法”向其中的任何代表“形状”的对象发送消息都不必担心对象如何处理信息。\n",
"\n",
"这样的代码不会受添加的新类型影响,并且添加新类型是扩展面向对象程序以处理新情况的常用方法。 例如,你可以通过通用的“形状”基类派生出新的“五角形”形状的子类,而不需要修改通用\"形状\"基类的方法。通过派生新的子类来扩展设计的这种能力是封装变化的基本方法之一。\n",
"\n",
"这种能力改善了我们的设计且减少了软件的维护代价。如果我们把派生的对象类型统一看成是它本身的基类“圆”当作“形状”“自行车”当作“车”“鸬鹚”当作“鸟”等等编译器compiler在编译时期就无法准确地知道什么“形状”被擦除哪一种“车”在行驶或者是哪种“鸟”在飞行。这就是关键所在当程序接收这种消息时程序员并不想知道哪段代码会被执行。“绘图”的方法可以平等地应用到每种可能的“形状”上形状会依据自身的具体类型执行恰当的代码。\n",
"\n",
"如果不需要知道执行了哪部分代码,那我们就能添加一个新的不同执行方式的子类而不需要更改调用它的方法。那么编译器在不确定该执行哪部分代码时是怎么做的呢?举个例子,下图的 **BirdController** 对象和通用 **Bird** 对象中,**BirdController** 不知道 **Bird** 的确切类型却还能一起工作。从 **BirdController** 的角度来看,这是很方便的,因为它不需要编写特别的代码来确定 **Bird** 对象的确切类型或行为。那么,在调用 **move()** 方法时是如何保证发生正确的行为(鹅走路、飞或游泳、企鹅走路或游泳)的呢?\n",
"\n",
"![Bird-example](../images/1545839316314.png)\n",
"\n",
"这个问题的答案,是面向对象程序设计的妙诀:在传统意义上,编译器不能进行函数调用。由非 OOP 编译器产生的函数调用会引起所谓的**早期绑定**,这个术语你可能从未听说过,不会想过其他的函数调用方式。这意味着编译器生成对特定函数名的调用,该调用会被解析为将执行的代码的绝对地址。\n",
"\n",
"通过继承,程序直到运行时才能确定代码的地址,因此发送消息给对象时,还需要其他一些方案。为了解决这个问题,面向对象语言使用**后期绑定**的概念。当向对象发送信息时,被调用的代码直到运行时才确定。编译器确保方法存在,并对参数和返回值执行类型检查,但是它不知道要执行的确切代码。\n",
"\n",
"为了执行后期绑定Java 使用一个特殊的代码位来代替绝对调用。这段代码使用对象中存储的信息来计算方法主体的地址此过程在多态性章节中有详细介绍。因此每个对象的行为根据特定代码位的内容而不同。当你向对象发送消息时对象知道该如何处理这条消息。在某些语言中必须显式地授予方法后期绑定属性的灵活性。例如C++ 使用 **virtual** 关键字。在这些语言中,默认情况下方法不是动态绑定的。在 Java 中,动态绑定是默认行为,不需要额外的关键字来实现多态性。\n",
"\n",
"为了演示多态性,我们编写了一段代码,它忽略了类型的具体细节,只与基类对话。该代码与具体类型信息分离,因此更易于编写和理解。而且,如果通过继承添加了一个新类型(例如,一个六边形),那么代码对于新类型的 Shape 就像对现有类型一样有效。因此,该程序是可扩展的。\n",
"\n",
"代码示例:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"java"
],
"id": ""
}
},
"outputs": [],
"source": [
"void doSomething(Shape shape) {\n",
" shape.erase();\n",
" // ...\n",
" shape.draw();\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"此方法与任何 **Shape** 对话,因此它与所绘制和擦除的对象的具体类型无关。如果程序的其他部分使用 `doSomething()` 方法:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"java"
],
"id": ""
}
},
"outputs": [],
"source": [
" Circle circle = new Circle();\n",
" Triangle triangle = new Triangle();\n",
" Line line = new Line();\n",
" doSomething(circle);\n",
" doSomething(triangle);\n",
" doSomething(line);\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"可以看到无论传入的“形状”是什么,程序都正确的执行了。\n",
"\n",
"![shape-example](../images/1545841270997.png)\n",
"\n",
"这是一个非常令人惊奇的编程技巧。分析下面这行代码:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"java"
],
"id": ""
}
},
"outputs": [],
"source": [
" doSomething(circle);"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"当预期接收 **Shape** 的方法被传入了 **Circle**,会发生什么。由于 **Circle** 也是一种 **Shape**,所\n",
"以 `doSomething(circle)` 能正确地执行。也就是说,`doSomething()` 能接收任意发送给 **Shape** 的消息。这是完全安全和合乎逻辑的事情。\n",
"\n",
"这种把子类当成其基类来处理的过程叫做“向上转型”(**upcasting**)。在面向对象的编程里,经常利用这种方法来给程序解耦。再看下面的 `doSomething()` 代码示例:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"java"
],
"id": ""
}
},
"outputs": [],
"source": [
" shape.erase();\n",
" // ...\n",
" shape.draw();"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"我们可以看到程序并未这样表达:“如果你是一个 Circle ,就这样做;如果你是一个 Square就那样做...”。若那样编写代码,就需检查 Shape 所有可能的类型,如圆、矩形等等。这显然是非常麻烦的,而且每次添加了一种新的 Shape 类型后,都要相应地进行修改。在这里,我们只需说:“你是一种几何形状,我知道你能删掉 `erase()` 和绘制 `draw()`,你自己去做吧,注意细节。”\n",
"\n",
"尽管我们没作出任何特殊指示,程序的操作也是完全正确和恰当的。我们知道,为 Circle 调用`draw()` 时执行的代码与为一个 Square 或 Line 调用 `draw()` 时执行的代码是不同的。但在将 `draw()` 信息发给一个匿名 Shape 时,根据 Shape 句柄当时连接的实际类型,会相应地采取正确的操作。这非常神奇,因为当 Java 编译器为 `doSomething()` 编译代码时,它并不知道自己要操作的准确类型是什么。\n",
"\n",
"尽管我们确实可以保证最终会为 Shape 调用 `erase()` 和 `draw()`,但并不能确定特定的 CircleSquare 或者 Line 调用什么。最后,程序执行的操作却依然是正确的,这是怎么做到的呢?\n",
"\n",
"发送消息给对象时如果程序不知道接收的具体类型是什么但最终执行是正确的这就是对象的“多态性”Polymorphism。面向对象的程序设计语言是通过“动态绑定”的方式来实现对象的多态性的。编译器和运行时系统会负责对所有细节的控制我们只需知道要做什么以及如何利用多态性来更好地设计程序。\n",
"\n",
"## 单继承结构\n",
"\n",
"自从 C++ 引入以来,一个 OOP 问题变得尤为突出:是否所有的类都应该默认从一个基类继承呢?这个答案在 Java 中是肯定的(实际上,除 C++ 以外的几乎所有OOP语言中也是这样。在 Java 中,这个最终基类的名字就是 `Object`。\n",
"\n",
"Java 的单继承结构有很多好处。由于所有对象都具有一个公共接口,因此它们最终都属于同一个基类。相反的,对于 C++ 所使用的多继承的方案则是不保证所有的对象都属于同一个基类。从向后兼容的角度看,多继承的方案更符合 C 的模型,而且受限较少。\n",
"\n",
"对于完全面向对象编程,我们必须要构建自己的层次结构,以提供与其他 OOP 语言同样的便利。我们经常会使用到新的类库和不兼容的接口。为了整合它们而花费大气力(有可能还要用上多继承)以获得 C++ 样的“灵活性”值得吗如果从零开始Java 这样的替代方案会是更好的选择。\n",
"\n",
"另外,单继承的结构使得垃圾收集器的实现更为容易。这也是 Java 在 C++ 基础上的根本改进之一。\n",
"\n",
"由于运行期的类型信息会存在于所有对象中,所以我们永远不会遇到判断不了对象类型的情况。这对于系统级操作尤其重要,例如[异常处理](#异常处理)。同时,这也让我们的编程具有更大的灵活性。\n",
"\n",
"## 集合\n",
"\n",
"通常,我们并不知道解决某个具体问题需要的对象数量和持续时间,以及对象的存储方式。那么我们如何知悉程序在运行时需要分配的内存空间呢?\n",
"\n",
"在面向对象的设计中问题的解决方案有些过于轻率创建一个新类型的对象来引用、容纳其他的对象。当然我们也可以使用多数编程语言都支持的“数组”array。在 Java 中“集合”Collection的使用率更高。也可称之为“容器”但“集合”这个称呼更通用。\n",
"\n",
"“集合”这种类型的对象可以存储任意类型、数量的其他对象。它能根据需要自动扩容,我们不用关心过程是如何实现的。\n",
"\n",
"还好,一般优秀的 OOP 语言都会将“集合”作为其基础包。在 C++ 中,“集合”是其标准库的一部分,通常被称为 STLStandard Template Library标准模板库。SmallTalk 有一套非常完整的集合库。同样Java 的标准库中也提供许多现成的集合类。\n",
"\n",
"在一些库中一两个泛型集合就能满足我们所有的需求了而在其他一些类库Java不同类型的集合对应不同的需求常见的有 List常用于保存序列Map也称为关联数组常用于将对象与其他对象关联Set只能保存非重复的值其他还包括如队列Queue、树Tree、栈Stack、堆Heap等等。从设计的角度来看我们真正想要的是一个能够解决某个问题的集合。如果一种集合就满足所有需求那么我们就不需要剩下的了。之所以选择集合有以下两个原因\n",
"\n",
"1. 集合可以提供不同类型的接口和外部行为。堆栈、队列的应用场景和集合、列表不同,它们中的一种提供的解决方案可能比其他灵活得多。\n",
"\n",
"2. 不同的集合对某些操作有不同的效率。例如List 的两种基本类型ArrayList 和 LinkedList。虽然两者具有相同接口和外部行为但是在某些操作中它们的效率差别很大。在 ArrayList 中随机查找元素是很高效的,而 LinkedList 随机查找效率低下。反之,在 LinkedList 中插入元素的效率要比在 ArrayList 中高。由于底层数据结构的不同,每种集合类型在执行相同的操作时会表现出效率上的差异。\n",
"\n",
"我们可以一开始使用 LinkedList 构建程序,在优化系统性能时改用 ArrayList。通过对 List 接口的抽象,我们可以很容易地将 LinkedList 改为 ArrayList。\n",
"\n",
"在 Java 5 泛型出来之前,集合中保存的是通用类型 `Object`。Java 单继承的结构意味着所有元素都基于 `Object` 类,所以在集合中可以保存任何类型的数据,易于重用。要使用这样的集合,我们先要往集合添加元素。由于 Java 5 版本前的集合只保存 `Object`,当我们往集合中添加元素时,元素便向上转型成了 `Object`,从而丢失自己原有的类型特性。这时我们再从集合中取出该元素时,元素的类型变成了 `Object`。那么我们该怎么将其转回原先具体的类型呢这里我们使用了强制类型转换将其转为更具体的类型这个过程称为对象的“向下转型”。通过“向上转型”我们知道“圆形”也是一种“形状”这个过程是安全的。可是我们不能从“Object”看出其就是“圆形”或“形状”所以除非我们能确定元素的具体类型信息否则“向下转型”就是不安全的。也不能说这样的错误就是完全危险的因为一旦我们转化了错误的类型程序就会运行出错抛出“运行时异常”RuntimeException后面的章节会提到 无论如何我们要寻找一种在取出集合元素时确定其具体类型的方法。另外每次取出元素都要做额外的“向下转型”对程序和程序员都是一种开销。以某种方式创建集合以确认保存元素的具体类型减少集合元素“向下转型”的开销和可能出现的错误难道不好吗这种解决方案就是参数化类型机制Parameterized Type Mechanism。\n",
"\n",
"参数化类型机制可以使得编译器能够自动识别某个 `class` 的具体类型并正确地执行。举个例子对集合的参数化类型机制可以让集合仅接受“形状”这种类型的元素并以“形状”类型取出元素。Java 5 版本支持了参数化类型机制称之为“泛型”Generic。泛型是 Java 5 的主要特性之一。你可以按以下方式向 ArrayList 中添加 Shape形状"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"java"
],
"id": ""
}
},
"outputs": [],
"source": [
" ArrayList<Shape> shapes = new ArrayList<>();"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"泛型的应用,让 Java 的许多标准库和组件都发生了改变。在本书的代码示例中,你也会经常看到泛型的身影。\n",
"\n",
"## 对象创建与生命周期\n",
"\n",
"我们在使用对象时要注意的一个关键问题就是对象的创建和销毁方式。每个对象的生存都需要资源,尤其是内存。为了资源的重复利用,当对象不再被使用时,我们应该及时释放资源,清理内存。\n",
"\n",
"在简单的编程场景下,对象的清理并不是问题。我们创建对象,按需使用,最后销毁它。然而,情况往往要比这更复杂:\n",
"\n",
"假设,我们正在为机场设计一个空中交通管制的系统(该例也适用于仓库货柜管理、影带出租或者宠物寄养仓库系统)。第一步比较简单:创建一个用来保存飞机的集合,每当有飞机进入交通管制区域时,我们就创建一个“飞机”对象并将其加入到集合中,等到飞机离开时将其从这个集合中清除。与此同时,我们还需要一个记录飞机信息的系统,也许这些数据不像主要控制功能那样引人注意。比如,我们要记录所有飞机中的小型飞机的的信息(比如飞行计划)。此时,我们又创建了第二个集合来记录所有小型飞机。 每当创建一个“飞机”对象的时候,将其放入第一个集合;若它属于小型飞机,也必须同时将其放入第二个集合里。\n",
"\n",
"现在问题开始棘手了:我们怎么知道何时该清理这些对象呢?当某一个系统处理完成,而其他系统可能还没有处理完成。这样的问题在其他的场景下也可能发生。在 C++ 程序设计中,当使用完一个对象后,必须明确将其删除,这就让问题变复杂了。\n",
"\n",
"对象的数据在哪?它的生命周期是怎么被控制的? 在 C++ 设计中采用的观点是效率第一因此它将选择权交给了程序员。为了获得最大的运行时速度程序员可以在编写程序时通过将对象放在栈Stack有时称为自动变量或作用域变量或静态存储区域static storage area中来确定内存占用和生存时间。这些区域的对象会被优先分配内存和释放。这种控制在某些情况下非常有用。\n",
"\n",
"然而相对的,我们也牺牲了程序的灵活性。因为在编写代码时,我们必须要弄清楚对象的数量、生存时间还有类型。如果我们要用它来解决一个相当普遍的问题时(如计算机辅助设计、仓库管理或空中交通管制等),限制就太大了。\n",
"\n",
"第二种方法是在堆内存Heap中动态地创建对象。在这种方式下直到程序运行我们才能确定需要创建的对象数量、生存时间和类型。什么时候需要什么时候在堆内存中创建。 因为内存的占用是动态管理的,所以在运行时,在堆内存上开辟空间所需的时间可能比在栈内存上要长(但也不一定)。在栈内存开辟和释放空间通常是一条将栈指针向下移动和一条将栈指针向上移动的汇编指令。开辟堆内存空间的时间取决于内存机制的设计。\n",
"\n",
"动态方法有这样一个合理假设:对象通常是复杂的,相比于对象创建的整体开销,寻找和释放内存空间的开销微不足道。(原文:*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.*)此外,更好的灵活性对于问题的解决至关重要。\n",
"\n",
"Java 使用动态内存分配。每次创建对象时,使用 `new` 关键字构建该对象的动态实例。这又带来另一个问题:对象的生命周期。较之堆内存,在栈内存中创建对象,编译器能够确定该对象的生命周期并自动销毁它;然而如果你在堆内存创建对象的话,编译器是不知道它的生命周期的。在 C++ 中你必须以编程方式确定何时销毁对象否则可能导致内存泄漏。Java 的内存管理是建立在垃圾收集器上的,它能自动发现对象不再被使用并释放内存。垃圾收集器的存在带来了极大的便利,它减少了我们之前必须要跟踪的问题和编写相关代码的数量。因此,垃圾收集器提供了更高级别的保险,以防止潜在的内存泄漏问题,这个问题使得许多 C++ 项目没落。\n",
"\n",
"Java 的垃圾收集器被设计用来解决内存释放的问题虽然这不包括对象清理的其他方面。垃圾收集器知道对象什么时候不再被使用并且自动释放内存。结合单继承和仅可在堆中创建对象的机制Java 的编码过程比用 C++ 要简单得多。我们所要做的决定和要克服的障碍也会少很多!\n",
"\n",
"## 异常处理\n",
"\n",
"自编程语言被发明以来,程序的错误处理一直都是个难题。因为很难设计出一个好的错误处理方案,所以许多编程语言都忽略了这个问题,把这个问题丢给了程序类库的设计者。他们提出了在许多情况下都可以工作但很容易被规避的半途而废的措施,通常只需忽略错误。多数错误处理方案的主要问题是:它们依赖程序员之间的约定俗成而不是语言层面的限制。换句话说,如果程序员赶时间或没想起来,这些方案就很容易被忘记。\n",
"\n",
"异常处理机制将程序错误直接交给编程语言甚至是操作系统。“异常”Exception是一个从出错点“抛出”thrown后能被特定类型的异常处理程序捕获(catch)的一个对象。它不会干扰程序的正常运行,仅当程序出错的时候才被执行。这让我们的编码更简单:不用再反复检查错误了。另外,异常不像方法返回的错误值和方法设置用来表示发生错误的标志位那样可以被忽略。异常的发生是不会被忽略的,它终究会在某一时刻被处理。\n",
"\n",
"最后,“异常机制”提供了一种可靠地从错误状况中恢复的方法,使得我们可以编写出更健壮的程序。有时你只要处理好抛出的异常情况并恢复程序的运行即可,无需退出。\n",
"\n",
"Java 的异常处理机制在编程语言中脱颖而出。Java 从一开始就内置了异常处理,因此你不得不使用它。这是 Java 语言唯一接受的错误报告方法。如果没有编写适当的异常处理代码,你将会收到一条编译时错误消息。这种有保障的一致性有时会让程序的错误处理变得更容易。值得注意的是,异常处理并不是面向对象的特性。尽管在面向对象的语言中异常通常由对象表示,但是在面向对象语言之前也存在异常处理。\n",
"\n",
"## 本章小结\n",
"\n",
"面向过程程序包含数据定义和函数调用。要找到程序的意图你必须要在脑中建立一个模型弄清函数调用和更底层的概念。这些程序令人困扰因为它们的表示更多地面向计算机而不是我们要解决的问题这就是我们在设计程序时需要中间表示的原因。OOP 在面向过程编程的基础上增加了许多新的概念,所以有人会认为使用 Java 来编程会比同等的面向过程编程要更复杂。在这里,我想给大家一个惊喜:通常按照 Java 规范编写的程序会比面向过程程序更容易被理解。\n",
"\n",
"你看到的是对象的概念,这些概念是站在“问题空间”的(而不是站在计算机角度的“解决方案空间”),以及发送消息给对象以指示该空间中的活动。面向对象编程的一个优点是:设计良好的 Java 程序代码更容易被人阅读理解。由于 Java 类库的复用性,通常程序要写的代码也会少得多。\n",
"\n",
"OOP 和 Java 不一定适合每个人。评估自己的需求以及与现有方案作比较是很重要的。请充分考虑后再决定是不是选择 Java。如果在可预见的未来Java 并不能很好的满足你的特定需求,那么你应该去寻找其他替代方案(特别是,我推荐看 Python。如果你依然选择 Java 作为你的开发语言,我希望你至少应该清楚你选择的是什么,以及为什么选择这个方向。\n",
"<!-- 分页 -->\n",
"\n",
"<div style=\"page-break-after: always;\"></div>"
]
}
],
"metadata": {},
"nbformat": 4,
"nbformat_minor": 4
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,111 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# 简介\n",
"\n",
"> “我的语言极限,即是我的世界的极限。” ——路德维希·维特根斯坦(*Wittgenstein*\n",
"\n",
"这句话无论对于自然语言还是编程语言来说都是一样的。你所使用的编程语言会将你的思维模式固化并逐渐远离其他语言而且往往发生在潜移默化中。Java 作为一门傲娇的语言尤其如此。\n",
"\n",
"Java 是一门派生语言,早期语言设计者为了不想在项目中使用 C++ 而创造了这种看起来很像 C++,却比 C++ 有了改进的新语言原始的项目并未成功。Java 最核心的变化就是加入了“虚拟机”和“垃圾回收机制”,这两个概念在之后的章节会有详细描述。 此外Java 还在其他方面推动了行业发展。例如,现在绝大多数编程语言都支持文档注释语法和 HTML 文档生成工具。\n",
"\n",
"Java 最主要的概念之一“对象”来自 SmallTalk 语言。SmallTalk 语言恪守“对象”(在下一章中描述)是编程的最基本单元。于是,万物皆对象。历经时间的检验,人们发现这种信念太过狂热。有些人甚至认为“对象”的概念是完全错误的,应该舍弃。就我个人而言,把一切事物都抽象成对象不仅是一项不必要的负担,同时还会招致许多设计朝着不好的方向发展。尽管如此,“对象”的概念依然有其闪光点。固执地要求所有东西都是一个对象(特别是一直到最底层级别)是一种设计错误;相反,完全逃避“对象”的概念似乎同样太过苛刻。\n",
"\n",
"Java 语言曾规划设计的许多功能并未按照承诺兑现。本书中,我将尝试解释这些原因,力争让读者知晓这些功能,并明白为什么这些功能最终并不适用。这无关 Java 是一种好语言或者坏语言,一旦你了解了该语言的缺陷和局限性,你就能够:\n",
"\n",
"1. 明白有些功能特性为什么会被“废弃”。\n",
"\n",
"2. 熟悉语言边界,更好地设计和编码。\n",
"\n",
"编程的过程就是复杂性管理的过程:业务问题的复杂性,以及依赖的计算机的复杂性。由于这种复杂性,我们的大多数软件项目都失败了。\n",
"\n",
"许多语言设计决策时都考虑到了复杂性并试图降低语言的复杂性但在设计过程中遇到了一些更棘手的问题最终导致语言设计不可避免地“碰壁”复杂性增加。例如C++ 必须向后兼容 C允许 C 程序员轻松迁移),并且效率很高。这些目标非常实用,并且也是 C++ 在编程界取得了成功的原因之一,但同时也引入了额外的复杂性,导致某些用 C++ 编写的项目开发失败。当然,你可以责怪程序员和管理人员手艺不精,但如果有一种编程语言可以帮助你在开发过程中发现错误,那岂不是更好?\n",
"\n",
"虽然 VBVisual BASIC绑定在 BASIC 上,但 BASIC 实际上并不是一种可扩展的语言。大量扩展的堆积造成 VB 的语法难以维护。Perl 向后兼容 awk、sed、grep 以及其它要替换的 Unix 工具。因此它常常被诟病产生了一堆“只写代码”(*write-only code*写代码的人自己都看不懂的代码。另一方面C ++VBPerl 和其他语言(如 SmallTalk在设计时重点放在了对某些复杂问题的处理上因而在解决这些特定类型的问题方面非常成功。\n",
"\n",
"通信革命使我们相互沟通更加便利。无论是一对一沟通,还是团队里的互相沟通,甚至是地球上不同地区的沟通。据说下一次革命需要的是一种全球性的思维,这种思维源于足量的人以及足量相互连接。我不知道 Java 是否能成为这场革命的工具之一,但至少这种可能性让我觉得:我现在正在做的传道授业的事情是有意义的!\n",
"\n",
"## 前提条件\n",
"\n",
"阅读本书需要读者对编程有基本的了解:\n",
"\n",
"- 程序是一系列“陈述(语句、代码)”构成\n",
"\n",
"- 子程序、方法、宏的概念\n",
"\n",
"- 控制语句(例如 **if**),循环结构(例如 **while**\n",
"\n",
"可能你已在学校、书籍或网络上了学过这些。只要你觉得对上述的编程基本概念熟悉,你就可以完成本书的学习。\n",
"\n",
"你可以通过在 On Java 8 的网站上免费下载 《Think in C》来补充学习 Java 所需要的前置知识。本书介绍了 Java 语言的基本控制机制以及面向对象编程OOP的概念。在本书中我引述了一些 C/C++ 语言中的一些特性来帮助读者更好的理解 Java。毕竟 Java 是在它们的基础之上发明的,理解他们之间的区别,有助于读者更好地学习 Java。我会试图简化这些引述尽量让没有 C/C++ 基础的读者也能很好地理解。\n",
"\n",
"## JDK文档\n",
"\n",
"甲骨文公司已经提供了免费的标准 JDK 文档。除非有必要,否则本书中将不再赘述 API 相关的使用细节。使用浏览器来即时搜索最新最全的 JDK 文档好过翻阅本书来查找。只有在需要补充特定的示例时,我才会提供有关的额外描述。\n",
"\n",
"## C编程思想\n",
"\n",
"*Thinking in C* 已经可以在 [www.OnJava8.com](https://archive.org/details/ThinkingInC) 免费下载。Java 的基础语法是基于 C 语言的。*Thinking in C* 中有更适合初学者的编程基础介绍。 我已经委托 Chuck Allison 将这本 C 基础的书籍作为独立产品附赠于本书的 CD 中。希望大家在阅读本书时,都已具备了学习 Java 的良好基础。\n",
"\n",
"## 源码下载\n",
"\n",
"本书中所有源代码的示例都在版权保护的前提下通过 GitHub 免费提供。你可以将这些代码用于教育。任何人不得在未经正确引用代码来源的情况下随意重新发布此代码示例。在每个代码文件中,你都可以找到以下版权声明文件作为参考:\n",
"\n",
"**Copyright.txt**\n",
"\n",
"©2017 MindView LLC。版权所有。如果上述版权声明本段和以下内容特此授予免费使用复制修改和分发此计算机源代码源代码及其文档的许可且无需出于下述目的的书面协议所有副本中都有五个编号的段落。\n",
"\n",
"1. 允许编译源代码并将编译代码仅以可执行格式包含在个人和商业软件程序中。\n",
"\n",
"2. 允许在课堂情况下使用源代码而不修改源代码,包括在演示材料中,前提是 “On Java 8” 一书被引用为原点。\n",
"\n",
"3. 可以通过以下方式获得将源代码合并到印刷媒体中的许可MindView LLCPO Box 969Crested ButteCO 81224 MindViewInc@gmail.com \n",
"\n",
"4. 源代码和文档的版权归 MindView LLC 所有。提供的源代码没有任何明示或暗示的担保包括任何适销性适用于特定用途或不侵权的默示担保。MindView LLC 不保证任何包含源代码的程序的运行不会中断或没有错误。MindView LLC 不对任何目的的源代码或包含源代码的任何软件的适用性做出任何陈述。包含源代码的任何程序的质量和性能的全部风险来自源代码的用户。用户理解源代码是为研究和教学目的而开发的,建议不要仅仅因任何原因依赖源代码或任何包含源代码的程序。如果源代码或任何产生的软件证明有缺陷,则用户承担所有必要的维修,修理或更正的费用。\n",
"\n",
"5. 在任何情况下MINDVIEW LLC 或其出版商均不对任何一方根据任何法律理论对直接,间接,特殊,偶发或后果性损害承担任何责任,包括利润损失,业务中断,商业信息丢失或任何其他保险公司。由于 MINDVIEW LLC 或其出版商已被告知此类损害的可能性因此使用本源代码及其文档或因无法使用任何结果程序而导致的个人受伤或者个人受伤。MINDVIEW LLC 特别声明不提供任何担保包括但不限于对适销性和特定用途适用性的暗示担保。此处提供的源代码和文档基于“原样”基础没有MINDVIEW LLC的任何随附服务MINDVIEW LLC 没有义务提供维护,支持,更新,增强或修改。\n",
"\n",
"**请注意**MindView LLC 仅提供以下唯一网址发布更新书中的代码示例https://github.com/BruceEckel/OnJava8-examples 。你可在上述条款范围内将示例免费使用于项目和课堂中。\n",
"\n",
"如果你在源代码中发现错误请在下面的网址提交更正https://github.com/BruceEckel/OnJava8-examples/issues \n",
"\n",
"## 编码样式\n",
"\n",
"本书中代码标识符(关键字,方法,变量和类名)以粗体,固定宽度代码字体显示。像 “*class” 这种在代码中高频率出现的关键字可能让你觉得粗体有点乏味。(译者注:由于中英排版差异,中文翻译过程并未完全参照原作者的说明。具体排版格式请参考[此处](https://github.com/ruanyf/document-style-guide))其他显示为正常字体。本书文本格式尽可能遵循 Oracle 常见样式,并保证在大多数 Java 开发环境中被支持。书中我使用了自己喜欢的字体风格。Java 是一种自由的编程语言,你也可以使用 IDE集成开发环境工具如 IntelliJ IDEAEclipse 或 NetBeans将格式更改为适合你的格式。\n",
"\n",
"本书代码文件使用自动化工具进行测试,并在最新版本的 Java 编译通过(除了那些特别标记的错误之外)。本书重点介绍并使用 Java 8 进行测试。如果你必须了解更早的语言版本,可以在 [www.OnJava8.com](http://www.OnJava8.com) 免费下载 《Thinking in Java》。\n",
"\n",
"## BUG提交\n",
"\n",
"本书经过多重校订,但还是难免有所遗漏被新读者发现。如果你在正文或示例中发现任何错误的内容,请在[此处](https://github.com/BruceEckel/OnJava8-examples/issues)提交错误以及建议更正,作者感激不尽。\n",
"\n",
"## 邮箱订阅\n",
"\n",
"你可以在 [www.OnJava8.com上](http://www.OnJava8.com) 订阅邮件。邮件不含广告并尽量提供干货。\n",
"\n",
"## Java图形界面\n",
"\n",
"Java 在图形用户界面和桌面程序方面的发展可以说是一段悲伤的历史。Java 1.0 中图形用户界面GUI库的原始设计目标是让用户能在所有平台提供一个漂亮的界面。但遗憾的是这个理想没有实现。相反Java 1.0 AWT抽象窗口工具包在所有平台都表现平平并且有诸多限制。你只能使用四种字体。另外Java 1.0 AWT 编程模型也很笨拙且非面向对象。我的一个曾在 Java 设计期间工作过的学生道出了缘由:早期的 AWT 设计是在仅仅在一个月内构思、设计和实施的。不得不说这是一个“奇迹”,但同时更是“设计失败”的绝佳教材。\n",
"\n",
"在 Java 1.1 版本的 AWT 中 情况有所改善事件模型带来更加清晰的面向对象方法并添加了JavaBeans致力于面向易于创建可视化编程环境的组件编程模型已废弃。\n",
"\n",
"Java 2Java 1.2)通过使用 Java 基类JFC内容替换来完成从旧版 Java 1.0 AWT 的转换。其中 GUI 部分称为 Swing。这是一组丰富的 JavaBeans它们创建了一个合理的 GUI。修订版 33之前都不好比以往更适用于开发图形界面程序。\n",
"\n",
"Sun 在图形界面的最后一次尝试,称为 JavaFX。当 Oracle 收购 Sun 时,他们将原来雄心勃勃的项目(包括脚本语言)改为库,现在它似乎是 Java 官方唯一还在开发中的 UI 工具包(参见维基百科关于 JavaFX 的文章) - 但即使如此JavaFX 最终似乎也失败了。\n",
"\n",
"现今 Swing 依然是 Java 发行版的一部分(只接受维护,不再有新功能开发)。而 Java 现在是一个开源项目它应该始终可用。此外Swing 和 JavaFX 有一些有限的交互性。这些可能是为了帮助开发者过渡到 JavaFX。\n",
"\n",
"桌面程序领域似乎从未尝勾起 Java 设计师的野心。Java 没有在图形界面取得该有的一席之地。另外,曾被大肆吹嘘的 JavaBeans 也没有获得任何影响力。(许多不幸的作者花了很多精力在 Swing 上编写书籍,甚至只用 JavaBeans 编写书籍。Java 图形界面程序大多数情况下仅用于 IDE集成开发环境和一些企业内部应用程序。你可以采用 Java 开发图形界面,但这并非 Java 最擅长的领域。如果你必须学习 Swing可以参考 *Thinking in Java* 第4版可从 www.OnJava8.com 获得)或者通过其他专门的书籍学习。\n",
"\n",
"<!-- 分页 -->\n",
"<div style=\"page-break-after: always;\"></div>"
]
}
],
"metadata": {},
"nbformat": 4,
"nbformat_minor": 4
}

View File

@@ -1,76 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<div align=\"center\">\n",
" <img src=\"https://raw.githubusercontent.com/LingCoder/OnJava8/master/docs/images/cover.jpg\" alt=\"cover\" width=\"100%\"/>\n",
"</div>\n",
"\n",
"<div style=\"page-break-after: always;\"></div>\n",
"\n",
"<center style=\"165px 0px 0px\">\n",
"<p style=\"font-size: 6em;font-weight: bold;text-align: center\"> On Java 8 </p>\n",
"</center>\n",
"\n",
"<center style=\"165px 0px 0px\">\n",
"<p style=\"font-size: 2em;font-weight: bold;text-align: center\">Bruce Eckel</p>\n",
"\n",
"</center>\n",
"\n",
"<center> MindView LLC </center>\n",
"\n",
"\n",
"<center>2017</center>\n",
"\n",
"\n",
"<center>©MindView LLC 版权所有</center>\n",
"\n",
"\n",
"\n",
"\n",
"\n",
"<div style=\"page-break-after: always;\"></div>\n",
"\n",
"\n",
"\n",
"\n",
"# On Java 8\n",
"\n",
"\n",
"\n",
"**版权©2017**\n",
"\n",
"\n",
"**作者 Bruce Eckel, President, MindView LLC.**\n",
"\n",
"\n",
"**版本号7**\n",
"\n",
"\n",
"**ISBN 978-0-9818725-2-0**\n",
"\n",
"\n",
"**原书可在该网站购买 [www.OnJava8.com](http://www.OnJava8.com)** \n",
"\n",
"\n",
"\n",
"\n",
"本书出版自美国,版权所有,翻版必究。未经授权不得非法存储在检索系统中,或以电子,机械,影印,录制任何形式传输等。制造商和销售商使用商标用来区分其产品标识。如果这些名称出现在这本书中,并且出版商知道商标要求,则这些名称已经用大写字母或所有大写字母打印。\n",
"\n",
"Java 是甲骨文公司Oracle. Inc.的商标。Windows 95Windows NTWindows 2000Windows XPWindows 7Windows 8 和 Windows 10 是微软公司Microsoft Corporation的商标。\n",
"此处提及的所有其他产品名称和公司名称均为其各自所有者的财产。作者和出版商在编写本书时已经仔细校对过,但不作任何明示或暗示的保证,对错误或遗漏不承担任何责任。对于因使用此处包含的信息或程序而产生的偶然或间接损失,我们不承担任何责任。\n",
"\n",
"这本书是以平板电脑和计算机为载体的电子书,非传统纸质版书籍。 \n",
"故所有布局和格式设计旨在优化您在各种电子书阅读平台和系统上的观看体验。\n",
"封面由 Daniel Will-Harris 设计,[www.Will-Harris.com](http://www.Will-Harris.com)。\n",
"\n",
"<div style=\"page-break-after: always;\"></div>"
]
}
],
"metadata": {},
"nbformat": 4,
"nbformat_minor": 4
}

View File

@@ -1,112 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# 前言\n",
"\n",
"> 本书基于 Java 8 版本来教授当前 Java 编程的最优实践。\n",
"\n",
"此前,我的另一本 Java 书籍 *Thinking in Java, 4th Edition*《Java编程思想》 第 4 版 Prentice Hall 2006依然适用于 Java 5 编程。Android 编程就是始于此语言版本。\n",
"\n",
"随着 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编程思想》专题知识。\n",
"\n",
"与几年前我们依赖印刷媒体相比YouTube博客和 StackOverflow 等网站的出现让寻找答案变得简单。请结合这些学习途径和努力坚持下去。本书可作为编程入门书籍,同时也适用于想要扩展知识的在职程序员。每次在世界各地的演讲中,我都非常感谢 《*Thinking in Java*》 这本书给我带来的所有荣誉。它对于我重塑 [Reinventing Business](http://www.reinventing-business.com) 项目和促进交流是非常宝贵的。最后,写这本书的原因之一 希望这本书可以为我的这个项目众筹。似乎下一步要创建一个所谓的蓝绿色组织Teal Organization才合乎逻辑的。\n",
"\n",
"## 教学目标\n",
"\n",
"每章教授一个或一组相关的概念,并且这些知识不依赖于尚未学习到的章节。如此,学习者可以在当前知识的背景框架下循序渐进地掌握 Java。\n",
"\n",
"本书的教学目标:\n",
"\n",
"1. 循序渐进地呈现学习内容,以便于你在不依赖后置知识框架的情况下轻松完成现有的学习任务,同时尽量保证前面章节的内容在后面的学习中得到运用。如果确有必要引入我们还没学习到的知识概念,我会做个简短地介绍。\n",
"\n",
"2. 尽可能地使用简单和简短的示例,方便读者理解。而不强求引入解决实际问题的例子。因为我发现,相比解决某个实际问题,读者更乐于看到自己真正理解了示例的每个细节。或许我会因为这些“玩具示例”而被一些人所诟病,但我更愿意看到我的读者们因此能保持饶有兴趣地学习。\n",
"\n",
"3. 把我知道以及我认为对于你学习语言很重要的东西都告诉你。我认为信息的重要性是分层次结构的。绝大多数情况下我们没必要弄清问题的所有本质。好比编程语言中的某些特性和实现细节95% 的程序员都不需要去知道。这些细节除了会加重你的学习成本,还让你更觉得这门语言好复杂。如果你非要考虑这些细节,那么它还会迷惑该代码的阅读者/维护者,所以我主张选择简单的方法解决问题。\n",
"\n",
"4. 希望本书能为你打下坚实的基础,方便你将来学习更难的课程和书籍。\n",
"\n",
"## 语言设计错误\n",
"\n",
"每种语言都有设计错误。当新手程序员涉足语言特性并猜测应用场景和使用方式时,他们体验到极大的不确定性和挫折感。承认错误令人尴尬,但这种糟糕的初学者经历比认识到你错在哪里还要糟糕。唉,每一种语言/库的设计错误都会永久地嵌入在 Java 的发行版中。\n",
"\n",
"诺贝尔经济学奖得主约瑟夫·斯蒂格利茨(*Joseph Stiglitz*)有一套适用于这里的人生哲学,叫做“承诺升级理论”:继续犯错误的成本由别人承担,而承认错误的成本由自己承担。\n",
"\n",
"看过我此前作品的读者们应该清楚我一般倾向于指出这些错误。Java 拥有一批狂热的粉丝。他们把语言当成是阵营而不是纯粹的编程工具。我写过 Java 书籍,所以他们兀自认为我自然也是这个“阵营”的一份子。当我指出 Java 的这些错误时,会造成两种影响:\n",
"\n",
"1. 早先许多错误“阵营”的人成为了牺牲品。最终,时隔多年后,大家都意识到这是个设计上的错误。然而错误已然成为 Java 历史的一部分了。\n",
"\n",
"2. 更重要的是,新手程序员并没有经历过“语言为何采用某种方式实现”的争议过程。特别是那些隐约察觉不对却依然说服自己“我必须要这么做”或“我只是没学明白”从而继续错下去的人。更糟糕的是,教授这些编程知识的老师们没能深入地去研究这里是否有设计上的错误,而是继续错误的解读。总之,通过了解语言设计上的错误,能让开发者们更好地理解和意识到错误的本质,从而更快地进步。\n",
"\n",
"对编程语言的设计错误理解至关重要,甚至影响程序员的开发效率。部分公司在开发过程中避免使用语言的某些功能特性。这些功能特性表面上看起来高大上,但是弄不好却可能出现意料之外的错误,影响整个开发进程。\n",
"\n",
"已知的语言设计错误会给新的一门编程语言的作者提供参考。探索一门语言能做什么是很有趣的一件事,而语言设计错误能提醒你哪些“坑”是不能再趟的。多年以来,我一直感觉 Java 的设计者们有点脱离群众。Java 的有些设计错误错的太明显我甚至怀疑设计者们到底是为出于服务用户还是其他动机设计了这些功能。Java 语言有许多臭名昭著的设计错误很可能这也是诱惑所在。Java 似乎并不尊重开发者。为此我很长时间内不想与 Java 有任何瓜葛。很大程度上,这也是我不想碰 Java 的原因吧。\n",
"\n",
"如今再审视 Java 8我发现了许多变化。设计者们对于语言和用户的态度似乎发生了根本性上的改变。忽视用户投诉多年之后Java 的许多功能和类库都已被搞砸了。\n",
"\n",
"新功能的设计与以往有很大不同。掌舵者开始重视程序员的编程经验。新功能的开发都是在努力使语言变得更好,而非仅仅停留在快速堆砌功能而不去深入研究它们的含义。甚至有些新特性的实现方式非常优雅(至少在 Java 约束下尽可能优雅)。\n",
"\n",
"我猜测可能是部分设计者的离开让他们意识到了这点。说真的我没想到会有这些变化因为这些原因写这本书的体验要比以往好很多。Java 8 包含了一系列基础和重要的改进。遗憾的是,为了严格地“向后兼容”,我们不大可能看到戏剧性的变化,当然我希望我是错的。尽管如此,我很赞赏那些敢于自我颠覆,并为 Java 设定更好路线的人。第一次,对于自己所写的部分 Java 8 代码我终于可以说“赞!”\n",
"\n",
"最后,本书所著时间似乎也还不错,因为 Java 8 引入的新功能已经强烈地影响了今后 Java 的编码方式。截止我在写这本书时Java 9 似乎更专注于对语言底层的基础结构功能的重要更新,而非本书所关注的新编码方式。话说回来,得益于电子书出版形式的便捷,假如我发现本书有需要更新或添加的内容,我可以第一时间将新版本推送给现有读者。\n",
"\n",
"## 测试用例\n",
"\n",
"书中代码示例基于 Java 8 和 Gradle 编译构建,并且代码示例都保存在[这个自由访问的GitHub的仓库](https://github.com/BruceEckel/OnJava8-Examples) 中。我们需要内置的测试框架,以便于在每次构建系统时自动运行。否则,你将无法保证自己代码的可靠性。为了实现这一点,我创建了一个测试系统来显示和验证大多数示例的输出结果。这些输出结果我会附加在示例结尾的代码块中。有时仅显示必要的那几行或者首尾行。利用这种方式来改善读者的阅读和学习体验,同时也提供了一种验证示例正确性的方法。\n",
"\n",
"## 普及性\n",
"\n",
"Java 的普及性对于其受欢迎程度有重要意义。学习 Java 会让你更容易找到工作。相关的培训材料,课程和其他可用的学习资源也很多。对于企业来说,招聘 Java 程序员相对容易。如果你不喜欢 Java 语言,那么最好不要拿他当作你谋生的工具,因为这种生活体验并不好。作为一家公司,在技术选型前一定不要单单只考虑 Java 程序员好招。每种语言都有其适用的范围,有可能你们的业务更适用于另一种编程语言来达到事半功倍的效果。如果你真的喜欢 Java那么欢迎你。希望这本书能丰富你的编程经验\n",
"\n",
"## 关于安卓\n",
"\n",
"本书基于 Java 8 版本。如果你是 Andriod 程序员,请务必学习 Java 5。在《On Java 8》出版的时候我的另一本基于 Java 5 的著作 *Thinking in Java 4th Edition*《Java编程思想》第四版已经可以在[www.OnJava8.com](http://www.OnJava8.com)上免费下载了。此外,还有许多其他专用于 Andriod 编程的资源。\n",
"\n",
"## 电子版权声明\n",
"\n",
"《On Java 8》仅提供电子版并且仅通过 [www.OnJava8.com](http://www.OnJava8.com) 提供。任何未经 <mindviewinc@gmail.com> 授权的其他来源或流传送机构都是非法的。本作品受版权保护!未经许可,请勿通过以任何方式分享或发布。你可以使用这些示例进行教学,只要不对本书非法重新出版。有关完整详细信息,请参阅示例分发中的 Copyright.txt 文件。对于视觉障碍者,电子版本有可搜索性,字体大小调整或文本到语音等诸多好处。\n",
"\n",
"任何购买这本书的读者还需要一台计算机来运行和写作代码。另外电子版在计算机上和移动设备上的显示效果俱佳推荐使用平板设备阅读。相比购买传统纸质版的价格平板电脑价格都足够便宜。在床上阅读电子版比看这样一本厚厚的实体书要方便得多。起初你可能会有些不习惯但我相信很快你就会发现它带来的优点远胜过不适。我已经走过这个阶段Google Play 图书的浏览器阅读体验非常好,包括在 Linux 和 iOS 设备上。作为一次尝试,我决定尝试通过 Google 图书进行出版。\n",
"\n",
"**注意**:在撰写本文时,通过 Google Play 图书网络浏览器应用阅读图书虽然可以忍受,但体验还是有点差强人意,我强烈推荐读者们使用平板电脑来阅读。\n",
"\n",
"## 版本说明\n",
"\n",
"本书采用 [Pandoc](http://pandoc.org) 风格的 Markdown 编写,使用 Pandoc 生成 ePub v3 格式。\n",
"\n",
"正文字体为 Georgia标题字体为 Verdana。 代码字体使用的 Ubuntu Mono因为它特别紧凑单行能容纳更多的代码。 我选择将代码内联(而不是将列表放入图像,参照其他书籍),因为我觉得这个功能很重要:让代码块能适应字体大小得改变而改变(否则,买电子版,还图什么呢?)。\n",
"\n",
"书中的提取,编译和测试代码示例的构建过程都是自动化的。所有自动化操作都是通过我在 Python 3 中编写的程序来实现的。\n",
"\n",
"## 封面设计\n",
"\n",
"《On Java 8》的封面是根据 W.P.A.Works Progress Administration 1935年至1943年美国大萧条期间的一个巨大项目它使数百万失业人员重新就业的马赛克创作的。它还让我想起了《绿野仙踪》*The Wizard of Oz*)系列丛书中的插图。 我的好朋友、设计师丹 *Daniel Will-Harris*[www.will-harris.com](http://www.will-harris.com))和我都喜欢这个形象。\n",
"\n",
"## 感谢的人\n",
"\n",
"感谢 *Domain-Driven Design*(《领域驱动设计》 )的作者 *Eric Evans* 建议书名,以及其他新闻组校对的帮助。\n",
"\n",
"感谢 *James Ward* 为我开始使用 Gradle 工具构建这本书,以及他多年来的帮助和友谊。\n",
"\n",
"感谢 *Ben Muschko* 在整理构建文件方面的工作,还有感谢 *Hans Dockter* 给 *Ben* 提供了时间。\n",
"\n",
"感谢 *Jeremy Cerise* 和 *Bill Frasure* 来到开发商务聚会预订,并随后提供了宝贵的帮助。\n",
"\n",
"感谢所有花时间和精力来科罗拉多州克雷斯特德比特Crested Butte, Colorado镇参加我的研讨会开发商务聚会和其他活动的人你们的贡献可能不容易看到但却非常重要\n",
"\n",
"## 献礼\n",
"\n",
"> 谨以此书献给我敬爱的父亲 E. Wayne Eckel。\n",
"> 1924年4月1日至2016年11月23日\n",
"\n",
"<!-- 分页 -->\n",
"<div style=\"page-break-after: always;\"></div>"
]
}
],
"metadata": {},
"nbformat": 4,
"nbformat_minor": 4
}

View File

@@ -1,422 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# 第一章 对象的概念\n",
"\n",
"> “我们没有意识到惯用语言的结构有多大的力量。可以毫不夸张地说,它通过语义反应机制奴役我们。语言表现出来并在无意识中给我们留下深刻印象的结构会自动投射到我们周围的世界。” -- Alfred Korzybski (1930)\n",
"\n",
"计算机革命的起源来自机器。编程语言就像是那台机器。它不仅是我们思维放大的工具与另一种表达媒介,更像是我们思想的一部分。语言的灵感来自其他形式的表达,如写作,绘画,雕塑,动画和电影制作。编程语言就是创建应用程序的思想结构。\n",
"\n",
"面向对象编程Object-Oriented Programming OOP是一种编程思维方式和编码架构。本章讲述 OOP 的基本概述。如果读者对此不太理解,可先行跳过本章。等你具备一定编程基础后,请务必再回头看。只有这样你才能深刻理解面向对象编程的重要性及设计方式。\n",
"\n",
"## 抽象\n",
"\n",
"所有编程语言都提供抽象机制。从某种程度上来说,问题的复杂度直接取决于抽象的类型和质量。这里的“类型”意思是:抽象的内容是什么?汇编语言是对底层机器的轻微抽象。接着出现的“命令式”语言(如 FORTRANBASIC 和 C是对汇编语言的抽象。与汇编相比这类语言已有了长足的改进但它们的抽象原理依然要求我们着重考虑计算机的结构而非问题本身的结构。\n",
"\n",
"程序员必须要在机器模型(“解决方案空间”)和实际解决的问题模型(“问题空间”)之间建立起一种关联。这个过程既费精力,又脱离编程语言本身的范畴。这使得程序代码很难编写,维护代价高昂。同时还造就了一个副产业“编程方法”学科。\n",
"\n",
"为机器建模的另一个方法是为要解决的问题制作模型。对一些早期语言来说,如 LISP 和 APL它们的做法是“从不同的角度观察世界”——“所有问题都归纳为列表”或“所有问题都归纳为算法”。PROLOG 则将所有\n",
"问题都归纳为决策链。对于这些语言,我们认为它们一部分是“基于约束”的编程,另一部分则是专为\n",
"处理图形符号设计的(后者被证明限制性太强)。每种方法都有自己特殊的用途,适合解决某一类的问题。只要超出了它们力所能及的范围,就会显得非常笨拙。\n",
"\n",
"面向对象的程序设计在此基础上跨出了一大步,程序员可利用一些工具表达“问题空间”内的元素。由于这种表达非常具有普遍性,所以不必受限于特定类型的问题。我们将问题空间中的元素以及它们在解决方案空间的表示称作“对象”(**Object**。当然还有一些在问题空间没有对应的对象体。通过添加新的对象类型程序可进行灵活的调整以便与特定的问题配合。所以当你在阅读描述解决方案的代码时也是在阅读问题的表述。与我们以前见过的相比这无疑是一种更加灵活、更加强大的语言抽象方法。总之OOP 允许我们根据问题来描述问题,而不是根据运行解决方案的计算机。然而,它仍然与计算机有联系,每个对象都类似一台小计算机:它们有自己的状态并且可以进行特定的操作。这与现实世界的“对象”或者“物体”相似:它们都有自己的特征和行为。\n",
"\n",
"Smalltalk 作为第一个成功的面向对象并影响了 Java 的程序设计语言 *Alan Kay* 总结了其五大基本特征。通过这些特征,我们可理解“纯粹”的面向对象程序设计方法是什么样的:\n",
"\n",
"> 1. **万物皆对象**。你可以将对象想象成一种特殊的变量。它存储数据,但可以在你对其“发出请求”时执行本身的操作。理论上讲,你总是可以从要解决的问题身上抽象出概念性的组件,然后在程序中将其表示为一个对象。\n",
"> 2. **程序是一组对象,通过消息传递来告知彼此该做什么**。要请求调用一个对象的方法,你需要向该对象发送消息。\n",
"> 3. **每个对象都有自己的存储空间,可容纳其他对象**。或者说,通过封装现有对象,可制作出新型对象。所以,尽管对象的概念非常简单,但在程序中却可达到任意高的复杂程度。\n",
"> 4. **每个对象都有一种类型**。根据语法每个对象都是某个“类”的一个“实例”。其中“类”Class是“类型”Type的同义词。一个类最重要的特征就是“能将什么消息发给它”。\n",
"> 5. **同一类所有对象都能接收相同的消息**。这实际是别有含义的一种说法大家不久便能理解。由于类型为“圆”Circle的一个对象也属于类型为“形状”Shape的一个对象所以一个圆完全能接收发送给\"形状”的消息。这意味着可让程序代码统一指挥“形状”令其自动控制所有符合“形状”描述的对象其中自然包括“圆”。这一特性称为对象的“可替换性”是OOP最重要的概念之一。\n",
"\n",
"*Grady Booch* 提供了对对象更简洁的描述:一个对象具有自己的状态,行为和标识。这意味着对象有自己的内部数据(提供状态)、方法 (产生行为),并彼此区分(每个对象在内存中都有唯一的地址)。\n",
"\n",
"## 接口\n",
"\n",
"亚里士多德(*Aristotle*)大概是第一个认真研究“类型”的哲学家,他曾提出过“鱼类和鸟类”这样的概念。所有对象都是唯一的,但同时也是具有相同的特性和行为的对象所归属的类的一部分。这种思想被首次应用于第一个面向对象编程语言 Simula-67它在程序中使用基本关键字 **class** 来引入新的类型class 和 type 通常可互换使用,有些人对它们进行了进一步区分,他们强调 type 决定了接口,而 class 是那个接口的一种特殊实现方式)。\n",
"\n",
"Simula 是一个很好的例子。正如这个名字所暗示的它的作用是“模拟”Simulate类似“银行出纳员”这样的经典问题。在这个例子里我们有一系列出纳员、客户、帐号、交易和货币单位等许多\"对象”。每类成员(元素)都具有一些通用的特征:每个帐号都有一定的余额;每名出纳都能接收客户的存款;等等。与此同时,每个成员都有自己的状态;每个帐号都有不同的余额;每名出纳都有一个名字。所以在计算机程序中,能用独一无二的实体分别表示出纳员、客户、帐号以及交易。这个实体便是“对象”,而且每个对象都隶属一个特定的“类”,那个类具有自己的通用特征与行为。\n",
"\n",
"因此在面向对象的程序设计中尽管我们真正要做的是新建各种各样的数据“类型”Type但几乎所有面向对象的程序设计语言都采用了 `class` 关键字。当你看到 “type” 这个词的时候,请同时想到 `class`;反之亦然。\n",
"\n",
"创建好一个类后,可根据情况生成许多对象。随后,可将那些对象作为要解决问题中存在的元素进行处理。事实上,当我们进行面向对象的程序设计时,面临的最大一项挑战是:如何在“问题空间”(问题实际存在的地方)的元素与“方案空间”(对实际问题进行建模的地方,如计算机)的元素之间建立理想的“一对一”的映射关系。\n",
"\n",
"那么如何利用对象完成真正有用的工作呢必须有一种办法能向对象发出请求令其解决一些实际的问题比如完成一次交易、在屏幕上画一些东西或者打开一个开关等等。每个对象仅能接受特定的请求。我们向对象发出的请求是通过它的“接口”Interface定义的对象的“类型”或“类”则规定了它的接口形式。“类型”与“接口”的对应关系是面向对象程序设计的基础。\n",
"\n",
"下面让我们以电灯泡为例:\n",
"\n",
"![reader](../images/reader.png)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"java"
],
"id": ""
}
},
"outputs": [],
"source": [
"Light lt = new Light();\n",
"lt.on();"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"在这个例子中,类型/类的名称是 **Light**,可向 **Light** 对象发出的请求包括打开 `on`、关闭 `off`、变得更明亮 `brighten` 或者变得更暗淡 `dim`。通过声明一个引用,如 `lt` 和 `new` 关键字,我们创建了一个 **Light** 类型的对象,再用等号将其赋给引用。\n",
"\n",
"为了向对象发送消息,我们使用句点符号 `.` 将 `lt` 和消息名称 `on` 连接起来。可以看出,使用一些预先定义好的类时,我们在程序里采用的代码是非常简单直观的。\n",
"\n",
"上图遵循 **UML**Unified Modeling Language统一建模语言的格式。每个类由一个框表示框的顶部有类型名称框中间部分是要描述的任何数据成员方法属于此对象的方法它们接收任何发送到该对象的消息在框的底部。通常只有类的名称和公共方法在 **UML** 设计图中显示,因此中间部分未显示,如本例所示。如果你只对类名感兴趣,则也不需要显示方法信息。\n",
"\n",
"## 服务提供\n",
"\n",
"在开发或理解程序设计时,我们可以将对象看成是“服务提供者”。你的程序本身将为用户提供服务,并且它能通过调用其他对象提供的服务来实现这一点。我们的最终目标是开发或调用工具库中已有的一些对象,提供理想的服务来解决问题。\n",
"\n",
"那么问题来了:我们该选择哪个对象来解决问题呢?例如,你正在开发一个记事本程序。*你可能会想到在屏幕输入默认的记事本对象*,一个用于检测不同类型打印机并执行打印的对象。这些对象中的某些已经有了。那对于还没有的对象,我们该设计成啥样呢?这些对象需要提供哪些服务,以及还需要调用其他哪些对象?\n",
"\n",
"我们可以将这些问题一一分解,抽象成一组服务。软件设计的基本原则是高内聚:每个组件的内部作用明确,功能紧密相关。然而经常有人将太多功能塞进一个对象中。例如:在支票打印模块中,你需要设计一个可以同时读取文本格式又能正确识别不同打印机型号的对象。正确的做法是提供三个或更多对象:一个对象检查所有排版布局的目录;一个或一组可以识别不同打印机型号的对象展示通用的打印界面;第三个对象组合上述两个服务来完成任务。这样,每个对象都提供了一组紧密的服务。在良好的面向对象设计中,每个对象功能单一且高效。这样的程序设计可以提高我们代码的复用性,同时也方便别人阅读和理解我们的代码。只有让人知道你提供什么服务,别人才能更好地将其应用到其他模块或程序中。\n",
"\n",
"## 封装\n",
"\n",
"我们可以把编程的侧重领域划分为研发和应用。应用程序员调用研发程序员构建的基础工具类来做快速开发。研发程序员开发一个工具类,该工具类仅向应用程序员公开必要的内容,并隐藏内部实现的细节。这样可以有效地避免该工具类被错误的使用和更改,从而减少程序出错的可能。彼此职责划分清晰,相互协作。当应用程序员调用研发程序员开发的工具类时,双方建立了关系。应用程序员通过使用现成的工具类组装应用程序或者构建更大的工具库。如果工具类的创建者将类的内部所有信息都公开给调用者,那么有些使用规则就不容易被遵守。因为前者无法保证后者是否会按照正确的规则来使用,甚至是改变该工具类。只有设定访问控制,才能从根本上阻止这种情况的发生。\n",
"\n",
"因此,使用访问控制的原因有以下两点:\n",
"\n",
"1. 让应用程序员不要触摸他们不应该触摸的部分。(请注意,这也是一个哲学决策。部分编程语言认为如果程序员有需要,则应该让他们访问细节部分。);\n",
"\n",
"2. 使类库的创建者(研发程序员)在不影响后者使用的情况下完善更新工具库。例如,我们开发了一个功能简单的工具类,后来发现可以通过优化代码来提高执行速度。假如工具类的接口和实现部分明确分开并受到保护,那我们就可以轻松地完成改造。\n",
"\n",
"Java 有三个显式关键字来设置类中的访问权限:`public`(公开),`private`(私有)和`protected`(受保护)。这些访问修饰符决定了谁能使用它们修饰的方法、变量或类。\n",
"\n",
" 1. `public`(公开)表示任何人都可以访问和使用该元素;\n",
"\n",
" 2. `private`(私有)除了类本身和类内部的方法,外界无法直接访问该元素。`private` 是类和调用者之间的屏障。任何试图访问私有成员的行为都会报编译时错误;\n",
"\n",
" 3. `protected`(受保护)类似于 `private`,区别是子类(下一节就会引入继承的概念)可以访问 `protected` 的成员,但不能访问 `private` 成员;\n",
"\n",
" 4. `default`(默认)如果你不使用前面的三者,默认就是 `default` 访问权限。`default` 被称为包访问,因为该权限下的资源可以被同一包(库组件)中其他类的成员访问。\n",
"\n",
"## 复用\n",
"\n",
"一个类经创建和测试后,理应是可复用的。然而很多时候,由于程序员没有足够的编程经验和远见,我们的代码复用性并不强。\n",
"\n",
"代码和设计方案的复用性是面向对象程序设计的优点之一。我们可以通过重复使用某个类的对象来达到这种复用性。同时,我们也可以将一个类的对象作为另一个类的成员变量使用。新的类可以是由任意数量和任意类型的其他对象构成。这里涉及到“组合”和“聚合”的概念:\n",
"\n",
"* **组合**Composition经常用来表示“拥有”关系has-a relationship。例如“汽车拥有引擎”。\n",
"\n",
"* **聚合**Aggregation动态的**组合**。\n",
"\n",
"![UML-example](../images/1545758268350.png)\n",
"\n",
"上图中实心三角形指向“ **Car** ”表示 **组合** 的关系;如果是 **聚合** 关系,可以使用空心三角形。\n",
"\n",
"**译者注**:组合和聚合都属于关联关系的一种,只是额外具有整体-部分的意义。至于是聚合还是组合,需要根据实际的业务需求来判断。可能相同超类和子类,在不同的业务场景,关联关系会发生变化。只看代码是无法区分聚合和组合的,具体是哪一种关系,只能从语义级别来区分。聚合关系中,整件不会拥有部件的生命周期,所以整件删除时,部件不会被删除。再者,多个整件可以共享同一个部件。组合关系中,整件拥有部件的生命周期,所以整件删除时,部件一定会跟着删除。而且,多个整件不可以同时共享同一个部件。这个区别可以用来区分某个关联关系到底是组合还是聚合。两个类生命周期不同步,则是聚合关系,生命周期同步就是组合关系。)\n",
"\n",
"使用“组合”关系给我们的程序带来极大的灵活性。通常新建的类中,成员对象会使用 `private` 访问权限,这样应用程序员则无法对其直接访问。我们就可以在不影响客户代码的前提下,从容地修改那些成员。我们也可以在“运行时\"改变成员对象从而动态地改变程序的行为,这进一步增大了灵活性。下面一节要讲到的“继承”并不具备这种灵活性,因为编译器对通过继承创建的类进行了限制。\n",
"\n",
"在面向对象编程中经常重点强调“继承”。在新手程序员的印象里,或许先入为主地认为“继承应当随处可见”。沿着这种思路产生的程序设计通常拙劣又复杂。相反,在创建新类时首先要考虑“组合”,因为它更简单灵活,而且设计更加清晰。等我们有一些编程经验后,一旦需要用到继承,就会明显意识到这一点。\n",
"\n",
"## 继承\n",
"\n",
"“继承”给面向对象编程带来极大的便利。它在概念上允许我们将各式各样的数据和功能封装到一起,这样便可恰当表达“问题空间”的概念,而不用受制于必须使用底层机器语言。\n",
"\n",
"通过使用 `class` 关键字,这些概念形成了编程语言中的基本单元。遗憾的是,这么做还是有很多麻烦:在创建了一个类之后,即使另一个新类与其具有相似的功能,你还是得重新创建一个新类。但我们若能利用现成的数据类型,对其进行“克隆”,再根据情况进行添加和修改,情况就显得理想多了。“继承”正是针对这个目标而设计的。但继承并不完全等价于克隆。在继承过程中,若原始类(正式名称叫作基类、超类或父类)发生了变化,修改过的“克隆”类(正式名称叫作继承类或者子类)也会反映出这种变化。\n",
"\n",
"![Inheritance-example](../images/1545763399825.png)\n",
"\n",
"这个图中的箭头从派生类指向基类。正如你将看到的,通常有多个派生类。类型不仅仅描述一组对象的约束,它还涉及其他类型。两种类型可以具有共同的特征和行为,但是一种类型可能包含比另一种类型更多的特征,并且还可以处理更多的消息(或者以不同的方式处理它们)。继承通过基类和派生类的概念来表达这种相似性。基类包含派生自它的类型之间共享的所有特征和行为。创建基类以表示思想的核心。从基类中派生出其他类型来表示实现该核心的不同方式。\n",
"\n",
"![1545764724202](../images/1545764724202.png)\n",
"\n",
"例如,垃圾回收机对垃圾进行分类。基类是“垃圾”。每块垃圾都有重量、价值等特性,它们可以被切碎、熔化或分解。在此基础上,可以通过添加额外的特性(瓶子有颜色,钢罐有磁性)或行为(铝罐可以被压碎)派生出更具体的垃圾类型。此外,一些行为可以不同(纸张的价值取决于它的类型和状态)。使用继承,你将构建一个类型层次结构,来表示你试图解决的某种类型的问题。第二个例子是常见的“形状”例子,可能用于计算机辅助设计系统或游戏模拟。基类是“形状”,每个形状都有大小、颜色、位置等等。每个形状可以绘制、擦除、移动、着色等。由此,可以派生出(继承出)具体类型的形状——圆形、正方形、三角形等等——每个形状可以具有附加的特征和行为。\n",
"\n",
"![1545764780795](../images/1545764780795.png)\n",
"\n",
"例如,某些形状可以翻转。有些行为可能不同,比如计算形状的面积。类型层次结构体现了形状之间的相似性和差异性。以相同的术语将解决方案转换成问题是有用的,因为你不需要在问题描述和解决方案描述之间建立许多中间模型。通过使用对象,类型层次结构成为了主要模型,因此你可以直接从真实世界中对系统的描述过渡到用代码对系统进行描述。事实上,有时候,那些善于寻找复杂解决方案的人会被面向对象设计的简单性难倒。从现有类型继承创建新类型。这种新类型不仅包含现有类型的所有成员(尽管私有成员被隐藏起来并且不可访问),而且更重要的是它复制了基类的接口。也就是说,基类对象接收的所有消息也能被派生类对象接收。根据类接收的消息,我们知道类的类型,因此派生类与基类是相同的类型。\n",
"\n",
"在前面的例子中,“圆是形状”。这种通过继承的类型等价性是理解面向对象编程含义的基本门槛之一。因为基类和派生类都具有相同的基本接口,所以伴随此接口的必定有某些具体实现。也就是说,当对象接收到特定消息时,必须有可执行代码。如果继承一个类而不做其他任何事,则来自基类接口的方法直接进入派生类。这意味着派生类和基类不仅具有相同的类型,而且具有相同的行为,这么做没什么特别意义。\n",
"\n",
"有两种方法可以区分新的派生类与原始的基类。第一种方法很简单:在派生类中添加新方法。这些新方法不是基类接口的一部分。这意味着基类不能满足你的所有需求,所以你添加了更多的方法。继承的这种简单而原始的用途有时是解决问题的完美解决方案。然而,还是要仔细考虑是否在基类中也要有这些额外的方法。这种设计的发现与迭代过程在面向对象程序设计中会经常发生。\n",
"\n",
"尽管继承有时意味着你要在接口中添加新方法(尤其是在以 **extends** 关键字表示继承的 Java 中),但并非总需如此。第二种也是更重要地区分派生类和基类的方法是改变现有基类方法的行为,这被称为覆盖 (overriding)。要想覆盖一个方法,只需要在派生类中重新定义这个方法即可。\n",
"\n",
"### \"是一个\"与\"像是一个\"的关系\n",
"\n",
"对于继承可能会引发争论:继承应该只覆盖基类的方法(不应该添加基类中没有的方法)吗?如果这样的话,基类和派生类就是相同的类型了,因为它们具有相同的接口。这会造成,你可以用一个派生类对象完全替代基类对象,这叫作\"纯粹替代\",也经常被称作\"替代原则\"。在某种意义上这是一种处理继承的理想方式。我们经常把这种基类和派生类的关系称为是一个is-a关系因为可以说\"圆是一个形状\"。判断是否继承,就看在你的类之间有无这种 is-a 关系。\n",
"\n",
"有时你在派生类添加了新的接口元素,从而扩展接口。虽然新类型仍然可以替代基类,但是这种替代不完美,原因在于基类无法访问新添加的方法。这种关系称为像是一个(is-like-a)关系。新类型不但拥有旧类型的接口,而且包含其他方法,所以不能说新旧类型完全相同。\n",
"\n",
"![1545764820176](../images/1545764820176.png)\n",
"\n",
"以空调为例假设房间里已经安装好了制冷设备的控制器即你有了控制制冷设备的接口。想象一下现在空调坏了你重新安装了一个既制冷又制热的热力泵。热力泵就像是一个is-like-a空调但它可以做更多。因为当初房间的控制系统被设计成只能控制制冷设备所以它只能与新对象(热力泵)的制冷部分通信。新对象的接口已经扩展了,现有控制系统却只知道原来的接口,一旦看到这个设计,你就会发现,作为基类的制冷系统不够一般化,应该被重新命名为\"温度控制系统\",也应该包含制热功能,这样的话,我们就可以使用替代原则了。上图反映了在现实世界中进行设计时可能会发生的事情。\n",
"\n",
"当你看到替代原则时,很容易会认为纯粹替代是唯一可行的方式,并且使用纯粹替代的设计是很好的。但有些时候,你会发现必须得在派生(扩展)类中添加新方法(提供新的接口)。只要仔细审视,你可以很明显地区分两种设计方式的使用场合。\n",
"\n",
"## 多态\n",
"\n",
"我们在处理类的层次结构时通常把一个对象看成是它所属的基类而不是把它当成具体类。通过这种方式我们可以编写出不局限于特定类型的代码。在上个“形状”的例子中“方法”method操纵的是通用“形状”而不关心它们是“圆”、“正方形”、“三角形”还是某种尚未定义的形状。所有的形状都可以被绘制、擦除和移动因此“方法”向其中的任何代表“形状”的对象发送消息都不必担心对象如何处理信息。\n",
"\n",
"这样的代码不会受添加的新类型影响,并且添加新类型是扩展面向对象程序以处理新情况的常用方法。 例如,你可以通过通用的“形状”基类派生出新的“五角形”形状的子类,而不需要修改通用\"形状\"基类的方法。通过派生新的子类来扩展设计的这种能力是封装变化的基本方法之一。\n",
"\n",
"这种能力改善了我们的设计且减少了软件的维护代价。如果我们把派生的对象类型统一看成是它本身的基类“圆”当作“形状”“自行车”当作“车”“鸬鹚”当作“鸟”等等编译器compiler在编译时期就无法准确地知道什么“形状”被擦除哪一种“车”在行驶或者是哪种“鸟”在飞行。这就是关键所在当程序接收这种消息时程序员并不想知道哪段代码会被执行。“绘图”的方法可以平等地应用到每种可能的“形状”上形状会依据自身的具体类型执行恰当的代码。\n",
"\n",
"如果不需要知道执行了哪部分代码,那我们就能添加一个新的不同执行方式的子类而不需要更改调用它的方法。那么编译器在不确定该执行哪部分代码时是怎么做的呢?举个例子,下图的 **BirdController** 对象和通用 **Bird** 对象中,**BirdController** 不知道 **Bird** 的确切类型却还能一起工作。从 **BirdController** 的角度来看,这是很方便的,因为它不需要编写特别的代码来确定 **Bird** 对象的确切类型或行为。那么,在调用 **move()** 方法时是如何保证发生正确的行为(鹅走路、飞或游泳、企鹅走路或游泳)的呢?\n",
"\n",
"![Bird-example](../images/1545839316314.png)\n",
"\n",
"这个问题的答案,是面向对象程序设计的妙诀:在传统意义上,编译器不能进行函数调用。由非 OOP 编译器产生的函数调用会引起所谓的**早期绑定**,这个术语你可能从未听说过,不会想过其他的函数调用方式。这意味着编译器生成对特定函数名的调用,该调用会被解析为将执行的代码的绝对地址。\n",
"\n",
"通过继承,程序直到运行时才能确定代码的地址,因此发送消息给对象时,还需要其他一些方案。为了解决这个问题,面向对象语言使用**后期绑定**的概念。当向对象发送信息时,被调用的代码直到运行时才确定。编译器确保方法存在,并对参数和返回值执行类型检查,但是它不知道要执行的确切代码。\n",
"\n",
"为了执行后期绑定Java 使用一个特殊的代码位来代替绝对调用。这段代码使用对象中存储的信息来计算方法主体的地址此过程在多态性章节中有详细介绍。因此每个对象的行为根据特定代码位的内容而不同。当你向对象发送消息时对象知道该如何处理这条消息。在某些语言中必须显式地授予方法后期绑定属性的灵活性。例如C++ 使用 **virtual** 关键字。在这些语言中,默认情况下方法不是动态绑定的。在 Java 中,动态绑定是默认行为,不需要额外的关键字来实现多态性。\n",
"\n",
"为了演示多态性,我们编写了一段代码,它忽略了类型的具体细节,只与基类对话。该代码与具体类型信息分离,因此更易于编写和理解。而且,如果通过继承添加了一个新类型(例如,一个六边形),那么代码对于新类型的 Shape 就像对现有类型一样有效。因此,该程序是可扩展的。\n",
"\n",
"代码示例:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"java"
],
"id": ""
}
},
"outputs": [],
"source": [
"void doSomething(Shape shape) {\n",
" shape.erase();\n",
" // ...\n",
" shape.draw();\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"此方法与任何 **Shape** 对话,因此它与所绘制和擦除的对象的具体类型无关。如果程序的其他部分使用 `doSomething()` 方法:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"java"
],
"id": ""
}
},
"outputs": [],
"source": [
" Circle circle = new Circle();\n",
" Triangle triangle = new Triangle();\n",
" Line line = new Line();\n",
" doSomething(circle);\n",
" doSomething(triangle);\n",
" doSomething(line);\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"可以看到无论传入的“形状”是什么,程序都正确的执行了。\n",
"\n",
"![shape-example](../images/1545841270997.png)\n",
"\n",
"这是一个非常令人惊奇的编程技巧。分析下面这行代码:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"java"
],
"id": ""
}
},
"outputs": [],
"source": [
" doSomething(circle);"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"当预期接收 **Shape** 的方法被传入了 **Circle**,会发生什么。由于 **Circle** 也是一种 **Shape**,所\n",
"以 `doSomething(circle)` 能正确地执行。也就是说,`doSomething()` 能接收任意发送给 **Shape** 的消息。这是完全安全和合乎逻辑的事情。\n",
"\n",
"这种把子类当成其基类来处理的过程叫做“向上转型”(**upcasting**)。在面向对象的编程里,经常利用这种方法来给程序解耦。再看下面的 `doSomething()` 代码示例:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"java"
],
"id": ""
}
},
"outputs": [],
"source": [
" shape.erase();\n",
" // ...\n",
" shape.draw();"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"我们可以看到程序并未这样表达:“如果你是一个 Circle ,就这样做;如果你是一个 Square就那样做...”。若那样编写代码,就需检查 Shape 所有可能的类型,如圆、矩形等等。这显然是非常麻烦的,而且每次添加了一种新的 Shape 类型后,都要相应地进行修改。在这里,我们只需说:“你是一种几何形状,我知道你能删掉 `erase()` 和绘制 `draw()`,你自己去做吧,注意细节。”\n",
"\n",
"尽管我们没作出任何特殊指示,程序的操作也是完全正确和恰当的。我们知道,为 Circle 调用`draw()` 时执行的代码与为一个 Square 或 Line 调用 `draw()` 时执行的代码是不同的。但在将 `draw()` 信息发给一个匿名 Shape 时,根据 Shape 句柄当时连接的实际类型,会相应地采取正确的操作。这非常神奇,因为当 Java 编译器为 `doSomething()` 编译代码时,它并不知道自己要操作的准确类型是什么。\n",
"\n",
"尽管我们确实可以保证最终会为 Shape 调用 `erase()` 和 `draw()`,但并不能确定特定的 CircleSquare 或者 Line 调用什么。最后,程序执行的操作却依然是正确的,这是怎么做到的呢?\n",
"\n",
"发送消息给对象时如果程序不知道接收的具体类型是什么但最终执行是正确的这就是对象的“多态性”Polymorphism。面向对象的程序设计语言是通过“动态绑定”的方式来实现对象的多态性的。编译器和运行时系统会负责对所有细节的控制我们只需知道要做什么以及如何利用多态性来更好地设计程序。\n",
"\n",
"## 单继承结构\n",
"\n",
"自从 C++ 引入以来,一个 OOP 问题变得尤为突出:是否所有的类都应该默认从一个基类继承呢?这个答案在 Java 中是肯定的(实际上,除 C++ 以外的几乎所有OOP语言中也是这样。在 Java 中,这个最终基类的名字就是 `Object`。\n",
"\n",
"Java 的单继承结构有很多好处。由于所有对象都具有一个公共接口,因此它们最终都属于同一个基类。相反的,对于 C++ 所使用的多继承的方案则是不保证所有的对象都属于同一个基类。从向后兼容的角度看,多继承的方案更符合 C 的模型,而且受限较少。\n",
"\n",
"对于完全面向对象编程,我们必须要构建自己的层次结构,以提供与其他 OOP 语言同样的便利。我们经常会使用到新的类库和不兼容的接口。为了整合它们而花费大气力(有可能还要用上多继承)以获得 C++ 样的“灵活性”值得吗如果从零开始Java 这样的替代方案会是更好的选择。\n",
"\n",
"另外,单继承的结构使得垃圾收集器的实现更为容易。这也是 Java 在 C++ 基础上的根本改进之一。\n",
"\n",
"由于运行期的类型信息会存在于所有对象中,所以我们永远不会遇到判断不了对象类型的情况。这对于系统级操作尤其重要,例如[异常处理](#异常处理)。同时,这也让我们的编程具有更大的灵活性。\n",
"\n",
"## 集合\n",
"\n",
"通常,我们并不知道解决某个具体问题需要的对象数量和持续时间,以及对象的存储方式。那么我们如何知悉程序在运行时需要分配的内存空间呢?\n",
"\n",
"在面向对象的设计中问题的解决方案有些过于轻率创建一个新类型的对象来引用、容纳其他的对象。当然我们也可以使用多数编程语言都支持的“数组”array。在 Java 中“集合”Collection的使用率更高。也可称之为“容器”但“集合”这个称呼更通用。\n",
"\n",
"“集合”这种类型的对象可以存储任意类型、数量的其他对象。它能根据需要自动扩容,我们不用关心过程是如何实现的。\n",
"\n",
"还好,一般优秀的 OOP 语言都会将“集合”作为其基础包。在 C++ 中,“集合”是其标准库的一部分,通常被称为 STLStandard Template Library标准模板库。SmallTalk 有一套非常完整的集合库。同样Java 的标准库中也提供许多现成的集合类。\n",
"\n",
"在一些库中一两个泛型集合就能满足我们所有的需求了而在其他一些类库Java不同类型的集合对应不同的需求常见的有 List常用于保存序列Map也称为关联数组常用于将对象与其他对象关联Set只能保存非重复的值其他还包括如队列Queue、树Tree、栈Stack、堆Heap等等。从设计的角度来看我们真正想要的是一个能够解决某个问题的集合。如果一种集合就满足所有需求那么我们就不需要剩下的了。之所以选择集合有以下两个原因\n",
"\n",
"1. 集合可以提供不同类型的接口和外部行为。堆栈、队列的应用场景和集合、列表不同,它们中的一种提供的解决方案可能比其他灵活得多。\n",
"\n",
"2. 不同的集合对某些操作有不同的效率。例如List 的两种基本类型ArrayList 和 LinkedList。虽然两者具有相同接口和外部行为但是在某些操作中它们的效率差别很大。在 ArrayList 中随机查找元素是很高效的,而 LinkedList 随机查找效率低下。反之,在 LinkedList 中插入元素的效率要比在 ArrayList 中高。由于底层数据结构的不同,每种集合类型在执行相同的操作时会表现出效率上的差异。\n",
"\n",
"我们可以一开始使用 LinkedList 构建程序,在优化系统性能时改用 ArrayList。通过对 List 接口的抽象,我们可以很容易地将 LinkedList 改为 ArrayList。\n",
"\n",
"在 Java 5 泛型出来之前,集合中保存的是通用类型 `Object`。Java 单继承的结构意味着所有元素都基于 `Object` 类,所以在集合中可以保存任何类型的数据,易于重用。要使用这样的集合,我们先要往集合添加元素。由于 Java 5 版本前的集合只保存 `Object`,当我们往集合中添加元素时,元素便向上转型成了 `Object`,从而丢失自己原有的类型特性。这时我们再从集合中取出该元素时,元素的类型变成了 `Object`。那么我们该怎么将其转回原先具体的类型呢这里我们使用了强制类型转换将其转为更具体的类型这个过程称为对象的“向下转型”。通过“向上转型”我们知道“圆形”也是一种“形状”这个过程是安全的。可是我们不能从“Object”看出其就是“圆形”或“形状”所以除非我们能确定元素的具体类型信息否则“向下转型”就是不安全的。也不能说这样的错误就是完全危险的因为一旦我们转化了错误的类型程序就会运行出错抛出“运行时异常”RuntimeException后面的章节会提到 无论如何我们要寻找一种在取出集合元素时确定其具体类型的方法。另外每次取出元素都要做额外的“向下转型”对程序和程序员都是一种开销。以某种方式创建集合以确认保存元素的具体类型减少集合元素“向下转型”的开销和可能出现的错误难道不好吗这种解决方案就是参数化类型机制Parameterized Type Mechanism。\n",
"\n",
"参数化类型机制可以使得编译器能够自动识别某个 `class` 的具体类型并正确地执行。举个例子对集合的参数化类型机制可以让集合仅接受“形状”这种类型的元素并以“形状”类型取出元素。Java 5 版本支持了参数化类型机制称之为“泛型”Generic。泛型是 Java 5 的主要特性之一。你可以按以下方式向 ArrayList 中添加 Shape形状"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"java"
],
"id": ""
}
},
"outputs": [],
"source": [
" ArrayList<Shape> shapes = new ArrayList<>();"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"泛型的应用,让 Java 的许多标准库和组件都发生了改变。在本书的代码示例中,你也会经常看到泛型的身影。\n",
"\n",
"## 对象创建与生命周期\n",
"\n",
"我们在使用对象时要注意的一个关键问题就是对象的创建和销毁方式。每个对象的生存都需要资源,尤其是内存。为了资源的重复利用,当对象不再被使用时,我们应该及时释放资源,清理内存。\n",
"\n",
"在简单的编程场景下,对象的清理并不是问题。我们创建对象,按需使用,最后销毁它。然而,情况往往要比这更复杂:\n",
"\n",
"假设,我们正在为机场设计一个空中交通管制的系统(该例也适用于仓库货柜管理、影带出租或者宠物寄养仓库系统)。第一步比较简单:创建一个用来保存飞机的集合,每当有飞机进入交通管制区域时,我们就创建一个“飞机”对象并将其加入到集合中,等到飞机离开时将其从这个集合中清除。与此同时,我们还需要一个记录飞机信息的系统,也许这些数据不像主要控制功能那样引人注意。比如,我们要记录所有飞机中的小型飞机的的信息(比如飞行计划)。此时,我们又创建了第二个集合来记录所有小型飞机。 每当创建一个“飞机”对象的时候,将其放入第一个集合;若它属于小型飞机,也必须同时将其放入第二个集合里。\n",
"\n",
"现在问题开始棘手了:我们怎么知道何时该清理这些对象呢?当某一个系统处理完成,而其他系统可能还没有处理完成。这样的问题在其他的场景下也可能发生。在 C++ 程序设计中,当使用完一个对象后,必须明确将其删除,这就让问题变复杂了。\n",
"\n",
"对象的数据在哪?它的生命周期是怎么被控制的? 在 C++ 设计中采用的观点是效率第一因此它将选择权交给了程序员。为了获得最大的运行时速度程序员可以在编写程序时通过将对象放在栈Stack有时称为自动变量或作用域变量或静态存储区域static storage area中来确定内存占用和生存时间。这些区域的对象会被优先分配内存和释放。这种控制在某些情况下非常有用。\n",
"\n",
"然而相对的,我们也牺牲了程序的灵活性。因为在编写代码时,我们必须要弄清楚对象的数量、生存时间还有类型。如果我们要用它来解决一个相当普遍的问题时(如计算机辅助设计、仓库管理或空中交通管制等),限制就太大了。\n",
"\n",
"第二种方法是在堆内存Heap中动态地创建对象。在这种方式下直到程序运行我们才能确定需要创建的对象数量、生存时间和类型。什么时候需要什么时候在堆内存中创建。 因为内存的占用是动态管理的,所以在运行时,在堆内存上开辟空间所需的时间可能比在栈内存上要长(但也不一定)。在栈内存开辟和释放空间通常是一条将栈指针向下移动和一条将栈指针向上移动的汇编指令。开辟堆内存空间的时间取决于内存机制的设计。\n",
"\n",
"动态方法有这样一个合理假设:对象通常是复杂的,相比于对象创建的整体开销,寻找和释放内存空间的开销微不足道。(原文:*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.*)此外,更好的灵活性对于问题的解决至关重要。\n",
"\n",
"Java 使用动态内存分配。每次创建对象时,使用 `new` 关键字构建该对象的动态实例。这又带来另一个问题:对象的生命周期。较之堆内存,在栈内存中创建对象,编译器能够确定该对象的生命周期并自动销毁它;然而如果你在堆内存创建对象的话,编译器是不知道它的生命周期的。在 C++ 中你必须以编程方式确定何时销毁对象否则可能导致内存泄漏。Java 的内存管理是建立在垃圾收集器上的,它能自动发现对象不再被使用并释放内存。垃圾收集器的存在带来了极大的便利,它减少了我们之前必须要跟踪的问题和编写相关代码的数量。因此,垃圾收集器提供了更高级别的保险,以防止潜在的内存泄漏问题,这个问题使得许多 C++ 项目没落。\n",
"\n",
"Java 的垃圾收集器被设计用来解决内存释放的问题虽然这不包括对象清理的其他方面。垃圾收集器知道对象什么时候不再被使用并且自动释放内存。结合单继承和仅可在堆中创建对象的机制Java 的编码过程比用 C++ 要简单得多。我们所要做的决定和要克服的障碍也会少很多!\n",
"\n",
"## 异常处理\n",
"\n",
"自编程语言被发明以来,程序的错误处理一直都是个难题。因为很难设计出一个好的错误处理方案,所以许多编程语言都忽略了这个问题,把这个问题丢给了程序类库的设计者。他们提出了在许多情况下都可以工作但很容易被规避的半途而废的措施,通常只需忽略错误。多数错误处理方案的主要问题是:它们依赖程序员之间的约定俗成而不是语言层面的限制。换句话说,如果程序员赶时间或没想起来,这些方案就很容易被忘记。\n",
"\n",
"异常处理机制将程序错误直接交给编程语言甚至是操作系统。“异常”Exception是一个从出错点“抛出”thrown后能被特定类型的异常处理程序捕获(catch)的一个对象。它不会干扰程序的正常运行,仅当程序出错的时候才被执行。这让我们的编码更简单:不用再反复检查错误了。另外,异常不像方法返回的错误值和方法设置用来表示发生错误的标志位那样可以被忽略。异常的发生是不会被忽略的,它终究会在某一时刻被处理。\n",
"\n",
"最后,“异常机制”提供了一种可靠地从错误状况中恢复的方法,使得我们可以编写出更健壮的程序。有时你只要处理好抛出的异常情况并恢复程序的运行即可,无需退出。\n",
"\n",
"Java 的异常处理机制在编程语言中脱颖而出。Java 从一开始就内置了异常处理,因此你不得不使用它。这是 Java 语言唯一接受的错误报告方法。如果没有编写适当的异常处理代码,你将会收到一条编译时错误消息。这种有保障的一致性有时会让程序的错误处理变得更容易。值得注意的是,异常处理并不是面向对象的特性。尽管在面向对象的语言中异常通常由对象表示,但是在面向对象语言之前也存在异常处理。\n",
"\n",
"## 本章小结\n",
"\n",
"面向过程程序包含数据定义和函数调用。要找到程序的意图你必须要在脑中建立一个模型弄清函数调用和更底层的概念。这些程序令人困扰因为它们的表示更多地面向计算机而不是我们要解决的问题这就是我们在设计程序时需要中间表示的原因。OOP 在面向过程编程的基础上增加了许多新的概念,所以有人会认为使用 Java 来编程会比同等的面向过程编程要更复杂。在这里,我想给大家一个惊喜:通常按照 Java 规范编写的程序会比面向过程程序更容易被理解。\n",
"\n",
"你看到的是对象的概念,这些概念是站在“问题空间”的(而不是站在计算机角度的“解决方案空间”),以及发送消息给对象以指示该空间中的活动。面向对象编程的一个优点是:设计良好的 Java 程序代码更容易被人阅读理解。由于 Java 类库的复用性,通常程序要写的代码也会少得多。\n",
"\n",
"OOP 和 Java 不一定适合每个人。评估自己的需求以及与现有方案作比较是很重要的。请充分考虑后再决定是不是选择 Java。如果在可预见的未来Java 并不能很好的满足你的特定需求,那么你应该去寻找其他替代方案(特别是,我推荐看 Python。如果你依然选择 Java 作为你的开发语言,我希望你至少应该清楚你选择的是什么,以及为什么选择这个方向。\n",
"<!-- 分页 -->\n",
"\n",
"<div style=\"page-break-after: always;\"></div>"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Java",
"language": "java",
"name": "java"
},
"language_info": {
"codemirror_mode": "java",
"file_extension": ".jshell",
"mimetype": "text/x-java-source",
"name": "Java",
"pygments_lexer": "java",
"version": "14.0.1+7"
}
},
"nbformat": 4,
"nbformat_minor": 4
}

View File

@@ -1,305 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"[TOC]\n",
"\n",
"# 第二章 安装Java和本书用例\n",
"\n",
"现在,我们来为这次阅读之旅做些准备吧!\n",
"\n",
"在开始学习 Java 之前,你必须要先安装好 Java 和本书的源代码示例。因为考虑到可能有“专门的初学者”从本书开始学习编程,所以我会详细地教你如何使用命令行。 如果你已经有此方面的经验了,可以跳过这段安装说明。如果你对此处描述的任何术语或过程仍不清楚,还可以通过 [Google](https://google.com/) 搜索找到答案。具体的问题或困难请试着在 [StackOverflow](https://stackoverflow.com/) 上提问。或者去 [YouTube](https://youtube.com) 看有没有相关的安装说明。\n",
"\n",
"## 编辑器\n",
"\n",
"首先你需要安装一个编辑器来创建和修改本书用例里的 Java 代码。有可能你还需要使用编辑器来更改系统配置文件。\n",
"\n",
"相比一些重量级的 IDEIntegrated Development Environments集成开发环境如 Eclipse、NetBeans 和 IntelliJ IDEA (译者注做项目强烈推荐IDEA),编辑器是一种更纯粹的文本编辑器。如果你已经有了一个用着顺手的 IDE那就可以直接用了。为了方便后面的学习和统一下教学环境我推荐大家使用 Atom 这个编辑器。大家可以在 [atom.io](https://atom.io) 上下载。\n",
"\n",
"Atom 是一个免费开源、易于安装且跨平台(支持 Window、Mac和Linux的文本编辑器。内置支持 Java 文件。相比 IDE 的厚重它比较轻量级是学习本书的理想工具。Atom 包含了许多方便的编辑功能,相信你一定会爱上它!更多关于 Atom 使用的细节问题可以到它的网站上寻找。\n",
"\n",
"还有很多其他的编辑器。有一种亚文化的群体,他们热衷于争论哪个更好用!如果你找到一个你更喜欢的编辑器,换一种使用也没什么难度。重要的是,你要找一个用着舒服的。\n",
"\n",
"## Shell\n",
"\n",
"如果你之前没有接触过编程,那么有可能对 Shell命令行窗口 不太熟悉。shell 的历史可以追溯到早期的计算时代,当时在计算机上的操作是都通过输入命令进行的,计算机通过回显响应。所有的操作都是基于文本的。\n",
"\n",
"尽管和现在的图形用户界面相比Shell 操作方式很原始。但是同时 shell 也为我们提供了许多有用的功能特性。在学习本书的过程中,我们会经常使用到 Shell包括现在这部分的安装还有运行 Java 程序。\n",
"\n",
"Mac单击聚光灯屏幕右上角的放大镜图标然后键入 `terminal`。单击看起来像小电视屏幕的应用程序你也可以单击“return”。这就启动了你的用户下的 shell 窗口。\n",
"\n",
"windows首先通过目录打开 windows 资源管理器:\n",
"\n",
"- Windows 7: 单击屏幕左下角的“开始”图标输入“explorer”后按回车键。\n",
"- Windows 8: 按 Windows+Q输入 “explorer” 后按回车键。\n",
"- Windows 10: 按 Windows+E 打开资源管理器,导航到所需目录,单击窗口左上角的“文件“选项卡,选择“打开 Window PowerShell”启动 Shell。\n",
"\n",
"Linux: 在 home 目录打开 Shell。\n",
"\n",
"- Debian: 按 Alt+F2 在弹出的对话框中输入“gnome-terminal”\n",
"- Ubuntu: 在屏幕中鼠标右击,选择 “打开终端”,或者按住 Ctrl+Alt+T\n",
"- Redhat: 在屏幕中鼠标右击,选择 “打开终端”\n",
"- Fedora: 按 Alt+F2在弹出的对话框中输入“gnome-terminal”\n",
"\n",
"**目录**\n",
"\n",
"目录是 Shell 的基础元素之一。目录用来保存文件和其他目录。目录就好比树的分支。如果书籍是你系统上的一个目录并且它有两个其他目录作为分支例如数学和艺术那么我们就可以说你有一个书籍目录它包含数学和艺术两个子目录。注意Windows 使用 `\\` 而不是 `/` 来分隔路径。\n",
"\n",
"**Shell基本操作**\n",
"\n",
"我在这展示的 Shell 操作和系统中大体相同。出于本书的原因,下面列举一些在 Shell 中的基本操作:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"%%bash\n",
"更改目录: cd <路径> \n",
" cd .. 移动到上级目录 \n",
" pushd <路径> 记住来源的同时移动到其他目录popd 返回来源\n",
"\n",
"目录列举: ls 列举出当前目录下所有的文件和子目录名(不包含隐藏文件),\n",
" 可以选择使用通配符 * 来缩小搜索范围。\n",
" 示例(1) 列举所有以“.java”结尾的文件输入 ls *.java (Windows: dir *.java)\n",
" 示例(2) 列举所有以“F”开头“.java”结尾的文件输入ls F*.java (Windows: dir F*.java)\n",
"\n",
"创建目录: \n",
" Mac/Linux 系统mkdir \n",
" 示例mkdir books \n",
" Windows 系统md \n",
" 示例md books\n",
"\n",
"移除文件: \n",
" Mac/Linux 系统rm\n",
" 示例rm somefile.java\n",
" Windows 系统del \n",
" 示例del somefile.java\n",
"\n",
"移除目录: \n",
" Mac/Linux 系统rm -r\n",
" 示例rm -r books\n",
" Windows 系统deltree \n",
" 示例deltree books\n",
"\n",
"重复命令: !! 重复上条命令\n",
" 示例:!n 重复倒数第n条命令\n",
"\n",
"命令历史: \n",
" Mac/Linux 系统history\n",
" Windows 系统:按 F7 键\n",
"\n",
"文件解压:\n",
" Linux/Mac 都有命令行解压程序 unzip你可以通过互联网为 Windows 安装命令行解压程序 unzip。\n",
" 图形界面下Windows 资源管理器Mac FinderLinux Nautilus 或其他等效软件)右键单击该文件,\n",
" 在 Mac 上选择“open”在 Linux 上选择“extract here”或在 Windows 上选择“extract all…”。\n",
" 要了解关于 shell 的更多信息,请在维基百科中搜索 Windows shellMac/Linux用户可搜索 bash shell。\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Java安装\n",
"\n",
"为了编译和运行代码示例,首先你必须安装 JDKJava Development KitJAVA 软件开发工具包)。本书中采用的是 JDK 8。\n",
"\n",
"\n",
"**Windows**\n",
"\n",
"1. 以下为 Chocolatey 的[安装说明](https://chocolatey.org/)。\n",
"2. 在命令行提示符下输入下面的命令,等待片刻,结束后 Java 安装完成并自动完成环境变量设置。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"%%bash\n",
" choco install jdk8"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Macintosh**\n",
"\n",
"Mac 系统自带的 Java 版本太老,为了确保本书的代码示例能被正确执行,你必须将它先更新到 Java 8。我们需要管理员权限来运行下面的步骤\n",
"\n",
"1. 以下为 HomeBrew 的[安装说明](https://brew.sh/)。安装完成后执行命令 `brew update` 更新到最新版本\n",
"2. 在命令行下执行下面的命令来安装 Java。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"%%bash\n",
" brew cask install java"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"当以上安装都完成后,如果你有需要,可以使用游客账户来运行本书中的代码示例。\n",
"\n",
"**Linux**\n",
"\n",
"* **Ubuntu/Debian**"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"%%bash\n",
" sudo apt-get update\n",
" sudo apt-get install default-jdk"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"* **Fedora/Redhat**"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"%%bash\n",
" su-c \"yum install java-1.8.0-openjdk\"(注:执行引号内的内容就可以安装)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 校验安装\n",
"\n",
"打开新的命令行输入:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"%%bash\n",
"java -version"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"正常情况下 你应该看到以下类似信息(版本号信息可能不一样):"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"%%bash\n",
"java version \"1.8.0_112\"\n",
"Java(TM) SE Runtime Environment (build 1.8.0_112-b15)\n",
"Java HotSpot(TM) 64-Bit Server VM (build 25.112-b15, mixed mode)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"如果提示命令找不到或者无法被识别,请根据安装说明重试;如果还不行,尝试到 [StackOverflow](https://stackoverflow.com/search?q=installing+java) 寻找答案。\n",
"\n",
"## 安装和运行代码示例\n",
"\n",
"当 Java 安装完毕,下一步就是安装本书的代码示例了。安装步骤所有平台一致:\n",
"\n",
"1. 从 [GitHub 仓库](https://github.com/BruceEckel/OnJava8-Examples/archive/master.zip)中下载本书代码示例\n",
"2. 解压到你所选目录里。\n",
"3. 使用 Windows 资源管理器Mac FinderLinux 的 Nautilus 或其他等效工具浏览,在该目录下打开 Shell。\n",
"4. 如果你在正确的目录中,你应该看到该目录中名为 gradlew 和 gradlew.bat 的文件,以及许多其他文件和目录。目录与书中的章节相对应。\n",
"5. 在shell中输入下面的命令运行"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"%%bash\n",
" Windows 系统:\n",
" gradlew run\n",
"\n",
" Mac/Linux 系统:\n",
" ./gradlew run"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"第一次安装时 Gradle 需要安装自身和其他的相关的包,请稍等片刻。安装完成后,后续的安装将会快很多。\n",
"\n",
"**注意** 第一次运行 gradlew 命令时必须连接互联网。\n",
"\n",
"**Gradle 基础任务**\n",
"\n",
"本书构建的大量 Gradle 任务都可以自动运行。Gradle 使用约定大于配置的方式,简单设置即可具备高可用性。本书中“一起去骑行”的某些任务不适用于此或无法执行成功。以下是你通常会使用上的 Gradle 任务列表:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"%%bash\n",
" 编译本书中的所有 java 文件,除了部分错误示范的\n",
" gradlew compileJava\n",
"\n",
" 编译并执行 java 文件(某些文件是库组件)\n",
" gradlew run\n",
"\n",
" 执行所有的单元测试在本书第16章会有详细介绍\n",
" gradlew test\n",
"\n",
" 编译并运行一个具体的示例程序\n",
" gradlew <本书章节>:<示例名称>\n",
" 示例gradlew objects:HelloDate"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<!-- 分页 -->\n",
"\n",
"<div style=\"page-break-after: always;\"></div>"
]
}
],
"metadata": {},
"nbformat": 4,
"nbformat_minor": 4
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,132 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"[TOC]\n",
"\n",
"<!-- Appendix: Becoming a Programmer -->\n",
"# 附录:成为一名程序员\n",
"\n",
">我分别于200320062007和2009年撰写的博客文章混搭\n",
"\n",
"<!-- How I Got Started in Programming -->\n",
"## 如何开始\n",
"\n",
"这是一条相当漫长和曲折的道路。我在高一学代数时(1971年)有个非常古怪的老师有一台计算机还弄到了一台配有一个300波特的音频电话耦合器的ASR-33电传打字机我学会了如何执行命令并得到响应以及一个可以在高中区使用的HP-1000计算机上的帐户。我们能够创建和运行BASIC程序并将它们保存在打孔磁带上。我对此非常着迷所以尽可能地把它带回家后在晚上写程序。我写了一个赛马模拟游戏--HOSRAC.BAS用星号来代表马的移动由于是在纸上打印输出所以需要一点想象力。\n",
"\n",
"我的朋友丹尼尔(就是设计我的书封面的人)有一个兄弟,他有段时间通过向酒吧和餐馆提供弹球机来赚钱。他有一台投币式街机(老虎机),最早的《乓》游戏之一,我对此全然不知,到现在我还忍受不了这东西(现在我几乎不玩电脑游戏,这样看来我可能是个没有幽默的人,但似乎编程比玩电脑游戏更有趣、更具挑战性。)\n",
"\n",
"后来我在高中参与了摄影和新闻工作,在大学的第一年就主修新闻学。我觉得自己已经从学校学到了足够多的东西,又转修了物理学。后来我在加州大学欧文分校完成了物理学位,如果我当时选择了一个特定的工程领域,修了足够的工程课就能拿到双专业,但我试图走得更远一些,所以最后我获得的本科学位是 \"应用物理\"。作为一名本科生,我多多少少学习了一些可以自娱自乐,但又没有任何深度的计算机编程课程。我个人认为在这些课程细细熏陶下,帮我打下了一定的基础,但事实我理解的这些东西没有任何深度。我不知道计算机、编译器或解释器有什么区别(只是对编译器和解释器一点点的理解)。对我来说计算机是绝对可靠的,而且我从来没有想过在程序语言和操作系统中会有出现错误的可能。\n",
"\n",
"后来我去了在加州州立理工大学攻读研究生,主要有三点原因\n",
"\n",
"1. 我真的非常喜欢物理学这个领域\n",
"\n",
"2. 他们接受了我,甚至给了我一份教学工作和奖学金 \n",
"\n",
"3. 出乎意料的是他们给我的工作时间不止一个夏天\n",
"\n",
"而我完全没做好上班的准备。\n",
"\n",
"作为一名物理专业的学生,我学习的是太阳能发电系统,当时太阳能发电系统很大 (如果你的房子上装了太阳能或生意上是关于太阳能系统,加州就会给予税收抵免,因此也兴起很多生意),加州理工大学也承诺会在工程系开设相应的课程。然而因为学校没有提供必要的课程,要想获得在太阳能工程的学位得花好几年时间。所以我学习了研究生其他的工程课,包括介绍机械,太阳能,电气和电子工程。我上的课是非电气工程专业的电气工程导论。最常见的研究生工程课程是计算机工程专业,所以最后我拿了那个学位。我还上了艺术课,几门舞蹈课,还有一些计算机科学课程 (Pascal和数据结构),在计算机工程中,我终于弄清楚了处理器的工作流程,从那以后我一直带着一个处理器在身上。这些就是我学的计算机基础知识。\n",
"\n",
"刚开始工作的时候凭借着一堆硬件和相对简单低水平的编程做了一名计算机工程师。因为C语言似乎是理想的嵌入式系统语言于是我开始自学并慢慢开始了解更多关于编程语言的东西。我们在这家公司从源代码构建编译器这让我大开眼界。 (想象一下一个编译器只是另一个软件的一部分!\n",
"\n",
"当我去华盛顿大学海洋学院为Tom Keffer后来创建了“疯狗浪”工作时我们决定使用C++。我只有一本Stroustrup写的非初学者书可以参考最终不得不通过检查C++预处理器生成的中间C代码来了解语言的功能。这个过程非常痛苦但学习的效果很好。从那以后我就用相同的方式学习因为它让我学习了如何剖析一种语言并看到它本质的能力与此同时开始有了批判性思维。 \n",
"\n",
"我并没有理解清楚所有的概念。只是在之后的日子里不断反复,我所知道的一切需要时间才能消化吸收。如果我现在能很容易地理解一个新概念,那只是因为它是我已经知道的积累概念的一个变种。在加州理工大学招收非计算机本科学历的计算机科学研究生项目中,学生们曾经说他们花了一年的时间才弄清楚他们对计算机的困惑(他们正在沉浸程序之中)。当人们学习计算机时,他们往往会对自己抱有不切实际的期望,通常是他们听说学计算机编程的好处,就希望在几周内找到一份高薪的工作。但是,最好的学习过程是先对计算机感兴趣,随着时间的推移,学习的越来越多,自然的就开始自学。\n",
"\n",
"这些就是我主要做的事,尽管我通过学计算机工程有还算扎实的基础,但我没上过编程课,而是通过自学。在此期间我也在不断地学习新事物,在这个行业里,不断学习是非常重要的一部分。\n",
"\n",
"<!-- A Career in Computing -->\n",
"## 码农生涯\n",
"\n",
"我会定期收到有关职业建议的请求,所以我尝试在这里回答一下这个问题。\n",
"\n",
"人们提出的问题通常是错误的问题:“我应该学习 C++ 还是 Java ?”在本文中,我将尝试阐述我对选择计算机职业所涉及的真正问题的看法。\n",
"\n",
"请注意,我在这里并不是和那些已经知道自己使命的人聊(译者注:指计划成为程序员或者已经从业的程序员,暗指这里是讲给外行的小白的)。因为无论别人怎么说,你都要去做,因为它已经渗入你的血液,并且你将无法摆脱它。你已经知道答案了:你当然会学到 C++ Java shell 脚本Python 和许多其他语言和技术。即使你只有14岁你也已经知道其中几种语言。\n",
"\n",
"问我这个问题的人可能来自另一职业。也许他们来自 Web 开发等领域,他们已经发现 HTML 只是一种类似编程,他们想尝试构建更实质的内容。但是,我特别希望,如果你提出这个问题,你就已经意识到,要在计算机领域取得成功,你必须教自己如何学习,并且永不停止学习。\n",
"\n",
"随着我做的越来越多,在我看来,软件越发比其他任何东西都更像写作。而且我们还没有弄清怎样成为一个好的作家,我们只知道何时我们喜欢别人写的东西。这不是像一些工程那样,我们要做的只是将某些东西放到一端,然后转动曲柄。诱人的是将软件视为确定性的,这就是我们想要的,这就是我们不断推出工具来帮助我们实现所需行为的原因。但是我的经验不断表明事实是相反的:它更多地是关于人而不是过程,并且它在确定性机器上运行的事实变得越来越没有影响力(指运行环境受机器影响,与机器相关这个事实),就像海森堡原理(不确定性原理:不可能同时知道一个粒子的位置和它的速度)不会在人类规模上影响事物一样。\n",
"\n",
"在我青年时期,父亲是建造民居的,我偶尔会为他工作,大部分时间都从事艰苦的工作,有时还得悬挂石膏板。他和他的木匠会告诉我说,他们是为了我才把这些工作交给了我 —— 为了不让我从事这项工作。这确实是有效的。\n",
"\n",
"因此,我也可以用比喻说,建造软件就像盖房子一样。我们并不是指每个在房屋上工作的人都一样。有混凝土泥瓦匠,屋顶工,水管工,电工,石膏板工人,抹灰工,瓷砖铺砌工,普通劳工,粗木匠,精整木匠,当然还有总承包商。这些中的每一个都需要一套不同的技能,这需要花费不同的时间和精力 房屋建造也受制于繁荣和萧条的周期,例如编程。为了快速起步,你可能需要当普通劳工或石膏板工人工作,在那里你可以在没有太多学习曲线的情况下开始获得报酬。只要需求旺盛,你就可以稳定工作,而且如果没有足够的人来工作,你的薪水甚至可能会上涨。但是一旦经济低迷,木匠甚至总承包商就可以自己将石膏板挂起来。\n",
"\n",
"当 Internet 刚兴起时,你所要做的就是花一些时间学习 HTML 就可以找到一份工作并赚到很多钱。但是当情况恶化时你很快就会发现需要的技能层次结构很深HTML 程序员(例如劳工和石膏板工)排在第一位,而高技能的码农和木匠则被保留。\n",
"\n",
"我想在这里说的是:除非你准备致力于终身学习,否则请不要从事这项业务。有时,编程似乎是一份报酬丰厚,值得信赖的工作,但确保这一点的唯一方法是,始终使自己变得更有价值。\n",
"\n",
"当然,也可以找到例外。总会有一些人只学习一种语言,并且足够精通,甚至足够聪明,那么可以在不用多学很多其他知识的情况下继续工作。但是他们靠运气生存,最终很脆弱。为了减少自身的脆弱性,必须通过阅读,参加用户组,会议和研讨会来不断提高自己的能力。你在该领域的走得越深,你的价值就越大,这意味着你的工作前景更稳定,并且可以获得更高的薪水。\n",
"\n",
"另一种方法是从总体上看待该领域,并找到一个你能成为专家的点。例如,我的兄弟对软件感兴趣,并且涉足软件,但是他的业务是安装计算机,维修计算机和升级计算机。他一直都很细致,因此,当他安装或修理计算机时,你会知道计算机状态良好。不仅是软件,而且一直到电缆,电缆都整齐地捆扎在一起,并且不成束。他的工作多到做不完,而且他从不关心网络泡沫破灭。毋庸置疑,他是不可能失业的。\n",
"\n",
"我在大学待了很长时间,并以各种方式设法度过了难关。我甚至开始在加州大学洛杉矶分校攻读博士学位。这里的课程很短,我欣慰地说是因为我不再爱上大学了,而我在大学待了这么长时间的原因是因为我非常喜欢。但是我喜欢的通常是跑偏的东西。例如艺术,舞蹈课程,在大学报社工作,以及我参加的少数计算机编程课程(由于我是物理本科生和计算机工程专业的研究生,所以也算跑偏)。尽管我在学业上还算是出色的(具有讽刺意味的是,当时许多不接受我作为学生的大学现在都在课程中使用我的书),但我确实很享受大学生的生活,并且完成了博士学位。我可能会走上简单的道路,最终成为一名教授。\n",
"\n",
"但是事实证明,我从大学获得的最大价值一部分来自那些跑偏的课程,这些课程使我的思维超出了“我们已经知道的东西”。我认为在计算机领域尤其如此,因为你总是通过编程来实现其他目标,而你对该目标越了解,你的表现就会越好(我学习了一些欧洲研究生课程,这些课程要求结合其他一些专业研究计算,通过解决这个领域相关的问题,你就会形成一种新的理论体系并可以将它用在别处)。\n",
"\n",
"我还认为,不仅编程,多了解一些其它的知识,还可以大大提高你的解决问题的能力(就像了解一种以上的编程语言可以极大地提高你的编程能力一样)。在很多情况下,我遇到过仅接受过计算机科学训练的人,他们的思维似乎比其他背景(例如数学或物理学)的人更受限制,但其实这些人(数学或物理学领域的人)才更需要严格的思维。\n",
"\n",
"在我组织的一次会议上,主题之一是为理想的求职者提供一系列功能:\n",
"\n",
"- 将学习作为一种生活方式。例如,学习一种以上的语言;没有什么比学习另一种语言更能吸引你的眼球。\n",
"- 知道在哪里以及如何获得新知识。\n",
"- 研究现有技术。\n",
"- 我们是工具使用者,即要善于利用工具。\n",
"- 学习做最简单的事情。\n",
"- 了解业务(阅读杂志。从 *fast company*(国外一家商业杂志)开始,该公司的文章非常简短有趣。然后你就会知道是否要阅读其他的)\n",
"- 应对错误负责。 “我用着没事”是不可接受的策略。查找自己的错误。\n",
"- 成为领导者:那些沟通和鼓舞别人的人。\n",
"- 你在为谁服务?\n",
"- 没有正确的答案……但总是更好的方法。展示和讨论你的代码,不要有情感上的依恋。你不是你的代码。\n",
"- 这是通往完美的渐进旅程。\n",
"\n",
"承担一切可能的风险,最好的风险是那些可怕的风险,但是在尝试时你会比想象中的更加活跃。最好不要刻意去预测某个特定的结果,因为如果你过于重视某个结果,就会经常错过真正的可能性。应该“让我们做一点实验,看看会把我们带到哪里”。这些实验是我最好的冒险。\n",
"\n",
"有些人对这个答案感到失望,然后回答“是的,这都是非常有趣和有用的。但是实际上,我应该学习什么? C++ 还是 Java ?”,以防这些问题,我将在这里重复一遍:我知道似乎所有的 1 和 0 都应该使一切具有确定性因此此类问题应该有一个简单的答案,但事实并非如此。这与做出选择并完成选择无关,这是有关持续学习和有时需要大胆的选择。相信我,这样你的生活会更加令人兴奋。\n",
"\n",
"### 延伸阅读\n",
"* [Teach Yourself Programming In Ten Years](http://norvig.com/21-days.html), by Peter Norvig.\n",
"* [How To Be A Programmer](http://samizdat.mines.edu/howto/HowToBeAProgrammer.html), by Robert Read.\n",
"* A [speech by Steve Jobs](http://news.stanford.edu/news/2005/june15/jobs-061505.html) to inspire a group of graduating college students.\n",
"* Kathy Sierra: [Does College Matter](https://headrush.typepad.com/creating_passionate_users/2005/07/does_college_ma.html)?\n",
"* Paul Graham [on College](http://www.paulgraham.com/college.html).\n",
"* Joel Spolsky: [Advice for Computer Science College Students](https://www.joelonsoftware.com/2005/01/02/advice-for-computer-science-college-students/).\n",
"* James Shore: [Five Design Skills Every Programmer Should Have](https://www.jamesshore.com/Blog/Five-Design-Skills.html).\n",
"* Steve Yegge: [The Truth About Interviewing](http://steve-yegge.blogspot.com/2006/03/truth-about-interviewing.html).\n",
"\n",
"<!-- The Mythical 5% -->\n",
"## 百分之五的神话\n",
"\n",
"\n",
"<!-- Writing Software Is Like … Writing -->\n",
"## 重在动手\n",
"\n",
"\n",
"<!-- Programming as Typing -->\n",
"## 像打字般编程\n",
"\n",
"\n",
"<!-- Do What You Love -->\n",
"## 做你喜欢的事\n",
"\n",
"*“1960年一位研究人员对1500名商学院学生进行了访谈并将他们分为两类那些为了钱财来这里上学的人1245人以及那些打算利用学位做他们非常关心的事情的人255人。二十年后研究人员再次访谈了这些毕业生发现其中有101位百万富翁除了其中一位所有百万富翁都来自追求他们喜欢做的事的那255人”*\n",
"\n",
"“现在你可能觉得你对巴洛克时期的冰岛诗歌或者蝴蝶收集或者高尔夫抑或是对社会正义的热情会因为要养家糊口而让你和你喜欢做的事分道扬镳并非一定要如此。弗拉基米尔·纳博科夫Vladimir Nabokov是本世纪最伟大的小说家之一他对蝴蝶收藏的热情远远超过写作。事实上他的第一个大学教学工作是关于鳞翅类昆虫。在过去40年里对40万美国群众的研究表明即使是部分的、零散的追求培养你的激情也可以帮助你充分利用你目前的能力激励你培养新的能力。”--摘自《The Other 90%》 Robert K.Cooper\n",
"\n",
"当然你可以看Po Bronson写的《 What Should I Do With My Life?》这本书,对这些想法进行更多的探索。\n",
"\n",
"<!-- 分页 -->\n",
"\n",
"<div style=\"page-break-after: always;\"></div>"
]
}
],
"metadata": {},
"nbformat": 4,
"nbformat_minor": 4
}

View File

@@ -1,42 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"[TOC]\n",
"\n",
"<!-- Appendix: Benefits and Costs of Static Type Checking -->\n",
"# 附录:静态语言类型检查\n",
"> 这是一本我多年来撰写的经过编辑过的论文集,论文集试图将静态检查语言和动态语言之间的争论放到一个正确的角度。还有一个前言部分,描述了我最近对这个话题的思考和见解。\n",
"\n",
"<!-- Foreword -->\n",
"## 前言\n",
"\n",
"\n",
"<!-- Static Type Checking vs. Testing -->\n",
"## 静态类型检查和测试\n",
"\n",
"\n",
"<!-- How to Argue about Typing -->\n",
"## 如何提升打字\n",
"\n",
"\n",
"<!-- The Cost of Productivity -->\n",
"## 生产力的成本\n",
"\n",
"\n",
"<!-- Static vs. Dynamic -->\n",
"## 静态和动态\n",
"\n",
"\n",
"<!-- 分页 -->\n",
"\n",
"<div style=\"page-break-after: always;\"></div>"
]
}
],
"metadata": {},
"nbformat": 4,
"nbformat_minor": 4
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,417 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"[TOC]\n",
"\n",
"<!-- Appendix: Data Compression -->\n",
"# 附录:数据压缩\n",
"\n",
"Java I/O 类库提供了可以读写压缩格式流的类。你可以将其他 I/O 类包装起来用于提供压缩功能。\n",
"\n",
"这些类不是从 **Reader** 和 **Writer** 类派生的,而是 **InputStream** 和 **OutputStream** 层级结构的一部分。这是由于压缩库处理的是字节,而不是字符。但是,你可能会被迫混合使用两种类型的流(请记住,你可以使用 **InputStreamReader** 和 **OutputStreamWriter**,这两个类可以在字节类型和字符类型之间轻松转换)。\n",
"\n",
"| 压缩类 | 功能 |\n",
"| ------------------------ | ------------------------------------------------------------ |\n",
"| **CheckedInputStream** | `getCheckSum()` 可以对任意 **InputStream** 计算校验和(而不只是解压) |\n",
"| **CheckedOutputStream** | `getCheckSum()` 可以对任意 **OutputStream** 计算校验和(而不只是压缩) |\n",
"| **DeflaterOutputStream** | 压缩类的基类 |\n",
"| **ZipOutputStream** | **DeflaterOutputStream** 类的一种,用于压缩数据到 Zip 文件结构 |\n",
"| **GZIPOutputStream** | **DeflaterOutputStream** 类的一种,用于压缩数据到 GZIP 文件结构 |\n",
"| **InflaterInputStream** | 解压类的基类 |\n",
"| **ZipInputStream** | **InflaterInputStream** 类的一种,用于解压 Zip 文件结构的数据 |\n",
"| **GZIPInputStream** | **InflaterInputStream** 类的一种,用于解压 GZIP 文件结构的数据 |\n",
"\n",
"尽管存在很多压缩算法,但是 Zip 和 GZIP 可能是最常见的。你可以使用许多用于读取和写入这些格式的工具,来轻松操作压缩数据。\n",
"\n",
"<!-- Simple Compression with GZIP -->\n",
"\n",
"## 使用 Gzip 简单压缩\n",
"\n",
"<!-- Multifile Storage with Zip -->\n",
"\n",
"GZIP 接口十分简单,因此当你有一个需要压缩的数据流(而不是一个包含不同数据分片的容器)时,使用 GZIP 更为合适。如下是一个压缩单个文件的示例:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"java"
],
"id": ""
}
},
"outputs": [],
"source": [
"// compression/GZIPcompress.java\n",
"// (c)2017 MindView LLC: see Copyright.txt\n",
"// We make no guarantees that this code is fit for any purpose.\n",
"// Visit http://OnJava8.com for more book information.\n",
"// {java GZIPcompress GZIPcompress.java}\n",
"// {VisuallyInspectOutput}\n",
"\n",
"public class GZIPcompress {\n",
" public static void main(String[] args) {\n",
" if (args.length == 0) {\n",
" System.out.println(\n",
" \"Usage: \\nGZIPcompress file\\n\" +\n",
" \"\\tUses GZIP compression to compress \" +\n",
" \"the file to test.gz\");\n",
" System.exit(1);\n",
" }\n",
" try (\n",
" InputStream in = new BufferedInputStream(\n",
" new FileInputStream(args[0]));\n",
" BufferedOutputStream out =\n",
" new BufferedOutputStream(\n",
" new GZIPOutputStream(\n",
" new FileOutputStream(\"test.gz\")))\n",
" ) {\n",
" System.out.println(\"Writing file\");\n",
" int c;\n",
" while ((c = in.read()) != -1)\n",
" out.write(c);\n",
" } catch (IOException e) {\n",
" throw new RuntimeException(e);\n",
" }\n",
" System.out.println(\"Reading file\");\n",
" try (\n",
" BufferedReader in2 = new BufferedReader(\n",
" new InputStreamReader(new GZIPInputStream(\n",
" new FileInputStream(\"test.gz\"))))\n",
" ) {\n",
" in2.lines().forEach(System.out::println);\n",
" } catch (IOException e) {\n",
" throw new RuntimeException(e);\n",
" }\n",
" }\n",
"}\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"使用压缩类非常简单,你只需要把你的输出流包装在 **GZIPOutputStream** 或 **ZipOutputStream** 中,将输入流包装在 **GZIPInputStream** 或 **ZipInputStream**。其他的一切就只是普通的 I/O 读写。这是面向字符流和面向字节流的混合示例in 使用 Reader 类,而 **GZIPOutputStreams** 构造函数只能接受 **OutputStream** 对象,而不能接受 **Writer** 对象。当打开文件的时候,**GZIPInputStream** 会转换成为 **Reader**。\n",
"\n",
"## 使用 zip 多文件存储\n",
"\n",
"支持 Zip 格式的库比 GZIP 库更广泛。有了它,你可以轻松存储多个文件,甚至还有一个单独的类可以轻松地读取 Zip 文件。该库使用标准 Zip 格式,因此它可以与当前可在 Internet 上下载的所有 Zip 工具无缝协作。以下示例与前一个示例具有相同的形式,但它可以根据需要处理任意数量的命令行参数。此外,它还显示了 **Checksum** 类计算和验证文件的校验和。有两种校验和类型Adler32更快和 CRC32更慢但更准确。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"java"
],
"id": ""
}
},
"outputs": [],
"source": [
"// compression/ZipCompress.java\n",
"// (c)2017 MindView LLC: see Copyright.txt\n",
"// We make no guarantees that this code is fit for any purpose.\n",
"// Visit http://OnJava8.com for more book information.\n",
"// Uses Zip compression to compress any\n",
"// number of files given on the command line\n",
"// {java ZipCompress ZipCompress.java}\n",
"// {VisuallyInspectOutput}\n",
"public class ZipCompress {\n",
" public static void main(String[] args) {\n",
" try (\n",
" FileOutputStream f =\n",
" new FileOutputStream(\"test.zip\");\n",
" CheckedOutputStream csum =\n",
" new CheckedOutputStream(f, new Adler32());\n",
" ZipOutputStream zos = new ZipOutputStream(csum);\n",
" BufferedOutputStream out =\n",
" new BufferedOutputStream(zos)\n",
" ) {\n",
" zos.setComment(\"A test of Java Zipping\");\n",
" // No corresponding getComment(), though.\n",
" for (String arg : args) {\n",
" System.out.println(\"Writing file \" + arg);\n",
" try (\n",
" InputStream in = new BufferedInputStream(\n",
" new FileInputStream(arg))\n",
" ) {\n",
" zos.putNextEntry(new ZipEntry(arg));\n",
" int c;\n",
" while ((c = in.read()) != -1)\n",
" out.write(c);\n",
" }\n",
" out.flush();\n",
" }\n",
" // Checksum valid only after the file is closed!\n",
" System.out.println(\n",
" \"Checksum: \" + csum.getChecksum().getValue());\n",
" } catch (IOException e) {\n",
" throw new RuntimeException(e);\n",
" }\n",
" // Now extract the files:\n",
" System.out.println(\"Reading file\");\n",
" try (\n",
" FileInputStream fi =\n",
" new FileInputStream(\"test.zip\");\n",
" CheckedInputStream csumi =\n",
" new CheckedInputStream(fi, new Adler32());\n",
" ZipInputStream in2 = new ZipInputStream(csumi);\n",
" BufferedInputStream bis =\n",
" new BufferedInputStream(in2)\n",
" ) {\n",
" ZipEntry ze;\n",
" while ((ze = in2.getNextEntry()) != null) {\n",
" System.out.println(\"Reading file \" + ze);\n",
" int x;\n",
" while ((x = bis.read()) != -1)\n",
" System.out.write(x);\n",
" }\n",
" if (args.length == 1)\n",
" System.out.println(\n",
" \"Checksum: \" + csumi.getChecksum().getValue());\n",
" } catch (IOException e) {\n",
" throw new RuntimeException(e);\n",
" }\n",
" // Alternative way to open and read Zip files:\n",
" try (\n",
" ZipFile zf = new ZipFile(\"test.zip\")\n",
" ) {\n",
" Enumeration e = zf.entries();\n",
" while (e.hasMoreElements()) {\n",
" ZipEntry ze2 = (ZipEntry) e.nextElement();\n",
" System.out.println(\"File: \" + ze2);\n",
" // ... and extract the data as before\n",
" }\n",
" } catch (IOException e) {\n",
" throw new RuntimeException(e);\n",
" }\n",
" }\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"对于要添加到存档的每个文件,必须调用 `putNextEntry()` 并传递 **ZipEntry** 对象。 **ZipEntry** 对象包含一个扩展接口,用于获取和设置 Zip 文件中该特定条目的所有可用数据名称压缩和未压缩大小日期CRC 校验和,额外字段数据,注释,压缩方法以及它是否是目录条目。但是,即使 Zip 格式有设置密码的方法Java 的 Zip 库也不支持。虽然 **CheckedInputStream** 和 **CheckedOutputStream** 都支持 Adler32 和 CRC32 校验和,但 **ZipEntry** 类仅支持 CRC 接口。这是对基础 Zip 格式的限制,但它可能会限制你使用更快的 Adler32。\n",
"\n",
"要提取文件,**ZipInputStream** 有一个 `getNextEntry()` 方法,这个方法在有文件存在的情况下调用,会返回下一个 **ZipEntry**。作为一个更简洁的替代方法,你可以使用 **ZipFile** 对象读取该文件,该对象具有方法 entries() 返回一个包裹 **ZipEntries** 的 **Enumeration**。\n",
"\n",
"要读取校验和,你必须以某种方式访问关联的 **Checksum** 对象。这里保留了对 **CheckedOutputStream** 和 **CheckedInputStream** 对象的引用,但你也可以保持对 **Checksum** 对象的引用。 Zip 流中的一个令人困惑的方法是 `setComment()`。如 **ZipCompress** 所示。在 Java 中,你可以在编写文件时设置注释,但是没有办法恢复 **ZipInputStream** 中的注释。注释似乎仅通过 **ZipEntry** 在逐个条目的基础上完全支持。\n",
"\n",
"使用 GZIP 或 Zip 库时,你不仅被限制于文件——你可以压缩任何内容,包括通过网络连接发送的数据。\n",
"\n",
"\n",
"\n",
"<!-- Java Archives (Jars) -->\n",
"\n",
"## Java 的 jar\n",
"\n",
"Zip 格式也用于 JARJava ARchive文件格式这是一种将一组文件收集到单个压缩文件中的方法就像 Zip 一样。但是,与 Java 中的其他所有内容一样JAR 文件是跨平台的,因此你不必担心平台问题。你还可以将音频和图像文件像类文件一样包含在其中。\n",
"\n",
"JAR 文件由一个包含压缩文件集合的文件和一个描述它们的“清单manifest”组成。你可以创建自己的清单文件否则jar 程序将为你执行此操作。)你可以在 JDK 文档中,找到更多关于 JAR 清单的信息。\n",
"\n",
"JDK 附带的 jar 工具会自动压缩你选择的文件。你可以在命令行上调用它:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"shell"
],
"id": ""
}
},
"outputs": [],
"source": [
"jar [options] destination [manifest] inputfile(s)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"选项是一组字母(不需要连字符或任何其他指示符)。 Unix / Linux 用户会注意到这些选项与 tar 命令选项的相似性。这些是:\n",
"\n",
"| 选项 | 功能 |\n",
"| ---------- | ------------------------------------------------------------ |\n",
"| **c** | 创建一个新的或者空的归档文件 |\n",
"| **t** | 列出内容目录 |\n",
"| **x** | 提取所有文件 |\n",
"| **x** file | 提取指定的文件 |\n",
"| **f** | 这代表着“传递文件的名称。”如果你不使用它jar 假定它的输入将来自标准输入,或者,如果它正在创建一个文件,它的输出将转到标准输出。 |\n",
"| **m** | 代表第一个参数是用户创建的清单文件的名称。 |\n",
"| **v** | 生成详细的输出用于表述 jar 所作的事情 |\n",
"| **0** | 仅存储文件;不压缩文件(用于创建放在类路径中的 JAR 文件)。 |\n",
"| **M** | 不要自动创建清单文件 |\n",
"\n",
"如果放入 JAR 文件的文件中包含子目录,则会自动添加该子目录,包括其所有子目录等。还会保留路径信息。\n",
"\n",
"以下是一些调用 jar 的典型方法。以下命令创建名为 myJarFile 的 JAR 文件。 jar 包含当前目录中的所有类文件,以及自动生成的清单文件:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"shell"
],
"id": ""
}
},
"outputs": [],
"source": [
"jar cf myJarFile.jar *.class"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"下一个命令与前面的示例类似,但它添加了一个名为 myManifestFile.mf 的用户创建的清单文件。 "
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"shell"
],
"id": ""
}
},
"outputs": [],
"source": [
"jar cmf myJarFile.jar myManifestFile.mf *.class"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"这个命令输出了 myJarFile.jar 中的文件目录:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"shell"
],
"id": ""
}
},
"outputs": [],
"source": [
"jar tf myJarFile.jar"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"如下添加了一个“verbose”的标志用于生成更多关于 myJarFile.jar 中文件的详细信息:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"shell"
],
"id": ""
}
},
"outputs": [],
"source": [
"jar tvf myJarFile.jar"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"假设 audioclasses 和 image 都是子目录,它将所有子目录组合到文件 myApp.jar 中。还包括“verbose”标志以便在 jar 程序工作时提供额外的反馈:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"shell"
],
"id": ""
}
},
"outputs": [],
"source": [
"jar cvf myApp.jar audio classes image"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"如果你在创建 JAR 文件时使用了 0 选项该文件将会被替换在你的类路径CLASSPATH"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"shell"
],
"id": ""
}
},
"outputs": [],
"source": [
"CLASSPATH=\"lib1.jar;lib2.jar;\""
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"然后 Java 可以搜索到 lib1.jar 和 lib2.jar 的类文件。\n",
"\n",
"jar 工具不像 Zip 实用程序那样通用。例如,你无法将文件添加或更新到现有 JAR 文件;只能从头开始创建 JAR 文件。\n",
"\n",
"此外,你无法将文件移动到 JAR 文件中,在移动文件时将其删除。\n",
"\n",
"但是,在一个平台上创建的 JAR 文件可以通过任何其他平台上的 jar 工具透明地读取(这个问题有时会困扰 Zip 实用程序)。\n",
"\n",
"<!-- 分页 -->\n",
"\n",
"<div style=\"page-break-after: always;\"></div>"
]
}
],
"metadata": {},
"nbformat": 4,
"nbformat_minor": 4
}

View File

@@ -1,716 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"[TOC]\n",
"\n",
"<!-- Appendix: I/O Streams -->\n",
"# 附录:流式IO\n",
"\n",
"> Java 7 引入了一种简单明了的方式来读写文件和操作目录。大多情况下,[文件](./17-Files.md)这一章所介绍的那些库和技术就足够你用了。但是,如果你必须面对一些特殊的需求和比较底层的操作,或者处理一些老版本的代码,那么你就必须了解本附录中的内容。\n",
"\n",
"对于编程语言的设计者来说,实现良好的输入/输出I/O系统是一项比较艰难的任务不同实现方案的数量就可以证明这点。其中的挑战似乎在于要涵盖所有的可能性你不仅要覆盖到不同的 I/O 源和 I/O 接收器(如文件、控制台、网络连接等),还要实现多种与它们进行通信的方式(如顺序、随机访问、缓冲、二进制、字符、按行和按字等)。\n",
"\n",
"Java 类库的设计者通过创建大量的类来解决这一难题。一开始,你可能会对 Java I/O 系统提供了如此多的类而感到不知所措。Java 1.0 之后Java 的 I/O 类库发生了明显的改变,在原来面向字节的类中添加了面向字符和基于 Unicode 的类。在 Java 1.4 中,为了改进性能和功能,又添加了 `nio` 类(全称是 “new I/O”Java 1.4 引入,到现在已经很多年了)。这部分在[附录:新 I/O](./Appendix-New-IO.md) 中介绍。\n",
"\n",
"因此,要想充分理解 Java I/O 系统以便正确运用它,我们需要学习一定数量的类。另外,理解 I/O 类库的演化过程也很有必要,因为如果缺乏历史的眼光,很快我们就会对什么时候该使用哪些类,以及什么时候不该使用它们而感到困惑。\n",
"\n",
"编程语言的 I/O 类库经常使用**流**这个抽象概念,它将所有数据源或者数据接收器表示为能够产生或者接收数据片的对象。\n",
"\n",
"> **注意**Java 8 函数式编程中的 `Stream` 类和这里的 I/O stream 没有任何关系。这又是另一个例子,如果再给设计者一次重来的机会,他们将使用不同的术语。\n",
"\n",
"I/O 流屏蔽了实际的 I/O 设备中处理数据的细节:\n",
"\n",
"1. 字节流对应原生的二进制数据;\n",
"2. 字符流对应字符数据,它会自动处理与本地字符集之间的转换;\n",
"3. 缓冲流可以提高性能,通过减少底层 API 的调用次数来优化 I/O。\n",
"\n",
"从 JDK 文档的类层次结构中可以看到Java 类库中的 I/O 类分成了输入和输出两部分。在设计 Java 1.0 时,类库的设计者们就决定让所有与输入有关系的类都继承自 `InputStream`,所有与输出有关系的类都继承自 `OutputStream`。所有从 `InputStream` 或 `Reader` 派生而来的类都含有名为 `read()` 的基本方法,用于读取单个字节或者字节数组。同样,所有从 `OutputStream` 或 `Writer` 派生而来的类都含有名为 `write()` 的基本方法,用于写单个字节或者字节数组。但是,我们通常不会用到这些方法,它们之所以存在是因为别的类可以使用它们,以便提供更有用的接口。\n",
"\n",
"我们很少使用单一的类来创建流对象,而是通过叠合多个对象来提供所期望的功能(这是**装饰器设计模式**)。为了创建一个流,你却要创建多个对象,这也是 Java I/O 类库让人困惑的主要原因。\n",
"\n",
"这里我只会提供这些类的概述,并假定你会使用 JDK 文档来获取它们的详细信息(比如某个类的所以方法的详细列表)。\n",
"\n",
"<!-- Types of InputStream -->\n",
"## 输入流类型\n",
"\n",
"`InputStream` 表示那些从不同数据源产生输入的类,如[表 I/O-1](#table-io-1) 所示,这些数据源包括:\n",
"\n",
"1. 字节数组;\n",
"2. `String` 对象;\n",
"3. 文件;\n",
"4. “管道”,工作方式与实际生活中的管道类似:从一端输入,从另一端输出;\n",
"5. 一个由其它种类的流组成的序列,然后我们可以把它们汇聚成一个流;\n",
"6. 其它数据源,如 Internet 连接。\n",
"\n",
"每种数据源都有相应的 `InputStream` 子类。另外,`FilterInputStream` 也属于一种 `InputStream`,它的作用是为“装饰器”类提供基类。其中,“装饰器”类可以把属性或有用的接口与输入流连接在一起,这个我们稍后再讨论。\n",
"\n",
"<span id=\"table-io-1\">**表 I/O-1 `InputStream` 类型**</span>\n",
"\n",
"| 类 | 功能 | 构造器参数 | 如何使用 |\n",
"| :--: | :-- | :-------- | :----- |\n",
"| `ByteArrayInputStream` | 允许将内存的缓冲区当做 `InputStream` 使用 | 缓冲区,字节将从中取出 | 作为一种数据源:将其与 `FilterInputStream` 对象相连以提供有用接口 |\n",
"| `StringBufferInputStream` | 将 `String` 转换成 `InputStream` | 字符串。底层实现实际使用 `StringBuffer` | 作为一种数据源:将其与 `FilterInputStream` 对象相连以提供有用接口 |\n",
"| `FileInputStream` | 用于从文件中读取信息 | 字符串,表示文件名、文件或 `FileDescriptor` 对象 | 作为一种数据源:将其与 `FilterInputStream` 对象相连以提供有用接口 |\n",
"| `PipedInputStream` | 产生用于写入相关 `PipedOutputStream` 的数据。实现“管道化”概念 | `PipedOutputSteam` | 作为多线程中的数据源:将其与 `FilterInputStream` 对象相连以提供有用接口 |\n",
"| `SequenceInputStream` | 将两个或多个 `InputStream` 对象转换成一个 `InputStream` | 两个 `InputStream` 对象或一个容纳 `InputStream` 对象的容器 `Enumeration` | 作为一种数据源:将其与 `FilterInputStream` 对象相连以提供有用接口 |\n",
"| `FilterInputStream` | 抽象类,作为“装饰器”的接口。其中,“装饰器”为其它的 `InputStream` 类提供有用的功能。见[表 I/O-3](#table-io-3) | 见[表 I/O-3](#table-io-3) | 见[表 I/O-3](#table-io-3) |\n",
"\n",
"<!-- Types of OutputStream -->\n",
"## 输出流类型\n",
"\n",
"如[表 I/O-2](#table-io-2) 所示,该类别的类决定了输出所要去往的目标:字节数组(但不是 `String`,当然,你也可以用字节数组自己创建)、文件或管道。\n",
"\n",
"另外,`FilterOutputStream` 为“装饰器”类提供了一个基类,“装饰器”类把属性或者有用的接口与输出流连接了起来,这些稍后会讨论。\n",
"\n",
"<span id=\"table-io-2\">**表 I/O-2`OutputStream` 类型**</span>\n",
"\n",
"| 类 | 功能 | 构造器参数 | 如何使用 |\n",
"| :--: | :-- | :-------- | :----- |\n",
"| `ByteArrayOutputStream` | 在内存中创建缓冲区。所有送往“流”的数据都要放置在此缓冲区 | 缓冲区初始大小(可选) | 用于指定数据的目的地:将其与 `FilterOutputStream` 对象相连以提供有用接口 |\n",
"| `FileOutputStream` | 用于将信息写入文件 | 字符串,表示文件名、文件或 `FileDescriptor` 对象 | 用于指定数据的目的地:将其与 `FilterOutputStream` 对象相连以提供有用接口 |\n",
"| `PipedOutputStream` | 任何写入其中的信息都会自动作为相关 `PipedInputStream` 的输出。实现“管道化”概念 | `PipedInputStream` | 指定用于多线程的数据的目的地:将其与 `FilterOutputStream` 对象相连以提供有用接口 |\n",
"| `FilterOutputStream` | 抽象类,作为“装饰器”的接口。其中,“装饰器”为其它 `OutputStream` 提供有用功能。见[表 I/O-4](#table-io-4) | 见[表 I/O-4](#table-io-4) | 见[表 I/O-4](#table-io-4) |\n",
"\n",
"<!-- Adding Attributes and Useful Interfaces -->\n",
"\n",
"## 添加属性和有用的接口\n",
"\n",
"装饰器在[泛型](./20-Generics.md)这一章引入。Java I/O 类库需要多种不同功能的组合,这正是使用装饰器模式的原因所在[^1]。而之所以存在 **filter**(过滤器)类,是因为让抽象类 **filter** 作为所有装饰器类的基类。装饰器必须具有和它所装饰对象相同的接口,但它也可以扩展接口,不过这种情况只发生在个别 **filter** 类中。\n",
"\n",
"但是装饰器模式也有一个缺点在编写程序的时候它给我们带来了相当多的灵活性因为我们可以很容易地对属性进行混搭但它同时也增加了代码的复杂性。Java I/O 类库操作不便的原因在于:我们必须创建许多类(“核心” I/O 类型加上所有的装饰器)才能得到我们所希望的单个 I/O 对象。\n",
"\n",
"`FilterInputStream` 和 `FilterOutputStream` 是用来提供装饰器类接口以控制特定输入流 `InputStream` 和 输出流 `OutputStream` 的两个类,但它们的名字并不是很直观。`FilterInputStream` 和 `FilterOutputStream` 分别从 I/O 类库中的基类 `InputStream` 和 `OutputStream` 派生而来,这两个类是创建装饰器的必要条件(这样它们才能为所有被装饰的对象提供统一接口)。\n",
"\n",
"### 通过 `FilterInputStream` 从 `InputStream` 读取\n",
"\n",
"`FilterInputStream` 类能够完成两件截然不同的事情。其中,`DataInputStream` 允许我们读取不同的基本数据类型和 `String` 类型的对象(所有方法都以 “read” 开头,例如 `readByte()`、`readFloat()`等等)。搭配其对应的 `DataOutputStream`,我们就可以通过数据“流”将基本数据类型的数据从一个地方迁移到另一个地方。具体是那些“地方”是由[表 I/O-1](#table-io-1) 中的那些类决定的。\n",
"\n",
"其它 `FilterInputStream` 类则在内部修改 `InputStream` 的行为方式:是否缓冲,是否保留它所读过的行(允许我们查询行数或设置行数),以及是否允许把单个字符推回输入流等等。最后两个类看起来就像是为了创建编译器提供的(它们被添加进来可能是为了对“用 Java 构建编译器”实现提供支持),因此我们在一般编程中不会用到它们。\n",
"\n",
"在实际应用中,不管连接的是什么 I/O 设备,我们基本上都会对输入进行缓冲。所以当初 I/O 类库如果能默认都让输入进行缓冲,同时将无缓冲输入作为一种特殊情况(或者只是简单地提供一个方法调用),这样会更加合理,而不是像现在这样迫使我们基本上每次都得手动添加缓冲。\n",
"<!-- 译者注感觉第四版中文版536页把上面这一段的意思弄反了 -->\n",
"\n",
"<span id=\"table-io-3\">**表 I/O-3`FilterInputStream` 类型**</span>\n",
"\n",
"| 类 | 功能 | 构造器参数 | 如何使用 |\n",
"| :--: | :-- | :-------- | :----- |\n",
"| `DataInputStream` | 与 `DataOutputStream` 搭配使用,按照移植方式从流读取基本数据类型(`int`、`char`、`long` 等) | `InputStream` | 包含用于读取基本数据类型的全部接口 |\n",
"| `BufferedInputStream` | 使用它可以防止每次读取时都得进行实际写操作。代表“使用缓冲区” | `InputStream`,可以指定缓冲区大小(可选) | 本质上不提供接口,只是向进程添加缓冲功能。与接口对象搭配 |\n",
"| `LineNumberInputStream` | 跟踪输入流中的行号,可调用 `getLineNumber()` 和 `setLineNumber(int)` | `InputStream` | 仅增加了行号,因此可能要与接口对象搭配使用 |\n",
"| `PushbackInputStream` | 具有能弹出一个字节的缓冲区,因此可以将读到的最后一个字符回退 | `InputStream` | 通常作为编译器的扫描器,我们可能永远也不会用到 |\n",
"\n",
"### 通过 `FilterOutputStream` 向 `OutputStream` 写入\n",
"\n",
"与 `DataInputStream` 对应的是 `DataOutputStream`,它可以将各种基本数据类型和 `String` 类型的对象格式化输出到“流”中,。这样一来,任何机器上的任何 `DataInputStream` 都可以读出它们。所有方法都以 “write” 开头,例如 `writeByte()`、`writeFloat()` 等等。\n",
"\n",
"`PrintStream` 最初的目的就是为了以可视化格式打印所有基本数据类型和 `String` 类型的对象。这和 `DataOutputStream` 不同,后者的目的是将数据元素置入“流”中,使 `DataInputStream` 能够可移植地重构它们。\n",
"\n",
"`PrintStream` 内有两个重要方法:`print()` 和 `println()`。它们都被重载了,可以打印各种各种数据类型。`print()` 和 `println()` 之间的差异是,后者在操作完毕后会添加一个换行符。\n",
"\n",
"`PrintStream` 可能会造成一些问题,因为它捕获了所有 `IOException`(因此,我们必须使用 `checkError()` 自行测试错误状态,如果出现错误它会返回 `true`)。另外,`PrintStream` 没有处理好国际化问题。这些问题都在 `PrintWriter` 中得到了解决,这在后面会讲到。\n",
"\n",
"`BufferedOutputStream` 是一个修饰符,表明这个“流”使用了缓冲技术,因此每次向流写入的时候,不是每次都会执行物理写操作。我们在进行输出操作的时候可能会经常用到它。\n",
"\n",
"<span id=\"table-io-4\">**表 I/O-4`FilterOutputStream` 类型**</span>\n",
"\n",
"| 类 | 功能 | 构造器参数 | 如何使用 |\n",
"| :--: | :-- | :-------- | :----- |\n",
"| `DataOutputStream` | 与 `DataInputStream` 搭配使用,因此可以按照移植方式向流中写入基本数据类型(`int`、`char`、`long` 等) | `OutputStream` | 包含用于写入基本数据类型的全部接口 |\n",
"| `PrintStream` | 用于产生格式化输出。其中 `DataOutputStream` 处理数据的存储,`PrintStream` 处理显示 | `OutputStream`,可以用 `boolean` 值指示是否每次换行时清空缓冲区(可选) | 应该是对 `OutputStream` 对象的 `final` 封装。可能会经常用到它 |\n",
"| `BufferedOutputStream` | 使用它以避免每次发送数据时都进行实际的写操作。代表“使用缓冲区”。可以调用 `flush()` 清空缓冲区 | `OutputStream`,可以指定缓冲区大小(可选) | 本质上并不提供接口,只是向进程添加缓冲功能。与接口对象搭配 |\n",
"\n",
"<!-- Readers & Writers -->\n",
"\n",
"## Reader和Writer\n",
"\n",
"Java 1.1 对基本的 I/O 流类库做了重大的修改。你初次遇到 `Reader` 和 `Writer` 时,可能会以为这两个类是用来替代 `InputStream` 和 `OutputStream` 的,但实际上并不是这样。尽管一些原始的“流”类库已经过时了(如果使用它们,编译器会发出警告),但是 `InputStream` 和 `OutputStream` 在面向字节 I/O 这方面仍然发挥着极其重要的作用,而 `Reader` 和 `Writer` 则提供兼容 Unicode 和面向字符 I/O 的功能。另外:\n",
"\n",
"1. Java 1.1 往 `InputStream` 和 `OutputStream` 的继承体系中又添加了一些新类,所以这两个类显然是不会被取代的;\n",
"\n",
"2. 有时我们必须把来自“字节”层级结构中的类和来自“字符”层次结构中的类结合起来使用。为了达到这个目的需要用到“适配器adapter类”`InputStreamReader` 可以把 `InputStream` 转换为 `Reader`,而 `OutputStreamWriter` 可以把 `OutputStream` 转换为 `Writer`。\n",
"\n",
"设计 `Reader` 和 `Writer` 继承体系主要是为了国际化。老的 I/O 流继承体系仅支持 8 比特的字节流,并且不能很好地处理 16 比特的 Unicode 字符。由于 Unicode 用于字符国际化Java 本身的 `char` 也是 16 比特的 Unicode所以添加 `Reader` 和 `Writer` 继承体系就是为了让所有的 I/O 操作都支持 Unicode。另外新类库的设计使得它的操作比旧类库要快。\n",
"\n",
"### 数据的来源和去处\n",
"\n",
"几乎所有原始的 Java I/O 流类都有相应的 `Reader` 和 `Writer` 类来提供原生的 Unicode 操作。但是在某些场合,面向字节的 `InputStream` 和 `OutputStream` 才是正确的解决方案。特别是 `java.util.zip` 类库就是面向字节而不是面向字符的。因此,最明智的做法是尽量**尝试**使用 `Reader` 和 `Writer`,一旦代码没法成功编译,你就会发现此时应该使用面向字节的类库了。\n",
"\n",
"下表展示了在两个继承体系中,信息的来源和去处(即数据物理上来自哪里又去向哪里)之间的对应关系:\n",
"\n",
"| 来源与去处Java 1.0 类 | 相应的 Java 1.1 类 |\n",
"| :-------------------: | :--------------: |\n",
"| `InputStream` | `Reader` <br/> 适配器:`InputStreamReader` |\n",
"| `OutputStream` | `Writer` <br/> 适配器:`OutputStreamWriter` |\n",
"| `FileInputStream` | `FileReader` |\n",
"| `FileOutputStream` | `FileWriter` |\n",
"| `StringBufferInputStream`(已弃用) | `StringReader` |\n",
"| (无相应的类) | `StringWriter` |\n",
"| `ByteArrayInputStream` | `CharArrayReader` |\n",
"| `ByteArrayOutputStream` | `CharArrayWriter` |\n",
"| `PipedInputStream` | `PipedReader` |\n",
"| `PipedOutputStream` | `PipedWriter` |\n",
"\n",
"总的来说,这两个不同的继承体系中的接口即便不能说完全相同,但也是非常相似的。\n",
"\n",
"### 更改流的行为\n",
"\n",
"对于 `InputStream` 和 `OutputStream` 来说,我们会使用 `FilterInputStream` 和 `FilterOutputStream` 的装饰器子类来修改“流”以满足特殊需要。`Reader` 和 `Writer` 的类继承体系沿用了相同的思想——但是并不完全相同。\n",
"\n",
"在下表中,左右之间对应关系的近似程度现比上一个表格更加粗略一些。造成这种差别的原因是类的组织形式不同,`BufferedOutputStream` 是 `FilterOutputStream` 的子类,但 `BufferedWriter` 却不是 `FilterWriter` 的子类(尽管 `FilterWriter` 是抽象类,但却没有任何子类,把它放在表格里只是占个位置,不然你可能奇怪 `FilterWriter` 上哪去了)。然而,这些类的接口却又十分相似。\n",
"\n",
"| 过滤器Java 1.0 类 | 相应 Java 1.1 类 |\n",
"| :--------------- | :-------------- |\n",
"| `FilterInputStream` | `FilterReader` |\n",
"| `FilterOutputStream` | `FilterWriter` (抽象类,没有子类) |\n",
"| `BufferedInputStream` | `BufferedReader`(也有 `readLine()`) |\n",
"| `BufferedOutputStream` | `BufferedWriter` |\n",
"| `DataInputStream` | 使用 `DataInputStream` 如果必须用到 `readLine()`,那你就得使用 `BufferedReader`。否则,一般情况下就用 `DataInputStream` |\n",
"| `PrintStream` | `PrintWriter` |\n",
"| `LineNumberInputStream` | `LineNumberReader` |\n",
"| `StreamTokenizer` | `StreamTokenizer`(使用具有 `Reader` 参数的构造器) |\n",
"| `PushbackInputStream` | `PushbackReader` |\n",
"\n",
"有一条限制需要明确:一旦要使用 `readLine()`,我们就不应该用 `DataInputStream`(否则,编译时会得到使用了过时方法的警告),而应该使用 `BufferedReader`。除了这种情况之外的情形中,`DataInputStream` 仍是 I/O 类库的首选成员。\n",
"\n",
"为了使用时更容易过渡到 `PrintWriter`,它提供了一个既能接受 `Writer` 对象又能接受任何 `OutputStream` 对象的构造器。`PrintWriter` 的格式化接口实际上与 `PrintStream` 相同。\n",
"\n",
"Java 5 添加了几种 `PrintWriter` 构造器,以便在将输出写入时简化文件的创建过程,你马上就会见到它们。\n",
"\n",
"其中一种 `PrintWriter` 构造器还有一个执行**自动 flush**[^2] 的选项。如果构造器设置了该选项,就会在每个 `println()` 调用之后,自动执行 flush。\n",
"\n",
"### 未发生改变的类\n",
"\n",
"有一些类在 Java 1.0 和 Java 1.1 之间未做改变。\n",
"\n",
"| 以下这些 Java 1.0 类在 Java 1.1 中没有相应类 |\n",
"| --- |\n",
"| `DataOutputStream` |\n",
"| `File` |\n",
"| `RandomAccessFile` |\n",
"| `SequenceInputStream` |\n",
"\n",
"特别是 `DataOutputStream`,在使用时没有任何变化;因此如果想以可传输的格式存储和检索数据,请用 `InputStream` 和 `OutputStream` 继承体系。\n",
"\n",
"<!-- Off By Itself: RandomAccessFile -->\n",
"## RandomAccessFile类\n",
"\n",
"`RandomAccessFile` 适用于由大小已知的记录组成的文件,所以我们可以使用 `seek()` 将文件指针从一条记录移动到另一条记录,然后对记录进行读取和修改。文件中记录的大小不一定都相同,只要我们能确定那些记录有多大以及它们在文件中的位置即可。\n",
"\n",
"最初,我们可能难以相信 `RandomAccessFile` 不是 `InputStream` 或者 `OutputStream` 继承体系中的一部分。除了实现了 `DataInput` 和 `DataOutput` 接口(`DataInputStream` 和 `DataOutputStream` 也实现了这两个接口)之外,它和这两个继承体系没有任何关系。它甚至都不使用 `InputStream` 和 `OutputStream` 类中已有的任何功能。它是一个完全独立的类,其所有的方法(大多数都是 `native` 方法)都是从头开始编写的。这么做是因为 `RandomAccessFile` 拥有和别的 I/O 类型本质上不同的行为,因为我们可以在一个文件内向前和向后移动。在任何情况下,它都是自我独立的,直接继承自 `Object`。\n",
"\n",
"从本质上来讲,`RandomAccessFile` 的工作方式类似于把 `DataIunputStream` 和 `DataOutputStream` 组合起来使用。另外它还有一些额外的方法,比如使用 `getFilePointer()` 可以得到当前文件指针在文件中的位置,使用 `seek()` 可以移动文件指针,使用 `length()` 可以得到文件的长度。另外,其构造器还需要传入第二个参数(和 C 语言中的 `fopen()` 相同)用来表示我们是准备对文件进行 “随机读”r还是“读写”rw。它并不支持只写文件从这点来看如果当初 `RandomAccessFile` 能设计成继承自 `DataInputStream`,可能也是个不错的实现方式。\n",
"\n",
"在 Java 1.4 中,`RandomAccessFile` 的大多数功能(但不是全部)都被 nio 中的**内存映射文件**mmap取代详见[附录:新 I/O](./Appendix-New-IO.md)。\n",
"\n",
"<!-- Typical Uses of I/O Streams -->\n",
"\n",
"## IO流典型用途\n",
"\n",
"尽管我们可以用不同的方式来组合 I/O 流类,但常用的也就其中几种。你可以下面的例子可以作为 I/O 典型用法的基本参照(在你确定无法使用[文件](./17-Files.md)这一章所述的库之后)。\n",
"\n",
"在这些示例中,异常处理都被简化为将异常传递给控制台,但是这样做只适用于小型的示例和工具。在你自己的代码中,你需要考虑更加复杂的错误处理方式。\n",
"\n",
"### 缓冲输入文件\n",
"\n",
"如果想要打开一个文件进行字符输入,我们可以使用一个 `FileInputReader` 对象,然后传入一个 `String` 或者 `File` 对象作为文件名。为了提高速度,我们希望对那个文件进行缓冲,那么我们可以将所产生的引用传递给一个 `BufferedReader` 构造器。`BufferedReader` 提供了 `line()` 方法,它会产生一个 `Stream<String>` 对象:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"java"
],
"id": ""
}
},
"outputs": [],
"source": [
"// iostreams/BufferedInputFile.java\n",
"// {VisuallyInspectOutput}\n",
"import java.io.*;\n",
"import java.util.stream.*;\n",
"\n",
"public class BufferedInputFile {\n",
" public static String read(String filename) {\n",
" try (BufferedReader in = new BufferedReader(\n",
" new FileReader(filename))) {\n",
" return in.lines()\n",
" .collect(Collectors.joining(\"\\n\"));\n",
" } catch (IOException e) {\n",
" throw new RuntimeException(e);\n",
" }\n",
" }\n",
"\n",
" public static void main(String[] args) {\n",
" System.out.print(\n",
" read(\"BufferedInputFile.java\"));\n",
" }\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"`Collectors.joining()` 在其内部使用了一个 `StringBuilder` 来累加其运行结果。该文件会通过 `try-with-resources` 子句自动关闭。\n",
"\n",
"### 从内存输入\n",
"\n",
"下面示例中,从 `BufferedInputFile.read()` 读入的 `String` 被用来创建一个 `StringReader` 对象。然后调用其 `read()` 方法,每次读取一个字符,并把它显示在控制台上:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"java"
],
"id": ""
}
},
"outputs": [],
"source": [
"// iostreams/MemoryInput.java\n",
"// {VisuallyInspectOutput}\n",
"import java.io.*;\n",
"\n",
"public class MemoryInput {\n",
" public static void\n",
" main(String[] args) throws IOException {\n",
" StringReader in = new StringReader(\n",
" BufferedInputFile.read(\"MemoryInput.java\"));\n",
" int c;\n",
" while ((c = in.read()) != -1)\n",
" System.out.print((char) c);\n",
" }\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"注意 `read()` 是以 `int` 形式返回下一个字节,所以必须类型转换为 `char` 才能正确打印。\n",
"\n",
"### 格式化内存输入\n",
"\n",
"要读取格式化数据,我们可以使用 `DataInputStream`,它是一个面向字节的 I/O 类(不是面向字符的)。这样我们就必须使用 `InputStream` 类而不是 `Reader` 类。我们可以使用 `InputStream` 以字节形式读取任何数据(比如一个文件),但这里使用的是字符串。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"java"
],
"id": ""
}
},
"outputs": [],
"source": [
"// iostreams/FormattedMemoryInput.java\n",
"// {VisuallyInspectOutput}\n",
"import java.io.*;\n",
"\n",
"public class FormattedMemoryInput {\n",
" public static void main(String[] args) {\n",
" try (\n",
" DataInputStream in = new DataInputStream(\n",
" new ByteArrayInputStream(\n",
" BufferedInputFile.read(\n",
" \"FormattedMemoryInput.java\")\n",
" .getBytes()))\n",
" ) {\n",
" while (true)\n",
" System.out.write((char) in.readByte());\n",
" } catch (EOFException e) {\n",
" System.out.println(\"\\nEnd of stream\");\n",
" } catch (IOException e) {\n",
" throw new RuntimeException(e);\n",
" }\n",
" }\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"`ByteArrayInputStream` 必须接收一个字节数组,所以这里我们调用了 `String.getBytes()` 方法。所产生的的 `ByteArrayInputStream` 是一个适合传递给 `DataInputStream` 的 `InputStream`。\n",
"\n",
"如果我们用 `readByte()` 从 `DataInputStream` 一次一个字节地读取字符,那么任何字节的值都是合法结果,因此返回值不能用来检测输入是否结束。取而代之的是,我们可以使用 `available()` 方法得到剩余可用字符的数量。下面例子演示了怎么一次一个字节地读取文件:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"java"
],
"id": ""
}
},
"outputs": [],
"source": [
"// iostreams/TestEOF.java\n",
"// Testing for end of file\n",
"// {VisuallyInspectOutput}\n",
"import java.io.*;\n",
"\n",
"public class TestEOF {\n",
" public static void main(String[] args) {\n",
" try (\n",
" DataInputStream in = new DataInputStream(\n",
" new BufferedInputStream(\n",
" new FileInputStream(\"TestEOF.java\")))\n",
" ) {\n",
" while (in.available() != 0)\n",
" System.out.write(in.readByte());\n",
" } catch (IOException e) {\n",
" throw new RuntimeException(e);\n",
" }\n",
" }\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"注意,`available()` 的工作方式会随着所读取媒介类型的不同而有所差异,它的字面意思就是“在没有阻塞的情况下所能读取的字节数”。对于文件,能够读取的是整个文件;但是对于其它类型的“流”,可能就不是这样,所以要谨慎使用。\n",
"\n",
"我们也可以通过捕获异常来检测输入的末尾。但是,用异常作为控制流是对异常的一种错误使用方式。\n",
"\n",
"### 基本文件的输出\n",
"\n",
"`FileWriter` 对象用于向文件写入数据。实际使用时,我们通常会用 `BufferedWriter` 将其包装起来以增加缓冲的功能(可以试试移除此包装来感受一下它对性能的影响——缓冲往往能显著地增加 I/O 操作的性能)。在本例中,为了提供格式化功能,它又被装饰成了 `PrintWriter`。按照这种方式创建的数据文件可作为普通文本文件来读取。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"java"
],
"id": ""
}
},
"outputs": [],
"source": [
"// iostreams/BasicFileOutput.java\n",
"// {VisuallyInspectOutput}\n",
"import java.io.*;\n",
"\n",
"public class BasicFileOutput {\n",
" static String file = \"BasicFileOutput.dat\";\n",
"\n",
" public static void main(String[] args) {\n",
" try (\n",
" BufferedReader in = new BufferedReader(\n",
" new StringReader(\n",
" BufferedInputFile.read(\n",
" \"BasicFileOutput.java\")));\n",
" PrintWriter out = new PrintWriter(\n",
" new BufferedWriter(new FileWriter(file)))\n",
" ) {\n",
" in.lines().forEach(out::println);\n",
" } catch (IOException e) {\n",
" throw new RuntimeException(e);\n",
" }\n",
" // Show the stored file:\n",
" System.out.println(BufferedInputFile.read(file));\n",
" }\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"`try-with-resources` 语句会自动 flush 并关闭文件。\n",
"\n",
"### 文本文件输出快捷方式\n",
"\n",
"Java 5 在 `PrintWriter` 中添加了一个辅助构造器,有了它,你在创建并写入文件时,就不必每次都手动执行一些装饰的工作。下面的代码使用这种快捷方式重写了 `BasicFileOutput.java`"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"java"
],
"id": ""
}
},
"outputs": [],
"source": [
"// iostreams/FileOutputShortcut.java\n",
"// {VisuallyInspectOutput}\n",
"import java.io.*;\n",
"\n",
"public class FileOutputShortcut {\n",
" static String file = \"FileOutputShortcut.dat\";\n",
"\n",
" public static void main(String[] args) {\n",
" try (\n",
" BufferedReader in = new BufferedReader(\n",
" new StringReader(BufferedInputFile.read(\n",
" \"FileOutputShortcut.java\")));\n",
" // Here's the shortcut:\n",
" PrintWriter out = new PrintWriter(file)\n",
" ) {\n",
" in.lines().forEach(out::println);\n",
" } catch (IOException e) {\n",
" throw new RuntimeException(e);\n",
" }\n",
" System.out.println(BufferedInputFile.read(file));\n",
" }\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"使用这种方式仍具备了缓冲的功能,只是现在不必自己手动添加缓冲了。但遗憾的是,其它常见的写入任务都没有快捷方式,因此典型的 I/O 流依旧涉及大量冗余的代码。本书[文件](./17-Files.md)一章中介绍的另一种方式,对此类任务进行了极大的简化。\n",
"\n",
"### 存储和恢复数据\n",
"\n",
"`PrintWriter` 是用来对可读的数据进行格式化。但如果要输出可供另一个“流”恢复的数据,我们可以用 `DataOutputStream` 写入数据,然后用 `DataInputStream` 恢复数据。当然,这些流可能是任何形式,在下面的示例中使用的是一个文件,并且对读写都进行了缓冲。注意 `DataOutputStream` 和 `DataInputStream` 是面向字节的,因此要使用 `InputStream` 和 `OutputStream` 体系的类。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"java"
],
"id": ""
}
},
"outputs": [],
"source": [
"// iostreams/StoringAndRecoveringData.java\n",
"import java.io.*;\n",
"\n",
"public class StoringAndRecoveringData {\n",
" public static void main(String[] args) {\n",
" try (\n",
" DataOutputStream out = new DataOutputStream(\n",
" new BufferedOutputStream(\n",
" new FileOutputStream(\"Data.txt\")))\n",
" ) {\n",
" out.writeDouble(3.14159);\n",
" out.writeUTF(\"That was pi\");\n",
" out.writeDouble(1.41413);\n",
" out.writeUTF(\"Square root of 2\");\n",
" } catch (IOException e) {\n",
" throw new RuntimeException(e);\n",
" }\n",
" try (\n",
" DataInputStream in = new DataInputStream(\n",
" new BufferedInputStream(\n",
" new FileInputStream(\"Data.txt\")))\n",
" ) {\n",
" System.out.println(in.readDouble());\n",
" // Only readUTF() will recover the\n",
" // Java-UTF String properly:\n",
" System.out.println(in.readUTF());\n",
" System.out.println(in.readDouble());\n",
" System.out.println(in.readUTF());\n",
" } catch (IOException e) {\n",
" throw new RuntimeException(e);\n",
" }\n",
" }\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"输出结果:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"3.14159\n",
"That was pi\n",
"1.41413\n",
"Square root of 2"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"如果我们使用 `DataOutputStream` 进行数据写入,那么 Java 就保证了即便读和写数据的平台多么不同,我们仍可以使用 `DataInputStream` 准确地读取数据。这一点很有价值,众所周知,人们曾把大量精力耗费在数据的平台相关性问题上。但现在,只要两个平台上都有 Java就不会存在这样的问题[^3]。\n",
"\n",
"当我们使用 `DastaOutputStream` 时,写字符串并且让 `DataInputStream` 能够恢复它的唯一可靠方式就是使用 UTF-8 编码,在这个示例中是用 `writeUTF()` 和 `readUTF()` 来实现的。UTF-8 是一种多字节格式,其编码长度根据实际使用的字符集会有所变化。如果我们使用的只是 ASCII 或者几乎都是 ASCII 字符(只占 7 比特),那么就显得及其浪费空间和带宽,所以 UTF-8 将 ASCII 字符编码成一个字节的形式,而非 ASCII 字符则编码成两到三个字节的形式。另外,字符串的长度保存在 UTF-8 字符串的前两个字节中。但是,`writeUTF()` 和 `readUTF()` 使用的是一种适用于 Java 的 UTF-8 变体JDK 文档中有这些方法的详尽描述),因此如果我们用一个非 Java 程序读取用 `writeUTF()` 所写的字符串时,必须编写一些特殊的代码才能正确读取。\n",
"\n",
"有了 `writeUTF()` 和 `readUTF()`,我们就可以在 `DataOutputStream` 中把字符串和其它数据类型混合使用。因为字符串完全可以作为 Unicode 格式存储,并且可以很容易地使用 `DataInputStream` 来恢复它。\n",
"\n",
"`writeDouble()` 将 `double` 类型的数字存储在流中,并用相应的 `readDouble()` 恢复它(对于其它的书类型,也有类似的方法用于读写)。但是为了保证所有的读方法都能够正常工作,我们必须知道流中数据项所在的确切位置,因为极有可能将保存的 `double` 数据作为一个简单的字节序列、`char` 或其它类型读入。因此,我们必须:要么为文件中的数据采用固定的格式;要么将额外的信息保存到文件中,通过解析额外信息来确定数据的存放位置。注意,对象序列化和 XML (二者都在[附录:对象序列化](Appendix-Object-Serialization.md)中介绍)是存储和读取复杂数据结构的更简单的方式。\n",
"\n",
"### 读写随机访问文件\n",
"\n",
"使用 `RandomAccessFile` 就像是使用了一个 `DataInputStream` 和 `DataOutputStream` 的结合体(因为它实现了相同的接口:`DataInput` 和 `DataOutput`)。另外,我们还可以使用 `seek()` 方法移动文件指针并修改对应位置的值。\n",
"\n",
"在使用 `RandomAccessFile` 时,你必须清楚文件的结构,否则没法正确使用它。`RandomAccessFile` 有一套专门的方法来读写基本数据类型的数据和 UTF-8 编码的字符串:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"java"
],
"id": ""
}
},
"outputs": [],
"source": [
"// iostreams/UsingRandomAccessFile.java\n",
"import java.io.*;\n",
"\n",
"public class UsingRandomAccessFile {\n",
" static String file = \"rtest.dat\";\n",
"\n",
" public static void display() {\n",
" try (\n",
" RandomAccessFile rf =\n",
" new RandomAccessFile(file, \"r\")\n",
" ) {\n",
" for (int i = 0; i < 7; i++)\n",
" System.out.println(\n",
" \"Value \" + i + \": \" + rf.readDouble());\n",
" System.out.println(rf.readUTF());\n",
" } catch (IOException e) {\n",
" throw new RuntimeException(e);\n",
" }\n",
" }\n",
"\n",
" public static void main(String[] args) {\n",
" try (\n",
" RandomAccessFile rf =\n",
" new RandomAccessFile(file, \"rw\")\n",
" ) {\n",
" for (int i = 0; i < 7; i++)\n",
" rf.writeDouble(i * 1.414);\n",
" rf.writeUTF(\"The end of the file\");\n",
" rf.close();\n",
" display();\n",
" } catch (IOException e) {\n",
" throw new RuntimeException(e);\n",
" }\n",
" try (\n",
" RandomAccessFile rf =\n",
" new RandomAccessFile(file, \"rw\")\n",
" ) {\n",
" rf.seek(5 * 8);\n",
" rf.writeDouble(47.0001);\n",
" rf.close();\n",
" display();\n",
" } catch (IOException e) {\n",
" throw new RuntimeException(e);\n",
" }\n",
" }\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"输出结果:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"Value 0: 0.0\n",
"Value 1: 1.414\n",
"Value 2: 2.828\n",
"Value 3: 4.242\n",
"Value 4: 5.656\n",
"Value 5: 7.069999999999999\n",
"Value 6: 8.484\n",
"The end of the file\n",
"Value 0: 0.0\n",
"Value 1: 1.414\n",
"Value 2: 2.828\n",
"Value 3: 4.242\n",
"Value 4: 5.656\n",
"Value 5: 47.0001\n",
"Value 6: 8.484\n",
"The end of the file"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"`display()` 方法打开了一个文件,并以 `double` 值的形式显示了其中的七个元素。在 `main()` 中,首先创建了文件,然后打开并修改了它。因为 `double` 总是 8 字节长,所以如果要用 `seek()` 定位到第 5 个(从 0 开始计数) `double` 值,则要传入的地址值应该为 `5*8`。\n",
"\n",
"正如前面所诉,虽然 `RandomAccess` 实现了 `DataInput` 和 `DataOutput` 接口,但实际上它和 I/O 继承体系中的其它部分是分离的。它不支持装饰,故而不能将其与 `InputStream` 及 `OutputStream` 子类中的任何一个组合起来,所以我们也没法给它添加缓冲的功能。\n",
"\n",
"该类的构造器还有第二个必选参数:我们可以指定让 `RandomAccessFile` 以“只读”r方式或“读写”\n",
"rw方式打开文件。\n",
"\n",
"除此之外,还可以使用 `nio` 中的“内存映射文件”代替 `RandomAccessFile`,这在[附录:新 I/O](Appendix-New-IO.md)中有介绍。\n",
"\n",
"<!-- Summary -->\n",
"## 本章小结\n",
"\n",
"Java 的 I/O 流类库的确能够满足我们的基本需求:我们可以通过控制台、文件、内存块,甚至因特网进行读写。通过继承,我们可以创建新类型的输入和输出对象。并且我们甚至可以通过重新定义“流”所接受对象类型的 `toString()` 方法,进行简单的扩展。当我们向一个期望收到字符串的方法传送一个非字符串对象时,会自动调用对象的 `toString()` 方法(这是 Java 中有限的“自动类型转换”功能之一)。\n",
"\n",
"在 I/O 流类库的文档和设计中,仍留有一些没有解决的问题。例如,我们打开一个文件用于输出,如果在我们试图覆盖这个文件时能抛出一个异常,这样会比较好(有的编程系统只有当该文件不存在时,才允许你将其作为输出文件打开)。在 Java 中,我们应该使用一个 `File` 对象来判断文件是否存在,因为如果我们用 `FileOutputStream` 或者 `FileWriter` 打开,那么这个文件肯定会被覆盖。\n",
"\n",
"I/O 流类库让我们喜忧参半。它确实挺有用的而且还具有可移植性。但是如果我们没有理解“装饰器”模式那么这种设计就会显得不是很直观。所以它的学习成本相对较高。而且它并不完善比如说在过去我不得不编写相当数量的代码去实现一个读取文本文件的工具——所幸的是Java 7 中的 nio 消除了此类需求。\n",
"\n",
"一旦你理解了装饰器模式,并且开始在某些需要这种灵活性的场景中使用该类库,那么你就开始能从这种设计中受益了。到那时候,为此额外多写几行代码的开销应该不至于让人觉得太麻烦。但还是请务必检查一下,确保使用[文件](./17-Files.md)一章中的库和技术没法解决问题后,再考虑使用本章的 I/O 流库。\n",
"\n",
"[^1]: 很难说这就是一个很好的设计选择,尤其是与其它编程语言中简单的 I/O 类库相比较。但它确实是如此选择的一个正当理由。\n",
"\n",
"[^2]: 译者注“flush” 直译是“清空”,意思是把缓冲中的数据清空,输送到对应的目的地(如文件和屏幕)。\n",
"\n",
"[^3]: XML 是另一种方式,可以解决在不同计算平台之间移动数据,而不依赖于所有平台上都有 Java 这一问题。XML 将在[附录:对象序列化](./Appendix-Object-Serialization.md)一章中进行介绍。\n",
"\n",
"<!-- 分页 -->\n",
"\n",
"<div style=\"page-break-after: always;\"></div>"
]
}
],
"metadata": {},
"nbformat": 4,
"nbformat_minor": 4
}

View File

@@ -1,400 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"[TOC]\n",
"\n",
"<!-- Appendix: Javadoc -->\n",
"# 附录:文档注释\n",
"\n",
"编写代码文档的最大问题可能是维护该文档。如果文档和代码是分开的那么每次更改代码时更改文档都会变得很繁琐。解决方案似乎很简单将代码链接到文档。最简单的方法是将所有内容放在同一个文件中。然而要完成这完整的画面您需要一个特殊的注释语法来标记文档以及一个工具来将这些注释提取为有用的表单中。这就是Java所做的。\n",
"\n",
"提取注释的工具称为Javadoc它是 JDK 安装的一部分。它使用Java编译器中的一些技术来寻找特殊的注释标记。它不仅提取由这些标记所标记的信息还提取与注释相邻的类名或方法名。通过这种方式您就可以用最少的工作量来生成合适的程序文档。\n",
"\n",
"Javadoc输出为一个html文件您可以使用web浏览器查看它。对于Javadoc您有一个简单的标准来创建文档因此您可以期望所有Java libraries都有文档。\n",
"\n",
"此外您可以编写自己的Javadoc处理程序doclet对于 Javadoc例如以不同的格式生成输出。\n",
"\n",
"以下是对Javadoc基础知识的介绍和概述。在 JDK 文档中可以找到完整的描述。\n",
"\n",
"## 句法规则\n",
"\n",
"所有Javadoc指令都发生在以 **/**** 开头(但仍然以 ***/** 结尾)的注释中。\n",
"\n",
"使用Javadoc有两种主要方法:\n",
"\n",
"嵌入HTML或使用“doc标签”。独立的doc标签是指令它以 **@** 开头,放在注释行的开头。(然而,前面的 ***** 将被忽略。)可能会出现内联doc标签\n",
"\n",
"Javadoc注释中的任何位置也可以以一个 **@** 开头,但是被花括号包围。\n",
"\n",
"有三种类型的注释文档,它们对应于注释前面的元素:类、字段或方法。也就是说,类注释出现在类定义之前,字段注释出现在字段定义之前,方法注释出现在方法定义之前。举个简单的例子:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"java"
],
"id": ""
}
},
"outputs": [],
"source": [
"// javadoc/Documentation1.java \n",
"/** 一个类注释 */\n",
"public class Documentation1 {\n",
" /** 一个属性注释 */\n",
" public int i;\n",
" /** 一个方法注释 */ \n",
" public void f() {}\n",
"}\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Javadoc处理注释文档仅适用于 **公共** 和 **受保护** 的成员。 \n",
"\n",
"默认情况下,将忽略对 **私有成员** 和包访问成员的注释(请参阅[\"隐藏实现\"](/docs/book/07-Implementation-Hiding.md)一章),并且您将看不到任何输出。 \n",
"\n",
"这是有道理的,因为仅客户端程序员的观点是,在文件外部可以使用 **公共成员** 和 **受保护成员** 。 您可以使用 **-private** 标志和包含 **私人** 成员。\n",
"\n",
"要通过Javadoc处理前面的代码命令是"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"cmd"
],
"id": ""
}
},
"outputs": [],
"source": [
"javadoc Documentation1.java"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"这将产生一组HTML文件。 如果您在浏览器中打开index.html您将看到结果与所有其他Java文档具有相同的标准格式因此用户对这种格式很熟悉并可以轻松地浏览你的类。\n",
"\n",
"## 内嵌 HTML\n",
"\n",
"Javadoc传递未修改的HTML代码用以生成的HTML文档。这使你可以充分利用HTML。但是这样做的主要目的是让你格式化代码例如"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"java"
],
"id": ""
}
},
"outputs": [],
"source": [
"// javadoc/Documentation2.java\n",
"/** <pre>\n",
"* System.out.println(new Date());\n",
"* </pre>\n",
"*/\n",
"public class Documentation2 {}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"您你也可以像在其他任何Web文档中一样使用HTML来格式化说明中的文字"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"java"
],
"id": ""
}
},
"outputs": [],
"source": [
"// javadoc/Documentation3.java\n",
"/** You can <em>even</em> insert a list:\n",
"* <ol>\n",
"* <li> Item one\n",
"* <li> Item two\n",
"* <li> Item three\n",
"* </ol>\n",
"*/\n",
"public class Documentation3 {}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"请注意在文档注释中Javadoc删除了行首的星号以及前导空格。 Javadoc重新格式化了所有内容使其符合标准文档的外观。不要将诸如 \\<h1\\>或 \\<hr\\>之类的标题用作嵌入式HTML因为Javadoc会插入自己的标题后插入的标题将对其生成的文档产生干扰。\n",
"\n",
"所有类型的注释文档字段和方法都可以支持嵌入式HTML。\n",
"\n",
"## 示例标签\n",
"\n",
"以下是一些可用于代码文档的Javadoc标记。在尝试使用Javadoc进行任何认真的操作之前请查阅JDK文档中的Javadoc参考以了解使用Javadoc的所有不同方法。\n",
"\n",
"### @see\n",
"\n",
"这个标签可以将其他的类连接到文档中Javadoc 将使用 @see 标记超链接到其他文档中,形式为:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"java"
],
"id": ""
}
},
"outputs": [],
"source": [
"@see classname\n",
"@see fully-qualified-classname\n",
"@see fully-qualified-classname#method-name"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"每个都向生成的文档中添加超链接的“另请参阅”条目。 Javadoc 不会检查超链接的有效性。\n",
"\n",
"### {@link package.class#member label}\n",
"\n",
"和 @see 非常相似,不同之处在于它可以内联使用,并使用标签作为超链接文本,而不是“另请参阅”。\n",
"\n",
"### {@docRoot}\n",
"\n",
"生成文档根目录的相对路径。对于显式超链接到文档树中的页面很有用。\n",
"\n",
"### {@inheritDoc}\n",
"\n",
"将文档从此类的最近基类继承到当前文档注释中。\n",
"\n",
"### @version\n",
"\n",
"其形式为:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"java"
],
"id": ""
}
},
"outputs": [],
"source": [
"@version version-information"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"其中 version-information 是你认为适合包含的任何重要信息。当在Javadoc命令行上放置 -version 标志时特别在生成的HTML文档中用于生成version信息。\n",
"\n",
"### @author\n",
"\n",
"其形式为:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"@author author-information"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"author-information 大概率是你的名字,但是一样可以包含你的 email 地址或者其他合适的信息。当在 Javadoc 命令行上放置 -author 标志的时候在生成的HTML文档中特别注明了作者信息。\n",
"\n",
"你可以对作者列表使用多个作者标签但是必须连续放置它们。所有作者信息都集中在生成的HTML中的单个段落中。\n",
"\n",
"### @since\n",
"\n",
"此标记指示此代码的版本开始使用特定功能。例如它出现在HTML Java文档中以指示功能首次出现的JDK版本。\n",
"\n",
"### @param\n",
"\n",
"这将生成有关方法参数的文档:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"java"
],
"id": ""
}
},
"outputs": [],
"source": [
"@param parameter-name description"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"其中parameter-name是方法参数列表中的标识符description 是可以在后续行中继续的文本。当遇到新的文档标签时,说明被视为完成。@param 标签的可以任意使用,大概每个参数一个。\n",
"\n",
"### @return\n",
"\n",
"这记录了返回值:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"java"
],
"id": ""
}
},
"outputs": [],
"source": [
"@return description"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"其中description给出了返回值的含义。它可延续到后面的行内。\n",
"\n",
"### @throws\n",
"\n",
"一个方法可以产生许多不同类型的异常,所有这些异常都需要描述。异常标记的形式为:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"java"
],
"id": ""
}
},
"outputs": [],
"source": [
"@throws fully-qualified-class-name description"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"fully-qualified-class-name 给出明确的异常分类名称,并且 description (可延续到后面的行内)告诉你为什么这特定类型的异常会在方法调用后出现。\n",
"\n",
"### @deprecated\n",
"\n",
"这表示已被改进的功能取代的功能。deprecated 标记表明你不再使用此特定功能,因为将来有可能将其删除。标记为@不赞成使用的方法会导致编译器在使用时发出警告。在Java 5中@deprecated Javadoc 标记已被 @Deprecated 注解取代(在[注解]()一章中进行了描述)。 \n",
"\n",
"## 文档示例\n",
"\n",
"**objects/HelloDate.java** 是带有文档注释的例子。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"java"
],
"id": ""
}
},
"outputs": [],
"source": [
"// javadoc/HelloDateDoc.java\n",
"import java.util.*;\n",
"/** The first On Java 8 example program.\n",
" * Displays a String and today's date.\n",
" * @author Bruce Eckel\n",
" * @author www.MindviewInc.com\n",
" * @version 5.0\n",
" */\n",
"public class HelloDateDoc {\n",
" /** Entry point to class & application.\n",
" * @param args array of String arguments\n",
" * @throws exceptions No exceptions thrown\n",
" */\n",
" public static void main(String[] args) {\n",
" System.out.println(\"Hello, it's: \");\n",
" System.out.println(new Date());\n",
" }\n",
"}\n",
"/* Output:\n",
"Hello, it's:\n",
"Tue May 09 06:07:27 MDT 2017\n",
"*/"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"你可以在Java标准库的源代码中找到许多Javadoc注释文档的示例。\n",
"\n",
"\n",
"\n",
"<!-- 分页 -->\n",
"\n",
"<div style=\"page-break-after: always;\"></div>"
]
}
],
"metadata": {},
"nbformat": 4,
"nbformat_minor": 4
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,88 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"[TOC]\n",
"\n",
"<!-- Appendix: Passing and Returning Objects -->\n",
"# 附录:对象传递和返回\n",
"\n",
"> 到现在为止,你已经对“传递”对象实际上是传递引用这一想法想法感到满意。\n",
"\n",
"在许多编程语言中,你可以使用该语言的“常规”方式来传递对象,并且大多数情况下一切正常。 但是通常会出现这种情况,你必须做一些不平常的事情,突然事情变得更加复杂。 Java也不例外当您传递对象并对其进行操作时准确了解正在发生的事情很重要。 本附录提供了这种见解。\n",
"\n",
"提出本附录问题的另一种方法是如果你之前使用类似C++的编程语言,则是“ Java是否有指针” Java中的每个对象标识符除原语外都是这些指针之一但它们的用法是不仅受编译器的约束而且受运行时系统的约束。 换一种说法Java有指针但没有指针算法。 这些就是我一直所说的“引用”,您可以将它们视为“安全指针”,与小学的安全剪刀不同-它们不敏锐,因此您不费吹灰之力就无法伤害自己,但是它们有时可能很乏味。\n",
"\n",
"<!-- Passing References -->\n",
"\n",
"## 传递引用\n",
"\n",
"<!-- Making Local Copies -->\n",
"\n",
"当你将引用传递给方法时,它仍指向同一对象。 一个简单的实验演示了这一点:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"java"
],
"id": ""
}
},
"outputs": [],
"source": [
"// references/PassReferences.java\n",
"public class PassReferences {\n",
"public static void f(PassReferences h) {\n",
" \tSystem.out.println(\"h inside f(): \" + h);\n",
" }\n",
" public static void main(String[] args) {\n",
" PassReferences p = new PassReferences();\n",
" System.out.println(\"p inside main(): \" + p);\n",
" f(p);\n",
" }\n",
"}\n",
"/* Output:\n",
"p inside main(): PassReferences@15db9742\n",
"h inside f(): PassReferences@15db9742\n",
"*/"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"方法 `toString() ` 在打印语句中自动调用,并且 `PassReferences` 直接从 `Object` 继承而无需重新定义 `toString` 。 因此,使用的是 `Object` 的 `toString` 版本,它打印出对象的类,然后打印出该对象所在的地址(不是引用,而是实际的对象存储)。\n",
"\n",
"## 本地拷贝\n",
"\n",
"\n",
"<!-- Controlling Cloneability -->\n",
"## 控制克隆\n",
"\n",
"\n",
"<!-- Immutable Classes -->\n",
"## 不可变类\n",
"\n",
"\n",
"<!-- Summary -->\n",
"## 本章小结\n",
"\n",
"\n",
"\n",
"<!-- 分页 -->\n",
"\n",
"<div style=\"page-break-after: always;\"></div>"
]
}
],
"metadata": {},
"nbformat": 4,
"nbformat_minor": 4
}

View File

@@ -1,187 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"[TOC]\n",
"\n",
"<!-- Appendix: Programming Guidelines -->\n",
"# 附录:编程指南\n",
"\n",
"> 本附录包含了有助于指导你进行低级程序设计和编写代码的建议。\n",
"\n",
"当然,这些只是指导方针,而不是规则。我们的想法是将它们用作灵感,并记住偶尔会违反这些指导方针的特殊情况。\n",
"\n",
"<!-- Design -->\n",
"## 设计\n",
"\n",
"1. **优雅总是会有回报**。从短期来看,似乎需要更长的时间才能找到一个真正优雅的问题解决方案,但是当该解决方案第一次应用并能轻松适应新情况,而不需要数小时,数天或数月的挣扎时,你会看到奖励(即使没有人可以测量它们)。它不仅为你提供了一个更容易构建和调试的程序,而且它也更容易理解和维护,这也正是经济价值所在。这一点可以通过一些经验来理解,因为当你想要使一段代码变得优雅时,你可能看起来效率不是很高。抵制急于求成的冲动,它只会减慢你的速度。\n",
"\n",
"2. **先让它工作,然后再让它变快**。即使你确定一段代码非常重要并且它是你系统中的主要瓶颈**,也是如此。不要这样做。使用尽可能简单的设计使系统首先运行。然后如果速度不够快,请对其进行分析。你几乎总会发现“你的”瓶颈不是问题。节省时间,才是真正重要的东西。\n",
"\n",
"3. **记住“分而治之”的原则**。如果所面临的问题太过混乱**就去想象一下程序的基本操作因为存在一个处理困难部分的神奇“片段”piece。该“片段”是一个对象编写使用该对象的代码然后查看该对象并将其困难部分封装到其他对象中等等。\n",
"\n",
"4. **将类创建者与类用户(客户端程序员)分开**。类用户是“客户”,不需要也不想知道类幕后发生了什么。类创建者必须是设计类的专家,他们编写类,以便新手程序员都可以使用它,并仍然可以在应用程序中稳健地工作。将该类视为其他类的*服务提供者*service provider。只有对其它类透明才能很容易地使用这个类。\n",
"\n",
"5. **创建类时,给类起个清晰的名字,就算不需要注释也能理解这个类**。你的目标应该是使客户端程序员的接口在概念上变得简单。为此,在适当时使用方法重载来创建直观,易用的接口。\n",
"\n",
"6. **你的分析和设计必须至少能够产生系统中的类、它们的公共接口以及它们与其他类的关系,尤其是基类**。 如果你的设计方法产生的不止于此,就该问问自己,该方法生成的所有部分是否在程序的生命周期内都具有价值。如果不是,那么维护它们会很耗费精力。对于那些不会影响他们生产力的东西,开发团队的成员往往不会去维护,这是许多设计方法都没有考虑的生活现实。\n",
"\n",
"7. **让一切自动化**。首先在编写类之前编写测试代码并将其与类保持一致。通过构建工具自动运行测试。你可能会使用事实上的标准Java构建工具Gradle。这样通过运行测试代码可以自动验证任何更改将能够立即发现错误。因为你知道自己拥有测试框架的安全网所以当发现需要时可以更大胆地进行彻底的更改。请记住语言的巨大改进来自内置的测试包括类型检查异常处理等但这些内置功能很有限你必须完成剩下的工作针对具体的类或程序去完善这些测试内容从而创建一个强大的系统。\n",
"\n",
"8. **在编写类之前,先编写测试代码,以验证类的设计是完善的**。如果不编写测试代码,那么就不知道类是什么样的。此外,通过编写测试代码,往往能够激发出类中所需的其他功能或约束。而这些功能或约束并不总是出现在分析和设计过程中。测试还会提供示例代码,显示了如何使用这个类。\n",
"\n",
"9. **所有的软件设计问题都可以通过引入一个额外的间接概念层次extra level of conceptual indirection来解决**。这个软件工程的基本规则[^1]是抽象的基础,是面向对象编程的主要特征。在面向对象编程中,我们也可以这样说:“如果你的代码太复杂,就要生成更多的对象。”\n",
"\n",
"10. **间接indirection应具有意义与准则9一致**。这个含义可以简单到“将常用代码放在单个方法中。”如果添加没有意义的间接(抽象,封装等)级别,那么它就像没有足够的间接性那样糟糕。\n",
"\n",
"11. **使类尽可能原子化**。 为每个类提供一个明确的目的,它为其他类提供一致的服务。如果你的类或系统设计变得过于复杂,请将复杂类分解为更简单的类。最直观的指标是尺寸大小,如果一个类很大,那么它可能是做的事太多了,应该被拆分。建议重新设计类的线索是:\n",
" - 一个复杂的*switch*语句:考虑使用多态。\n",
" - 大量方法涵盖了很多不同类型的操作:考虑使用多个类。\n",
" - 大量成员变量涉及很多不同的特征:考虑使用多个类。\n",
" - 其他建议可以参见Martin Fowler的*Refactoring: Improving the Design of Existing Code*重构改善既有代码的设计Addison-Wesley 1999。\n",
"\n",
"12. **注意长参数列表**。那样方法调用会变得难以编写,读取和维护。相反,尝试将方法移动到更合适的类,并且(或者)将对象作为参数传递。\n",
"\n",
"13. **不要重复自己**。如果一段代码出现在派生类的许多方法中,则将该代码放入基类中的单个方法中,并从派生类方法中调用它。这样不仅可以节省代码空间,而且可以轻松地传播更改。有时,发现这个通用代码会为接口添加有价值的功能。此指南的更简单版本也可以在没有继承的情况下发生:如果类具有重复代码的方法,则将该重复代码放入一个公共方,法并在其他方法中调用它。\n",
"\n",
"14. **注意*switch*语句或链式*if-else*子句**。一个*类型检查编码*type-check coding的指示器意味着需要根据某种类型信息选择要执行的代码确切的类型最初可能不明显。很多时候可以用继承和多态替换这种代码多态方法调用将会执行类型检查并提供了更可靠和更容易的可扩展性。 \n",
"\n",
"15. **从设计的角度,寻找和分离那些因不变的事物而改变的事物**。也就是说,在不强制重新设计的情况下搜索可能想要更改的系统中的元素,然后将这些元素封装在类中。\n",
"\n",
"16. **不要通过子类扩展基本功能**。如果一个接口元素对于类来说是必不可少的,则它应该在基类中,而不是在派生期间添加。如果要在继承期间添加方法,请考虑重新设计。\n",
"\n",
"17. **少即是多**。从一个类的最小接口开始,尽可能小而简单,以解决手头的问题,但不要试图预测类的所有使用方式。在使用该类时,就将会了解如何扩展接口。但是,一旦这个类已经在使用了,就无法在不破坏客户端代码的情况下缩小接口。如果必须添加更多方法,那很好,它不会破坏代码。但即使新方法取代旧方法的功能,也只能是保留现有接口(如果需要,可以结合底层实现中的功能)。如果必须通过添加更多参数来扩展现有方法的接口,请使用新参数创建重载方法,这样,就不会影响到对现有方法的任何调用。\n",
"\n",
"18. **大声读出你的类以确保它们合乎逻辑**。将基类和派生类之间的关系称为“is-a”将成员对象称为“has-a”。\n",
"\n",
"19. **在需要在继承和组合之间作决定时,问一下自己是否必须向上转换为基类型**。如果不是则使用组合成员对象更好。这可以消除对多种基类型的感知需求perceived need。如果使用继承则用户会认为他们应该向上转型。\n",
"\n",
"20. **注意重载**。方法不应该基于参数的值而有条件地执行代码。在这里,应该创建两个或多个重载方法。\n",
"\n",
"21. **使用异常层次结构**最好是从标准Java异常层次结构中的特定适当类派生。然后捕获异常的人可以为特定类型的异常编写处理程序然后为基类型编写处理程序。如果添加新的派生异常现有客户端代码仍将通过基类型捕获异常。\n",
"\n",
"22. **有时简单的聚合可以完成工作**。航空公司的“乘客舒适系统”由独立的元素组成:座位,空调,影视等,但必须在飞机上创建许多这样的元素。你创建私有成员并建立一个全新的接口了吗?如果不是,在这种情况下,组件也应该是公共接口的一部分,因此应该创建公共成员对象。这些对象有自己的私有实现,这些实现仍然是安全的。请注意,简单聚合不是经常使用的解决方案,但确实会有时候会用到。\n",
"\n",
"23. **考虑客户程序员和维护代码的人的观点**。设计类以便尽可能直观地被使用。预测要进行的更改,并精心设计类,以便轻松地进行更改。\n",
"\n",
"24. **注意“巨型对象综合症”**giant object syndrome。这通常是程序员的痛苦他们是面向对象编程的新手总是编写面向过程程序并将其粘贴在一个或两个巨型对象中。除应用程序框架外对象代表应用程序中的概念而不是应用程序本身。\n",
"\n",
"25. **如果你必须做一些丑陋的事情,至少要把类内的丑陋本地化**。\n",
"\n",
"26. **如果必须做一些不可移植的事情,那就对这个事情做一个抽象,并在一个类中进行本地化**。这种额外的间接级别可防止在整个程序中扩散这种不可移植性。 (这个原则也体现在*桥接*模式中,等等)。\n",
"\n",
"27. **对象不应该仅仅只是持有一些数据**。它们也应该有明确的行为。有时候“数据传输对象”data transfer objects是合适的但只有在泛型集合不合适时才被明确用于打包和传输一组元素。\n",
"\n",
"28. **在从现有类创建新类时首先选择组合**。仅在设计需要时才使用继承。如果在可以使用组合的地方使用继承,那么设计将会变得很复杂,这是没必要的。\n",
"\n",
"29. **使用继承和覆盖方法来表达行为的差异,而不是使用字段来表示状态的变化**。如果发现一个类使用了状态变量,并且有一些方法是基于这些变量切换行为的,那么请重新设计它,以表示子类和覆盖方法中的行为差异。一个极端的反例是继承不同的类来表示颜色,而不是使用“颜色”字段。\n",
"\n",
"30. **注意*协变*variance**。两个语义不同的对象可能具有相同的操作或职责。为了从继承中受益,会试图让其中一个成为另一个的子类,这是一种很自然的诱惑。这称为协变,但没有真正的理由去强制声明一个并不存在的父子类关系。更好的解决方案是创建一个通用基类,并为两者生成一个接口,使其成为这个通用基类的派生类。这仍然可以从继承中受益,并且这可能是关于设计的一个重要发现。\n",
"\n",
"31. **在继承期间注意*限定*limitation**。最明确的设计为继承的类增加了新的功能。含糊的设计在继承期间删除旧功能而不添加新功能。但是规则是用来打破的,如果是通过调用一个旧的类库来工作,那么将一个现有类限制在其子类型中,可能比重构层次结构更有效,因此新类适合在旧类的上层。\n",
"\n",
"32. **使用设计模式来消除“裸功能”naked functionality**。也就是说如果类只需要创建一个对象请不要推进应用程序并写下注释“只生成一个。”应该将其包装成一个单例singleton。如果主程序中有很多乱七八糟的代码去创建对象那么找一个像工厂方法一样的创建模式可以在其中封装创建过程。消除“裸功能”不仅会使代码更易于理解和维护而且还会使其能够更加防范应对后面的善意维护者well-intentioned maintainers。\n",
"\n",
"33. **注意“分析瘫痪”analysis paralysis**。记住不得不经常在不了解整个项目的情况下推进项目并且通常了解那些未知因素的最好、最快的方式是进入下一步而不是尝试在脑海中弄清楚。在获得解决方案之前往往无法知道解决方案。Java有内置的防火墙让它们为你工作。你在一个类或一组类中的错误不会破坏整个系统的完整性。\n",
"\n",
"34. **如果认为自己有很好的分析,设计或实施,请做一个演练**。从团队外部带来一些人,不一定是顾问,但可以是公司内其他团体的人。用一双新眼睛评审你的工作,可以在一个更容易修复它们的阶段发现问题,而不仅仅是把大量时间和金钱全扔到演练过程中。\n",
"\n",
"<!-- Implementation -->\n",
"## 实现\n",
"\n",
"36. **遵循编码惯例**。有很多不同的约定,例如,[谷歌使用的约定](https://google.github.io/styleguide/javaguide.html)本书中的代码尽可能地遵循这些约定。如果坚持使用其他语言的编码风格那么读者就会很难去阅读。无论决定采用何种编码约定都要确保它们在整个项目中保持一致。集成开发环境通常包含内置的重新格式化reformatter和检查器checker。\n",
"\n",
"37. **无论使用何种编码风格,如果你的团队(甚至更好是公司)对其进行标准化,它就确实会产生重大影响**。这意味着,如果不符合这个标准,那么每个人都认为修复别人的编码风格是公平的游戏。标准化的价值在于解析代码可以花费较少的脑力,因此可以更专注于代码的含义。\n",
"\n",
"38. **遵循标准的大写规则**。类名的第一个字母大写。字段,方法和对象(引用)的第一个字母应为小写。所有标识符应该将各个单词组合在一起,并将所有中间单词的首字母大写。例如:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"- **ThisIsAClassName**\n",
"- **thisIsAMethodOrFieldName**\n",
"\n",
"将 **static final** 类型的标识符的所有字母全部大写,并用下划线分隔各个单词,这些标识符在其定义中具有常量初始值。这表明它们是编译时常量。\n",
"- **包是一个特例**它们都是小写的字母即使是中间词。域扩展comorgnetedu等也应该是小写的。这是Java 1.1和Java 2之间的变化。"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"39. **不要创建自己的“装饰”私有字段名称**。这通常以前置下划线和字符的形式出现。匈牙利命名法(译者注:一种命名规范,基本原则是:变量名=属性+类型+对象描述。Win32程序风格采用这种命名法如`WORD wParam1;LONG lParam2;HANDLE hInstance`)是最糟糕的例子,你可以在其中附加额外字符用于指示数据类型,用途,位置等,就好像你正在编写汇编语言一样,编译器根本没有提供额外的帮助。这些符号令人困惑,难以阅读,并且难以执行和维护。让类和包来指定名称范围。如果认为必须装饰名称以防止混淆,那么代码就可能过于混乱,这应该被简化。\n",
"\n",
"40. 在创建一般用途的类时,**遵循“规范形式”**。包括**equals()****hashCode()****toString()****clone()**的定义(实现**Cloneable**,或选择其他一些对象复制方法,如序列化),并实现**Comparable**和**Serializable**。\n",
"\n",
"41. **对读取和更改私有字段的方法使用“get”“set”和“is”命名约定**。这种做法不仅使类易于使用,而且也是命名这些方法的标准方法,因此读者更容易理解。\n",
"\n",
"42. **对于所创建的每个类请包含该类的JUnit测试**(请参阅*junit.org*以及[第十六章:代码校验]()中的示例)。无需删除测试代码即可在项目中使用该类,如果进行更改,则可以轻松地重新运行测试。测试代码也能成为如何使用这个类的示例。\n",
"\n",
"43. **有时需要继承才能访问基类的protected成员**。这可能导致对多种基类型的感知需求perceived need。如果不需要向上转型则可以首先派生一个新类来执行受保护的访问。然后把该新类作为使用它的任何类中的成员对象以此来代替直接继承。\n",
"\n",
"44. **为了提高效率,避免使用*final*方法**。只有在分析后发现方法调用是瓶颈时,才将**final**用于此目的。\n",
"\n",
"45. **如果两个类以某种功能方式相互关联(例如集合和迭代器),则尝试使一个类成为另一个类的内部类**。这不仅强调了类之间的关联而且通过将类嵌套在另一个类中可以允许在单个包中重用类名。Java集合库通过在每个集合类中定义内部**Iterator**类来实现此目的,从而为集合提供通用接口。使用内部类的另一个原因是作为**私有**实现的一部分。这里,内部类将有利于实现隐藏,而不是上面提到的类关联和防止命名空间污染。\n",
"\n",
"46. **只要你注意到类似乎彼此之间具有高耦合,请考虑如果使用内部类可能获得的编码和维护改进**。内部类的使用不会解耦类,而是明确耦合关系,并且更方便。\n",
"\n",
"47. **不要成为过早优化的牺牲品**。过早优化是很疯狂的行为。特别是不要担心编写或避免本机方法native methods将某些方法设置为**final**,或者在首次构建系统时调整代码以使其高效。你的主要目标应该是验证设计。即使设计需要一定的效率,也*先让它工作,然后再让它变快*。\n",
"\n",
"48. **保持作用域尽可能小,以便能见度和对象的寿命尽可能小**。这减少了在错误的上下文中使用对象并隐藏了难以发现的bug的机会。例如假设有一个集合和一段迭代它的代码。如果复制该代码以用于一个新集合那么可能会意外地将旧集合的大小用作新集合的迭代上限。但是如果旧集合比较大则会在编译时捕获错误。\n",
"\n",
"49. **使用标准Java库中的集合**。熟练使用它们,将会大大提高工作效率。首选**ArrayList**用于序列,**HashSet**用于集合,**HashMap**用于关联数组,**LinkedList**用于堆栈(而不是**Stack**,尽管也可以创建一个适配器来提供堆栈接口)和队列(也可以使用适配器,如本书所示)。当使用前三个时,将其分别向上转型为**List****Set**和**Map**,那么就可以根据需要轻松更改为其他实现。\n",
"\n",
"50. **为使整个程序健壮,每个组件必须健壮**。在所创建的每个类中使用Java所提供的所有工具如访问控制异常类型检查同步等。这样就可以在构建系统时安全地进入下一级抽象。\n",
"\n",
"51. **编译时错误优于运行时错误**。尝试尽可能在错误发生点处理错误。在最近的处理程序中尽其所能地捕获它能处理的所有异常。在当前层面处理所能处理的所有异常,如果解决不了,就重新抛出异常。 \n",
"\n",
"52. **注意长方法定义**。方法应该是简短的功能单元,用于描述和实现类接口的离散部分。维护一个冗长而复杂的方法是很困难的,而且代价很大,并且这个方法可能是试图做了太多事情。如果看到这样的方法,这表明,至少应该将它分解为多种方法。也可能建议去创建一个新类。小的方法也可以促进类重用。(有时方法必须很大,但它们应该只做一件事。)\n",
"\n",
"53. **保持“尽可能私有”**。一旦公开了你的类库中的一个方面(一个方法,一个类,一个字段),你就永远无法把它拿回来。如果这样做,就将破坏某些人的现有代码,迫使他们重写和重新设计。如果你只公开了必须公开的内容,就可以轻易地改变其他一切,而不会对其他人造成影响,而且由于设计趋于发展,这是一个重要的自由。通过这种方式,更改具体实现将对派生类造成的影响最小。在处理多线程时,私有尤其重要,只有**私有**字段可以防止不同步使用。具有包访问权限的类应该仍然具有**私有**字段,但通常有必要提供包访问权限的方法而不是将它们**公开**。\n",
"\n",
"54. **大量使用注释,并使用*Javadoc commentdocumentation*语法生成程序文档**。但是注释应该为代码增加真正的意义如果注释只是重申了代码已经清楚表达的内容这是令人讨厌的。请注意Java类和方法名称的典型详细信息减少了对某些注释的需求。\n",
"\n",
"55. **避免使用“魔法数字”**。这些是指硬编码到代码中的数字。如果后续必须要更改它们那将是一场噩梦因为你永远不知道“100”是指“数组大小”还是“完全不同的东西”。相反创建一个带有描述性名称的常量并在整个程序中使用常量标识符。这使程序更易于理解更易于维护。\n",
"\n",
"56. **在创建构造方法时,请考虑异常**。最好的情况是,构造方法不会做任何抛出异常的事情。次一级的最佳方案是,该类仅由健壮的类组成或继承自健壮的类,因此如果抛出异常则不需要处理。否则,必须清除**finally**子句中的组合类。如果构造方法必然失败,则适当的操作是抛出异常,因此调用者不会认为该对象是正确创建的而盲目地继续下去。\n",
"\n",
"57. **在构造方法内部,只需要将对象设置为正确的状态**。主动避免调用其他方法(**final**方法除外),因为这些方法可以被其他人覆盖,从而在构造期间产生意外结果。(有关详细信息,请参阅[第六章:初始化和清理]()章节。)较小,较简单的构造方法不太可能抛出异常或导致问题。\n",
"\n",
"58. **如果类在客户端程序员用完对象时需要进行任何清理,请将清理代码放在一个明确定义的方法中**,并使用像 **dispose()** 这样的名称来清楚地表明其目的。另外,在类中放置一个 **boolean** 标志来指示是否调用了 **dispose()** ,因此 **finalize()** 可以检查“终止条件”(参见[第六章:初始化和清理]()章节)。\n",
"\n",
"59. ***finalize()* 的职责只能是验证对象的“终止条件”以进行调试**。(参见[第六章:初始化和清理]()一章)在特殊情况下,可能需要释放垃圾收集器无法释放的内存。因为可能无法为对象调用垃圾收集器,所以无法使用 **finalize()** 执行必要的清理。为此,必须创建自己的 **dispose()** 方法。在类的 **finalize()** 方法中,检查以确保对象已被清理,如果没有被清理,则抛出一个派生自**RuntimeException**的异常,以指示编程错误。在依赖这样的计划之前,请确保 **finalize()** 适用于你的系统。(可能需要调用 **System.gc()** 来确保此行为。)\n",
"\n",
"60. **如果必须在特定范围内清理对象(除了通过垃圾收集器),请使用以下准则:** 初始化对象,如果成功,立即进入一个带有 **finally** 子句的 **try** 块,并在 **finally**中执行清理操作。\n",
"\n",
"61. **在继承期间覆盖 *finalize()* 时,记得调用 *super.finalize()***。(如果是直接继承自 **Object** 则不需要这样做。)调用 **super.finalize()** 作为重写的 **finalize()** 的最终行为而不是在第一行调用它,这样可以确保基类组件在需要时仍然有效。\n",
"\n",
"62. **创建固定大小的对象集合时,将它们转换为数组,** 尤其是在从方法中返回此集合时。这样就可以获得数组编译时类型检查的好处,并且数组的接收者可能不需要在数组中强制转换对象来使用它们。请注意,集合库的基类 **java.util.Collection** 有两个 **toArray()** 方法来完成此任务。\n",
"\n",
"63. **优先选择 *接口* 而不是 *抽象类***。如果知道某些东西应该是基类,那么第一选择应该是使其成为一个接口,并且只有在需要方法定义或成员变量时才将其更改为抽象类。一个接口关心客户端想要做什么,而一个类倾向于关注(或允许)实现细节。\n",
"\n",
"64. **为了避免非常令人沮丧的经历,请确保类路径中的每个名称只对应一个不在包中的类**。否则,编译器可以首先找到具有相同名称的其他类,并报告没有意义的错误消息。如果你怀疑自己有类路径问题,请尝试在类路径的每个起始点查找具有相同名称的 **.class** 文件。理想情况下,应该将所有类放在包中。\n",
"\n",
"65. **注意意外重载**。如果尝试覆盖基类方法但是拼写错误,则最终会添加新方法而不是覆盖现有方法。但是,这是完全合法的,因此在编译时或运行时不会获得任何错误消息,但代码将无法正常工作。始终使用 **@Override** 注释来防止这种情况。\n",
"\n",
"66. **注意过早优化**。先让它工作,然后再让它变快。除非发现代码的特定部分存在性能瓶颈。除非是使用分析器发现瓶颈,否则过早优化会浪费时间。性能调整所隐藏的额外成本是代码将变得难以理解和维护。\n",
"\n",
"67. **请注意,相比于编写代码,代码被阅读的机会更多**。清晰的设计可能产生易于理解的程序但注释详细解释测试和示例是非常宝贵的它们可以帮助你和你的所有后继者。如果不出意外试图从JDK文档中找出有用信息的挫败感应该可以说服你。\n",
"\n",
"[^1]: Andrew Koenig向我解释了它。\n",
"\n",
"<!-- 分页 -->\n",
"\n",
"<div style=\"page-break-after: always;\"></div>"
]
}
],
"metadata": {},
"nbformat": 4,
"nbformat_minor": 4
}

View File

@@ -1,335 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"[TOC]\n",
"\n",
"<!-- Appendix: Standard I/O -->\n",
"# 附录:标准IO\n",
"\n",
">*标准 I/O*这个术语参考Unix中的概念指程序所使用的单一信息流这种思想在大多数操作系统中也有相似形式的实现。\n",
"\n",
"程序的所有输入都可以来自于*标准输入*,其所有输出都可以流向*标准输出*,并且其所有错误信息均可以发送到*标准错误*。*标准 I/O* 的意义在于程序之间可以很容易地连接起来,一个程序的标准输出可以作为另一个程序的标准输入。这是一个非常强大的工具。\n",
"\n",
"## 从标准输入中读取\n",
"\n",
"遵循标准 I/O 模型Java 提供了标准输入流 `System.in`、标准输出流 `System.out` 和标准错误流 `System.err`。在本书中,你已经了解到如何使用 `System.out`将数据写到标准输出。 `System.out` 已经预先包装[^1]成了 `PrintStream` 对象。标准错误流 `System.err` 也预先包装为 `PrintStream` 对象,但是标准输入流 `System.in` 是原生的没有经过包装的 `InputStream`。这意味着尽管可以直接使用标准输出流 `System.in` 和标准错误流 `System.err`,但是在读取 `System.in` 之前必须先对其进行包装。\n",
"\n",
"我们通常一次一行地读取输入。为了实现这个功能,将 `System.in` 包装成 `BufferedReader` 来使用,这要求我们用 `InputStreamReader` 把 `System.in` 转换[^2]成 `Reader` 。下面这个例子将键入的每一行显示出来:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"java"
],
"id": ""
}
},
"outputs": [],
"source": [
"// standardio/Echo.java\n",
"// How to read from standard input\n",
"import java.io.*;\n",
"import onjava.TimedAbort;\n",
"\n",
"public class Echo {\n",
" public static void main(String[] args) {\n",
" TimedAbort abort = new TimedAbort(2);\n",
" new BufferedReader(\n",
" new InputStreamReader(System.in))\n",
" .lines()\n",
" .peek(ln -> abort.restart())\n",
" .forEach(System.out::println);\n",
" // Ctrl-Z or two seconds inactivity\n",
" // terminates the program\n",
" }\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"`BufferedReader` 提供了 `lines()` 方法,返回类型是 `Stream<String>` 。这显示出流模型的的灵活性:仅使用标准输入就能很好地工作。 `peek()` 方法重启 `TimeAbort`,只要保证至少每隔两秒有输入就能够使程序保持开启状态。\n",
"\n",
"## 将`System.out` 转换成 `PrintWriter`\n",
"\n",
"`System.out` 是一个 `PrintStream`,而 `PrintStream` 是一个`OutputStream`。 `PrintWriter` 有一个把 `OutputStream` 作为参数的构造器。因此,如果你需要的话,可以使用这个构造器把 `System.out` 转换成 `PrintWriter` 。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"java"
],
"id": ""
}
},
"outputs": [],
"source": [
"// standardio/ChangeSystemOut.java\n",
"// Turn System.out into a PrintWriter\n",
"\n",
"import java.io.*;\n",
"\n",
"public class ChangeSystemOut {\n",
" public static void main(String[] args) {\n",
" PrintWriter out =\n",
" new PrintWriter(System.out, true);\n",
" out.println(\"Hello, world\");\n",
" }\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"输出结果:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"Hello, world"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"要使用 `PrintWriter` 带有两个参数的构造器,并设置第二个参数为 `true`,从而使能自动刷新到输出缓冲区的功能;否则,可能无法看到打印输出。\n",
"\n",
"## 重定向标准 I/O\n",
"\n",
"Java的 `System` 类提供了简单的 `static` 方法调用,从而能够重定向标准输入流、标准输出流和标准错误流:\n",
"- setInInputStream\n",
"- setOutPrintStream\n",
"- setErr(PrintStream)\n",
"\n",
"如果我们突然需要在显示器上创建大量的输出,而这些输出滚动的速度太快以至于无法阅读时,重定向输出就显得格外有用,可把输出内容重定向到文件中供后续查看。对于我们想重复测试特定的用户输入序列的命令行程序来说,重定向输入就很有价值。下例简单演示了这些方法的使用:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"java"
],
"id": ""
}
},
"outputs": [],
"source": [
"// standardio/Redirecting.java\n",
"// Demonstrates standard I/O redirection\n",
"import java.io.*;\n",
"\n",
"public class Redirecting {\n",
" public static void main(String[] args) {\n",
" PrintStream console = System.out;\n",
" try (\n",
" BufferedInputStream in = new BufferedInputStream(\n",
" new FileInputStream(\"Redirecting.java\"));\n",
" PrintStream out = new PrintStream(\n",
" new BufferedOutputStream(\n",
" new FileOutputStream(\"Redirecting.txt\")))\n",
" ) {\n",
" System.setIn(in);\n",
" System.setOut(out);\n",
" System.setErr(out);\n",
" new BufferedReader(\n",
" new InputStreamReader(System.in))\n",
" .lines()\n",
" .forEach(System.out::println);\n",
" } catch (IOException e) {\n",
" throw new RuntimeException(e);\n",
" } finally {\n",
" System.setOut(console);\n",
" }\n",
" }\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"该程序将文件中内容载入到标准输入,并把标准输出和标准错误重定向到另一个文件。它在程序的开始保存了最初对 `System.out` 对象的引用,并且在程序结束时将系统输出恢复到了该对象上。\n",
"\n",
"I/O重定向操作的是字节流而不是字符流因此使用 `InputStream` 和 `OutputStream`,而不是 `Reader` 和 `Writer`。\n",
"\n",
"<!-- Process Control -->\n",
"## 执行控制\n",
"\n",
"你经常需要在Java内部直接执行操作系统的程序并控制这些程序的输入输出Java类库提供了执行这些操作的类。\n",
"\n",
"一项常见的任务是运行程序并将输出结果发送到控制台。本节包含了一个可以简化此任务的实用工具。\n",
"\n",
"在使用这个工具时可能会产生两种类型的错误:导致异常的普通错误——对于这些错误我们只需要重新抛出一个 `RuntimeException` 即可,以及进程自身的执行过程中导致的错误——我们需要用单独的异常来报告这些错误:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"java"
],
"id": ""
}
},
"outputs": [],
"source": [
"// onjava/OSExecuteException.java\n",
"package onjava;\n",
"\n",
"public class OSExecuteException extends RuntimeException {\n",
" public OSExecuteException(String why) {\n",
" super(why);\n",
" }\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"为了运行程序,我们需要传递给 `OSExecute.command()` 一个 `String command`,我们可以在控制台键入同样的指令运行程序。该命令传递给 `java.lang.ProcessBuilder` 的构造器(需要将其作为 `String` 对象的序列),然后启动生成的 `ProcessBuilder` 对象。"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"java"
],
"id": ""
}
},
"outputs": [],
"source": [
"// onjava/OSExecute.java\n",
"// Run an operating system command\n",
"// and send the output to the console\n",
"package onjava;\n",
"import java.io.*;\n",
"\n",
"public class OSExecute {\n",
" public static void command(String command) {\n",
" boolean err = false;\n",
" try {\n",
" Process process = new ProcessBuilder(\n",
" command.split(\" \")).start();\n",
" try (\n",
" BufferedReader results = new BufferedReader(\n",
" new InputStreamReader(\n",
" process.getInputStream()));\n",
" BufferedReader errors = new BufferedReader(\n",
" new InputStreamReader(\n",
" process.getErrorStream()))\n",
" ) {\n",
" results.lines()\n",
" .forEach(System.out::println);\n",
" err = errors.lines()\n",
" .peek(System.err::println)\n",
" .count() > 0;\n",
" }\n",
" } catch (IOException e) {\n",
" throw new RuntimeException(e);\n",
" }\n",
" if (err)\n",
" throw new OSExecuteException(\n",
" \"Errors executing \" + command);\n",
" }\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"为了捕获在程序执行时产生的标准输出流,我们可以调用 `getInputStream()`。这是因为 `InputStream` 是我们可以从中读取信息的流。\n",
"\n",
"这里这些行只是被打印了出来,但是你也可以从 `command()` 捕获和返回它们。\n",
"\n",
"该程序的错误被发送到了标准错误流,可以调用 `getErrorStream()` 捕获。如果存在任何错误,它们都会被打印并且抛出 `OSExcuteException` ,以便调用程序处理这个问题。\n",
"\n",
"下面是展示如何使用 `OSExecute` 的示例:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"attributes": {
"classes": [
"java"
],
"id": ""
}
},
"outputs": [],
"source": [
"// standardio/OSExecuteDemo.java\n",
"// Demonstrates standard I/O redirection\n",
"// {javap -cp build/classes/main OSExecuteDemo}\n",
"import onjava.*;\n",
"\n",
"public class OSExecuteDemo {}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"这里使用 `javap` 反编译器随JDK发布来反编译程序编译结果"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"Compiled from \"OSExecuteDemo.java\"\n",
"public class OSExecuteDemo {\n",
" public OSExecuteDemo();\n",
"}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"[^1]: 译者注:这里用到了**装饰器模式**。\n",
"\n",
"[^2]: 译者注:这里用到了**适配器模式**。\n",
"\n",
"<!-- 分页 -->\n",
"\n",
"<div style=\"page-break-after: always;\"></div>"
]
}
],
"metadata": {},
"nbformat": 4,
"nbformat_minor": 4
}

View File

@@ -1,39 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"[TOC]\n",
"\n",
"<!-- Appendix: Supplements -->\n",
"# 附录:补充\n",
"\n",
"> 本书有许多补充内容包括MindView网站提供的项目和服务。\n",
"\n",
"本附录介绍了这些补充内容,你可以自行决定它们是否对你有所帮助。\n",
"\n",
"<!-- Downloadable Supplements -->\n",
"## 可下载的补充\n",
"\n",
"可以从 [https://github.com/BruceEckel/OnJava8-examples](https://github.com/BruceEckel/OnJava8-examples) 免费下载本书的代码。这里包括Gradle构建文件和其它一些必要的支持文件以便成功构建和执行本书中所有的示例代码。\n",
"\n",
"<!-- Thinking in C: Foundations for Java -->\n",
"## 通过Thinking-in-C来巩固Java基础\n",
"\n",
"在 [www.OnJava8.com](www.OnJava8.com) 上,可以免费下载*Thinking in C*的演示文稿。 此演示文稿由Chuck Allison创建由MindView有限责任公司开发。这是一个电子演示文稿介绍了Java语法所基于的C语法运算符和函数。\n",
"\n",
"<!-- Hands-On Java eSeminar -->\n",
"## Hand-On Java 电子演示文稿\n",
"\n",
"*Hand-On Java 电子演示文稿*Hands-On Java eSeminar是基于*Thinking in Java*第2版。对应于该书中的每一章它附带有一个音频讲解和相应的幻灯片。我创建了这个电子演示文稿并讲述了这些材料。这个资料是HTML5格式的所以它应该可以在大多数现代浏览器上运行。该演示文稿将在[www.OnJava8.com](www.OnJava8.com)上发售,你可以在该网站上找到该产品的试用版演示。\n",
"\n",
"<!-- 分页 -->\n",
"<div style=\"page-break-after: always;\"></div>"
]
}
],
"metadata": {},
"nbformat": 4,
"nbformat_minor": 4
}

View File

@@ -1,44 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"[TOC]\n",
"\n",
"<!-- Appendix: The Positive Legacy of C++ and Java -->\n",
"# 附录:C++和Java的优良传统\n",
"\n",
"> 在各种讨论声中有一些人认为C++是一种设计糟糕的语言。 我认为理解C++和Java语言的选择有助于了解更大的视角。\n",
"\n",
"也就是说我几乎不再使用C++了。当我使用它的时候要么是用来检查遗留代码要么是编写性能关键performance-critical部分程序通常尽可能小以便用其他语言编写的其他程序来调用。\n",
"\n",
"因为我在最初的8年里一直在C++标准委员会工作所以我见证了那些被做出的决定。它们都经过了极其谨慎的考虑远远超过了许多在Java中做出的决定。\n",
"\n",
"然而正如人们正确地指出的那样由此产生的语言使用起来既复杂又痛苦而且只要我一段时间不使用它我就会忘记那些古怪的规则。在我写书的时候我是从第一原理first principles处了解这些规则的而不是记住了它们。\n",
"\n",
"为了理解C++语言为何既令人不愉快且复杂同时又是精心设计的必须要牢记C++中所有内容的主要设计决策与C. Bjarne Stroustrup该语言的创造者即“C++之父”的兼容性决定。这样的设计似乎是为了可以让大量的C程序员透明地转移到对象代指C++允许他们在C++下编译他们的C代码。这是一个巨大的限制一直是C++最大的优势......而且也是它的祸根。这就是使得C++成功的原因,也是使它复杂的原因。\n",
"\n",
"它也欺骗了那些不太了解C++的Java设计师。例如他们认为运算符重载对于程序员来说很难正确使用。这在C++中基本上是正确的因为C++既有栈分配又有堆分配你必须重载运算符来处理所有情况而且不要造成内存泄漏。这确实很困难。然而Java有单一的内存分配机制和一个垃圾收集器这使得运算符重载变得微不足道正如C中那样但在早于Java的Python中已经可以看到。但多年来来自Java团队的一贯态度是“运算符重载太复杂了”。这里还有许多决策所做的事明显不应该是他们做的。正是由于这些原因让我有了蔑视Gosling即“Java之父”和Java团队决策的名声。Java 7和8由于某种原因包含了更好的决策。但是向后兼容性这个约束总是会阻碍真正的改进。语言永远不会是它本来的样子。\n",
"\n",
"还有很多其他的例子。“为了提高效率必须包含基本类型”坚持“万物皆对象”是正确的当对性能有要求的时候提供一个陷阱门trap door来做低级别的活动lower-level activities这里也可以使用hotspot技术透明地提高性能正如他们最终做的那样不能直接使用浮点处理器去计算超越函数它用软件来完成。我已经尽可能多地提出了这样的问题但我得到的却一直是类似“这是Java方式”这样的回复。\n",
"\n",
"当我提出关于泛型的设计有多糟糕的时候我得到了相同的回复以及“我们必须向后兼容那样以前用Java做出的决策”即使它们是糟糕的决策。最近越来越多的人已经获得了足够的泛型经验可以发现泛型真的很难用。事实上C++模板更强大、更一致现在更容易使用因为编译器的错误消息是可以容忍的。人们一直在认真对待物化reification这可能是有用的东西但是在那种被严格约束所削弱的设计中并没有多大影响。\n",
"\n",
"这样的例子还有很多很多。这是否意味着Java失败了绝对不。Java将程序员的主流带入了垃圾收集、虚拟机和一致的错误处理模型的世界。由于它的所有缺陷它将我们提升到了一个水平现在我们已经准备好使用更高级别的语言了。\n",
"\n",
"有一点C++是领先的语言人们认为它总是如此。许多人对Java有同样的看法但由于JVMJava使得取代自己变得更加容易。现在有可能会有人创建一种新语言并使其在短时间内像Java一样高效运行。以前为新语言开发一个正确有效的编译器需要花费大部分开发时间。\n",
"\n",
"这种情况已经发生了包括像Scala这样的高级静态语言以及动态语言新的且可移植的如GroovyClojureJRuby和Jython。这是未来并且过渡很顺畅因为可以很轻易地将这些新语言与现有Java代码结合使用并且必要时可以重写那些在Java中的瓶颈。\n",
"\n",
"在撰写本文时Java是世界上的头号编程语言。然而Java最终将会减弱就像C++一样沦只在特殊情况下使用或者只是用来支持传统的代码因为它不能像C++那样和硬件连接。但是无意中的好处也是Java真正意外的光彩之处在于它为自己的替代品创造了一条非常畅通的道路即使Java本身已经达到了无法再发展的程度。未来所有的语言都应该从中学习要么创建一个可以重构的文化像Python和Ruby做的那样要么就让竞争者茁壮成长。\n",
"\n",
"<!-- 分页 -->\n",
"<div style=\"page-break-after: always;\"></div>"
]
}
],
"metadata": {},
"nbformat": 4,
"nbformat_minor": 4
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,23 +0,0 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 词汇表\n",
"\n",
"| 词汇 | 解释 |\n",
"| ----------------| ----------|\n",
"| **OOP** (*Object-oriented programming*) | 面向对象编程,一种编程思维模式和编程架构|\n",
"| **UML** (*Unified Modeling Language*) | 统一建模语言,类图 |\n",
"| **Aggregation** | 聚合,关联关系的一种,是强的关联关系|\n",
"| **Composition** | 组合,关联关系的一种,是比聚合关系强的关系 |\n",
"| **STL***the Standard Template Library*| C++ 标准模板库|\n",
"| **Fibonacci Sequence**| [斐波那契数列](https://zh.wikipedia.org/wiki/斐波那契数列),又称黄金分割数列 |"
]
}
],
"metadata": {},
"nbformat": 4,
"nbformat_minor": 4
}