mirror of
https://github.com/LingCoder/OnJava8.git
synced 2026-09-04 05:42:48 +08:00
Merge branch 'master' of github.com:LingCoder/OnJava8
update
This commit is contained in:
@@ -28,10 +28,10 @@
|
||||
Smalltalk 作为第一种成功的面向对象程序设计语言和 Java 的基础语言,Alan Kay 总结了其五大基本特征。通过这些特征,我们可理解“纯粹”的面向对象程序设计方法是什么样的:
|
||||
|
||||
> 1. **万物皆对象**。你可以将对象想象成一种特殊的变量。它可以存储数据,可以在你对其“发出请求”时执行本身的操作。理论上讲,你可以从要解决的问题身上抽象出概念性的组件,然后在程序中将其表达为一个对象。
|
||||
> 2. **程序是一组对象,通过消息传递来告知彼此该做什么**。要请求一个对象,你需要向该对象发送 一条消息。
|
||||
> 2. **程序是一组对象,通过信息传递来告知彼此该做什么**。要请求一个对象,你需要向该对象发送信息。
|
||||
> 3. **每个对象都有自己的存储空间,可容纳其他对象**。或者说,通过封装现有对象,可制作出新型对象。所以,尽管对象的概念非常简单,但在程序中却可达到任意高的复杂程度。
|
||||
> 4. **每个对象都有一种类型**。根据语法,每个对象都是某个“类”的一个“实例”。其中,“类”(Class)是“类型”(Type)的同义词。一个类最重要的特征就是“能将什么消息发给它?”。
|
||||
> 5. **同一类所有对象都能接收相同的消息**。这实际是别有含义的一种说法,大家不久便能理解。由于类型为“圆”(Circle)的一个对象也属于类型为“形状”(Shape)的一个对象,所以一个圆完全能接收形状消息。这意味着可让程序代码统一指挥“形状”,令其自动控制所有符合“形状”描述的对象,其中自然包括“圆”。这一特性称为对象的“可替换性”,是OOP最重要的概念之一。
|
||||
> 4. **每个对象都有一种类型**。根据语法,每个对象都是某个“类”的一个“实例”。其中,“类”(Class)是“类型”(Type)的同义词。一个类最重要的特征就是“能将什么信息发给它?”。
|
||||
> 5. **同一类所有对象都能接收相同的信息**。这实际是别有含义的一种说法,大家不久便能理解。由于类型为“圆”(Circle)的一个对象也属于类型为“形状”(Shape)的一个对象,所以一个圆完全能接收形状信息。这意味着可让程序代码统一指挥“形状”,令其自动控制所有符合“形状”描述的对象,其中自然包括“圆”。这一特性称为对象的“可替换性”,是OOP最重要的概念之一。
|
||||
|
||||
Grady Booch 提供了对对象更简洁的描述:一个对象具有自己的状态,行为和身份。这意味着对象有自己的内部数据(由状态提供)、方法 (由特性提供),并彼此区分(每个对象在内存中都有唯一的地址)。
|
||||
|
||||
@@ -68,9 +68,9 @@ lt.on();
|
||||
|
||||
在这个例子中,类型/类的名称是 Light,可向 Light 对象发出的请求包括包括打开(on)、关闭(off)、变得更明亮(brighten)或者变得更暗淡(dim)。通过简单地声明一个名字(lt),我们为 Light 对象创建了一个“句柄”。然后用new关键字新建类型为 Light 的一个对象。再用等号将其赋给句柄。
|
||||
|
||||
为了向对象发送一条消息,我们列出句柄名(lt),再用一个句点符号(.)把它同消息名称(on)连接起来。从中可以看出,使用一些预先定义好的类时,我们在程序里采用的代码是非常简单和直观的。
|
||||
为了向对象发送一条信息,我们列出句柄名(lt),再用一个句点符号(.)把它同信息名称(on)连接起来。从中可以看出,使用一些预先定义好的类时,我们在程序里采用的代码是非常简单和直观的。
|
||||
|
||||
上图遵循 UML(*Unified Modeling Language*,统一建模语言)的格式。每个类由一个框表示,框的顶部有类型名称,框中间部分要描述的任何数据成员,以及方法(属于此对象的方法,它们接收任何发送到该对象的消息)在框的底部。通常,只有类的名称和公共方法在 UML 设计图中显示,因此中间部分未显示,如本例所示。如果您只对类名感兴趣,则也不需要显示方法信息。
|
||||
上图遵循 UML(*Unified Modeling Language*,统一建模语言)的格式。每个类由一个框表示,框的顶部有类型名称,框中间部分要描述的任何数据成员,以及方法(属于此对象的方法,它们接收任何发送到该对象的信息)在框的底部。通常,只有类的名称和公共方法在 UML 设计图中显示,因此中间部分未显示,如本例所示。如果您只对类名感兴趣,则也不需要显示方法信息。
|
||||
|
||||
|
||||
## 服务提供
|
||||
@@ -117,9 +117,9 @@ Java 有三个显式关键字来设置类中的访问权限:`public`(公开
|
||||
|
||||
上图中实心棱形指向“**Car**”表示**组合**的关系;如果是**聚合**关系,可以使用空心棱形。
|
||||
|
||||
(**译者注**:组合和聚合都属于关联关系的一种,只是额外具有整体-部分的意义。至于是聚合还是组合,需要根据实际的业务需求来断断。可能相同超类和子类,在不同的业务场景,关联关系会发生变化。只看代码,你是无法区分聚合和组合的,具体是哪一种关系,只能从语义级别来区分。聚合关系中,整件不会拥有部件的生命周期,所以整件删除时,部件不会被删除。再者,多个整件可以共享同一个部件。组合关系中,整件拥有部件的生命周期,所以整件删除时,部件一定会跟着删除。而且,多个整件不可以同时间共享同一个部件。这个区别可以用来区分某个关联关系到底是组合还是聚合。两个类生命周期不同步,则是聚合关系,生命周期同步就是组合关系。)
|
||||
(**译者注**:组合和聚合都属于关联关系的一种,只是额外具有整体-部分的意义。至于是聚合还是组合,需要根据实际的业务需求来判断。可能相同超类和子类,在不同的业务场景,关联关系会发生变化。只看代码是无法区分聚合和组合的,具体是哪一种关系,只能从语义级别来区分。聚合关系中,整件不会拥有部件的生命周期,所以整件删除时,部件不会被删除。再者,多个整件可以共享同一个部件。组合关系中,整件拥有部件的生命周期,所以整件删除时,部件一定会跟着删除。而且,多个整件不可以同时间共享同一个部件。这个区别可以用来区分某个关联关系到底是组合还是聚合。两个类生命周期不同步,则是聚合关系,生命周期同步就是组合关系。)
|
||||
|
||||
使用“组合”关系会为我们的程序带来极大的灵活性。通常我们新组的`class`“成员对象”会使用`private`访问权限,这样应用程序员则无法对其直接访问。我们就可以在不干扰客户代码的前提下,从容地修改那些成员。也可以在“运行期”更改成员,这进一步增大了灵活性。下面一节要讲到的“继承”并不具备这种灵活性,因为编译器必须对通过继承创建的类加以限制。
|
||||
使用“组合”关系会为我们的程序带来极大的灵活性。通常新构建的`class`“成员对象”会使用`private`访问权限,这样应用程序员则无法对其直接访问。我们就可以在不干扰客户代码的前提下,从容地修改那些成员。也可以在“运行期”更改成员,这进一步增大了灵活性。下面一节要讲到的“继承”并不具备这种灵活性,因为编译器必须对通过继承创建的类加以限制。
|
||||
|
||||
在面向对象编程中经常重点强调“继承”。在新程序员的印象里,或许早已先入为主地认为“继承应当随处可见”。沿着这种思路产生的程序设计通常拙劣又复杂。相反,在创建新类时首先要考虑“组合”,因为它更简单灵活,并且设计逻辑清晰。等我们有一些编程经验后,一旦需要用到继承,就会明显意识到这一点。
|
||||
|
||||
@@ -133,7 +133,7 @@ Java 有三个显式关键字来设置类中的访问权限:`public`(公开
|
||||
|
||||

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

|
||||
|
||||
@@ -141,11 +141,11 @@ Java 有三个显式关键字来设置类中的访问权限:`public`(公开
|
||||
|
||||

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

|
||||
|
||||
在前面的例子中,“圆是形状”。这种通过继承的类型等价是理解面向对象编程含义的基本网关之一。因为基类和派生类都具有相同的基本接口,所以必须有一些实现来支持该接口。也就是说,当对象接收到特定消息时,必须有可执行代码。如果继承一个类并且不做其他任何事情,则来自基类接口的方法直接进入派生类。这意味着派生类的对象不仅具有相同的类型,而且具有相同的行为,这并不特别有趣。有两种方法可以区分新派生类与原始基类。第一种方法很简单:向派生类添加全新的方法。这些新方法不是基类接口的一部分。这意味着基类没有按照您想要的那样多,所以您添加了更多的方法。继承的这种简单而原始的用途有时是解决问题的完美解决方案。然而,事先还是要仔细调查自己的基础类是否真的需要这些额外的方法。
|
||||
在前面的例子中,“圆是形状”。这种通过继承的类型等价是理解面向对象编程含义的基本网关之一。因为基类和派生类都具有相同的基本接口,所以必须有一些实现来支持该接口。也就是说,当对象接收到特定信息时,必须有可执行代码。如果继承一个类并且不做其他任何事情,则来自基类接口的方法直接进入派生类。这意味着派生类的对象不仅具有相同的类型,而且具有相同的行为,这并不特别有趣。有两种方法可以区分新派生类与原始基类。第一种方法很简单:向派生类添加全新的方法。这些新方法不是基类接口的一部分。这意味着基类没有按照您想要的那样多,所以您添加了更多的方法。继承的这种简单而原始的用途有时是解决问题的完美解决方案。然而,事先还是要仔细调查自己的基础类是否真的需要这些额外的方法。
|
||||
|
||||
|
||||
## 多态
|
||||
@@ -162,9 +162,9 @@ Java 有三个显式关键字来设置类中的访问权限:`public`(公开
|
||||
|
||||
答案是继承的主要转折点:在传统意义上,编译器不能进行函数调用。由非 OOP 编译器生成的函数调用生成所谓的早期绑定,这个术语您可能从未听说过,因为您从未以其他方式考虑过。这意味着编译器生成对特定函数名的调用,该调用解析为要执行的代码的绝对地址。
|
||||
|
||||
通过继承,程序直到运行时才能确定代码的地址,因此当消息被发送到对象时,还需要其他一些方案。为了解决这个问题,面向对象语言使用后期绑定的概念。当向对象发送消息时,调用的代码直到运行时才确定。编译器确保方法存在,并对参数和返回值执行类型检查,但是它不知道要执行的确切代码。
|
||||
通过继承,程序直到运行时才能确定代码的地址,因此当信息被发送到对象时,还需要其他一些方案。为了解决这个问题,面向对象语言使用后期绑定的概念。当向对象发送信息时,调用的代码直到运行时才确定。编译器确保方法存在,并对参数和返回值执行类型检查,但是它不知道要执行的确切代码。
|
||||
|
||||
为了执行后期绑定,Java 使用一个特殊的代码位来代替绝对调用。这段代码使用对象中存储的信息来计算方法主体的地址(此过程在多态性章节中有详细介绍)。因此,每个对象的行为根据特定代码位的内容而不同。当您向对象发送消息时,该对象实际上确定如何处理该消息。在某些语言中,必须显式地授予方法后期绑定属性的灵活性。例如,C++使用虚拟关键字。在这些语言中,默认情况下方法没有动态绑定。在 Java 中,动态绑定是默认行为,不需要额外的关键字来生成多态性。
|
||||
为了执行后期绑定,Java 使用一个特殊的代码位来代替绝对调用。这段代码使用对象中存储的信息来计算方法主体的地址(此过程在多态性章节中有详细介绍)。因此,每个对象的行为根据特定代码位的内容而不同。当您向对象发送信息时,该对象实际上确定如何处理该信息。在某些语言中,必须显式地授予方法后期绑定属性的灵活性。例如,C++使用虚拟关键字。在这些语言中,默认情况下方法没有动态绑定。在 Java 中,动态绑定是默认行为,不需要额外的关键字来生成多态性。
|
||||
|
||||
为了演示多态性,我们编写了一段代码,它忽略了类型的特定细节,只与基类对话。该代码与特定于类型的信息分离,因此更易于编写和更容易理解。而且,如果通过继承添加了一个新类型(例如,一个六边形),那么代码对于新类型的 Shape 就像对现有类型一样有效。因此,该程序是可扩展的。
|
||||
|
||||
@@ -200,9 +200,9 @@ void doSomething(Shape shape) {
|
||||
doSomething(circle);
|
||||
```
|
||||
这里将 **Circle**(圆)句柄传递给一个本来期待 **Shape**(形状)句柄的方法。由于圆也是一种几何形状,所
|
||||
以 **doSomething(circle)** 能正确地执行。也就是说,**doSomething(circle)** 能接受任意 **Shape** 的消息。这是完全安全和合乎逻辑的事情。
|
||||
以 **doSomething(circle)** 能正确地执行。也就是说,**doSomething()** 能接受任意 **Shape** 的信息。这是完全安全和合乎逻辑的事情。
|
||||
|
||||
这种把子类当成其基类来处理的过程叫做“向上转型”(**upcasting**)。在面向对象的编程里,经常利用这种方法来给程序解耦。在看下面的 **doSomething()** 代码示例:
|
||||
这种把子类当成其基类来处理的过程叫做“向上转型”(**upcasting**)。在面向对象的编程里,经常利用这种方法来给程序解耦。再看下面的 **doSomething()** 代码示例:
|
||||
|
||||
```java
|
||||
shape.erase();
|
||||
@@ -213,11 +213,11 @@ void doSomething(Shape shape) {
|
||||
|
||||
我们可以看到程序并未这样表达:“如果你是一个 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,9 +225,9 @@ void doSomething(Shape shape) {
|
||||
|
||||
Java 的单继承结构有很多好处。由于所有对象都有继承自一个公共接口,因此它们最终都属于同一个基本类型。相反的,对于 C++ 所使用的多继承的方案则是不保证所有的对象都属于同一个的基类。这种方案的限制更少一点。从向后兼容的角度看,多继承的方案更符合 C 的模型。
|
||||
|
||||
对于完全面向对象编程,我们必须构建自己的层次结构,以提供与其他 OOP 语言同样的便利。我们经常会使用一些新的类库,和不兼容的接口。为了整合它们而花费大气力来(有可能还要用上多继承)以获得 C++ 样的“灵活性”值得吗?如果从零开始,像 Java 这样的替代方案会更有效率。
|
||||
对于完全面向对象编程,我们必须要构建自己的层次结构,以提供与其他 OOP 语言同样的便利。我们经常会使用到新的类库和不兼容的接口。为了整合它们而花费大气力(有可能还要用上多继承)以获得 C++ 样的“灵活性”值得吗?如果从零开始,Java 这样的替代方案会是更好的选择。
|
||||
|
||||
另外,单继承的结构使得垃圾收集器的实现更为容易。这也是 Java 在 C++ 之上的根本改进之一。
|
||||
另外,单继承的结构使得垃圾收集器的实现更为容易。这也是 Java 在 C++ 基础上的根本改进之一。
|
||||
|
||||
由于运行期的类型信息会存在于所有对象中,所以我们永远不会遇到判断不了对象类型的情况。这对于系统级操作尤其重要,例如[异常处理](#异常处理)。同时,这也让我们的编程具有更大的灵活性。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user