From 0ed90e8709d7173750093f6ac1bf1c5a56c0f9b0 Mon Sep 17 00:00:00 2001 From: LingCoder Date: Wed, 17 Apr 2019 22:57:11 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A0=A1=E8=AE=A2=E7=AC=AC1=E7=AB=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/book/01-What-is-an-Object.md | 32 +++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/book/01-What-is-an-Object.md b/docs/book/01-What-is-an-Object.md index a3f1dcd..9ba3f7b 100644 --- a/docs/book/01-What-is-an-Object.md +++ b/docs/book/01-What-is-an-Object.md @@ -40,7 +40,7 @@ Smalltalk 作为第一种成功的面向对象程序设计语言和 Java 的基 Simula 是一个很好的例子。正如这个名字所暗示的,它的作用是“模拟”(Simulate)类似“银行出纳员”这样的经典问题。在这个例子里,我们有一系列出纳员、客户、帐号以及交易等。每类成员(元素)都具有一些通用的特征:每个帐号都有一定的余额;每名出纳都能接收客户的存款;等等。与此同时,每个成员都有自己的状态;每个帐号都有不同的余额;每名出纳都有一个名字。所以在计算机程序中,能用独一无二的实体分别表示出纳员、客户、帐号以及交易。这个实体便是“对象”,而且每个对象都隶属一个特定的“类”,那个类具有自己的通用特征与行为。 -因此,在面向对象的程序设计中,尽管我们真正要做的是新建各种各样的数据“类型”(Type),但几乎所有面向对象的程序设计语言都采用了 `class` 关键字。当您看到“type”这个字的时候,请同时想到 `class`;反之亦然。 +因此,在面向对象的程序设计中,尽管我们真正要做的是新建各种各样的数据“类型”(Type),但几乎所有面向对象的程序设计语言都采用了 `class` 关键字。当你看到“type”这个字的时候,请同时想到 `class`;反之亦然。 创建好一个类后,可根据情况生成许多对象。随后,可将那些对象作为要解决问题中存在的元素进行处理。事实上,当我们进行面向对象的程序设计时,面临的最大一项挑战性就是:如何在“问题空间”(问题实际存在的地方)的元素与“方案空间”(对实际问题进行建模的地方,如计算机)的元素之间建立理想的“一对一”的映射关系。 @@ -55,18 +55,18 @@ Light lt = new Light(); lt.on(); ``` -在这个例子中,类型/类的名称是 **Light**,可向 **Light** 对象发出的请求包括包括打开(on)、关闭(off)、变得更明亮(brighten)或者变得更暗淡(dim)。通过简单地声明一个名字(lt),我们为 **Light** 对象创建了一个“句柄”。然后用new关键字新建类型为 **Light** 的一个对象。再用等号将其赋给句柄。 +在这个例子中,类型/类的名称是 **Light**,可向 **Light** 对象发出的请求包括包括打开 `on`、关闭 `off`、变得更明亮 `brighten` 或者变得更暗淡 `dim`。通过声明一个引用,如 `lt` 和 `new` 关键字,我们为 **Light** 类对象创建了一个对象,再用等号将其赋给引用。 -为了向对象发送一条信息,我们列出句柄名(lt),再用一个句点符号(`.`)把它同信息名称(on)连接起来。从中可以看出,使用一些预先定义好的类时,我们在程序里采用的代码是非常简单和直观的。 +为了向对象发送信息,我们使用句点符号 `.` 将 `lt` 和信息名称 `on` 连接起来。可以看出,使用一些预先定义好的类时,我们在程序里采用的代码是非常简单和直观的。 -上图遵循 **UML**(Unified Modeling Language,统一建模语言)的格式。每个类由一个框表示,框的顶部有类型名称,框中间部分要描述的任何数据成员,以及方法(属于此对象的方法,它们接收任何发送到该对象的信息)在框的底部。通常,只有类的名称和公共方法在 **UML** 设计图中显示,因此中间部分未显示,如本例所示。如果您只对类名感兴趣,则也不需要显示方法信息。 +上图遵循 **UML**(Unified Modeling Language,统一建模语言)的格式。每个类由一个框表示,框的顶部有类型名称,框中间部分要描述的任何数据成员,以及方法(属于此对象的方法,它们接收任何发送到该对象的信息)在框的底部。通常,只有类的名称和公共方法在 **UML** 设计图中显示,因此中间部分未显示,如本例所示。如果你只对类名感兴趣,则也不需要显示方法信息。 ## 服务提供 在开发或理解程序设计时,我们可以将对象看成是“服务提供者”。你的程序本身将为用户提供服务,并且它能通过调用其他对象提供的服务来实现这一点。我们的最终目标是开发或调用工具库中已有的一些对象,提供理想的服务来解决问题。 -那么问题来了:我们该选择哪个对像来解决问题呢? 例如,你正在开发一个记事本程序。你可能会想在屏幕输入一个默认的记事本对象,一个用于检测不同类型打印机并执行打印的对象。这些对象中的某些已经有了。那对于还没有的对象,我们该设计成啥样呢?这些对象需要提供哪些服务,以及还需要调用其他哪些对象? +那么问题来了:我们该选择哪个对像来解决问题呢?例如,你正在开发一个记事本程序。你可能会想在屏幕输入一个默认的记事本对象,一个用于检测不同类型打印机并执行打印的对象。这些对象中的某些已经有了。那对于还没有的对象,我们该设计成啥样呢?这些对象需要提供哪些服务,以及还需要调用其他哪些对象? 我们可以将这些问题一一分解,抽象成一组服务。软件设计的基本原则是高内聚:每个组件的内部作用明确,功能紧密相关。然而经常有人将太多功能塞进一个对象中。例如:在支票打印模块中,你需要设计一个可以同时读取文本格式又能正确识别不同打印机型号的对象。正确的做法是提供三个或更多对象:一个对象检查所有排版布局的目录;一个或一组可以识别不同打印机型号的对象展示通用的打印界面;第三个对象组合上述两个服务来完成任务。这样,每个对象都提供了一组紧密的服务。在良好的面向对象设计中,每个对象功能单一且高效。这样的程序设计可以提高我们代码的复用性,同时也方便别人阅读和理解我们的代码。只有让人知道你提供什么服务,别人才能更好地将其应用到其他模块或程序中。 @@ -122,19 +122,19 @@ Java 有三个显式关键字来设置类中的访问权限:`public`(公开 ![Inheritance-example](../images/1545763399825.png) -这个图中的箭头从派生类指向基类。正如您将看到的,通常有多个派生类。类型不仅仅描述一组对象的约束,它还涉及其他类型。两种类型可以具有共同的特征和行为,但是一种类型可能包含比另一种类型更多的特征,并且还可以处理更多的信息(或者以不同的方式处理它们)。继承通过基本类型和派生类型的概念来表达这种相似性。基类型包含派生自它的类型之间共享的所有特征和行为。创建基本类型以表示思想的核心。从基类型中,可以派生出其他类型来表示实现该核心的不同方式。 +这个图中的箭头从派生类指向基类。正如你将看到的,通常有多个派生类。类型不仅仅描述一组对象的约束,它还涉及其他类型。两种类型可以具有共同的特征和行为,但是一种类型可能包含比另一种类型更多的特征,并且还可以处理更多的信息(或者以不同的方式处理它们)。继承通过基本类型和派生类型的概念来表达这种相似性。基类型包含派生自它的类型之间共享的所有特征和行为。创建基本类型以表示思想的核心。从基类型中,可以派生出其他类型来表示实现该核心的不同方式。 ![1545764724202](../images/1545764724202.png) -例如,垃圾回收机对垃圾进行分类。基本类型是“垃圾”。每块垃圾都有重量、值等,并且可以被切碎、熔化或分解。由此,可以衍生出更具体的垃圾类型,这些垃圾具有附加特征(瓶子有颜色,钢罐有磁性)或行为(可以压碎铝罐)。此外,一些行为可以不同(纸张的价值取决于它的类型和条件)。使用继承,您将构建一个类型层次结构,该层次结构根据类型表达您试图解决的问题。第二个例子是常见的“形状”例子,可能用于计算机辅助设计系统或游戏模拟。基本类型是“形状”,每个形状都有大小、颜色、位置等等。每个形状可以绘制、擦除、移动、着色等。由此,可以导出(继承)特定类型的形状——圆形、正方形、三角形等等——每个形状可以具有附加的特征和行为。 +例如,垃圾回收机对垃圾进行分类。基本类型是“垃圾”。每块垃圾都有重量、值等,并且可以被切碎、熔化或分解。由此,可以衍生出更具体的垃圾类型,这些垃圾具有附加特征(瓶子有颜色,钢罐有磁性)或行为(可以压碎铝罐)。此外,一些行为可以不同(纸张的价值取决于它的类型和条件)。使用继承,你将构建一个类型层次结构,该层次结构根据类型表达你试图解决的问题。第二个例子是常见的“形状”例子,可能用于计算机辅助设计系统或游戏模拟。基本类型是“形状”,每个形状都有大小、颜色、位置等等。每个形状可以绘制、擦除、移动、着色等。由此,可以导出(继承)特定类型的形状——圆形、正方形、三角形等等——每个形状可以具有附加的特征和行为。 ![1545764780795](../images/1545764780795.png) -例如,某些形状可以翻转。有些行为可能不同,比如计算形状的面积时。类型层次结构体现了形状之间的相似性和差异。以与问题相同的术语转换解决方案是有用的,因为您不需要中间模型来从问题的描述获得解决方案的描述。对于对象,类型层次结构是模型的一个重要方面,因此您可以直接从真实世界中的系统描述转到代码中的系统描述。的确,有时候,那些被训练去寻找复杂解决方案的人在面向对象设计的简单性方面有困难。从现有类型继承创建新类型。这种新类型不仅包含现有类型的所有成员(尽管私有成员被隐藏起来并且不可访问),更重要的是它复制了基类的接口。也就是说,基类对象接受的所有信息也被派生类对象接受。根据类接受的信息,我们知道类的类型,因此派生类与基类是相同的类型。 +例如,某些形状可以翻转。有些行为可能不同,比如计算形状的面积时。类型层次结构体现了形状之间的相似性和差异。以与问题相同的术语转换解决方案是有用的,因为你不需要中间模型来从问题的描述获得解决方案的描述。对于对象,类型层次结构是模型的一个重要方面,因此你可以直接从真实世界中的系统描述转到代码中的系统描述。的确,有时候,那些被训练去寻找复杂解决方案的人在面向对象设计的简单性方面有困难。从现有类型继承创建新类型。这种新类型不仅包含现有类型的所有成员(尽管私有成员被隐藏起来并且不可访问),更重要的是它复制了基类的接口。也就是说,基类对象接受的所有信息也被派生类对象接受。根据类接受的信息,我们知道类的类型,因此派生类与基类是相同的类型。 ![1545764820176](../images/1545764820176.png) -在前面的例子中,“圆是形状”。这种通过继承的类型等价是理解面向对象编程含义的基本网关之一。因为基类和派生类都具有相同的基本接口,所以必须有一些实现来支持该接口。也就是说,当对象接收到特定信息时,必须有可执行代码。如果继承一个类并且不做其他任何事情,则来自基类接口的方法直接进入派生类。这意味着派生类的对象不仅具有相同的类型,而且具有相同的行为,这并不特别有趣。有两种方法可以区分新派生类与原始基类。第一种方法很简单:向派生类添加全新的方法。这些新方法不是基类接口的一部分。这意味着基类没有按照您想要的那样多,所以您添加了更多的方法。继承的这种简单而原始的用途有时是解决问题的完美解决方案。然而,事先还是要仔细调查自己的基础类是否真的需要这些额外的方法。 +在前面的例子中,“圆是形状”。这种通过继承的类型等价是理解面向对象编程含义的基本网关之一。因为基类和派生类都具有相同的基本接口,所以必须有一些实现来支持该接口。也就是说,当对象接收到特定信息时,必须有可执行代码。如果继承一个类并且不做其他任何事情,则来自基类接口的方法直接进入派生类。这意味着派生类的对象不仅具有相同的类型,而且具有相同的行为,这并不特别有趣。有两种方法可以区分新派生类与原始基类。第一种方法很简单:向派生类添加全新的方法。这些新方法不是基类接口的一部分。这意味着基类没有按照你想要的那样多,所以你添加了更多的方法。继承的这种简单而原始的用途有时是解决问题的完美解决方案。然而,事先还是要仔细调查自己的基础类是否真的需要这些额外的方法。 ## 多态 @@ -149,11 +149,11 @@ Java 有三个显式关键字来设置类中的访问权限:`public`(公开 ![Bird-example](../images/1545839316314.png) -答案是继承的主要转折点:在传统意义上,编译器不能进行函数调用。由非 OOP 编译器生成的函数调用生成所谓的早期绑定,这个术语您可能从未听说过,因为您从未以其他方式考虑过。这意味着编译器生成对特定函数名的调用,该调用解析为要执行的代码的绝对地址。 +答案是继承的主要转折点:在传统意义上,编译器不能进行函数调用。由非 OOP 编译器生成的函数调用生成所谓的早期绑定,这个术语你可能从未听说过,因为你从未以其他方式考虑过。这意味着编译器生成对特定函数名的调用,该调用解析为要执行的代码的绝对地址。 通过继承,程序直到运行时才能确定代码的地址,因此当信息被发送到对象时,还需要其他一些方案。为了解决这个问题,面向对象语言使用后期绑定的概念。当向对象发送信息时,调用的代码直到运行时才确定。编译器确保方法存在,并对参数和返回值执行类型检查,但是它不知道要执行的确切代码。 -为了执行后期绑定,Java 使用一个特殊的代码位来代替绝对调用。这段代码使用对象中存储的信息来计算方法主体的地址(此过程在多态性章节中有详细介绍)。因此,每个对象的行为根据特定代码位的内容而不同。当您向对象发送信息时,该对象实际上确定如何处理该信息。在某些语言中,必须显式地授予方法后期绑定属性的灵活性。例如,C++ 使用虚拟关键字。在这些语言中,默认情况下方法没有动态绑定。在 Java 中,动态绑定是默认行为,不需要额外的关键字来生成多态性。 +为了执行后期绑定,Java 使用一个特殊的代码位来代替绝对调用。这段代码使用对象中存储的信息来计算方法主体的地址(此过程在多态性章节中有详细介绍)。因此,每个对象的行为根据特定代码位的内容而不同。当你向对象发送信息时,该对象实际上确定如何处理该信息。在某些语言中,必须显式地授予方法后期绑定属性的灵活性。例如,C++ 使用虚拟关键字。在这些语言中,默认情况下方法没有动态绑定。在 Java 中,动态绑定是默认行为,不需要额外的关键字来生成多态性。 为了演示多态性,我们编写了一段代码,它忽略了类型的特定细节,只与基类对话。该代码与特定于类型的信息分离,因此更易于编写和更容易理解。而且,如果通过继承添加了一个新类型(例如,一个六边形),那么代码对于新类型的 Shape 就像对现有类型一样有效。因此,该程序是可扩展的。 @@ -200,13 +200,13 @@ void doSomething(Shape shape) { ``` -我们可以看到程序并未这样表达:“如果你是一个 Circle ,就这样做;如果你是一个 Square,就那样做;等等”。若那样编写代码,就需检查 Shape 所有可能的类型,如圆、矩形等等。这显然是非常麻烦的,而且每次添加了一种新的 Shape 类型后,都要相应地进行修改。在这里,我们只需说:“你是一种几何形状,我知道你能将自己删掉,即 erase();请自己采取具体行动,并控制所有的细节吧。” +我们可以看到程序并未这样表达:“如果你是一个 Circle ,就这样做;如果你是一个 Square,就那样做;等等”。若那样编写代码,就需检查 Shape 所有可能的类型,如圆、矩形等等。这显然是非常麻烦的,而且每次添加了一种新的 Shape 类型后,都要相应地进行修改。在这里,我们只需说:“你是一种几何形状,我知道你能将自己删掉,即 `erase()`;请自己采取具体行动,并控制所有的细节吧。” -尽管我们没作出任何特殊指示,程序的操作也是完全正确和恰当的。我们知道,为 Circle 调用draw()时执行的代码与为一个 Square 或 Line 调用 draw() 时执行的代码是不同的。但在将 draw() 信息发给一个匿名 Shape 时,根据 Shape 句柄当时连接的实际类型,会相应地采取正确的操作。这非常神奇,因为当 Java 编译器为 doSomething() 编译代码时,它并不知道自己要操作的准确类型是什么。 +尽管我们没作出任何特殊指示,程序的操作也是完全正确和恰当的。我们知道,为 Circle 调用`draw()` 时执行的代码与为一个 Square 或 Line 调用 `draw()` 时执行的代码是不同的。但在将 `draw()` 信息发给一个匿名 Shape 时,根据 Shape 句柄当时连接的实际类型,会相应地采取正确的操作。这非常神奇,因为当 Java 编译器为 `doSomething()` 编译代码时,它并不知道自己要操作的准确类型是什么。 -尽管我们确实可以保证最终会为 Shape 调用 erase()、 draw(),但并不能确定特定的 Circle,Square 或者 Line 调用什么。最后,程序执行的操作却依然是正确的,这是怎么做到的呢? +尽管我们确实可以保证最终会为 Shape 调用 `erase()`、 `draw()`,但并不能确定特定的 Circle,Square 或者 Line 调用什么。最后,程序执行的操作却依然是正确的,这是怎么做到的呢? -将信息发给对象时,如果程序不知道接受的具体类型是什么,但最终执行是正确的,这就是对象的“多态性”(**Polymorphism**)。面向对象的程序设计语言是通过“动态绑定”的方式来实现对象的多态性的。编译器和运行期系统会负责对所有细节的控制;我们只需知道要做什么,以及如何利用多态性来更好的设计程序。 +将信息发给对象时,如果程序不知道接受的具体类型是什么,但最终执行是正确的,这就是对象的“多态性”(Polymorphism)。面向对象的程序设计语言是通过“动态绑定”的方式来实现对象的多态性的。编译器和运行期系统会负责对所有细节的控制;我们只需知道要做什么,以及如何利用多态性来更好的设计程序。 ## 单继承 @@ -225,7 +225,7 @@ Java 的单继承结构有很多好处。由于所有对象都有继承自一个 通常,我们并不知道解决某个具体问题需要的对象数量,持续时间,以及对象的存储方式。那么我们如何知悉程序在创建时需要分配的内存空间呢? -在面向对象的设计中,问题的解决方案有些千篇一律:创建一个新类型的对象来引用、容纳其他的对象。当然,我们也可以使用多数编程语言都支持的“数组”(**Array**)。在 Java 中“集合”(**Collection**)的使用率更高。(也可称之为“容器”,但“集合”这个称呼更通用。) +在面向对象的设计中,问题的解决方案有些千篇一律:创建一个新类型的对象来引用、容纳其他的对象。当然,我们也可以使用多数编程语言都支持的“数组”(Array)。在 Java 中“集合”(Collection)的使用率更高。(也可称之为“容器”,但“集合”这个称呼更通用。) “集合”这种类型的对象可以存储任意类型、数量的其他对象。它能根据需要自动扩容,我们不用关心过程是如何实现的。